diff --git a/internal/handler/metadata_enrich.go b/internal/handler/metadata_enrich.go index 3fabfb0..9d383ec 100644 --- a/internal/handler/metadata_enrich.go +++ b/internal/handler/metadata_enrich.go @@ -2,17 +2,12 @@ package handler import ( "context" - "net/url" - "path" "regexp" "strconv" "strings" "sync" "time" - "go.uber.org/zap" - - "github.com/ShukeBta/MediaStationGo/internal/model" "github.com/ShukeBta/MediaStationGo/internal/service" ) @@ -26,276 +21,6 @@ type cachedDisplayMetadata struct { expiresAt time.Time } -func enrichSubscriptionArtwork(ctx context.Context, svc *service.Container, sub *model.Subscription) { - if svc == nil || sub == nil { - return - } - // 已有图片且媒体展示字段也齐全时才跳过;否则仍需查一次补 年份/评分/类型 等 - // 富通知字段(老订阅只存了 poster/overview 的情况)。 - if strings.TrimSpace(sub.PosterURL) != "" && strings.TrimSpace(sub.BackdropURL) != "" && - sub.Year > 0 && sub.Rating > 0 && strings.TrimSpace(sub.Genres) != "" { - return - } - meta := lookupDisplayMetadata(ctx, svc, sub.Name, sub.Filter, sub.MediaType) - if meta.Title == "" && meta.PosterURL == "" && meta.BackdropURL == "" && meta.Overview == "" { - return - } - if strings.TrimSpace(sub.Source) == "" { - sub.Source = meta.Source - } - if strings.TrimSpace(sub.PosterURL) == "" { - sub.PosterURL = meta.PosterURL - } - if strings.TrimSpace(sub.BackdropURL) == "" { - sub.BackdropURL = meta.BackdropURL - } - if strings.TrimSpace(sub.Overview) == "" { - sub.Overview = meta.Overview - } - if strings.TrimSpace(sub.OriginalName) == "" { - sub.OriginalName = meta.OriginalName - } - if strings.TrimSpace(sub.OriginalLanguage) == "" { - sub.OriginalLanguage = meta.OriginalLanguage - } - if sub.Year <= 0 && meta.Year > 0 { - sub.Year = meta.Year - } - if sub.Rating <= 0 && meta.Rating > 0 { - sub.Rating = meta.Rating - } - if strings.TrimSpace(sub.Genres) == "" { - sub.Genres = meta.Genres - } -} - -func enrichAndPersistSubscriptions(ctx context.Context, svc *service.Container, items []model.Subscription) { - for i := range items { - before := items[i] - enrichSubscriptionArtwork(ctx, svc, &items[i]) - if before.Source == items[i].Source && - before.PosterURL == items[i].PosterURL && - before.BackdropURL == items[i].BackdropURL && - before.Overview == items[i].Overview && - before.OriginalName == items[i].OriginalName && - before.OriginalLanguage == items[i].OriginalLanguage && - before.Year == items[i].Year && - before.Rating == items[i].Rating && - before.Genres == items[i].Genres { - continue - } - updates := map[string]any{} - if before.Source != items[i].Source { - updates["source"] = items[i].Source - } - if before.PosterURL != items[i].PosterURL { - updates["poster_url"] = items[i].PosterURL - } - if before.BackdropURL != items[i].BackdropURL { - updates["backdrop_url"] = items[i].BackdropURL - } - if before.Overview != items[i].Overview { - updates["overview"] = items[i].Overview - } - if before.OriginalName != items[i].OriginalName { - updates["original_name"] = items[i].OriginalName - } - if before.OriginalLanguage != items[i].OriginalLanguage { - updates["original_language"] = items[i].OriginalLanguage - } - if before.Year != items[i].Year { - updates["year"] = items[i].Year - } - if before.Rating != items[i].Rating { - updates["rating"] = items[i].Rating - } - if before.Genres != items[i].Genres { - updates["genres"] = items[i].Genres - } - if len(updates) == 0 { - continue - } - if err := svc.Repo.DB.WithContext(ctx).Model(&model.Subscription{}).Where("id = ?", items[i].ID).Updates(updates).Error; err != nil { - svc.Log.Debug("subscription artwork backfill failed", zap.String("id", items[i].ID), zap.Error(err)) - } - } -} - -func enrichAndPersistDownloadRows(ctx context.Context, svc *service.Container, rows []model.DownloadTask) { - for i := range rows { - before := rows[i] - title := strings.TrimSpace(rows[i].Title) - if title == "" { - title = downloadDisplayTitle(rows[i].URL) - rows[i].Title = title - } - needsMeta := strings.TrimSpace(rows[i].PosterURL) == "" || strings.TrimSpace(rows[i].BackdropURL) == "" || - strings.TrimSpace(rows[i].Overview) == "" || rows[i].Year <= 0 || rows[i].Rating <= 0 || - strings.TrimSpace(rows[i].Genres) == "" || strings.TrimSpace(rows[i].OriginalName) == "" || - strings.TrimSpace(rows[i].OriginalLanguage) == "" - if needsMeta { - meta := enrichDownloadTaskMeta(ctx, svc, service.DownloadTaskMeta{ - Title: rows[i].Title, - PosterURL: rows[i].PosterURL, - BackdropURL: rows[i].BackdropURL, - Overview: rows[i].Overview, - OriginalName: rows[i].OriginalName, - OriginalLanguage: rows[i].OriginalLanguage, - Year: rows[i].Year, - Rating: rows[i].Rating, - Genres: rows[i].Genres, - }, firstNonEmptyString(rows[i].Title, rows[i].URL), rows[i].MediaType) - rows[i].Title = firstNonEmptyString(rows[i].Title, meta.Title) - rows[i].PosterURL = meta.PosterURL - rows[i].BackdropURL = meta.BackdropURL - rows[i].Overview = meta.Overview - rows[i].OriginalName = meta.OriginalName - rows[i].OriginalLanguage = meta.OriginalLanguage - rows[i].Year = meta.Year - rows[i].Rating = meta.Rating - rows[i].Genres = meta.Genres - } - if before.Title == rows[i].Title && - before.PosterURL == rows[i].PosterURL && - before.BackdropURL == rows[i].BackdropURL && - before.Overview == rows[i].Overview && - before.OriginalName == rows[i].OriginalName && - before.OriginalLanguage == rows[i].OriginalLanguage && - before.Year == rows[i].Year && - before.Rating == rows[i].Rating && - before.Genres == rows[i].Genres { - continue - } - updates := map[string]any{} - if before.Title != rows[i].Title { - updates["title"] = rows[i].Title - } - if before.PosterURL != rows[i].PosterURL { - updates["poster_url"] = rows[i].PosterURL - } - if before.BackdropURL != rows[i].BackdropURL { - updates["backdrop_url"] = rows[i].BackdropURL - } - if before.Overview != rows[i].Overview { - updates["overview"] = rows[i].Overview - } - if before.OriginalName != rows[i].OriginalName { - updates["original_name"] = rows[i].OriginalName - } - if before.OriginalLanguage != rows[i].OriginalLanguage { - updates["original_language"] = rows[i].OriginalLanguage - } - if before.Year != rows[i].Year { - updates["year"] = rows[i].Year - } - if before.Rating != rows[i].Rating { - updates["rating"] = rows[i].Rating - } - if before.Genres != rows[i].Genres { - updates["genres"] = rows[i].Genres - } - if len(updates) == 0 { - continue - } - if err := svc.Repo.DB.WithContext(ctx).Model(&model.DownloadTask{}).Where("id = ?", rows[i].ID).Updates(updates).Error; err != nil { - svc.Log.Debug("download artwork backfill failed", zap.String("id", rows[i].ID), zap.Error(err)) - } - } -} - -func enrichDownloadTorrentViews(ctx context.Context, svc *service.Container, views []service.DownloadTorrentView) { - cache := map[string]displayMetadata{} - for i := range views { - if strings.TrimSpace(views[i].PosterURL) != "" && strings.TrimSpace(views[i].BackdropURL) != "" { - continue - } - query := firstNonEmptyString(views[i].Title, views[i].Name) - cacheKey, _ := metadataSearchQuery(query) - meta, ok := cache[cacheKey] - if !ok { - meta = lookupDisplayMetadata(ctx, svc, query, "", "") - cache[cacheKey] = meta - } - if strings.TrimSpace(views[i].PosterURL) == "" { - views[i].PosterURL = meta.PosterURL - } - if strings.TrimSpace(views[i].BackdropURL) == "" { - views[i].BackdropURL = meta.BackdropURL - } - if strings.TrimSpace(views[i].Overview) == "" { - views[i].Overview = meta.Overview - } - if (views[i].Title == "" || views[i].Title == views[i].Name) && meta.Title != "" { - views[i].Title = meta.Title - } - } -} - -func enrichDownloadTaskMeta(ctx context.Context, svc *service.Container, meta service.DownloadTaskMeta, fallbackTitle, mediaType string) service.DownloadTaskMeta { - if strings.TrimSpace(meta.Title) == "" { - meta.Title = strings.TrimSpace(downloadDisplayTitle(fallbackTitle)) - } - // 仅当展示字段都齐时才跳过查询(含媒体富通知所需的年份/评分/类型/原始信息)。 - if strings.TrimSpace(meta.PosterURL) != "" && strings.TrimSpace(meta.BackdropURL) != "" && - strings.TrimSpace(meta.Overview) != "" && meta.Year > 0 && meta.Rating > 0 && - strings.TrimSpace(meta.Genres) != "" && strings.TrimSpace(meta.OriginalName) != "" && - strings.TrimSpace(meta.OriginalLanguage) != "" { - return meta - } - found := lookupDisplayMetadata(ctx, svc, meta.Title, fallbackTitle, mediaType) - if strings.TrimSpace(meta.Title) == "" { - meta.Title = found.Title - } - if strings.TrimSpace(meta.PosterURL) == "" { - meta.PosterURL = found.PosterURL - } - if strings.TrimSpace(meta.BackdropURL) == "" { - meta.BackdropURL = found.BackdropURL - } - if strings.TrimSpace(meta.Overview) == "" { - meta.Overview = found.Overview - } - if strings.TrimSpace(meta.OriginalName) == "" { - meta.OriginalName = found.OriginalName - } - if strings.TrimSpace(meta.OriginalLanguage) == "" { - meta.OriginalLanguage = found.OriginalLanguage - } - if meta.Year <= 0 && found.Year > 0 { - meta.Year = found.Year - } - if meta.Rating <= 0 && found.Rating > 0 { - meta.Rating = found.Rating - } - if strings.TrimSpace(meta.Genres) == "" { - meta.Genres = found.Genres - } - return meta -} - -func downloadDisplayTitle(raw string) string { - raw = strings.TrimSpace(raw) - if raw == "" { - return "" - } - if u, err := url.Parse(raw); err == nil { - if dn := strings.TrimSpace(u.Query().Get("dn")); dn != "" { - if decoded, err := url.QueryUnescape(dn); err == nil && strings.TrimSpace(decoded) != "" { - return strings.TrimSpace(decoded) - } - return dn - } - if u.Host != "" { - base := path.Base(u.Path) - if base != "." && base != "/" && base != "" { - base = strings.TrimSuffix(base, path.Ext(base)) - return strings.TrimSpace(base) - } - } - } - return raw -} - type displayMetadata struct { Source string Title string diff --git a/internal/handler/metadata_enrich_download.go b/internal/handler/metadata_enrich_download.go new file mode 100644 index 0000000..f94a571 --- /dev/null +++ b/internal/handler/metadata_enrich_download.go @@ -0,0 +1,188 @@ +package handler + +import ( + "context" + "net/url" + "path" + "strings" + + "go.uber.org/zap" + + "github.com/ShukeBta/MediaStationGo/internal/model" + "github.com/ShukeBta/MediaStationGo/internal/service" +) + +func enrichAndPersistDownloadRows(ctx context.Context, svc *service.Container, rows []model.DownloadTask) { + for i := range rows { + before := rows[i] + enrichDownloadRow(ctx, svc, &rows[i]) + updates := downloadTaskMetadataUpdates(before, rows[i]) + if len(updates) == 0 { + continue + } + if err := svc.Repo.DB.WithContext(ctx).Model(&model.DownloadTask{}).Where("id = ?", rows[i].ID).Updates(updates).Error; err != nil { + svc.Log.Debug("download artwork backfill failed", zap.String("id", rows[i].ID), zap.Error(err)) + } + } +} + +func enrichDownloadRow(ctx context.Context, svc *service.Container, row *model.DownloadTask) { + if strings.TrimSpace(row.Title) == "" { + row.Title = downloadDisplayTitle(row.URL) + } + if !downloadTaskNeedsMetadata(*row) { + return + } + meta := enrichDownloadTaskMeta(ctx, svc, service.DownloadTaskMeta{ + Title: row.Title, + PosterURL: row.PosterURL, + BackdropURL: row.BackdropURL, + Overview: row.Overview, + OriginalName: row.OriginalName, + OriginalLanguage: row.OriginalLanguage, + Year: row.Year, + Rating: row.Rating, + Genres: row.Genres, + }, firstNonEmptyString(row.Title, row.URL), row.MediaType) + row.Title = firstNonEmptyString(row.Title, meta.Title) + row.PosterURL = meta.PosterURL + row.BackdropURL = meta.BackdropURL + row.Overview = meta.Overview + row.OriginalName = meta.OriginalName + row.OriginalLanguage = meta.OriginalLanguage + row.Year = meta.Year + row.Rating = meta.Rating + row.Genres = meta.Genres +} + +func downloadTaskNeedsMetadata(row model.DownloadTask) bool { + return strings.TrimSpace(row.PosterURL) == "" || strings.TrimSpace(row.BackdropURL) == "" || + strings.TrimSpace(row.Overview) == "" || row.Year <= 0 || row.Rating <= 0 || + strings.TrimSpace(row.Genres) == "" || strings.TrimSpace(row.OriginalName) == "" || + strings.TrimSpace(row.OriginalLanguage) == "" +} + +func downloadTaskMetadataUpdates(before, after model.DownloadTask) map[string]any { + updates := map[string]any{} + if before.Title != after.Title { + updates["title"] = after.Title + } + if before.PosterURL != after.PosterURL { + updates["poster_url"] = after.PosterURL + } + if before.BackdropURL != after.BackdropURL { + updates["backdrop_url"] = after.BackdropURL + } + if before.Overview != after.Overview { + updates["overview"] = after.Overview + } + if before.OriginalName != after.OriginalName { + updates["original_name"] = after.OriginalName + } + if before.OriginalLanguage != after.OriginalLanguage { + updates["original_language"] = after.OriginalLanguage + } + if before.Year != after.Year { + updates["year"] = after.Year + } + if before.Rating != after.Rating { + updates["rating"] = after.Rating + } + if before.Genres != after.Genres { + updates["genres"] = after.Genres + } + return updates +} + +func enrichDownloadTorrentViews(ctx context.Context, svc *service.Container, views []service.DownloadTorrentView) { + cache := map[string]displayMetadata{} + for i := range views { + if strings.TrimSpace(views[i].PosterURL) != "" && strings.TrimSpace(views[i].BackdropURL) != "" { + continue + } + query := firstNonEmptyString(views[i].Title, views[i].Name) + cacheKey, _ := metadataSearchQuery(query) + meta, ok := cache[cacheKey] + if !ok { + meta = lookupDisplayMetadata(ctx, svc, query, "", "") + cache[cacheKey] = meta + } + if strings.TrimSpace(views[i].PosterURL) == "" { + views[i].PosterURL = meta.PosterURL + } + if strings.TrimSpace(views[i].BackdropURL) == "" { + views[i].BackdropURL = meta.BackdropURL + } + if strings.TrimSpace(views[i].Overview) == "" { + views[i].Overview = meta.Overview + } + if (views[i].Title == "" || views[i].Title == views[i].Name) && meta.Title != "" { + views[i].Title = meta.Title + } + } +} + +func enrichDownloadTaskMeta(ctx context.Context, svc *service.Container, meta service.DownloadTaskMeta, fallbackTitle, mediaType string) service.DownloadTaskMeta { + if strings.TrimSpace(meta.Title) == "" { + meta.Title = strings.TrimSpace(downloadDisplayTitle(fallbackTitle)) + } + // 仅当展示字段都齐时才跳过查询(含媒体富通知所需的年份/评分/类型/原始信息)。 + if strings.TrimSpace(meta.PosterURL) != "" && strings.TrimSpace(meta.BackdropURL) != "" && + strings.TrimSpace(meta.Overview) != "" && meta.Year > 0 && meta.Rating > 0 && + strings.TrimSpace(meta.Genres) != "" && strings.TrimSpace(meta.OriginalName) != "" && + strings.TrimSpace(meta.OriginalLanguage) != "" { + return meta + } + found := lookupDisplayMetadata(ctx, svc, meta.Title, fallbackTitle, mediaType) + if strings.TrimSpace(meta.Title) == "" { + meta.Title = found.Title + } + if strings.TrimSpace(meta.PosterURL) == "" { + meta.PosterURL = found.PosterURL + } + if strings.TrimSpace(meta.BackdropURL) == "" { + meta.BackdropURL = found.BackdropURL + } + if strings.TrimSpace(meta.Overview) == "" { + meta.Overview = found.Overview + } + if strings.TrimSpace(meta.OriginalName) == "" { + meta.OriginalName = found.OriginalName + } + if strings.TrimSpace(meta.OriginalLanguage) == "" { + meta.OriginalLanguage = found.OriginalLanguage + } + if meta.Year <= 0 && found.Year > 0 { + meta.Year = found.Year + } + if meta.Rating <= 0 && found.Rating > 0 { + meta.Rating = found.Rating + } + if strings.TrimSpace(meta.Genres) == "" { + meta.Genres = found.Genres + } + return meta +} + +func downloadDisplayTitle(raw string) string { + raw = strings.TrimSpace(raw) + if raw == "" { + return "" + } + if u, err := url.Parse(raw); err == nil { + if dn := strings.TrimSpace(u.Query().Get("dn")); dn != "" { + if decoded, err := url.QueryUnescape(dn); err == nil && strings.TrimSpace(decoded) != "" { + return strings.TrimSpace(decoded) + } + return dn + } + if u.Host != "" { + base := path.Base(u.Path) + if base != "." && base != "/" && base != "" { + base = strings.TrimSuffix(base, path.Ext(base)) + return strings.TrimSpace(base) + } + } + } + return raw +} diff --git a/internal/handler/metadata_enrich_subscription.go b/internal/handler/metadata_enrich_subscription.go new file mode 100644 index 0000000..40c3b78 --- /dev/null +++ b/internal/handler/metadata_enrich_subscription.go @@ -0,0 +1,100 @@ +package handler + +import ( + "context" + "strings" + + "go.uber.org/zap" + + "github.com/ShukeBta/MediaStationGo/internal/model" + "github.com/ShukeBta/MediaStationGo/internal/service" +) + +func enrichSubscriptionArtwork(ctx context.Context, svc *service.Container, sub *model.Subscription) { + if svc == nil || sub == nil { + return + } + // 已有图片且媒体展示字段也齐全时才跳过;否则仍需查一次补 年份/评分/类型 等 + // 富通知字段(老订阅只存了 poster/overview 的情况)。 + if strings.TrimSpace(sub.PosterURL) != "" && strings.TrimSpace(sub.BackdropURL) != "" && + sub.Year > 0 && sub.Rating > 0 && strings.TrimSpace(sub.Genres) != "" { + return + } + meta := lookupDisplayMetadata(ctx, svc, sub.Name, sub.Filter, sub.MediaType) + if meta.Title == "" && meta.PosterURL == "" && meta.BackdropURL == "" && meta.Overview == "" { + return + } + if strings.TrimSpace(sub.Source) == "" { + sub.Source = meta.Source + } + if strings.TrimSpace(sub.PosterURL) == "" { + sub.PosterURL = meta.PosterURL + } + if strings.TrimSpace(sub.BackdropURL) == "" { + sub.BackdropURL = meta.BackdropURL + } + if strings.TrimSpace(sub.Overview) == "" { + sub.Overview = meta.Overview + } + if strings.TrimSpace(sub.OriginalName) == "" { + sub.OriginalName = meta.OriginalName + } + if strings.TrimSpace(sub.OriginalLanguage) == "" { + sub.OriginalLanguage = meta.OriginalLanguage + } + if sub.Year <= 0 && meta.Year > 0 { + sub.Year = meta.Year + } + if sub.Rating <= 0 && meta.Rating > 0 { + sub.Rating = meta.Rating + } + if strings.TrimSpace(sub.Genres) == "" { + sub.Genres = meta.Genres + } +} + +func enrichAndPersistSubscriptions(ctx context.Context, svc *service.Container, items []model.Subscription) { + for i := range items { + before := items[i] + enrichSubscriptionArtwork(ctx, svc, &items[i]) + updates := subscriptionMetadataUpdates(before, items[i]) + if len(updates) == 0 { + continue + } + if err := svc.Repo.DB.WithContext(ctx).Model(&model.Subscription{}).Where("id = ?", items[i].ID).Updates(updates).Error; err != nil { + svc.Log.Debug("subscription artwork backfill failed", zap.String("id", items[i].ID), zap.Error(err)) + } + } +} + +func subscriptionMetadataUpdates(before, after model.Subscription) map[string]any { + updates := map[string]any{} + if before.Source != after.Source { + updates["source"] = after.Source + } + if before.PosterURL != after.PosterURL { + updates["poster_url"] = after.PosterURL + } + if before.BackdropURL != after.BackdropURL { + updates["backdrop_url"] = after.BackdropURL + } + if before.Overview != after.Overview { + updates["overview"] = after.Overview + } + if before.OriginalName != after.OriginalName { + updates["original_name"] = after.OriginalName + } + if before.OriginalLanguage != after.OriginalLanguage { + updates["original_language"] = after.OriginalLanguage + } + if before.Year != after.Year { + updates["year"] = after.Year + } + if before.Rating != after.Rating { + updates["rating"] = after.Rating + } + if before.Genres != after.Genres { + updates["genres"] = after.Genres + } + return updates +}