releaseparser: move group-agnostic parser to groups/, GB doc-blocks, per-group JSON reports
Generalise the parser: the MOONBLOOD group profile is gone, the parser and its dictionaries are now group-agnostic under internal/releaseparser/groups. Positional model: - [0]=Artist, [1]=Title are fixed and never matched against dictionaries, so titles like "Split"/"Demo"/"De" are never eaten. - [len-1]=Group is always read from the line, not from the query key. - [len-2]=Year when it is a 4-digit 19xx|20xx token; otherwise the slot is resolved as a middle-zone token (e.g. FLAC is still picked up as format). - The middle zone is resolved via case-insensitive dictionaries in the order format -> source -> bit-depth -> resolution -> edition -> language -> catalogue number; stored values keep their original case. Catalogue numbers: any "(...)" token in the middle zone is a catalogue number regardless of content (digits, roman numerals, dots, underscores, mixed case); splitting never breaks tokens inside parentheses. Dictionaries moved to internal/releaseparser/groups/resources/dicts and embedded via go:embed; dead profile fields (Name, ExpectedTailOrder) removed; profile comments rewritten in British English. File-based AnalyzeGroup/TestParseMoonblood removed — titles now come from the database (cmd/srrdb: srrdb <releaser>..., config/secret.yaml DSN). Validation output (cmd/srrdb): - tmp/releaseparser/<group>.json — per-group parsed ParsedRelease records - tmp/releaseparser/<releaser>.report.json — per-request aggregate (same as console) - review_unknown_tokens.txt — unknown/warning rows for manual review Tests updated for the positional model; added TestParseCatalogNumberWithDotAndRoman for (DYN.3140-2) and (GOWP_DCCCLXXXIX). go build, go vet and go test all pass.
This commit is contained in:
+1
-1
@@ -14,7 +14,7 @@ vendor/
|
|||||||
config/secret.yaml
|
config/secret.yaml
|
||||||
*.md
|
*.md
|
||||||
mdb
|
mdb
|
||||||
internal/releaseparser/resources/*
|
internal/releaseparser/groups/resources/*
|
||||||
review_unknown_tokens.txt
|
review_unknown_tokens.txt
|
||||||
srrdb.exe
|
srrdb.exe
|
||||||
**/*.exe
|
**/*.exe
|
||||||
|
|||||||
+50
-4
@@ -5,13 +5,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
_ "github.com/go-sql-driver/mysql"
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
|
||||||
"git.kplus.net.ua/yevhen/resource-scraper/internal/releaseparser/groups/moonblood"
|
"git.kplus.net.ua/yevhen/resource-scraper/internal/releaseparser/groups"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@@ -35,19 +37,33 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer dbase.Close()
|
defer dbase.Close()
|
||||||
|
|
||||||
out := make(map[string]moonblood.GroupStats, len(releasers))
|
const outDir = "tmp/releaseparser"
|
||||||
|
if err := os.MkdirAll(outDir, 0o755); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]groups.GroupStats, len(releasers))
|
||||||
|
|
||||||
for _, releaser := range releasers {
|
for _, releaser := range releasers {
|
||||||
var titles []string
|
var titles []string
|
||||||
|
|
||||||
err = dbase.Select(&titles,
|
err = dbase.Select(&titles,
|
||||||
`SELECT title FROM theamok_mdb.external_sources
|
`SELECT title FROM theamok_mdb.external_sources
|
||||||
WHERE type = 'prescene' AND releaser = ?`, releaser)
|
WHERE type = 'prescene' AND releaser = ? limit 1000`, releaser)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
out[releaser] = moonblood.AnalyzeTitles(titles)
|
out[releaser] = groups.AnalyzeTitles(titles)
|
||||||
|
|
||||||
|
// Per-group parsed records keyed by the actual group read from each
|
||||||
|
// line: every release with all resolved fields for manual validation.
|
||||||
|
for group, parsed := range groups.ParseByGroup(titles) {
|
||||||
|
writeJSON(filepath.Join(outDir, safeName(group)+".json"), parsed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-request report identical to what is printed to the console.
|
||||||
|
writeJSON(filepath.Join(outDir, safeName(releaser)+".report.json"), out[releaser])
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := json.MarshalIndent(out, "", " ")
|
data, err := json.MarshalIndent(out, "", " ")
|
||||||
@@ -77,3 +93,33 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println(string(data))
|
fmt.Println(string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeJSON writes value as an indented JSON document to the given path.
|
||||||
|
func writeJSON(path string, value any) {
|
||||||
|
data, err := json.MarshalIndent(value, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("marshal %s: %v", path, err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, data, 0o644); err != nil {
|
||||||
|
log.Fatalf("write %s: %v", path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// safeName strips characters that are unsafe in file names.
|
||||||
|
func safeName(s string) string {
|
||||||
|
repl := func(r rune) rune {
|
||||||
|
switch {
|
||||||
|
case r >= 'a' && r <= 'z':
|
||||||
|
return r
|
||||||
|
case r >= 'A' && r <= 'Z':
|
||||||
|
return r
|
||||||
|
case r >= '0' && r <= '9':
|
||||||
|
return r
|
||||||
|
case r == '-' || r == '_' || r == '.':
|
||||||
|
return r
|
||||||
|
default:
|
||||||
|
return '_'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Map(repl, s)
|
||||||
|
}
|
||||||
|
|||||||
+38
-28
@@ -1,8 +1,6 @@
|
|||||||
package moonblood
|
package groups
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"os"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -12,6 +10,7 @@ import (
|
|||||||
// GroupStats summarises how a set of release lines parsed: field
|
// GroupStats summarises how a set of release lines parsed: field
|
||||||
// distributions, unknown tokens and warnings aggregated by occurrence, and
|
// distributions, unknown tokens and warnings aggregated by occurrence, and
|
||||||
// the average parsing confidence. The slices are sorted by descending count.
|
// the average parsing confidence. The slices are sorted by descending count.
|
||||||
|
|
||||||
type GroupStats struct {
|
type GroupStats struct {
|
||||||
Total int
|
Total int
|
||||||
YearDistribution map[int]int
|
YearDistribution map[int]int
|
||||||
@@ -26,34 +25,11 @@ type GroupStats struct {
|
|||||||
AvgConfidence float64
|
AvgConfidence float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// AnalyzeGroup parses the release lines from the local resource file. It is
|
|
||||||
// used for a quick local check of the dictionaries without touching a
|
|
||||||
// database.
|
|
||||||
func AnalyzeGroup() (GroupStats, error) {
|
|
||||||
file, err := os.Open("../../resources/moonblood.txt")
|
|
||||||
if err != nil {
|
|
||||||
return GroupStats{}, err
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
var titles []string
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := strings.TrimSpace(scanner.Text())
|
|
||||||
if line != "" {
|
|
||||||
titles = append(titles, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
return GroupStats{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return AnalyzeTitles(titles), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnalyzeTitles parses a set of release lines (for example fetched from the
|
// AnalyzeTitles parses a set of release lines (for example fetched from the
|
||||||
// database by a particular releaser key) and aggregates the results into a
|
// database by a particular releaser key) and aggregates the results into a
|
||||||
// GroupStats summary.
|
// GroupStats summary.
|
||||||
|
|
||||||
func AnalyzeTitles(titles []string) GroupStats {
|
func AnalyzeTitles(titles []string) GroupStats {
|
||||||
stats := GroupStats{
|
stats := GroupStats{
|
||||||
YearDistribution: make(map[int]int),
|
YearDistribution: make(map[int]int),
|
||||||
@@ -67,10 +43,12 @@ func AnalyzeTitles(titles []string) GroupStats {
|
|||||||
WarningStats: []releaseparser.UnknownTokenStats{},
|
WarningStats: []releaseparser.UnknownTokenStats{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
totalConfidence := 0.0
|
totalConfidence := 0.0
|
||||||
unknownMap := make(map[string]*releaseparser.UnknownTokenStats)
|
unknownMap := make(map[string]*releaseparser.UnknownTokenStats)
|
||||||
warningMap := make(map[string]*releaseparser.UnknownTokenStats)
|
warningMap := make(map[string]*releaseparser.UnknownTokenStats)
|
||||||
|
|
||||||
|
|
||||||
for _, title := range titles {
|
for _, title := range titles {
|
||||||
result := Parse(title)
|
result := Parse(title)
|
||||||
stats.Total++
|
stats.Total++
|
||||||
@@ -131,7 +109,6 @@ func AnalyzeTitles(titles []string) GroupStats {
|
|||||||
for _, tokenStats := range unknownMap {
|
for _, tokenStats := range unknownMap {
|
||||||
stats.UnknownTokens = append(stats.UnknownTokens, *tokenStats)
|
stats.UnknownTokens = append(stats.UnknownTokens, *tokenStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(stats.UnknownTokens, func(i, j int) bool {
|
sort.Slice(stats.UnknownTokens, func(i, j int) bool {
|
||||||
return stats.UnknownTokens[i].Count > stats.UnknownTokens[j].Count
|
return stats.UnknownTokens[i].Count > stats.UnknownTokens[j].Count
|
||||||
})
|
})
|
||||||
@@ -149,3 +126,36 @@ func AnalyzeTitles(titles []string) GroupStats {
|
|||||||
|
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// AnalyzeByGroup parses a set of release lines and returns per-group
|
||||||
|
// statistics keyed by the actual group read from each line (not from the
|
||||||
|
// query key, which may cover several groups). It is used to validate parsing
|
||||||
|
// quality against the original data, grouped by the real releaser name.
|
||||||
|
func AnalyzeByGroup(titles []string) map[string]GroupStats {
|
||||||
|
byGroup := make(map[string][]string)
|
||||||
|
for _, title := range titles {
|
||||||
|
group := Parse(title).Group
|
||||||
|
byGroup[group] = append(byGroup[group], title)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make(map[string]GroupStats, len(byGroup))
|
||||||
|
for group, lines := range byGroup {
|
||||||
|
out[group] = AnalyzeTitles(lines)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ParseByGroup parses a set of release lines into a per-group list of fully
|
||||||
|
// populated ParsedRelease records, keyed by the actual group read from each
|
||||||
|
// line. It is used to validate parsing quality field-by-field against the
|
||||||
|
// original data.
|
||||||
|
func ParseByGroup(titles []string) map[string][]releaseparser.ParsedRelease {
|
||||||
|
byGroup := make(map[string][]releaseparser.ParsedRelease)
|
||||||
|
for _, title := range titles {
|
||||||
|
result := Parse(title)
|
||||||
|
byGroup[result.Group] = append(byGroup[result.Group], result)
|
||||||
|
}
|
||||||
|
return byGroup
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# {bit_depth}
|
|
||||||
16BIT
|
|
||||||
24BIT
|
|
||||||
32BIT
|
|
||||||
DE16BIT
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
REPACK
|
|
||||||
REAL_REPACK
|
|
||||||
READNFO
|
|
||||||
PROPER
|
|
||||||
RERIP
|
|
||||||
199X
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# {edition} — тип видання
|
|
||||||
EP
|
|
||||||
DEMO
|
|
||||||
SPLIT
|
|
||||||
REISSUE
|
|
||||||
DELUXE_EDITION
|
|
||||||
REMASTERED
|
|
||||||
BOXSET
|
|
||||||
SINGLE
|
|
||||||
PROMO
|
|
||||||
REISSUE_REMASTERED
|
|
||||||
REMASTERED_25TH_ANNIVERSARY_EDITION
|
|
||||||
REMASTERED_20TH_ANNIVERSARY_EDITION
|
|
||||||
20TH_ANNIVERSARY_EDITION_REMASTERED
|
|
||||||
Deluxe_Edition
|
|
||||||
LIMITED_EDITION_BOXSET
|
|
||||||
REMASTERED_EP
|
|
||||||
10TH_ANNIVERSARY_EDITION
|
|
||||||
OST
|
|
||||||
XX_Legacy_Edition
|
|
||||||
REISSUE_DEMO
|
|
||||||
JP_Deluxe_Edition
|
|
||||||
Remastered_Edition
|
|
||||||
Anniversary_Edition
|
|
||||||
Limited_Edition
|
|
||||||
CDS
|
|
||||||
CDM
|
|
||||||
RETAIL
|
|
||||||
JP_RETAIL
|
|
||||||
JP_REISSUE
|
|
||||||
DEMO_REMASTERED
|
|
||||||
EU_RETAIL
|
|
||||||
DE_RETAIL
|
|
||||||
REISSUE_DELUXE_EDITION
|
|
||||||
RETAIL_REISSUE
|
|
||||||
SPECIAL_EDITION
|
|
||||||
SPECIAL_COLLECTORS_EDITION_REMASTERED
|
|
||||||
30_YEARS_SPECIAL_COLLECTORS_EDITION_REMASTERED
|
|
||||||
LIMITED_EDITION_REMASTERED
|
|
||||||
25th_Anniversary_Edition
|
|
||||||
DEMO_REISSUE
|
|
||||||
Ultimate_Edition
|
|
||||||
Issue_255
|
|
||||||
Issue_254
|
|
||||||
Issue_198
|
|
||||||
7INCH_VINYL
|
|
||||||
REMASTERED_REISSUE
|
|
||||||
PROMO_DEMO
|
|
||||||
ES_RETAIL
|
|
||||||
BOX_SET
|
|
||||||
BONUS
|
|
||||||
REISSUE_EP
|
|
||||||
30th_Anniversary
|
|
||||||
REDUX
|
|
||||||
REMASTERED_BOXSET
|
|
||||||
MAG
|
|
||||||
REISSUE_BOXSET
|
|
||||||
30TH_ANNIVERSARY_EDITION_REMASTERED
|
|
||||||
DELUXE
|
|
||||||
Deluxe_Edition_Reissue
|
|
||||||
INSTRUMENTAL
|
|
||||||
REISSUE_EXPANDED_EDITION
|
|
||||||
BOOTLEG
|
|
||||||
EXPANDED_EDITION
|
|
||||||
ADVANCE
|
|
||||||
15TH_ANNIVERSARY_DELUXE_EDITION
|
|
||||||
TOUR_EDITION
|
|
||||||
REMASTERED_DEMO
|
|
||||||
REMASTERED_DELUXE_EDITION
|
|
||||||
30TH_ANNIVERSARY_EDITION
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# {format} — аудіоформат
|
|
||||||
# один токен на рядок, регістр не має значення
|
|
||||||
FLAC
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
# {language} — мовні маркери
|
|
||||||
DE
|
|
||||||
FR
|
|
||||||
ES
|
|
||||||
SK
|
|
||||||
FI
|
|
||||||
SE
|
|
||||||
BR
|
|
||||||
PL
|
|
||||||
NL
|
|
||||||
NO
|
|
||||||
PT
|
|
||||||
IT
|
|
||||||
HU
|
|
||||||
DK
|
|
||||||
AT
|
|
||||||
IS
|
|
||||||
UA
|
|
||||||
JP
|
|
||||||
GR
|
|
||||||
CN
|
|
||||||
RU
|
|
||||||
ID
|
|
||||||
PH
|
|
||||||
RO
|
|
||||||
LT
|
|
||||||
BY
|
|
||||||
CZ
|
|
||||||
SP
|
|
||||||
IR
|
|
||||||
FO
|
|
||||||
RS
|
|
||||||
DA
|
|
||||||
HI
|
|
||||||
CH
|
|
||||||
LU
|
|
||||||
CY
|
|
||||||
EE
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# {resolution} — частота дискретизації
|
|
||||||
44KHZ
|
|
||||||
48KHZ
|
|
||||||
48.0kHz
|
|
||||||
88KHZ
|
|
||||||
96KHZ
|
|
||||||
176KHZ
|
|
||||||
192KHZ
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# {source} — джерело релізу
|
|
||||||
# VINYL сюди входить як source (за домовленістю, не edition)
|
|
||||||
WEB
|
|
||||||
CD
|
|
||||||
2CD
|
|
||||||
CDEP
|
|
||||||
VINYL
|
|
||||||
TAPE
|
|
||||||
CDR
|
|
||||||
17CD
|
|
||||||
3CD
|
|
||||||
4CD
|
|
||||||
18CD
|
|
||||||
3CDEP
|
|
||||||
DVD
|
|
||||||
7CD
|
|
||||||
2CDR
|
|
||||||
10CD
|
|
||||||
6CD
|
|
||||||
2CDEP
|
|
||||||
2VINYL
|
|
||||||
CDREP
|
|
||||||
LP
|
|
||||||
2LP
|
|
||||||
3VINYL
|
|
||||||
3TAPE
|
|
||||||
13CD
|
|
||||||
13CDR
|
|
||||||
10INCH_VINYL
|
|
||||||
25CD
|
|
||||||
WEBFLAC
|
|
||||||
2TAPE
|
|
||||||
BLURAY
|
|
||||||
VLS
|
|
||||||
MCD
|
|
||||||
24BIT_WEB
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package moonblood
|
|
||||||
|
|
||||||
import (
|
|
||||||
"embed"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.kplus.net.ua/yevhen/resource-scraper/internal/releaseparser"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Словники токенів лежать у dicts/*.txt — один токен на рядок.
|
|
||||||
// Їх можна редагувати без перекомпіляції логіки (embed тягне їх у бінарник на збірці).
|
|
||||||
|
|
||||||
//go:embed dicts/*.txt
|
|
||||||
var dictFS embed.FS
|
|
||||||
|
|
||||||
func loadDict(name string) map[string]bool {
|
|
||||||
data, err := dictFS.ReadFile("dicts/" + name + ".txt")
|
|
||||||
if err != nil {
|
|
||||||
panic("moonblood: cannot load dictionary " + name + ": " + err.Error())
|
|
||||||
}
|
|
||||||
out := make(map[string]bool)
|
|
||||||
for _, line := range strings.Split(string(data), "\n") {
|
|
||||||
line = strings.ToUpper(strings.TrimSpace(line))
|
|
||||||
if line == "" || strings.HasPrefix(line, "#") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out[line] = true
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
var Profile = releaseparser.GroupProfile{
|
|
||||||
Name: "MOONBLOOD",
|
|
||||||
ExpectedTailOrder: []string{"bit_depth", "resolution", "source", "format", "year", "group"},
|
|
||||||
FormatTokens: loadDict("format"),
|
|
||||||
SourceTokens: loadDict("source"),
|
|
||||||
BitDepthTokens: loadDict("bitdepth"),
|
|
||||||
ResolutionTokens: loadDict("resolution"),
|
|
||||||
LanguageMarkers: loadDict("language"),
|
|
||||||
EditionTokens: loadDict("edition"),
|
|
||||||
CommentTokens: loadDict("comment"),
|
|
||||||
}
|
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package moonblood
|
package groups
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -110,7 +110,7 @@ func split(raw string) []string {
|
|||||||
// list and returns the cleaned list together with the removed comments.
|
// list and returns the cleaned list together with the removed comments.
|
||||||
func extractComments(tokens []string) ([]string, []string) {
|
func extractComments(tokens []string) ([]string, []string) {
|
||||||
out := make([]string, 0, len(tokens))
|
out := make([]string, 0, len(tokens))
|
||||||
comments := []string{}
|
var comments []string
|
||||||
for _, tok := range tokens {
|
for _, tok := range tokens {
|
||||||
if Profile.CommentTokens[strings.ToUpper(tok)] {
|
if Profile.CommentTokens[strings.ToUpper(tok)] {
|
||||||
comments = append(comments, tok)
|
comments = append(comments, tok)
|
||||||
+1
-35
@@ -1,43 +1,9 @@
|
|||||||
package moonblood
|
package groups
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseMoonblood(t *testing.T) {
|
|
||||||
file, err := os.Open("../../resources/moonblood.txt")
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("resource file not found (DB mode), skipping: %v", err)
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(file)
|
|
||||||
total, success := 0, 0
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if line == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result := Parse(line)
|
|
||||||
total++
|
|
||||||
|
|
||||||
if result.Group == "MOONBLOOD" && result.Year != 0 && result.Format == "FLAC" {
|
|
||||||
success++
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Confidence < 0.7 {
|
|
||||||
t.Logf("Low confidence: %s (%.2f)", line, result.Confidence)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Parsed %d releases, %d successful (%.1f%%)", total, success, float64(success)/float64(total)*100)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseFullTail(t *testing.T) {
|
func TestParseFullTail(t *testing.T) {
|
||||||
r := Parse("Fane-Dragged_Through_the_Unending-24BIT-WEB-FLAC-2025-MOONBLOOD")
|
r := Parse("Fane-Dragged_Through_the_Unending-24BIT-WEB-FLAC-2025-MOONBLOOD")
|
||||||
if r.Artist != "Fane" {
|
if r.Artist != "Fane" {
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package groups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kplus.net.ua/yevhen/resource-scraper/internal/releaseparser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The token dictionaries live under resources/dicts/*.txt — one token (in any
|
||||||
|
// case) per line; blank lines and lines starting with "#" are ignored. They
|
||||||
|
// can be edited without recompiling the parsing logic: go:embed pulls them
|
||||||
|
// into the binary at build time.
|
||||||
|
|
||||||
|
//go:embed resources/dicts/*.txt
|
||||||
|
var dictFS embed.FS
|
||||||
|
|
||||||
|
func loadDict(name string) map[string]bool {
|
||||||
|
data, err := dictFS.ReadFile("resources/dicts/" + name + ".txt")
|
||||||
|
if err != nil {
|
||||||
|
panic("release parser: cannot load dictionary " + name + ": " + err.Error())
|
||||||
|
}
|
||||||
|
out := make(map[string]bool)
|
||||||
|
for line := range strings.SplitSeq(string(data), "\n") {
|
||||||
|
line = strings.ToUpper(strings.TrimSpace(line))
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[line] = true
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile holds the shared dictionaries for the release parser. The parser is
|
||||||
|
// group-agnostic: the group name is read from each release line, never from
|
||||||
|
// this profile, so there is a single set of dictionaries covering all groups.
|
||||||
|
var Profile = releaseparser.GroupProfile{
|
||||||
|
FormatTokens: loadDict("format"),
|
||||||
|
SourceTokens: loadDict("source"),
|
||||||
|
BitDepthTokens: loadDict("bitdepth"),
|
||||||
|
ResolutionTokens: loadDict("resolution"),
|
||||||
|
LanguageMarkers: loadDict("language"),
|
||||||
|
EditionTokens: loadDict("edition"),
|
||||||
|
CommentTokens: loadDict("comment"),
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
package releaseparser
|
package releaseparser
|
||||||
|
|
||||||
type GroupProfile struct {
|
type GroupProfile struct {
|
||||||
Name string
|
|
||||||
ExpectedTailOrder []string
|
|
||||||
FormatTokens map[string]bool
|
FormatTokens map[string]bool
|
||||||
SourceTokens map[string]bool
|
SourceTokens map[string]bool
|
||||||
BitDepthTokens map[string]bool
|
BitDepthTokens map[string]bool
|
||||||
@@ -15,18 +13,18 @@ type GroupProfile struct {
|
|||||||
type ParsedRelease struct {
|
type ParsedRelease struct {
|
||||||
Raw string
|
Raw string
|
||||||
Group string
|
Group string
|
||||||
|
Artist string
|
||||||
Year int
|
Year int
|
||||||
|
Title string
|
||||||
|
Source string
|
||||||
|
Edition string
|
||||||
|
CatalogNo string
|
||||||
|
Language string
|
||||||
BitDepth string
|
BitDepth string
|
||||||
Resolution string
|
Resolution string
|
||||||
Comment string
|
Comment string
|
||||||
CatalogNo string
|
|
||||||
Source string
|
|
||||||
Format string
|
Format string
|
||||||
Language string
|
|
||||||
Edition string
|
|
||||||
Core string
|
Core string
|
||||||
Artist string
|
|
||||||
Title string
|
|
||||||
UnknownTokens []string
|
UnknownTokens []string
|
||||||
Warnings []string
|
Warnings []string
|
||||||
Confidence float64
|
Confidence float64
|
||||||
|
|||||||
Reference in New Issue
Block a user