244 lines
8.5 KiB
Go
244 lines
8.5 KiB
Go
package scanner
|
|||
|
|
|
||
|
|
import "testing"
|
||
|
|
|
||
|
|
func TestMangaSeriesFromPath(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
path string
|
||
|
|
want string
|
||
|
|
}{
|
||
|
|
{"/m/manga/Official/Kurosagi Corpse Delivery Service/V2006/Kurosagi 10.cbz", "Kurosagi Corpse Delivery Service"},
|
||
|
|
{"/m/manga/One-Punch Man/One-Punch Man 178 (2023) (Digital) (LuCaZ).cbz", "One-Punch Man"},
|
||
|
|
{"/m/manga/Bakuman/v13/Bakuman v13 (2012).cbz", "Bakuman"},
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
t.Run(tc.want, func(t *testing.T) {
|
||
|
|
if got := mangaSeriesFromPath(tc.path); got != tc.want {
|
||
|
|
t.Fatalf("mangaSeriesFromPath(%q) = %q, want %q", tc.path, got, tc.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMangaSeriesGroupKey(t *testing.T) {
|
||
|
|
a := mangaSeriesGroupKey(8, "One-Punch Man")
|
||
|
|
b := mangaSeriesGroupKey(8, " one-punch man ")
|
||
|
|
c := mangaSeriesGroupKey(8, "Bakuman")
|
||
|
|
d := mangaSeriesGroupKey(9, "One-Punch Man")
|
||
|
|
if a == "" || a != b {
|
||
|
|
t.Fatalf("same series must yield same key: %q vs %q", a, b)
|
||
|
|
}
|
||
|
|
if a == c || a == d {
|
||
|
|
t.Fatalf("different series/library must differ: a=%q c=%q d=%q", a, c, d)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestParseMangaIndex(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
file string
|
||
|
|
wantVol string
|
||
|
|
wantIdx float64
|
||
|
|
wantHas bool
|
||
|
|
}{
|
||
|
|
{"bare chapter", "One-Punch Man 178 (2023) (Digital) (LuCaZ)", "", 178, true},
|
||
|
|
{"volume", "Bakuman v13 (2012) (Digital) (aKraa)", "v13", 13, true},
|
||
|
|
{"chapter c-prefix", "Dead Mount Death Play c128 (2025) (Digital) (UP!) (Oak)", "", 128, true},
|
||
|
|
{"vol-year issue", "Berserk Vol.2003 #04 (July, 2004)", "v04", 4, true},
|
||
|
|
{"vol-year issue real-world", "10 Things I Want to Do Before I Turn 40 Vol.2025 #01 (May, 2025)", "v01", 1, true},
|
||
|
|
{"vol-year no issue", "Berserk Vol.2003 (2004)", "", 0, false},
|
||
|
|
{"decimal chapter", "Kindergarten WARS 109.1 (2025) (Digital) (Rillant)", "", 109.1, true},
|
||
|
|
{"subtitle then volume", "The Ancient Magus' Bride - Wizard's Blue v04 (2022) (Digital)", "v04", 4, true},
|
||
|
|
{"no number", "Some Oneshot (2020) (Digital) (grp)", "", 0, false},
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
|
vol, idx, has := parseMangaIndex(tc.file)
|
||
|
|
if vol != tc.wantVol || has != tc.wantHas || idx != tc.wantIdx {
|
||
|
|
t.Fatalf("parseMangaIndex(%q) = (%q,%v,%v), want (%q,%v,%v)", tc.file, vol, idx, has, tc.wantVol, tc.wantIdx, tc.wantHas)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestCleanMangaSeriesName(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
input string
|
||
|
|
want string
|
||
|
|
}{
|
||
|
|
// Trailing parentheticals stripped.
|
||
|
|
{"404 Demons (Digital) (Oak)", "404 Demons"},
|
||
|
|
{"Arifureta - From Commonplace to World's Strongest (Digital) (1r0n)", "Arifureta - From Commonplace to World's Strongest"},
|
||
|
|
{"Angels of Death Episode.0 (2019-2024) (Digital) (LuCaZ)", "Angels of Death Episode.0"},
|
||
|
|
{"Angel of the Night - Lucian - One-shot (2026) (Digital)", "Angel of the Night - Lucian - One-shot"},
|
||
|
|
{"'Tis Time for 'Torture,' Princess (2019-2026) (Digital) (Antrill-Oak)", "'Tis Time for 'Torture,' Princess"},
|
||
|
|
{"A Certain Scientific Railgun - Astral Buddy (2019)", "A Certain Scientific Railgun - Astral Buddy"},
|
||
|
|
// No junk — must be returned unchanged.
|
||
|
|
{"Amefurashi", "Amefurashi"},
|
||
|
|
// Guardrail: folder that is ONLY parentheticals — return original trimmed input.
|
||
|
|
{"(2025) (Digital)", "(2025) (Digital)"},
|
||
|
|
// Middle parentheticals must be preserved.
|
||
|
|
{"JoJo's Bizarre Adventure - Part 8 - JoJolion (something) extra", "JoJo's Bizarre Adventure - Part 8 - JoJolion (something) extra"},
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
t.Run(tc.input, func(t *testing.T) {
|
||
|
|
got := cleanMangaSeriesName(tc.input)
|
||
|
|
if got != tc.want {
|
||
|
|
t.Fatalf("cleanMangaSeriesName(%q) = %q, want %q", tc.input, got, tc.want)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMangaIndexForFile(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
base string
|
||
|
|
seriesName string
|
||
|
|
wantVol string
|
||
|
|
wantIdx float64
|
||
|
|
wantHas bool
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "404 Demons ch01 — series prefix stripped",
|
||
|
|
base: "404 Demons 01 (Digital-Compilation) (Oak)",
|
||
|
|
seriesName: "404 Demons",
|
||
|
|
wantVol: "",
|
||
|
|
wantIdx: 1,
|
||
|
|
wantHas: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "404 Demons ch09",
|
||
|
|
base: "404 Demons 09 (Digital-Compilation) (Oak)",
|
||
|
|
seriesName: "404 Demons",
|
||
|
|
wantVol: "",
|
||
|
|
wantIdx: 9,
|
||
|
|
wantHas: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "404 Demons v01 — volume token unambiguous",
|
||
|
|
base: "404 Demons v01 (Digital-Compilation) (Oak)",
|
||
|
|
seriesName: "404 Demons",
|
||
|
|
wantVol: "v01",
|
||
|
|
wantIdx: 1,
|
||
|
|
wantHas: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "404 Demons v10",
|
||
|
|
base: "404 Demons v10 (Digital-Compilation) (Oak)",
|
||
|
|
seriesName: "404 Demons",
|
||
|
|
wantVol: "v10",
|
||
|
|
wantIdx: 10,
|
||
|
|
wantHas: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "One-Punch Man ch178",
|
||
|
|
base: "One-Punch Man 178 (2023) (Digital) (LuCaZ)",
|
||
|
|
seriesName: "One-Punch Man",
|
||
|
|
wantVol: "",
|
||
|
|
wantIdx: 178,
|
||
|
|
wantHas: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "365 Days to the Wedding v03 — series number not grabbed",
|
||
|
|
base: "365 Days to the Wedding v03 (2024)",
|
||
|
|
seriesName: "365 Days to the Wedding",
|
||
|
|
wantVol: "v03",
|
||
|
|
wantIdx: 3,
|
||
|
|
wantHas: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "404 Demons one-shot — no number after prefix",
|
||
|
|
base: "404 Demons (2025) (Digital)",
|
||
|
|
seriesName: "404 Demons",
|
||
|
|
wantVol: "",
|
||
|
|
wantIdx: 0,
|
||
|
|
wantHas: false,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "fallback — base does not start with series name",
|
||
|
|
base: "Random 12",
|
||
|
|
seriesName: "Different Series",
|
||
|
|
wantVol: "",
|
||
|
|
wantIdx: 12,
|
||
|
|
wantHas: true,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
t.Run(tc.name, func(t *testing.T) {
|
||
|
|
vol, idx, has := mangaIndexForFile(tc.base, tc.seriesName)
|
||
|
|
if vol != tc.wantVol || idx != tc.wantIdx || has != tc.wantHas {
|
||
|
|
t.Fatalf("mangaIndexForFile(%q, %q) = (%q, %v, %v), want (%q, %v, %v)",
|
||
|
|
tc.base, tc.seriesName, vol, idx, has, tc.wantVol, tc.wantIdx, tc.wantHas)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// TestParseMangaIndexCorpus is a regression test against real-world manga
|
||
|
|
// release filenames following common scanlation naming conventions.
|
||
|
|
// Extensions are already stripped (as parseMangaIndex expects).
|
||
|
|
// At least 95% of names must yield has==true; pure one-shots with no number
|
||
|
|
// are the only legitimate misses.
|
||
|
|
func TestParseMangaIndexCorpus(t *testing.T) {
|
||
|
|
corpus := []string{
|
||
|
|
// bare chapter numbers (most common pattern)
|
||
|
|
"One-Punch Man 178 (2023) (Digital) (LuCaZ)",
|
||
|
|
"One-Punch Man 001 (2012) (Digital) (LuCaZ)",
|
||
|
|
"Attack on Titan 139 (2021) (Digital) (Chromatic)",
|
||
|
|
"Chainsaw Man 097 (2021) (Digital) (LuCaZ)",
|
||
|
|
"Chainsaw Man 001 (2019) (Digital) (LuCaZ)",
|
||
|
|
"Spy x Family 090 (2024) (Digital) (Izar)",
|
||
|
|
"Demon Slayer - Kimetsu no Yaiba 205 (2020) (Digital) (LuCaZ)",
|
||
|
|
"My Hero Academia 430 (2024) (Digital) (LuCaZ)",
|
||
|
|
"Jujutsu Kaisen 271 (2024) (Digital) (LuCaZ)",
|
||
|
|
"Vinland Saga 215 (2024) (Digital) (dAY)",
|
||
|
|
// decimal chapter numbers
|
||
|
|
"Kindergarten WARS 109.1 (2025) (Digital) (Rillant)",
|
||
|
|
"Bleach 686.5 (2016) (Digital) (LuCaZ)",
|
||
|
|
"One Piece 1000.1 (2021) (Digital) (LuCaZ)",
|
||
|
|
"Berserk 364.1 (2022) (Digital) (Oak)",
|
||
|
|
// volume prefix (vNN form)
|
||
|
|
"Bakuman v13 (2012) (Digital) (aKraa)",
|
||
|
|
"Fullmetal Alchemist v27 (2011) (Digital) (Izar)",
|
||
|
|
"Death Note v12 (2006) (Digital) (Chromatic)",
|
||
|
|
"Vinland Saga v26 (2022) (Digital) (dAY)",
|
||
|
|
"The Ancient Magus' Bride - Wizard's Blue v04 (2022) (Digital)",
|
||
|
|
"Blue Period v14 (2023) (Digital) (LuCaZ)",
|
||
|
|
// volume prefix (vol. form)
|
||
|
|
"Dragon Ball Vol.001 (2003) (Digital) (Izar)",
|
||
|
|
"Naruto Vol.072 (2014) (Digital) (Chromatic)",
|
||
|
|
"Bleach Vol.074 (2016) (Digital) (LuCaZ)",
|
||
|
|
// chapter c-prefix
|
||
|
|
"Dead Mount Death Play c128 (2025) (Digital) (UP!) (Oak)",
|
||
|
|
"To Your Eternity c185 (2024) (Digital) (LuCaZ)",
|
||
|
|
"Kaiju No. 8 ch.100 (2024) (Digital) (Izar)",
|
||
|
|
// zero-padded chapter numbers
|
||
|
|
"Berserk 001 (1990) (Digital) (Scans)",
|
||
|
|
"Berserk 364 (2021) (Digital) (Oak)",
|
||
|
|
"Vagabond 327 (2015) (Digital) (LuCaZ)",
|
||
|
|
// series with hyphens and special chars in name
|
||
|
|
"One-Punch Man 001 (2012) (Digital) (LuCaZ)",
|
||
|
|
"Fullmetal Alchemist - Brotherhood 064 (2010) (Digital) (Izar)",
|
||
|
|
"JoJo's Bizarre Adventure - Part 8 - JoJolion 110 (2021) (Digital) (Chromatic)",
|
||
|
|
// high chapter numbers
|
||
|
|
"One Piece 1100 (2023) (Digital) (LuCaZ)",
|
||
|
|
"Fairy Tail 545 (2017) (Digital) (Chromatic)",
|
||
|
|
// two-digit volumes
|
||
|
|
"Berserk v41 (2022) (Digital) (Oak)",
|
||
|
|
"Vagabond v37 (2009) (Digital) (LuCaZ)",
|
||
|
|
}
|
||
|
|
|
||
|
|
misses := 0
|
||
|
|
for _, name := range corpus {
|
||
|
|
if _, _, has := parseMangaIndex(name); !has {
|
||
|
|
misses++
|
||
|
|
t.Logf("no index parsed: %q", name)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Allow a small fraction of legitimate one-shots with no number.
|
||
|
|
if float64(misses)/float64(len(corpus)) > 0.05 {
|
||
|
|
t.Fatalf("parser missed %d/%d (>5%%); investigate patterns above", misses, len(corpus))
|
||
|
|
}
|
||
|
|
}
|