mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
b0fe40142a
* Rebrand MMTL to MeBox across codebase and assets Rename the project display name, Go module path, environment variable prefix (MEBOX_*), Docker image references, and UI branding from MMTL/mmtl to MeBox/mebox. Replace logo assets with the new MeBox icon and keep legacy SQLite migration support for existing mmtl.db deployments. Co-authored-by: truewhile <truewhile@users.noreply.github.com> * Fix logo icons: use cube-only crop without truncated text Previous icon generation cropped too much of the source image, including partial MeBox wordmark text that was cut off in square icon containers. Regenerate logo-64/192/512, favicon, and SVG from cube-only region. Co-authored-by: truewhile <truewhile@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: truewhile <truewhile@users.noreply.github.com>
116 lines
3.5 KiB
Go
116 lines
3.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
)
|
|
|
|
// isSmartClassifyEnabled checks database settings first, then config.yaml.
|
|
func (o *OrganizerService) isSmartClassifyEnabled(ctx context.Context) bool {
|
|
if o.repo != nil && o.repo.Setting != nil {
|
|
val, err := o.repo.Setting.Get(ctx, "organizer.smart_classify")
|
|
if err == nil && val != "" {
|
|
return val == "true" || val == "1" || val == "on"
|
|
}
|
|
}
|
|
if o == nil || o.cfg == nil {
|
|
return false
|
|
}
|
|
return o.cfg.Organizer.SmartClassify
|
|
}
|
|
|
|
// SmartClassify determines the subcategory folder based on media metadata.
|
|
// It returns values such as "华语电影", "欧美剧", or "日番".
|
|
func (o *OrganizerService) SmartClassify(ctx context.Context, m *model.Media) string {
|
|
if !o.isSmartClassifyEnabled(ctx) {
|
|
return ""
|
|
}
|
|
lib, err := o.repo.Library.FindByID(ctx, m.LibraryID)
|
|
if err != nil || lib == nil {
|
|
return ""
|
|
}
|
|
return o.classifyMedia(ctx, m, lib.Type)
|
|
}
|
|
|
|
func (o *OrganizerService) smartClassifySourceFile(ctx context.Context, src, sourceRoot, mediaType, title, parsedTitle string, metadataMatch *Match) string {
|
|
if o == nil || !o.isSmartClassifyEnabled(ctx) {
|
|
return ""
|
|
}
|
|
seriesLike := isSeriesLibraryType(mediaType)
|
|
input := mediaClassifyInput{
|
|
MediaType: mediaType,
|
|
Title: strings.Join([]string{title, parsedTitle, filepath.Base(src)}, " "),
|
|
Category: strings.Join(organizeDirectoryCategoryCandidates(src, sourceRoot), " "),
|
|
}
|
|
if metadataMatch != nil {
|
|
input.Title = strings.Join([]string{
|
|
metadataMatch.OriginalName,
|
|
title,
|
|
parsedTitle,
|
|
filepath.Base(src),
|
|
}, " ")
|
|
input.Languages = metadataMatch.Languages
|
|
input.Countries = metadataMatch.Countries
|
|
input.Genres = metadataMatch.Genres
|
|
if metadataMatch.NSFW {
|
|
input.MediaType = "adult"
|
|
}
|
|
}
|
|
if meta, err := ReadLocalMetadata(src, sourceRoot, seriesLike); err == nil && meta != nil && meta.HasNFO {
|
|
mergeLocalClassificationMetadata(&input, meta, title, parsedTitle, filepath.Base(src))
|
|
}
|
|
return sanitizeFilename(classifyMediaCategory(input, o.categoryMap()))
|
|
}
|
|
|
|
// mergeLocalClassificationMetadata lets a local NFO refine scraper metadata
|
|
// without erasing fields that the NFO omitted. Episode NFO files commonly
|
|
// contain only a title/episode number; replacing countries/languages/genres
|
|
// with those empty values caused the organizer to lose a correct scraper
|
|
// region and fall back to the wrong directory category.
|
|
func mergeLocalClassificationMetadata(input *mediaClassifyInput, meta *LocalMetadata, titleParts ...string) {
|
|
if input == nil || meta == nil {
|
|
return
|
|
}
|
|
input.Title = strings.Join(append([]string{meta.Title, meta.OriginalName}, titleParts...), " ")
|
|
if values := parseCommaList(meta.Languages); len(values) > 0 {
|
|
input.Languages = values
|
|
}
|
|
if values := parseCommaList(meta.Countries); len(values) > 0 {
|
|
input.Countries = values
|
|
}
|
|
if values := parseCommaList(meta.Genres); len(values) > 0 {
|
|
input.Genres = values
|
|
}
|
|
if meta.NSFW {
|
|
input.MediaType = "adult"
|
|
}
|
|
}
|
|
|
|
// parseCommaList splits a comma-separated string into trimmed non-empty values.
|
|
func parseCommaList(s string) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(s, ",")
|
|
result := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
trimmed := strings.TrimSpace(p)
|
|
if trimmed != "" {
|
|
result = append(result, trimmed)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func contains(slice []string, s string) bool {
|
|
for _, v := range slice {
|
|
if v == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|