优化
This commit is contained in:
truewhile
2026-08-20 22:09:04 +08:00
parent d4c753b08a
commit cc7c898277
9 changed files with 171 additions and 38 deletions
+10 -6
View File
@@ -31,7 +31,10 @@ var (
var adultExcludedPrefixes = map[string]struct{}{
"AC": {}, "AAC": {}, "AVC": {}, "BD": {}, "CD": {}, "DDP": {}, "DTS": {},
"FHD": {}, "HD": {}, "HEVC": {}, "HDR": {}, "MP": {}, "SD": {}, "UHD": {},
"WEB": {}, "X264": {}, "X265": {},
"WEB": {}, "X264": {}, "X265": {}, "H264": {}, "H265": {}, "AV1": {},
"SEASON": {}, "EPISODE": {}, "EP": {}, "SP": {}, "OVA": {}, "OAD": {},
"TMDB": {}, "TMDBID": {}, "DOUBAN": {}, "BANGUMI": {}, "BGM": {}, "TVDB": {}, "THETVDB": {},
"PART": {}, "VOL": {}, "DISC": {}, "DISK": {}, "TRACK": {}, "CHAPTER": {}, "SAMPLE": {}, "TRAILER": {},
}
var defaultAdultBases = []string{
@@ -87,11 +90,12 @@ func (p *AdultProvider) Search(ctx context.Context, code string) (*Match, error)
}
continue
}
if match != nil {
match.OriginalName = code
match.NSFW = true
return match, nil
}
if match != nil {
match.OriginalName = code
match.Title = FormatAdultTitle(code, match.Title)
match.NSFW = true
return match, nil
}
}
return nil, lastErr
}
+54
View File
@@ -39,3 +39,57 @@ func normalizeAdultCode(input string) string {
}
return ""
}
// CleanAdultTitle 去除标题中冗余的前缀番号、方括号、分隔符,提取纯标题内容
func CleanAdultTitle(code, rawTitle string) string {
rawTitle = strings.TrimSpace(rawTitle)
code = normalizeAdultCode(code)
if rawTitle == "" {
return ""
}
clean := rawTitle
for {
trimmed := strings.TrimSpace(clean)
if trimmed == "" {
return ""
}
if code != "" {
upper := strings.ToUpper(trimmed)
upperCode := strings.ToUpper(code)
if strings.HasPrefix(upper, upperCode) {
clean = strings.TrimSpace(trimmed[len(upperCode):])
clean = strings.TrimLeft(clean, "-_ ::]】")
continue
}
}
if strings.HasPrefix(trimmed, "[") || strings.HasPrefix(trimmed, "【") {
idx := strings.IndexAny(trimmed, "]】")
if idx > 0 {
bracketContent := normalizeAdultCode(trimmed[1:idx])
if bracketContent != "" && (code == "" || bracketContent == code) {
clean = strings.TrimSpace(trimmed[idx+len("]"):])
clean = strings.TrimLeft(clean, "-_ ::")
continue
}
}
}
break
}
return strings.TrimSpace(clean)
}
// FormatAdultTitle 格式化番号标题:把番号放在最前面,如 "IPZZ-090-具体标题" 或 "IPZZ-090"
func FormatAdultTitle(code, rawTitle string) string {
code = normalizeAdultCode(code)
if code == "" {
code = normalizeAdultCode(rawTitle)
}
clean := CleanAdultTitle(code, rawTitle)
if code == "" {
return clean
}
if clean == "" || strings.EqualFold(clean, code) {
return code
}
return code + "-" + clean
}
+9 -3
View File
@@ -9,14 +9,17 @@ import (
)
func parseAdultDetailHTML(body, code, source, detailURL string) *Match {
code = normalizeAdultCode(code)
match := &Match{
OriginalName: code,
MediaType: "adult",
NSFW: true,
Genres: []string{"Adult", source},
}
if title := firstAdultTitle(body, code); title != "" {
match.Title = title
rawTitle := firstAdultTitle(body, code)
match.Title = FormatAdultTitle(code, rawTitle)
if match.Title == "" {
match.Title = code
}
if match.Title == "" {
return nil
@@ -48,7 +51,10 @@ func firstAdultTitle(body, code string) string {
if title == "" {
continue
}
title = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(title, code), strings.ToUpper(code)))
clean := CleanAdultTitle(code, title)
if clean != "" {
return clean
}
if title != "" {
return title
}
+10 -10
View File
@@ -36,13 +36,13 @@ func TestParseAdultDetailHTML(t *testing.T) {
<div>日期 2024-05-01</div>
</html>`
got := parseAdultDetailHTML(html, "SSIS-001", "javdb", "https://javdb.com/v/abc")
if got == nil {
t.Fatal("parseAdultDetailHTML returned nil")
}
if got.Title != "测试标题" || got.OriginalName != "SSIS-001" || !got.NSFW {
t.Fatalf("unexpected metadata: %+v", got)
}
got := parseAdultDetailHTML(html, "SSIS-001", "javdb", "https://javdb.com/v/abc")
if got == nil {
t.Fatal("parseAdultDetailHTML returned nil")
}
if got.Title != "SSIS-001-测试标题" || got.OriginalName != "SSIS-001" || !got.NSFW {
t.Fatalf("unexpected metadata: %+v", got)
}
if got.PosterURL != "https://javdb.com/covers/ssis001.jpg" || got.BackdropURL != "https://javdb.com/samples/1.jpg" {
t.Fatalf("unexpected artwork: %+v", got)
}
@@ -98,9 +98,9 @@ func TestAdultProviderUsesConfiguredMultipleSources(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if match == nil || match.Title != "多源命中标题" || match.OriginalName != "SSIS-001" || !match.NSFW {
t.Fatalf("multi-source adult match = %+v", match)
}
if match == nil || match.Title != "SSIS-001-多源命中标题" || match.OriginalName != "SSIS-001" || !match.NSFW {
t.Fatalf("multi-source adult match = %+v", match)
}
}
func TestAdultSourceKindRecognizesJavBusMirrors(t *testing.T) {
+13 -2
View File
@@ -152,9 +152,20 @@ func WriteMediaNFO(m *model.Media) (string, error) {
Language: splitNFOList(m.Languages),
}
} else {
title := m.Title
original := m.OriginalName
adultCode := normalizeAdultCode(firstText(m.OriginalName, AdultCodeFromMediaPath(m.Path), m.Title))
if adultCode != "" || m.NSFW {
if adultCode != "" {
title = FormatAdultTitle(adultCode, m.Title)
if original == "" || strings.EqualFold(original, m.Title) {
original = adultCode
}
}
}
doc = movieNFO{
Title: m.Title,
Original: m.OriginalName,
Title: title,
Original: original,
Year: m.Year,
Plot: m.Overview,
Rating: m.Rating,
+32
View File
@@ -68,3 +68,35 @@ func TestWriteMediaNFOUsesEpisodeTitleForEpisodeDetails(t *testing.T) {
t.Fatalf("episode nfo did not keep episode/show titles separate:\n%s", text)
}
}
func TestWriteMediaNFOAdultTitleWithCode(t *testing.T) {
root := t.TempDir()
mediaPath := filepath.Join(root, "成人", "IPZZ-090-测试成人影片", "IPZZ-090.mp4")
if err := os.MkdirAll(filepath.Dir(mediaPath), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(mediaPath, []byte("media"), 0o644); err != nil {
t.Fatal(err)
}
got, err := WriteMediaNFO(&model.Media{
Title: "测试成人影片",
OriginalName: "IPZZ-090",
Path: mediaPath,
NSFW: true,
})
if err != nil {
t.Fatal(err)
}
body, err := os.ReadFile(got)
if err != nil {
t.Fatal(err)
}
text := string(body)
if !strings.Contains(text, "<title>IPZZ-090-测试成人影片</title>") {
t.Fatalf("expected title with code at front in adult nfo, got:\n%s", text)
}
if !strings.Contains(text, "<originaltitle>IPZZ-090</originaltitle>") {
t.Fatalf("expected originaltitle with code, got:\n%s", text)
}
}
@@ -63,13 +63,13 @@ func TestOrganizeDirectoryUsesAdultMetadataBeforeRename(t *testing.T) {
if res.Organized != 1 || len(res.Items) != 1 {
t.Fatalf("result = %+v, want one organized preview", res)
}
if res.Items[0].Title != "整理成人标题" || res.Items[0].MediaType != "adult" {
t.Fatalf("adult organize item = %+v", res.Items[0])
}
wantSuffix := filepath.Join("成人", "整理成人标题 (2024)", "整理成人标题 (2024).mkv")
if !strings.Contains(res.Items[0].Target, wantSuffix) {
t.Fatalf("adult target = %q, want suffix %q", res.Items[0].Target, wantSuffix)
}
if res.Items[0].Title != "SSIS-001-整理成人标题" || res.Items[0].MediaType != "adult" {
t.Fatalf("adult organize item = %+v", res.Items[0])
}
wantSuffix := filepath.Join("成人", "SSIS-001-整理成人标题", "SSIS-001-整理成人标题.mkv")
if !strings.Contains(res.Items[0].Target, wantSuffix) {
t.Fatalf("adult target = %q, want suffix %q", res.Items[0].Target, wantSuffix)
}
}
func TestOrganizeDirectoryUsesBangumiForAnimeRename(t *testing.T) {
+25 -4
View File
@@ -31,9 +31,22 @@ func (o *OrganizerService) buildOrganizeTargetPath(ctx context.Context, in organ
if root == "" || root == "." {
return organizeTargetPath{}, fmt.Errorf("organize target root required")
}
isAdult := normalizeOrganizeMediaType(in.MediaType) == "adult" || normalizeOrganizeCategoryKey(in.Category) == normalizeOrganizeCategoryKey("成人")
var adultCode string
if isAdult {
adultCode = normalizeAdultCode(firstText(AdultCodeFromMediaPath(in.Source), normalizeAdultCode(in.Title)))
}
title := sanitizeFilename(strings.TrimSpace(in.Title))
if isAdult && adultCode != "" {
title = sanitizeFilename(FormatAdultTitle(adultCode, title))
}
if title == "" {
title = "Unknown"
if adultCode != "" {
title = adultCode
} else {
title = "Unknown"
}
}
ext := strings.TrimSpace(in.Ext)
if ext != "" && !strings.HasPrefix(ext, ".") {
@@ -56,10 +69,11 @@ func (o *OrganizerService) buildOrganizeTargetPath(ctx context.Context, in organ
template := strings.TrimSpace(o.organizeNamingFormat(ctx, in.MediaType, in.Series))
var rel string
if template == "" {
rel = defaultOrganizeRelativePath(title, ext, in.Year, in.Season, in.Episode, in.Series)
rel = defaultOrganizeRelativePath(title, ext, in.Year, in.Season, in.Episode, in.Series, isAdult)
} else {
rel = renderOrganizeNamingTemplate(template, organizeNamingData{
Title: title,
Code: adultCode,
Year: in.Year,
Season: in.Season,
Episode: in.Episode,
@@ -73,7 +87,7 @@ func (o *OrganizerService) buildOrganizeTargetPath(ctx context.Context, in organ
})
rel = cleanOrganizeRelativePath(rel)
if rel == "" {
rel = defaultOrganizeRelativePath(title, ext, in.Year, in.Season, in.Episode, in.Series)
rel = defaultOrganizeRelativePath(title, ext, in.Year, in.Season, in.Episode, in.Series, isAdult)
}
if ext != "" && !strings.EqualFold(filepath.Ext(rel), ext) {
rel += ext
@@ -98,6 +112,10 @@ func (o *OrganizerService) organizeNamingFormat(ctx context.Context, mediaType s
} else {
key = "organize.tv_format"
}
} else if normalizeOrganizeMediaType(mediaType) == "adult" {
if value, err := o.repo.Setting.Get(ctx, "organize.adult_format"); err == nil && strings.TrimSpace(value) != "" {
return strings.TrimSpace(value)
}
}
value, err := o.repo.Setting.Get(ctx, key)
if err != nil {
@@ -106,7 +124,7 @@ func (o *OrganizerService) organizeNamingFormat(ctx context.Context, mediaType s
return strings.TrimSpace(value)
}
func defaultOrganizeRelativePath(title, ext string, year, season, episode int, series bool) string {
func defaultOrganizeRelativePath(title, ext string, year, season, episode int, series bool, isAdult ...bool) string {
if series {
if season < 0 {
season = 1
@@ -117,6 +135,9 @@ func defaultOrganizeRelativePath(title, ext string, year, season, episode int, s
episodeTag := fmt.Sprintf("S%02dE%02d", season, episode)
return filepath.Join(title, fmt.Sprintf("Season %02d", season), fmt.Sprintf("%s - %s%s", title, episodeTag, ext))
}
if len(isAdult) > 0 && isAdult[0] {
return filepath.Join(title, title+ext)
}
folder := title
if year > 0 {
folder = fmt.Sprintf("%s (%d)", title, year)
+11 -6
View File
@@ -10,6 +10,7 @@ import (
type organizeNamingData struct {
Title string
Code string
Year int
Season int
Episode int
@@ -73,9 +74,11 @@ func renderOrganizeConditionals(template string, data organizeNamingData) string
func organizeTemplateTruthy(name string, data organizeNamingData) bool {
switch strings.ToLower(strings.TrimSpace(name)) {
case "title", "name":
return data.Title != ""
case "year":
case "title", "name":
return data.Title != ""
case "code", "num", "adult_code":
return data.Code != ""
case "year":
return data.Year > 0
case "season":
return data.Season >= 0 && data.Episode > 0
@@ -102,9 +105,11 @@ func organizeTemplateTruthy(name string, data organizeNamingData) bool {
func organizeTemplateValue(name, format string, data organizeNamingData, fallback string) string {
switch strings.ToLower(strings.TrimSpace(name)) {
case "title", "name":
return data.Title
case "year":
case "title", "name":
return data.Title
case "code", "num", "adult_code":
return data.Code
case "year":
if data.Year <= 0 {
return ""
}