mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-07 22:06:38 +08:00
refactor: split modules and harden scraping workflows
This commit is contained in:
@@ -3,11 +3,6 @@ package service
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
_ "image/png"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -18,6 +13,7 @@ import (
|
||||
type LocalMetadata struct {
|
||||
Title string
|
||||
OriginalName string
|
||||
EpisodeTitle string
|
||||
AdultCode string
|
||||
Year int
|
||||
Overview string
|
||||
@@ -311,6 +307,9 @@ func metadataFromDoc(doc *nfoDocument, baseDir string, seriesLike bool) *LocalMe
|
||||
Languages: joinNFOValues(doc.Languages),
|
||||
HasNFO: true,
|
||||
}
|
||||
if nfoIsEpisodeDetails(doc) {
|
||||
meta.EpisodeTitle = cleanXMLText(doc.Title)
|
||||
}
|
||||
if meta.AdultCode == "" {
|
||||
meta.AdultCode = normalizeAdultCode(firstText(doc.OriginalTitle, doc.SortTitle, doc.Title))
|
||||
}
|
||||
@@ -429,6 +428,9 @@ func mergeEpisodeMetadata(dst, episode *LocalMetadata, doc *nfoDocument) {
|
||||
}
|
||||
}
|
||||
// 注意: 不要把单集名 / 单集 originaltitle 写进 OriginalName(整剧原名,分组键)。
|
||||
if episodeTitle := firstText(episode.EpisodeTitle, doc.Title); episodeTitle != "" && !strings.EqualFold(episodeTitle, showTitle) {
|
||||
dst.EpisodeTitle = episodeTitle
|
||||
}
|
||||
|
||||
// 单集级展示字段: 每个媒体行本就对应一集,这些可安全按集回填。
|
||||
if episode.Year > 0 {
|
||||
@@ -465,6 +467,13 @@ func mergeEpisodeMetadata(dst, episode *LocalMetadata, doc *nfoDocument) {
|
||||
}
|
||||
}
|
||||
|
||||
func nfoIsEpisodeDetails(doc *nfoDocument) bool {
|
||||
if doc == nil {
|
||||
return false
|
||||
}
|
||||
return strings.EqualFold(strings.TrimSpace(doc.XMLName.Local), "episodedetails")
|
||||
}
|
||||
|
||||
func tmdbIDFromUniqueIDs(ids []nfoUniqueID) int {
|
||||
value := externalIDFromUniqueIDs(ids, "tmdb")
|
||||
if value == "" {
|
||||
@@ -508,243 +517,6 @@ func firstRemoteURL(baseDir string, values ...string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func localPosterCandidates(mediaPath string) []string {
|
||||
base := strings.TrimSuffix(filepath.Base(mediaPath), filepath.Ext(mediaPath))
|
||||
names := []string{
|
||||
base + "-poster",
|
||||
base + ".poster",
|
||||
"poster",
|
||||
"folder",
|
||||
"cover",
|
||||
"movie",
|
||||
"show",
|
||||
base + "-cover",
|
||||
base + ".cover",
|
||||
base,
|
||||
base + "-thumb",
|
||||
base + ".thumb",
|
||||
"thumb",
|
||||
}
|
||||
return append(adultArtworkNameCandidates(mediaPath, "poster"), names...)
|
||||
}
|
||||
|
||||
func localBackdropCandidates(mediaPath string) []string {
|
||||
base := strings.TrimSuffix(filepath.Base(mediaPath), filepath.Ext(mediaPath))
|
||||
names := []string{
|
||||
base + "-fanart",
|
||||
base + ".fanart",
|
||||
base + "-backdrop",
|
||||
base + ".backdrop",
|
||||
base + "-background",
|
||||
"fanart",
|
||||
"backdrop",
|
||||
"background",
|
||||
"landscape",
|
||||
"banner",
|
||||
"clearart",
|
||||
}
|
||||
return append(adultArtworkNameCandidates(mediaPath, "backdrop"), names...)
|
||||
}
|
||||
|
||||
func adultArtworkNameCandidates(mediaPath, kind string) []string {
|
||||
code := AdultCodeFromMediaPath(mediaPath)
|
||||
if code == "" {
|
||||
return nil
|
||||
}
|
||||
compact := strings.ReplaceAll(code, "-", "")
|
||||
bases := []string{code, compact}
|
||||
bases = append(bases, adultDMMNameCandidates(code)...)
|
||||
out := make([]string, 0, len(bases)*6)
|
||||
for _, base := range bases {
|
||||
if base == "" {
|
||||
continue
|
||||
}
|
||||
if kind == "poster" {
|
||||
out = append(out, base, base+"-poster", base+".poster", base+"-cover", base+".cover", base+"-thumb", base+".thumb", base+"pl", base+"-pl")
|
||||
} else {
|
||||
out = append(out, base+"-fanart", base+".fanart", base+"-backdrop", base+".backdrop", base+"-background", base+"-landscape", base+"jp", base+"jp-1")
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func adultDMMNameCandidates(code string) []string {
|
||||
parts := adultStandardPattern.FindStringSubmatch(code)
|
||||
if len(parts) < 3 {
|
||||
return nil
|
||||
}
|
||||
prefix := strings.ToLower(parts[1])
|
||||
num := strings.TrimLeft(parts[2], "0")
|
||||
if num == "" {
|
||||
num = "0"
|
||||
}
|
||||
padded := num
|
||||
for len(padded) < 5 {
|
||||
padded = "0" + padded
|
||||
}
|
||||
return []string{prefix + padded}
|
||||
}
|
||||
|
||||
func firstExistingImage(dir string, names ...string) string {
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
for _, name := range names {
|
||||
for _, ext := range []string{".jpg", ".jpeg", ".png", ".webp"} {
|
||||
path := filepath.Join(dir, name+ext)
|
||||
if fileExists(path) {
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func nfoPosterValues(doc *nfoDocument) []string {
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
values := []string{doc.Poster, doc.Art.Poster}
|
||||
for _, thumb := range doc.Thumbs {
|
||||
aspect := strings.ToLower(strings.TrimSpace(thumb.Aspect))
|
||||
if aspect == "" || aspect == "poster" || aspect == "cover" || aspect == "default" {
|
||||
values = append(values, thumb.Value)
|
||||
}
|
||||
}
|
||||
values = append(values, doc.Art.Thumb)
|
||||
return values
|
||||
}
|
||||
|
||||
func nfoBackdropValues(doc *nfoDocument) []string {
|
||||
if doc == nil {
|
||||
return nil
|
||||
}
|
||||
values := []string{doc.Fanart.Value, doc.Art.Fanart, doc.Art.Backdrop, doc.Art.Background, doc.Art.Landscape, doc.Art.Banner}
|
||||
for _, thumb := range doc.Thumbs {
|
||||
aspect := strings.ToLower(strings.TrimSpace(thumb.Aspect))
|
||||
if aspect == "fanart" || aspect == "backdrop" || aspect == "background" || aspect == "landscape" {
|
||||
values = append(values, thumb.Value)
|
||||
}
|
||||
}
|
||||
values = append(values, doc.Fanart.Thumbs...)
|
||||
return values
|
||||
}
|
||||
|
||||
func firstLocalPoster(mediaPath, showBaseDir string) string {
|
||||
mediaDir := filepath.Dir(mediaPath)
|
||||
dirs := []string{}
|
||||
if showBaseDir != "" && !samePath(showBaseDir, mediaDir) {
|
||||
dirs = append(dirs, showBaseDir)
|
||||
}
|
||||
dirs = append(dirs, mediaDir)
|
||||
for _, dir := range dirs {
|
||||
if localPoster := firstExistingPosterImage(dir, localPosterCandidates(mediaPath)...); localPoster != "" {
|
||||
return localPoster
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstExistingPosterImage(dir string, names ...string) string {
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
for _, name := range names {
|
||||
if isRejectedPosterName(name) {
|
||||
continue
|
||||
}
|
||||
for _, ext := range []string{".jpg", ".jpeg", ".png", ".webp"} {
|
||||
path := filepath.Join(dir, name+ext)
|
||||
if fileExists(path) && likelyPosterImage(path) {
|
||||
return filepath.Clean(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstAdultLooseImage(dir, kind string) string {
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
matches, _ := filepath.Glob(filepath.Join(dir, "*"))
|
||||
preferred := []string{}
|
||||
fallback := []string{}
|
||||
for _, path := range matches {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
if ext != ".jpg" && ext != ".jpeg" && ext != ".png" && ext != ".webp" {
|
||||
continue
|
||||
}
|
||||
name := strings.ToLower(strings.TrimSuffix(filepath.Base(path), ext))
|
||||
if kind == "poster" {
|
||||
if isRejectedPosterName(name) {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(name, "poster") || strings.Contains(name, "cover") || strings.Contains(name, "folder") || strings.Contains(name, "movie") || strings.HasSuffix(name, "pl") {
|
||||
preferred = append(preferred, path)
|
||||
}
|
||||
} else if strings.Contains(name, "fanart") || strings.Contains(name, "backdrop") || strings.Contains(name, "background") || strings.Contains(name, "landscape") || strings.Contains(name, "jp") {
|
||||
preferred = append(preferred, path)
|
||||
}
|
||||
if kind != "poster" || likelyPosterImage(path) {
|
||||
fallback = append(fallback, path)
|
||||
}
|
||||
}
|
||||
if len(preferred) > 0 {
|
||||
return filepath.Clean(preferred[0])
|
||||
}
|
||||
if kind == "poster" && len(fallback) == 1 {
|
||||
return filepath.Clean(fallback[0])
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isRejectedPosterName(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
rejected := []string{
|
||||
"actor", "actors", "actress", "cast", "avatar", "portrait", "person",
|
||||
"sample", "screenshot", "screen", "still", "scene", "extrafanart", "extrathumb",
|
||||
"fanart", "backdrop", "background", "landscape", "banner", "clearlogo", "clearart", "logo", "disc",
|
||||
}
|
||||
for _, token := range rejected {
|
||||
if strings.Contains(name, token) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func likelyPosterImage(path string) bool {
|
||||
file, err := os.Open(path) // #nosec G304 -- path is a discovered artwork sidecar under the configured library root.
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
cfg, _, err := image.DecodeConfig(file)
|
||||
if err != nil || cfg.Width <= 0 || cfg.Height <= 0 {
|
||||
return true
|
||||
}
|
||||
return cfg.Height >= cfg.Width
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
|
||||
func isHTTPURL(raw string) bool {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (u.Scheme == "http" || u.Scheme == "https") && u.Host != ""
|
||||
}
|
||||
|
||||
func isLocalPath(raw string) bool {
|
||||
raw = strings.TrimSpace(raw)
|
||||
return raw != "" && !isHTTPURL(raw)
|
||||
}
|
||||
|
||||
func firstText(values ...string) string {
|
||||
for _, value := range values {
|
||||
if text := cleanXMLText(value); text != "" {
|
||||
|
||||
Reference in New Issue
Block a user