refactor: centralize globalKey construction via buildGlobalKey()

This commit is contained in:
edde746
2026-02-23 17:53:20 +01:00
parent ef2c464dd4
commit e23f472d78
21 changed files with 65 additions and 48 deletions
+3 -2
View File
@@ -7,6 +7,7 @@ import 'package:path/path.dart' as p;
import 'tables.dart';
import '../models/download_models.dart';
import '../utils/app_logger.dart';
import '../utils/global_key_utils.dart';
part 'app_database.g.dart';
@@ -99,7 +100,7 @@ class AppDatabase extends _$AppDatabase {
required int duration,
required bool shouldMarkWatched,
}) async {
final globalKey = '$serverId:$ratingKey';
final globalKey = buildGlobalKey(serverId, ratingKey);
final now = DateTime.now().millisecondsSinceEpoch;
// Check for existing progress entry
@@ -144,7 +145,7 @@ class AppDatabase extends _$AppDatabase {
required String ratingKey,
required String actionType, // 'watched' or 'unwatched'
}) async {
final globalKey = '$serverId:$ratingKey';
final globalKey = buildGlobalKey(serverId, ratingKey);
final now = DateTime.now().millisecondsSinceEpoch;
// Remove conflicting actions (opposite action type and progress)
+2 -1
View File
@@ -1,6 +1,7 @@
import 'package:json_annotation/json_annotation.dart';
import 'mixins/multi_server_fields.dart';
import '../utils/global_key_utils.dart';
part 'plex_library.g.dart';
@@ -26,7 +27,7 @@ class PlexLibrary with MultiServerFields {
final String? serverName;
/// Global unique identifier across all servers (serverId:key)
String get globalKey => serverId != null ? '$serverId:$key' : key;
String get globalKey => serverId != null ? buildGlobalKey(serverId!, key) : key;
PlexLibrary({
required this.key,
+2 -1
View File
@@ -4,6 +4,7 @@ import '../services/settings_service.dart' show EpisodePosterMode;
import '../widgets/plex_optimized_image.dart' show kBlurArtwork, obfuscateText;
import 'mixins/multi_server_fields.dart';
import 'plex_role.dart';
import '../utils/global_key_utils.dart';
part 'plex_metadata.g.dart';
@@ -111,7 +112,7 @@ class PlexMetadata with MultiServerFields {
final String? clearLogo;
/// Global unique identifier across all servers (serverId:ratingKey)
String get globalKey => serverId != null ? '$serverId:$ratingKey' : ratingKey;
String get globalKey => serverId != null ? buildGlobalKey(serverId!, ratingKey) : ratingKey;
/// Parsed media type enum for type-safe comparisons
PlexMediaType get mediaType {
+2 -1
View File
@@ -2,6 +2,7 @@ import 'package:json_annotation/json_annotation.dart';
import '../widgets/plex_optimized_image.dart' show kBlurArtwork, obfuscateText;
import 'mixins/multi_server_fields.dart';
import '../utils/global_key_utils.dart';
part 'plex_playlist.g.dart';
@@ -65,7 +66,7 @@ class PlexPlaylist with MultiServerFields {
bool get isEditable => !smart;
/// Get globally unique key across all servers
String get globalKey => serverId != null ? '$serverId:$ratingKey' : ratingKey;
String get globalKey => serverId != null ? buildGlobalKey(serverId!, ratingKey) : ratingKey;
// Properties for MediaCard compatibility with PlexMetadata interface
+11 -11
View File
@@ -172,7 +172,7 @@ class DownloadProvider extends ChangeNotifier {
// Load show metadata
final showRatingKey = episode.grandparentRatingKey;
if (showRatingKey != null) {
final showGlobalKey = '$serverId:$showRatingKey';
final showGlobalKey = buildGlobalKey(serverId, showRatingKey);
if (!_metadata.containsKey(showGlobalKey)) {
final showMetadata = allMetadata[showGlobalKey];
if (showMetadata != null) {
@@ -187,7 +187,7 @@ class DownloadProvider extends ChangeNotifier {
// Load season metadata
final seasonRatingKey = episode.parentRatingKey;
if (seasonRatingKey != null) {
final seasonGlobalKey = '$serverId:$seasonRatingKey';
final seasonGlobalKey = buildGlobalKey(serverId, seasonRatingKey);
if (!_metadata.containsKey(seasonGlobalKey)) {
final seasonMetadata = allMetadata[seasonGlobalKey];
if (seasonMetadata != null) {
@@ -270,7 +270,7 @@ class DownloadProvider extends ChangeNotifier {
final showRatingKey = meta.grandparentRatingKey;
if (showRatingKey != null && !shows.containsKey(showRatingKey)) {
// Try to get stored show metadata first
final showGlobalKey = '${meta.serverId}:$showRatingKey';
final showGlobalKey = buildGlobalKey(meta.serverId!, showRatingKey);
final storedShow = _metadata[showGlobalKey];
if (storedShow != null && storedShow.type == 'show') {
@@ -384,7 +384,7 @@ class DownloadProvider extends ChangeNotifier {
required List<DownloadProgress> episodes,
required String entityType,
}) {
final globalKey = '$serverId:$ratingKey';
final globalKey = buildGlobalKey(serverId, ratingKey);
// DIAGNOSTIC: Check all sources of episode count
final meta = _metadata[globalKey];
@@ -604,7 +604,7 @@ class DownloadProvider extends ChangeNotifier {
/// For shows and seasons, fetches all child episodes and queues them.
/// Returns the number of items queued.
Future<int> queueDownload(PlexMetadata metadata, PlexClient client) async {
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
// Check if downloads are blocked on cellular
if (await DownloadManagerService.shouldBlockDownloadOnCellular()) {
@@ -646,7 +646,7 @@ class DownloadProvider extends ChangeNotifier {
/// Queue a single movie or episode for download
Future<void> _queueSingleDownload(PlexMetadata metadata, PlexClient client) async {
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
// Don't re-queue if already downloading or completed
if (_downloads.containsKey(globalKey)) {
@@ -697,7 +697,7 @@ class DownloadProvider extends ChangeNotifier {
// Fetch and store show metadata if not already stored
final showRatingKey = episode.grandparentRatingKey;
if (showRatingKey != null) {
final showGlobalKey = '$serverId:$showRatingKey';
final showGlobalKey = buildGlobalKey(serverId, showRatingKey);
// Try to use existing metadata (set when queueing an entire show)
PlexMetadata? showMetadata = _metadata[showGlobalKey];
@@ -734,7 +734,7 @@ class DownloadProvider extends ChangeNotifier {
// Fetch and store season metadata if not already stored
final seasonRatingKey = episode.parentRatingKey;
if (seasonRatingKey != null) {
final seasonGlobalKey = '$serverId:$seasonRatingKey';
final seasonGlobalKey = buildGlobalKey(serverId, seasonRatingKey);
PlexMetadata? seasonMetadata = _metadata[seasonGlobalKey];
if (seasonMetadata == null) {
@@ -768,7 +768,7 @@ class DownloadProvider extends ChangeNotifier {
/// Queue all episodes from a TV show for download
Future<int> _queueShowDownload(PlexMetadata show, PlexClient client) async {
final globalKey = '${show.serverId}:${show.ratingKey}';
final globalKey = show.globalKey;
int count = 0;
final seasons = await client.getChildren(show.ratingKey);
@@ -804,7 +804,7 @@ class DownloadProvider extends ChangeNotifier {
/// Queue all episodes from a season for download
Future<int> _queueSeasonDownload(PlexMetadata season, PlexClient client) async {
final globalKey = '${season.serverId}:${season.ratingKey}';
final globalKey = season.globalKey;
int count = 0;
final episodes = await client.getChildren(season.ratingKey);
@@ -883,7 +883,7 @@ class DownloadProvider extends ChangeNotifier {
if (episode.type == 'episode') {
final episodeWithServer = episode.serverId != null ? episode : episode.copyWith(serverId: season.serverId);
final episodeGlobalKey = '${episodeWithServer.serverId}:${episodeWithServer.ratingKey}';
final episodeGlobalKey = episodeWithServer.globalKey;
// Only queue if NOT already downloaded or in progress
final progress = _downloads[episodeGlobalKey];
+2 -1
View File
@@ -5,6 +5,7 @@ import '../services/offline_watch_sync_service.dart';
import '../services/plex_api_cache.dart';
import '../utils/watch_state_notifier.dart';
import 'download_provider.dart';
import '../utils/global_key_utils.dart';
/// Provider for offline watch status UI state.
///
@@ -174,7 +175,7 @@ class OfflineWatchProvider extends ChangeNotifier {
required bool isNowWatched,
required WatchStateChangeType changeType,
}) {
final globalKey = '$serverId:$ratingKey';
final globalKey = buildGlobalKey(serverId, ratingKey);
final metadata = _downloadProvider.getMetadata(globalKey);
if (metadata != null) {
WatchStateNotifier().notifyWatched(metadata: metadata, isNowWatched: isNowWatched);
+4 -5
View File
@@ -6,6 +6,7 @@ import 'package:plezy/widgets/app_icon.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:provider/provider.dart';
import '../focus/key_event_utils.dart';
import '../utils/global_key_utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import '../../services/plex_client.dart';
import '../utils/plex_image_helper.dart';
@@ -81,8 +82,6 @@ class _DiscoverScreenState extends State<DiscoverScreen>
bool _isAutoScrollPaused = false;
HiddenLibrariesProvider? _hiddenLibrariesProvider;
String _toGlobalKey(String ratingKey, String serverId) => '$serverId:$ratingKey';
// WatchStateAware: watch on-deck items and their parent shows/seasons
@override
Set<String>? get watchedRatingKeys {
@@ -106,12 +105,12 @@ class _DiscoverScreenState extends State<DiscoverScreen>
final serverId = item.serverId;
if (serverId == null) return null;
keys.add(_toGlobalKey(item.ratingKey, serverId));
keys.add(buildGlobalKey(serverId, item.ratingKey));
if (item.parentRatingKey != null) {
keys.add(_toGlobalKey(item.parentRatingKey!, serverId));
keys.add(buildGlobalKey(serverId, item.parentRatingKey!));
}
if (item.grandparentRatingKey != null) {
keys.add(_toGlobalKey(item.grandparentRatingKey!, serverId));
keys.add(buildGlobalKey(serverId, item.grandparentRatingKey!));
}
}
return keys;
@@ -32,6 +32,7 @@ import '../../../mixins/grid_focus_node_mixin.dart';
import '../../../mixins/item_updatable.dart';
import '../../../mixins/deletion_aware.dart';
import '../../../utils/deletion_notifier.dart';
import '../../../utils/global_key_utils.dart';
import '../../../utils/platform_detector.dart';
import '../../../i18n/strings.g.dart';
import '../../main_screen.dart';
@@ -61,7 +62,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<PlexMetadata, LibraryBr
PlexClient get client => getClientForLibrary();
String _toGlobalKey(String ratingKey, {String? serverId}) =>
'${serverId ?? widget.library.serverId ?? ''}:$ratingKey';
buildGlobalKey(serverId ?? widget.library.serverId ?? '', ratingKey);
@override
String? get deletionServerId => widget.library.serverId;
+3 -2
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:cached_network_image/cached_network_image.dart';
import '../utils/global_key_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter/services.dart';
@@ -106,7 +107,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
final _seasonsSectionKey = GlobalKey();
String _toGlobalKey(String ratingKey, {String? serverId}) =>
'${serverId ?? widget.metadata.serverId ?? ''}:$ratingKey';
buildGlobalKey(serverId ?? widget.metadata.serverId ?? '', ratingKey);
// WatchStateAware: watch the show/movie and all season ratingKeys
@override
@@ -424,7 +425,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
if (!widget.isOffline)
Consumer<DownloadProvider>(
builder: (context, downloadProvider, _) {
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
final progress = downloadProvider.getProgress(globalKey);
final isQueueing = downloadProvider.isQueueing(globalKey);
+4 -3
View File
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import '../../services/plex_client.dart';
import '../main.dart';
import '../focus/focusable_wrapper.dart';
import '../utils/global_key_utils.dart';
import '../focus/key_event_utils.dart';
import '../focus/dpad_navigator.dart';
import '../focus/input_mode_tracker.dart';
@@ -55,7 +56,7 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
bool _suppressNextBackKeyUp = false;
bool _routeSubscribed = false;
String _toGlobalKey(String ratingKey, {String? serverId}) => '${serverId ?? widget.season.serverId ?? ''}:$ratingKey';
String _toGlobalKey(String ratingKey, {String? serverId}) => buildGlobalKey(serverId ?? widget.season.serverId ?? '', ratingKey);
// WatchStateAware: watch all episode ratingKeys
@override
@@ -270,7 +271,7 @@ class _SeasonDetailScreenState extends State<SeasonDetailScreen>
String? localPosterPath;
if (widget.isOffline && episode.serverId != null) {
final downloadProvider = context.read<DownloadProvider>();
final globalKey = '${episode.serverId}:${episode.ratingKey}';
final globalKey = episode.globalKey;
// Get the artwork reference and convert to local file path
final artworkRef = downloadProvider.getArtworkPaths(globalKey);
localPosterPath = artworkRef?.getLocalPath(DownloadStorageService.instance, episode.serverId!);
@@ -499,7 +500,7 @@ class _EpisodeCardState extends State<_EpisodeCard> {
// Only show download status in online mode
if (!widget.isOffline && widget.episode.serverId != null) {
final globalKey = '${widget.episode.serverId}:${widget.episode.ratingKey}';
final globalKey = widget.episode.globalKey;
final progress = downloadProvider.getProgress(globalKey);
final isQueueing = downloadProvider.isQueueing(globalKey);
+2 -2
View File
@@ -951,7 +951,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// since the user may have watched further since downloading.
Duration? resumePosition;
if (widget.isOffline) {
final globalKey = '${widget.metadata.serverId}:${widget.metadata.ratingKey}';
final globalKey = widget.metadata.globalKey;
final localOffset = await offlineWatchService!.getLocalViewOffset(globalKey);
if (localOffset != null && localOffset > 0) {
resumePosition = Duration(milliseconds: localOffset);
@@ -1103,7 +1103,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Debug: log metadata info
appLogger.d('Offline playback - serverId: ${widget.metadata.serverId}, ratingKey: ${widget.metadata.ratingKey}');
final globalKey = '${widget.metadata.serverId}:${widget.metadata.ratingKey}';
final globalKey = widget.metadata.globalKey;
appLogger.d('Looking up video with globalKey: $globalKey');
final videoPath = await downloadProvider.getVideoFilePath(globalKey);
+3 -2
View File
@@ -5,6 +5,7 @@ import '../models/plex_hub.dart';
import '../models/plex_library.dart';
import '../models/plex_metadata.dart';
import '../utils/app_logger.dart';
import '../utils/global_key_utils.dart';
import 'multi_server_manager.dart';
import 'plex_auth_service.dart';
@@ -45,7 +46,7 @@ class DataAggregationService {
filteredOnDeck = allOnDeck.where((item) {
final librarySectionId = item.librarySectionID;
if (librarySectionId == null) return true; // Keep if no section ID
final globalKey = '${item.serverId}:$librarySectionId';
final globalKey = buildGlobalKey(item.serverId!, librarySectionId.toString());
return !hiddenLibraryKeys.contains(globalKey);
}).toList();
}
@@ -118,7 +119,7 @@ class DataAggregationService {
// Build the global key for the item's library section
final librarySectionId = item.librarySectionID;
if (librarySectionId == null) return true; // Keep if no section ID
final globalKey = '$serverId:$librarySectionId';
final globalKey = buildGlobalKey(serverId, librarySectionId.toString());
return !hiddenLibraryKeys.contains(globalKey);
}).toList();
+8 -7
View File
@@ -430,7 +430,7 @@ class DownloadManagerService {
bool downloadSubtitles = true,
bool downloadArtwork = true,
}) async {
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
// Check if already downloading or completed
final existing = await _database.getDownloadedMedia(globalKey);
@@ -1252,12 +1252,13 @@ class DownloadManagerService {
if (metadata == null) {
// Fallback: Try database record
final downloadRecord = await _database.getDownloadedMedia('$serverId:$ratingKey');
final gk = buildGlobalKey(serverId, ratingKey);
final downloadRecord = await _database.getDownloadedMedia(gk);
if (downloadRecord?.videoFilePath != null) {
await _deleteByFilePath(downloadRecord!);
return;
}
appLogger.w('Cannot delete - no metadata for $serverId:$ratingKey');
appLogger.w('Cannot delete - no metadata for $gk');
return;
}
@@ -1433,12 +1434,12 @@ class DownloadManagerService {
}) async {
for (int i = 0; i < episodes.length; i++) {
final episode = episodes[i];
final episodeGlobalKey = '$serverId:${episode.ratingKey}';
final episodeGlobalKey = buildGlobalKey(serverId, episode.ratingKey);
// Emit progress update
_emitDeletionProgress(
DeletionProgress(
globalKey: '$serverId:$parentKey',
globalKey: buildGlobalKey(serverId, parentKey),
itemTitle: parentTitle,
currentItem: i + 1,
totalItems: episodes.length,
@@ -1551,7 +1552,7 @@ class DownloadManagerService {
final otherEpisodes = await _database.getEpisodesBySeason(seasonKey);
// Check if any episodes besides this one
return otherEpisodes.any((e) => e.globalKey != '${episode.serverId}:${episode.ratingKey}');
return otherEpisodes.any((e) => e.globalKey != episode.globalKey);
}
/// Check if show artwork is in use
@@ -1562,7 +1563,7 @@ class DownloadManagerService {
final showEpisodes = await _database.getEpisodesByShow(showKey);
// Check if any episodes belong to this show besides the current item
return showEpisodes.any((item) => item.globalKey != '${metadata.serverId}:${metadata.ratingKey}');
return showEpisodes.any((item) => item.globalKey != metadata.globalKey);
}
/// Find file with any extension
+2 -1
View File
@@ -6,6 +6,7 @@ import 'package:drift/drift.dart';
import '../database/app_database.dart';
import '../models/plex_metadata.dart';
import '../utils/plex_cache_parser.dart';
import '../utils/global_key_utils.dart';
/// Key-value cache for Plex API responses using Drift/SQLite.
/// Stores raw JSON responses keyed by serverId:endpoint format.
@@ -152,7 +153,7 @@ class PlexApiCache {
final json = PlexCacheParser.extractFirstMetadata(data);
if (json == null) continue;
final metadata = PlexMetadata.fromJsonWithImages(json).copyWith(serverId: serverId);
result['$serverId:$ratingKey'] = metadata;
result[buildGlobalKey(serverId, ratingKey)] = metadata;
} catch (_) {
// Skip malformed entries
}
+2 -1
View File
@@ -1,6 +1,7 @@
import '../models/plex_metadata.dart';
import 'app_logger.dart';
import 'base_notifier.dart';
import 'global_key_utils.dart';
import 'hierarchical_event_mixin.dart';
/// Event representing a media item deletion with parent chain for hierarchical invalidation
@@ -42,7 +43,7 @@ class DeletionEvent with HierarchicalEventMixin {
required this.mediaType,
this.leafCount = 1,
this.isDownloadOnly = false,
}) : globalKey = '$serverId:$ratingKey';
}) : globalKey = buildGlobalKey(serverId, ratingKey);
@override
String toString() => 'DeletionEvent(deleted: $globalKey, type: $mediaType, parents: $parentChain)';
+3
View File
@@ -1,3 +1,6 @@
/// Builds a globalKey string from [serverId] and [ratingKey].
String buildGlobalKey(String serverId, String ratingKey) => '$serverId:$ratingKey';
/// Parses a globalKey string (format: "serverId:ratingKey") into its components.
///
/// Returns `null` if the key does not contain a colon separator.
+3 -1
View File
@@ -1,3 +1,5 @@
import 'global_key_utils.dart';
/// Mixin providing hierarchical event matching methods.
///
/// Events that represent changes to media items often need to check if they
@@ -24,7 +26,7 @@ mixin HierarchicalEventMixin {
/// Check if this event affects a specific globalKey.
bool affectsGlobalKey(String globalKey) =>
this.globalKey == globalKey || parentChain.any((pk) => '$serverId:$pk' == globalKey);
this.globalKey == globalKey || parentChain.any((pk) => buildGlobalKey(serverId, pk) == globalKey);
/// Check if this event affects any item in a collection.
bool affectsAnyOf(Iterable<String> ratingKeys) => ratingKeys.any(affectsItem);
+1 -1
View File
@@ -77,7 +77,7 @@ Future<bool?> navigateToVideoPlayer(
if (isOffline) {
// Offline mode: resolve local file path for the external player
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
final videoPath = await downloadProvider.getVideoFilePath(globalKey);
if (videoPath != null && context.mounted) {
final videoUrl = videoPath.contains('://') ? videoPath : 'file://$videoPath';
+2 -1
View File
@@ -1,6 +1,7 @@
import '../models/plex_metadata.dart';
import 'app_logger.dart';
import 'base_notifier.dart';
import 'global_key_utils.dart';
import 'hierarchical_event_mixin.dart';
/// Types of watch state changes
@@ -47,7 +48,7 @@ class WatchStateEvent with HierarchicalEventMixin {
required this.mediaType,
this.viewOffset,
this.isNowWatched,
}) : globalKey = '$serverId:$ratingKey';
}) : globalKey = buildGlobalKey(serverId, ratingKey);
@override
String toString() => 'WatchStateEvent($changeType, $globalKey, parents: $parentChain)';
+1 -1
View File
@@ -149,7 +149,7 @@ class MediaCardState extends State<MediaCard> {
if (metadata.serverId == null) return null;
final downloadProvider = context.read<DownloadProvider>();
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
// Get artwork reference and resolve to local path using hash (includes serverId)
final artwork = downloadProvider.getArtworkPaths(globalKey);
+3 -3
View File
@@ -263,7 +263,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
mediaType == PlexMediaType.show ||
mediaType == PlexMediaType.season) {
final downloadProvider = Provider.of<DownloadProvider>(context, listen: false);
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
final isDownloaded = downloadProvider.isDownloaded(globalKey);
if (isDownloaded) {
@@ -1063,7 +1063,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
// Check if the item is downloaded and use local file path if available
final downloadProvider = Provider.of<DownloadProvider>(context, listen: false);
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
if (downloadProvider.isDownloaded(globalKey)) {
final videoPath = await downloadProvider.getVideoFilePath(globalKey);
if (videoPath != null && context.mounted) {
@@ -1107,7 +1107,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
Future<void> _handleDeleteDownload(BuildContext context) async {
final downloadProvider = Provider.of<DownloadProvider>(context, listen: false);
final metadata = widget.item as PlexMetadata;
final globalKey = '${metadata.serverId}:${metadata.ratingKey}';
final globalKey = metadata.globalKey;
// Show confirmation dialog
final confirmed = await showDeleteConfirmation(