mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-04 04:26:38 +08:00
refactor: split modules and harden scraping workflows
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type cloudJSONArtwork struct {
|
||||
posterValues []string
|
||||
backdropValues []string
|
||||
}
|
||||
|
||||
func metadataFromCloudJSON(body []byte) (*LocalMetadata, cloudJSONArtwork) {
|
||||
var raw any
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
return nil, cloudJSONArtwork{}
|
||||
}
|
||||
obj := firstMetadataJSONObject(raw)
|
||||
if len(obj) == 0 {
|
||||
return nil, cloudJSONArtwork{}
|
||||
}
|
||||
meta := &LocalMetadata{
|
||||
Title: firstJSONString(obj, "title", "name", "showtitle", "show_title"),
|
||||
OriginalName: firstJSONString(obj, "original_title", "originaltitle", "original_name", "originalname", "sorttitle"),
|
||||
EpisodeTitle: firstJSONString(obj, "episode_title", "episodetitle", "episode_name", "episodename"),
|
||||
Year: firstJSONInt(obj, "year"),
|
||||
Overview: firstJSONString(obj, "overview", "plot", "outline", "summary", "description"),
|
||||
Rating: firstJSONFloat(obj, "rating", "vote_average", "score"),
|
||||
TMDbID: firstJSONInt(obj, "tmdb_id", "tmdbid", "tmdb"),
|
||||
BangumiID: firstJSONInt(obj, "bangumi_id", "bangumiid", "bgm_id"),
|
||||
DoubanID: firstJSONString(obj, "douban_id", "doubanid"),
|
||||
TheTVDBID: firstJSONString(obj, "thetvdb_id", "tvdb_id", "thetvdbid", "tvdbid"),
|
||||
SeasonNum: firstJSONInt(obj, "season", "season_num", "season_number"),
|
||||
EpisodeNum: firstJSONInt(obj, "episode", "episode_num", "episode_number"),
|
||||
Genres: firstJSONList(obj, "genres", "genre", "tags"),
|
||||
Countries: firstJSONList(obj, "countries", "country", "production_countries"),
|
||||
Languages: firstJSONList(obj, "languages", "language", "spoken_languages"),
|
||||
}
|
||||
if showTitle := firstJSONString(obj, "showtitle", "show_title", "series_title", "series_name"); showTitle != "" {
|
||||
if meta.EpisodeTitle == "" && meta.Title != "" && !strings.EqualFold(strings.TrimSpace(meta.Title), strings.TrimSpace(showTitle)) {
|
||||
meta.EpisodeTitle = meta.Title
|
||||
}
|
||||
meta.Title = showTitle
|
||||
}
|
||||
if meta.Year == 0 {
|
||||
meta.Year = yearFromDate(firstJSONString(obj, "release_date", "releasedate", "premiered", "aired", "date"))
|
||||
}
|
||||
artwork := cloudJSONArtwork{
|
||||
posterValues: firstJSONStrings(obj,
|
||||
"poster_url", "poster", "poster_path", "cover", "cover_url", "thumb", "thumbnail", "image"),
|
||||
backdropValues: firstJSONStrings(obj,
|
||||
"backdrop_url", "backdrop", "backdrop_path", "fanart", "fanart_url", "background", "landscape"),
|
||||
}
|
||||
if images, ok := jsonObject(obj["images"]); ok {
|
||||
artwork.posterValues = append(artwork.posterValues, firstJSONStrings(images, "poster", "large", "common", "medium", "small", "cover")...)
|
||||
artwork.backdropValues = append(artwork.backdropValues, firstJSONStrings(images, "backdrop", "fanart", "background", "landscape")...)
|
||||
}
|
||||
if art, ok := jsonObject(obj["art"]); ok {
|
||||
artwork.posterValues = append(artwork.posterValues, firstJSONStrings(art, "poster", "thumb", "cover")...)
|
||||
artwork.backdropValues = append(artwork.backdropValues, firstJSONStrings(art, "fanart", "backdrop", "background", "landscape")...)
|
||||
}
|
||||
if len(artwork.posterValues) > 0 {
|
||||
meta.PosterURL = firstHTTPJSONValue(artwork.posterValues)
|
||||
}
|
||||
if len(artwork.backdropValues) > 0 {
|
||||
meta.BackdropURL = firstHTTPJSONValue(artwork.backdropValues)
|
||||
}
|
||||
if meta.PosterURL != "" || meta.BackdropURL != "" {
|
||||
meta.HasArtwork = true
|
||||
}
|
||||
if localHasDescriptiveMetadata(meta) || meta.HasArtwork || len(artwork.posterValues) > 0 || len(artwork.backdropValues) > 0 {
|
||||
meta.HasNFO = true
|
||||
return meta, artwork
|
||||
}
|
||||
return nil, cloudJSONArtwork{}
|
||||
}
|
||||
|
||||
func applyCloudJSONArtwork(typ string, sidecars cloudSidecarSet, meta *LocalMetadata, artwork cloudJSONArtwork) *LocalMetadata {
|
||||
if meta == nil {
|
||||
meta = &LocalMetadata{}
|
||||
}
|
||||
if meta.PosterURL == "" {
|
||||
if ref := cloudImageRefFromNFOValues(sidecars, artwork.posterValues...); ref != "" {
|
||||
meta.PosterURL = cloudPlaybackURL(typ, ref)
|
||||
meta.HasArtwork = true
|
||||
}
|
||||
}
|
||||
if meta.BackdropURL == "" {
|
||||
if ref := cloudImageRefFromNFOValues(sidecars, artwork.backdropValues...); ref != "" {
|
||||
meta.BackdropURL = cloudPlaybackURL(typ, ref)
|
||||
meta.HasArtwork = true
|
||||
}
|
||||
}
|
||||
return meta
|
||||
}
|
||||
|
||||
func firstMetadataJSONObject(raw any) map[string]any {
|
||||
obj, ok := jsonObject(raw)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, key := range []string{"movie", "media", "metadata", "item", "data"} {
|
||||
if nested, ok := jsonObject(obj[key]); ok && jsonObjectLooksLikeMetadata(nested) {
|
||||
return nested
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
func jsonObjectLooksLikeMetadata(obj map[string]any) bool {
|
||||
for _, key := range []string{"title", "name", "overview", "plot", "tmdb_id", "tmdbid", "poster", "poster_url", "poster_path", "backdrop", "backdrop_path"} {
|
||||
if _, ok := obj[key]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func jsonObject(raw any) (map[string]any, bool) {
|
||||
obj, ok := raw.(map[string]any)
|
||||
return obj, ok
|
||||
}
|
||||
|
||||
func firstJSONString(obj map[string]any, keys ...string) string {
|
||||
values := firstJSONStrings(obj, keys...)
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
func firstJSONStrings(obj map[string]any, keys ...string) []string {
|
||||
out := []string{}
|
||||
for _, key := range keys {
|
||||
value, ok := lookupJSONKey(obj, key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
out = append(out, jsonStrings(value)...)
|
||||
if len(out) > 0 {
|
||||
return out
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func firstJSONInt(obj map[string]any, keys ...string) int {
|
||||
for _, key := range keys {
|
||||
value, ok := lookupJSONKey(obj, key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if i := jsonInt(value); i > 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func firstJSONFloat(obj map[string]any, keys ...string) float32 {
|
||||
for _, key := range keys {
|
||||
value, ok := lookupJSONKey(obj, key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if f := jsonFloat(value); f > 0 {
|
||||
return f
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func firstJSONList(obj map[string]any, keys ...string) string {
|
||||
seen := map[string]struct{}{}
|
||||
out := []string{}
|
||||
for _, key := range keys {
|
||||
value, ok := lookupJSONKey(obj, key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, part := range jsonStrings(value) {
|
||||
for _, item := range strings.Split(part, ",") {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
dedupeKey := strings.ToLower(item)
|
||||
if _, exists := seen[dedupeKey]; exists {
|
||||
continue
|
||||
}
|
||||
seen[dedupeKey] = struct{}{}
|
||||
out = append(out, item)
|
||||
}
|
||||
}
|
||||
if len(out) > 0 {
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func lookupJSONKey(obj map[string]any, key string) (any, bool) {
|
||||
for existing, value := range obj {
|
||||
if strings.EqualFold(strings.TrimSpace(existing), key) {
|
||||
return value, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func jsonStrings(value any) []string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if text := strings.TrimSpace(v); text != "" {
|
||||
return []string{text}
|
||||
}
|
||||
case []any:
|
||||
out := make([]string, 0, len(v))
|
||||
for _, item := range v {
|
||||
out = append(out, jsonStrings(item)...)
|
||||
}
|
||||
return out
|
||||
case map[string]any:
|
||||
return firstJSONStrings(v, "name", "title", "value", "iso_3166_1", "iso_639_1")
|
||||
case float64:
|
||||
if v > 0 {
|
||||
return []string{strconv.Itoa(int(v))}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func jsonInt(value any) int {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
return int(v)
|
||||
case string:
|
||||
i, _ := strconv.Atoi(strings.TrimSpace(v))
|
||||
return i
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func jsonFloat(value any) float32 {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
return float32(v)
|
||||
case string:
|
||||
f, _ := strconv.ParseFloat(strings.TrimSpace(v), 32)
|
||||
return float32(f)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func firstHTTPJSONValue(values []string) string {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if isHTTPURL(value) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user