From d2ed6cdeb7c37efcff5914f91ae23a6b203598b8 Mon Sep 17 00:00:00 2001 From: ngfchl Date: Sat, 18 Jul 2026 16:25:47 +0800 Subject: [PATCH] feat: enrich media matches with tmdb details --- lib/providers/media_library_provider.dart | 117 +++++++++++++++++++++- 1 file changed, 115 insertions(+), 2 deletions(-) diff --git a/lib/providers/media_library_provider.dart b/lib/providers/media_library_provider.dart index af63832..1b7198e 100644 --- a/lib/providers/media_library_provider.dart +++ b/lib/providers/media_library_provider.dart @@ -89,6 +89,7 @@ class MediaLibraryState { class MediaLibraryNotifier extends StateNotifier { GuangyaAPI? _api; final _store = MediaLibraryStore(); + final _tmdbDetailsRequests = >>{}; bool _loaded = false; bool _cancelScan = false; @@ -468,7 +469,24 @@ class MediaLibraryNotifier extends StateNotifier { MediaLibraryItem item, Map candidate, ) async { - final updated = _itemFromTMDBCandidate(item, candidate); + final apiKey = StorageManager.get(StorageKeys.tmdbApiKey) ?? ''; + var updated = _itemFromTMDBCandidate(item, candidate); + if (updated.tmdbID != null && apiKey.isNotEmpty) { + try { + final details = await _tmdbDetails( + updated.tmdbID!, + updated.mediaKind, + apiKey: apiKey, + proxyHost: + StorageManager.get(StorageKeys.tmdbProxyHost) ?? '', + proxyPort: + StorageManager.get(StorageKeys.tmdbProxyPort) ?? '', + ); + updated = _itemFromTMDBDetails(updated, details); + } catch (_) { + // A selected candidate is still valid if its detail request fails. + } + } await _store.upsertItems([updated]); state = state.copyWith( items: state.items @@ -511,7 +529,21 @@ class MediaLibraryNotifier extends StateNotifier { } } if (candidate == null) return fallback; - return _itemFromTMDBCandidate(fallback, candidate); + var item = _itemFromTMDBCandidate(fallback, candidate); + if (item.tmdbID == null) return item; + try { + final details = await _tmdbDetails( + item.tmdbID!, + item.mediaKind, + apiKey: apiKey, + proxyHost: proxyHost, + proxyPort: proxyPort, + ); + item = _itemFromTMDBDetails(item, details); + } catch (_) { + // The search result already has enough information for an initial row. + } + return item; } catch (_) { return fallback; } @@ -546,6 +578,87 @@ class MediaLibraryNotifier extends StateNotifier { ); } + Future> _tmdbDetails( + int id, + TMDBMediaKind? kind, { + required String apiKey, + required String proxyHost, + required String proxyPort, + }) { + final mediaKind = kind == TMDBMediaKind.tv ? 'tv' : 'movie'; + final key = '$mediaKind:$id'; + return _tmdbDetailsRequests.putIfAbsent( + key, + () => _api!.tmdbDetails( + id, + mediaKind: mediaKind, + apiKey: apiKey, + proxyHost: proxyHost, + proxyPort: proxyPort, + ), + ); + } + + MediaLibraryItem _itemFromTMDBDetails( + MediaLibraryItem item, + Map details, + ) { + final title = (details['title'] ?? details['name'])?.toString().trim(); + final originalTitle = + (details['original_title'] ?? details['original_name']) + ?.toString() + .trim(); + final releaseDate = + (details['release_date'] ?? details['first_air_date'])?.toString() ?? + item.releaseDate; + final collection = details['belongs_to_collection']; + final collectionMap = collection is Map + ? Map.from(collection) + : const {}; + return item.copyWith( + title: title == null || title.isEmpty ? item.title : title, + originalTitle: originalTitle == null || originalTitle.isEmpty + ? item.originalTitle + : originalTitle, + releaseDate: releaseDate, + overview: details['overview']?.toString().trim().isNotEmpty == true + ? details['overview'].toString() + : item.overview, + posterPath: + _preferredArtworkPath(details, 'posters') ?? + details['poster_path']?.toString() ?? + item.posterPath, + backdropPath: + _preferredArtworkPath(details, 'backdrops') ?? + details['backdrop_path']?.toString() ?? + item.backdropPath, + collectionID: _toInt(collectionMap['id']) ?? item.collectionID, + collectionName: collectionMap['name']?.toString() ?? item.collectionName, + updatedAt: DateTime.now(), + ); + } + + String? _preferredArtworkPath(Map details, String key) { + final images = details['images']; + if (images is! Map) return null; + final values = images[key]; + if (values is! List) return null; + const languages = ['zh-CN', 'zh', null, 'en']; + for (final language in languages) { + for (final value in values) { + if (value is! Map || value['iso_639_1'] != language) continue; + final path = value['file_path']?.toString(); + if (path != null && path.isNotEmpty) return path; + } + } + for (final value in values) { + if (value is! Map) continue; + final path = value['file_path']?.toString(); + if (path != null && path.isNotEmpty) return path; + } + return null; + } + int? _toInt(dynamic value) { if (value is int) return value; return int.tryParse(value?.toString() ?? '');