mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 20:46:37 +08:00
优化
优化
This commit is contained in:
@@ -31,7 +31,10 @@ var (
|
|||||||
var adultExcludedPrefixes = map[string]struct{}{
|
var adultExcludedPrefixes = map[string]struct{}{
|
||||||
"AC": {}, "AAC": {}, "AVC": {}, "BD": {}, "CD": {}, "DDP": {}, "DTS": {},
|
"AC": {}, "AAC": {}, "AVC": {}, "BD": {}, "CD": {}, "DDP": {}, "DTS": {},
|
||||||
"FHD": {}, "HD": {}, "HEVC": {}, "HDR": {}, "MP": {}, "SD": {}, "UHD": {},
|
"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{
|
var defaultAdultBases = []string{
|
||||||
@@ -87,11 +90,12 @@ func (p *AdultProvider) Search(ctx context.Context, code string) (*Match, error)
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if match != nil {
|
if match != nil {
|
||||||
match.OriginalName = code
|
match.OriginalName = code
|
||||||
match.NSFW = true
|
match.Title = FormatAdultTitle(code, match.Title)
|
||||||
return match, nil
|
match.NSFW = true
|
||||||
}
|
return match, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil, lastErr
|
return nil, lastErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,3 +39,57 @@ func normalizeAdultCode(input string) string {
|
|||||||
}
|
}
|
||||||
return ""
|
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,14 +9,17 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func parseAdultDetailHTML(body, code, source, detailURL string) *Match {
|
func parseAdultDetailHTML(body, code, source, detailURL string) *Match {
|
||||||
|
code = normalizeAdultCode(code)
|
||||||
match := &Match{
|
match := &Match{
|
||||||
OriginalName: code,
|
OriginalName: code,
|
||||||
MediaType: "adult",
|
MediaType: "adult",
|
||||||
NSFW: true,
|
NSFW: true,
|
||||||
Genres: []string{"Adult", source},
|
Genres: []string{"Adult", source},
|
||||||
}
|
}
|
||||||
if title := firstAdultTitle(body, code); title != "" {
|
rawTitle := firstAdultTitle(body, code)
|
||||||
match.Title = title
|
match.Title = FormatAdultTitle(code, rawTitle)
|
||||||
|
if match.Title == "" {
|
||||||
|
match.Title = code
|
||||||
}
|
}
|
||||||
if match.Title == "" {
|
if match.Title == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -48,7 +51,10 @@ func firstAdultTitle(body, code string) string {
|
|||||||
if title == "" {
|
if title == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
title = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(title, code), strings.ToUpper(code)))
|
clean := CleanAdultTitle(code, title)
|
||||||
|
if clean != "" {
|
||||||
|
return clean
|
||||||
|
}
|
||||||
if title != "" {
|
if title != "" {
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,13 +36,13 @@ func TestParseAdultDetailHTML(t *testing.T) {
|
|||||||
<div>日期 2024-05-01</div>
|
<div>日期 2024-05-01</div>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
got := parseAdultDetailHTML(html, "SSIS-001", "javdb", "https://javdb.com/v/abc")
|
got := parseAdultDetailHTML(html, "SSIS-001", "javdb", "https://javdb.com/v/abc")
|
||||||
if got == nil {
|
if got == nil {
|
||||||
t.Fatal("parseAdultDetailHTML returned nil")
|
t.Fatal("parseAdultDetailHTML returned nil")
|
||||||
}
|
}
|
||||||
if got.Title != "测试标题" || got.OriginalName != "SSIS-001" || !got.NSFW {
|
if got.Title != "SSIS-001-测试标题" || got.OriginalName != "SSIS-001" || !got.NSFW {
|
||||||
t.Fatalf("unexpected metadata: %+v", got)
|
t.Fatalf("unexpected metadata: %+v", got)
|
||||||
}
|
}
|
||||||
if got.PosterURL != "https://javdb.com/covers/ssis001.jpg" || got.BackdropURL != "https://javdb.com/samples/1.jpg" {
|
if got.PosterURL != "https://javdb.com/covers/ssis001.jpg" || got.BackdropURL != "https://javdb.com/samples/1.jpg" {
|
||||||
t.Fatalf("unexpected artwork: %+v", got)
|
t.Fatalf("unexpected artwork: %+v", got)
|
||||||
}
|
}
|
||||||
@@ -98,9 +98,9 @@ func TestAdultProviderUsesConfiguredMultipleSources(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if match == nil || match.Title != "多源命中标题" || match.OriginalName != "SSIS-001" || !match.NSFW {
|
if match == nil || match.Title != "SSIS-001-多源命中标题" || match.OriginalName != "SSIS-001" || !match.NSFW {
|
||||||
t.Fatalf("multi-source adult match = %+v", match)
|
t.Fatalf("multi-source adult match = %+v", match)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdultSourceKindRecognizesJavBusMirrors(t *testing.T) {
|
func TestAdultSourceKindRecognizesJavBusMirrors(t *testing.T) {
|
||||||
|
|||||||
+13
-2
@@ -152,9 +152,20 @@ func WriteMediaNFO(m *model.Media) (string, error) {
|
|||||||
Language: splitNFOList(m.Languages),
|
Language: splitNFOList(m.Languages),
|
||||||
}
|
}
|
||||||
} else {
|
} 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{
|
doc = movieNFO{
|
||||||
Title: m.Title,
|
Title: title,
|
||||||
Original: m.OriginalName,
|
Original: original,
|
||||||
Year: m.Year,
|
Year: m.Year,
|
||||||
Plot: m.Overview,
|
Plot: m.Overview,
|
||||||
Rating: m.Rating,
|
Rating: m.Rating,
|
||||||
|
|||||||
@@ -68,3 +68,35 @@ func TestWriteMediaNFOUsesEpisodeTitleForEpisodeDetails(t *testing.T) {
|
|||||||
t.Fatalf("episode nfo did not keep episode/show titles separate:\n%s", text)
|
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 {
|
if res.Organized != 1 || len(res.Items) != 1 {
|
||||||
t.Fatalf("result = %+v, want one organized preview", res)
|
t.Fatalf("result = %+v, want one organized preview", res)
|
||||||
}
|
}
|
||||||
if res.Items[0].Title != "整理成人标题" || res.Items[0].MediaType != "adult" {
|
if res.Items[0].Title != "SSIS-001-整理成人标题" || res.Items[0].MediaType != "adult" {
|
||||||
t.Fatalf("adult organize item = %+v", res.Items[0])
|
t.Fatalf("adult organize item = %+v", res.Items[0])
|
||||||
}
|
}
|
||||||
wantSuffix := filepath.Join("成人", "整理成人标题 (2024)", "整理成人标题 (2024).mkv")
|
wantSuffix := filepath.Join("成人", "SSIS-001-整理成人标题", "SSIS-001-整理成人标题.mkv")
|
||||||
if !strings.Contains(res.Items[0].Target, wantSuffix) {
|
if !strings.Contains(res.Items[0].Target, wantSuffix) {
|
||||||
t.Fatalf("adult target = %q, want suffix %q", res.Items[0].Target, wantSuffix)
|
t.Fatalf("adult target = %q, want suffix %q", res.Items[0].Target, wantSuffix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOrganizeDirectoryUsesBangumiForAnimeRename(t *testing.T) {
|
func TestOrganizeDirectoryUsesBangumiForAnimeRename(t *testing.T) {
|
||||||
|
|||||||
@@ -31,9 +31,22 @@ func (o *OrganizerService) buildOrganizeTargetPath(ctx context.Context, in organ
|
|||||||
if root == "" || root == "." {
|
if root == "" || root == "." {
|
||||||
return organizeTargetPath{}, fmt.Errorf("organize target root required")
|
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))
|
title := sanitizeFilename(strings.TrimSpace(in.Title))
|
||||||
|
if isAdult && adultCode != "" {
|
||||||
|
title = sanitizeFilename(FormatAdultTitle(adultCode, title))
|
||||||
|
}
|
||||||
if title == "" {
|
if title == "" {
|
||||||
title = "Unknown"
|
if adultCode != "" {
|
||||||
|
title = adultCode
|
||||||
|
} else {
|
||||||
|
title = "Unknown"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ext := strings.TrimSpace(in.Ext)
|
ext := strings.TrimSpace(in.Ext)
|
||||||
if ext != "" && !strings.HasPrefix(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))
|
template := strings.TrimSpace(o.organizeNamingFormat(ctx, in.MediaType, in.Series))
|
||||||
var rel string
|
var rel string
|
||||||
if template == "" {
|
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 {
|
} else {
|
||||||
rel = renderOrganizeNamingTemplate(template, organizeNamingData{
|
rel = renderOrganizeNamingTemplate(template, organizeNamingData{
|
||||||
Title: title,
|
Title: title,
|
||||||
|
Code: adultCode,
|
||||||
Year: in.Year,
|
Year: in.Year,
|
||||||
Season: in.Season,
|
Season: in.Season,
|
||||||
Episode: in.Episode,
|
Episode: in.Episode,
|
||||||
@@ -73,7 +87,7 @@ func (o *OrganizerService) buildOrganizeTargetPath(ctx context.Context, in organ
|
|||||||
})
|
})
|
||||||
rel = cleanOrganizeRelativePath(rel)
|
rel = cleanOrganizeRelativePath(rel)
|
||||||
if 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) {
|
if ext != "" && !strings.EqualFold(filepath.Ext(rel), ext) {
|
||||||
rel += ext
|
rel += ext
|
||||||
@@ -98,6 +112,10 @@ func (o *OrganizerService) organizeNamingFormat(ctx context.Context, mediaType s
|
|||||||
} else {
|
} else {
|
||||||
key = "organize.tv_format"
|
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)
|
value, err := o.repo.Setting.Get(ctx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -106,7 +124,7 @@ func (o *OrganizerService) organizeNamingFormat(ctx context.Context, mediaType s
|
|||||||
return strings.TrimSpace(value)
|
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 series {
|
||||||
if season < 0 {
|
if season < 0 {
|
||||||
season = 1
|
season = 1
|
||||||
@@ -117,6 +135,9 @@ func defaultOrganizeRelativePath(title, ext string, year, season, episode int, s
|
|||||||
episodeTag := fmt.Sprintf("S%02dE%02d", season, episode)
|
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))
|
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
|
folder := title
|
||||||
if year > 0 {
|
if year > 0 {
|
||||||
folder = fmt.Sprintf("%s (%d)", title, year)
|
folder = fmt.Sprintf("%s (%d)", title, year)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
type organizeNamingData struct {
|
type organizeNamingData struct {
|
||||||
Title string
|
Title string
|
||||||
|
Code string
|
||||||
Year int
|
Year int
|
||||||
Season int
|
Season int
|
||||||
Episode int
|
Episode int
|
||||||
@@ -73,9 +74,11 @@ func renderOrganizeConditionals(template string, data organizeNamingData) string
|
|||||||
|
|
||||||
func organizeTemplateTruthy(name string, data organizeNamingData) bool {
|
func organizeTemplateTruthy(name string, data organizeNamingData) bool {
|
||||||
switch strings.ToLower(strings.TrimSpace(name)) {
|
switch strings.ToLower(strings.TrimSpace(name)) {
|
||||||
case "title", "name":
|
case "title", "name":
|
||||||
return data.Title != ""
|
return data.Title != ""
|
||||||
case "year":
|
case "code", "num", "adult_code":
|
||||||
|
return data.Code != ""
|
||||||
|
case "year":
|
||||||
return data.Year > 0
|
return data.Year > 0
|
||||||
case "season":
|
case "season":
|
||||||
return data.Season >= 0 && data.Episode > 0
|
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 {
|
func organizeTemplateValue(name, format string, data organizeNamingData, fallback string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(name)) {
|
switch strings.ToLower(strings.TrimSpace(name)) {
|
||||||
case "title", "name":
|
case "title", "name":
|
||||||
return data.Title
|
return data.Title
|
||||||
case "year":
|
case "code", "num", "adult_code":
|
||||||
|
return data.Code
|
||||||
|
case "year":
|
||||||
if data.Year <= 0 {
|
if data.Year <= 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user