From 4307c49cd2d1f38512fea22952a77337179acdb8 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sat, 25 Jul 2026 18:25:30 +0200 Subject: [PATCH] refactor: remove unreachable code paths and unused members Drops dead code across services, models, utils and widgets, including the connection auth service, which had no implementer, and the Live TV DVR provisioning models, which had no caller. Tests that only covered deleted behaviour are removed or trimmed. No behaviour change. --- lib/connection/connection_auth_service.dart | 20 - lib/database/app_database.dart | 2 + lib/database/download_operations.dart | 3 + lib/main.dart | 12 +- lib/media/live_tv_support.dart | 68 ---- lib/media/media_kind.dart | 2 - lib/media/media_server_client.dart | 3 - lib/media/media_source_info.dart | 1 - lib/models/download_models.dart | 2 - lib/models/plex/plex_home.dart | 14 - lib/models/plex/plex_video_playback_data.dart | 2 - lib/models/seerr/seerr_details.dart | 99 +---- lib/models/seerr/seerr_details.g.dart | 55 --- lib/models/user_switch_response.dart | 40 -- lib/navigation/navigation_tabs.dart | 6 - lib/profiles/active_profile_binder.dart | 2 - .../base_media_list_detail_screen.dart | 36 -- .../playlist/playlist_detail_screen.dart | 15 +- lib/services/api_cache.dart | 21 -- .../companion_remote/remote_auth_service.dart | 4 - lib/services/download_artwork_service.dart | 7 - lib/services/episode_navigation_service.dart | 1 - lib/services/jellyfin_auth_service.dart | 16 +- .../jellyfin_client/parts/browse.dart | 21 -- .../jellyfin_client/parts/metadata_edit.dart | 7 - lib/services/macos_window_service.dart | 4 - lib/services/multi_server_manager.dart | 59 +-- .../music/music_playback_service.dart | 8 +- lib/services/music/music_source_resolver.dart | 7 - lib/services/play_queue_launcher.dart | 83 ++--- lib/services/playback_context.dart | 1 - lib/services/plex_api_cache.dart | 2 + lib/services/plex_client.dart | 23 -- lib/services/plex_client/parts/live_tv.dart | 351 +----------------- lib/services/seerr/seerr_client.dart | 4 - lib/services/seerr/seerr_constants.dart | 2 - lib/services/sync_rule_executor.dart | 2 - lib/services/trakt/trakt_constants.dart | 6 - lib/utils/app_logger.dart | 4 - lib/utils/content_utils.dart | 1 - .../continuation_pagination_coordinator.dart | 7 - lib/utils/hierarchical_event_mixin.dart | 4 - lib/utils/layout_constants.dart | 1 - lib/utils/media_image_helper.dart | 27 -- lib/utils/platform_detector.dart | 2 - .../providers/watch_together_provider.dart | 5 - .../services/guest_playback_reconciler.dart | 1 - .../services/host_playback_coordinator.dart | 1 - lib/widgets/hub_section.dart | 3 - .../helpers/track_selection_helper.dart | 29 -- .../player_chrome_controller.dart | 1 - test/models/livetv_flexible_parsing_test.dart | 45 --- test/services/plex_live_tv_support_test.dart | 35 +- 53 files changed, 59 insertions(+), 1118 deletions(-) delete mode 100644 lib/connection/connection_auth_service.dart diff --git a/lib/connection/connection_auth_service.dart b/lib/connection/connection_auth_service.dart deleted file mode 100644 index 304b0a39..00000000 --- a/lib/connection/connection_auth_service.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'connection.dart'; - -/// Backend-neutral auth service interface. Each backend's implementation -/// (`PlexConnectionAuthService`, `JellyfinConnectionAuthService`) drives its -/// own UX (PIN flow vs. password) but produces the same opaque -/// [Connection] record at the end. -abstract class ConnectionAuthService { - /// Best-effort check that an existing token still works. Returns false on - /// 401/403; throws on transport failures the caller should retry. - Future validate(Connection connection); - - /// Refresh whatever side-channel state belongs to a connection — for Plex - /// that's the discovered server list and Home users; for Jellyfin it's a - /// no-op once auth has succeeded. Returns the updated connection. - Future refresh(Connection connection); - - /// Revoke the token server-side and forget local credentials. The caller - /// is responsible for removing the row from [ConnectionRegistry]. - Future signOut(Connection connection); -} diff --git a/lib/database/app_database.dart b/lib/database/app_database.dart index cae5d3e1..6c54a441 100644 --- a/lib/database/app_database.dart +++ b/lib/database/app_database.dart @@ -950,6 +950,7 @@ class AppDatabase extends _$AppDatabase { } /// Get pending watch actions for a specific server + @visibleForTesting Future> getPendingWatchActionsForServer(ServerId serverId, {String? profileId}) { return (select(offlineWatchProgress) ..where( @@ -979,6 +980,7 @@ class AppDatabase extends _$AppDatabase { } /// Get the latest action for a specific item + @visibleForTesting Future getLatestWatchAction( String globalKey, { String? profileId, diff --git a/lib/database/download_operations.dart b/lib/database/download_operations.dart index ea0def15..eb22c743 100644 --- a/lib/database/download_operations.dart +++ b/lib/database/download_operations.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; import '../media/ids.dart'; import 'app_database.dart'; @@ -138,6 +139,7 @@ extension DownloadDatabaseOperations on AppDatabase { return (await _validDownloadOwnerRows(globalKey)).length; } + @visibleForTesting Future hasDownloadOwner(String globalKey, {String? excludingProfileId}) async { final rows = await _validDownloadOwnerRows(globalKey, excludingProfileId: excludingProfileId); return rows.isNotEmpty; @@ -573,6 +575,7 @@ extension DownloadDatabaseOperations on AppDatabase { return (await query.map((row) => row.read(count) ?? 0).getSingle()); } + @visibleForTesting Future> getReferencedDownloadSafRoots() async { final rows = await (selectOnly(downloadedMedia) diff --git a/lib/main.dart b/lib/main.dart index 9891c797..1237a6ed 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1043,7 +1043,7 @@ class _MainAppState extends State with WidgetsBindingObserver { return provider; }, update: (_, multiServerProvider, previous) { - final provider = previous ?? OfflineModeProvider(_serverManager, multiServerProvider: multiServerProvider); + final provider = previous!; provider.updateMultiServerProvider(multiServerProvider); provider.initialize(); // Idempotent - safe to call again return provider; @@ -1081,7 +1081,7 @@ class _MainAppState extends State with WidgetsBindingObserver { ChangeNotifierProxyProvider( create: (context) => DownloadProvider(downloadManager: _downloadManager, database: _appDatabase), update: (context, activeProfile, previous) { - final provider = previous ?? DownloadProvider(downloadManager: _downloadManager, database: _appDatabase); + final provider = previous!; provider.setActiveProfileId(activeProfile.activeId); return provider; }, @@ -1134,7 +1134,7 @@ class _MainAppState extends State with WidgetsBindingObserver { return _offlineWatchSyncService; }, update: (_, activeProfile, previous) { - final provider = previous ?? _offlineWatchSyncService; + final provider = previous!; provider.setActiveProfileId( activeProfile.activeId, availableProfileCount: activeProfile.isInitialized ? activeProfile.profiles.length : null, @@ -1147,14 +1147,12 @@ class _MainAppState extends State with WidgetsBindingObserver { syncService: context.read(), downloadProvider: context.read(), ), - update: (_, syncService, downloadProvider, previous) { - return previous ?? OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider); - }, + update: (_, syncService, downloadProvider, previous) => previous!, ), ChangeNotifierProxyProvider2( create: (context) => UserProfileProvider(storageService: context.read()), update: (context, activeProfile, connections, previous) { - final provider = previous ?? UserProfileProvider(storageService: context.read()); + final provider = previous!; provider.attach( connections: connections, activeProfile: activeProfile, diff --git a/lib/media/live_tv_support.dart b/lib/media/live_tv_support.dart index 050ef239..392bfe81 100644 --- a/lib/media/live_tv_support.dart +++ b/lib/media/live_tv_support.dart @@ -1,13 +1,8 @@ import '../models/livetv_capture_buffer.dart'; import '../models/livetv_channel.dart'; import '../models/livetv_dvr.dart'; -import '../models/livetv_lineup.dart'; import '../models/livetv_program.dart'; -import '../models/livetv_server_status.dart'; -import '../models/livetv_session.dart'; import '../models/media_grab_operation.dart'; -import '../models/media_grabber_device.dart'; -import '../models/media_provider_info.dart'; import '../models/media_subscription.dart'; class LiveTvActivityResult { @@ -202,67 +197,13 @@ abstract class LiveTvSupport { /// recording APIs. abstract class LiveTvDvrSupport { Future> fetchDvrs(); - Future fetchLiveTvServerStatus(); - Future fetchDvr(String dvrId); - Future> createDvr({ - required List devices, - required List lineups, - String? language, - String? country, - String? postalCode, - }); - Future deleteDvr(String dvrId); - Future updateDvrPrefs(String dvrId, Map prefs); - Future attachDeviceToDvr(String dvrId, String deviceId); - Future detachDeviceFromDvr(String dvrId, String deviceId); - Future addLineupToDvr(String dvrId, String lineupUri); - Future removeLineupFromDvr(String dvrId, String lineupUri); Future> reloadGuide(String dvrId); - Future cancelGuideReload(String dvrId); - - Future> fetchGrabbers({String? protocol}); - Future> fetchGrabberDevices(); - Future>> discoverGrabberDevices(); - Future fetchGrabberDevice(String deviceId); - Future addGrabberDevice(String uri, {String? grabberId}); - Future updateGrabberDevice(String deviceId, {bool? enabled, String? title}); - Future deleteGrabberDevice(String deviceId); - Future> fetchGrabberDeviceChannels(String deviceId); - Future> scanGrabberDevice( - String deviceId, { - String? source, - Map prefs = const {}, - String? network, - String? country, - }); - Future cancelGrabberDeviceScan(String deviceId); - Future saveGrabberDeviceChannelMap(String deviceId, MediaGrabberChannelMapRequest request); - Future updateGrabberDevicePrefs(String deviceId, Map prefs); - String buildGrabberDeviceThumbUrl(String deviceId, int version); - - Future> fetchEpgCountries(); - Future> fetchEpgLanguages(); - Future> fetchEpgRegions(String country, String epgId); - Future fetchEpgLineups(String country, String epgId, {String? postalCode, String? region}); - Future> fetchEpgChannelsForLineup(String lineupUri); - Future> fetchEpgChannelsForLineups(List lineupUris); - Future> computeEpgChannelMap({required String deviceUri, required String lineupUri}); - Future?>> findBestLineup({ - required String deviceUri, - required String lineupGroupUri, - }); Future> getSubscriptionTemplate(String guid); Future> fetchRecordingRules({bool includeGrabs = true, bool includeStorage = true}); - Future fetchRecordingRule( - String subscriptionId, { - bool includeGrabs = true, - bool includeStorage = true, - }); Future createRecordingRule(MediaSubscriptionCreateRequest request); Future updateRecordingRule(String subscriptionId, Map prefs); Future deleteRecordingRule(String subscriptionId); - Future moveRecordingRule(String subscriptionId, {String? afterSubscriptionId}); Future processRecordingRules(); Future> fetchScheduledRecordings(); Future cancelGrab(String operationId); @@ -271,13 +212,4 @@ abstract class LiveTvDvrSupport { required List ratingKeys, bool includeStorage = true, }); - - Future> fetchMediaProviders(); - Future registerMediaProvider(String url); - Future refreshMediaProviders(); - Future unregisterMediaProvider(String providerId); - Future> fetchLiveTvSessionsDetailed(); - Future fetchLiveTvSession(String sessionId); - Uri buildNotificationWebSocketUri({List? filters}); - Uri buildNotificationEventSourceUri({List? filters}); } diff --git a/lib/media/media_kind.dart b/lib/media/media_kind.dart index d1d64690..55cb53e0 100644 --- a/lib/media/media_kind.dart +++ b/lib/media/media_kind.dart @@ -26,8 +26,6 @@ enum MediaKind { bool get isVideo => this == movie || this == episode || this == clip; - bool get isShowRelated => this == show || this == season || this == episode; - bool get isMusic => this == artist || this == album || this == track; bool get isPlayable => isVideo || this == track; diff --git a/lib/media/media_server_client.dart b/lib/media/media_server_client.dart index ad323d04..161978b1 100644 --- a/lib/media/media_server_client.dart +++ b/lib/media/media_server_client.dart @@ -266,9 +266,6 @@ abstract class MediaServerClient { /// Free-text search across the user's libraries. Future> searchItems(String query, {int limit = 100}); - /// Recently-added items across all libraries. - Future> fetchRecentlyAdded({int limit = 50}); - /// Items the user has started but not finished. Plex calls this "On Deck" /// internally; the neutral name matches the Continue Watching UI surface. Future> fetchContinueWatching({int? count = 20}); diff --git a/lib/media/media_source_info.dart b/lib/media/media_source_info.dart index 1738f35f..79f3f88a 100644 --- a/lib/media/media_source_info.dart +++ b/lib/media/media_source_info.dart @@ -275,7 +275,6 @@ class MediaMarker { Duration get startTime => Duration(milliseconds: startTimeOffset); Duration get endTime => Duration(milliseconds: endTimeOffset); - bool get isIntro => type == 'intro'; bool get isCredits => type == 'credits'; bool containsPosition(Duration position) { diff --git a/lib/models/download_models.dart b/lib/models/download_models.dart index 50d8ce6c..ee591442 100644 --- a/lib/models/download_models.dart +++ b/lib/models/download_models.dart @@ -34,8 +34,6 @@ sealed class DownloadProgress with _$DownloadProgress { double get progressPercent => progress / 100.0; String get speedFormatted => ByteFormatter.formatSpeed(speed); - String get downloadedFormatted => ByteFormatter.formatBytes(downloadedBytes); - String get totalFormatted => ByteFormatter.formatBytes(totalBytes); bool get hasArtworkPaths => thumbPath != null; } diff --git a/lib/models/plex/plex_home.dart b/lib/models/plex/plex_home.dart index 69924b7b..21f54241 100644 --- a/lib/models/plex/plex_home.dart +++ b/lib/models/plex/plex_home.dart @@ -37,18 +37,4 @@ class PlexHome { Map toJson() => _$PlexHomeToJson(this); PlexHomeUser? get adminUser => users.where((user) => user.admin).firstOrNull; - - List get managedUsers => users.where((user) => !user.admin).toList(); - - List get restrictedUsers => users.where((user) => user.restricted).toList(); - - PlexHomeUser? getUserByUUID(String uuid) { - try { - return users.firstWhere((user) => user.uuid == uuid); - } catch (e) { - return null; - } - } - - bool get hasMultipleUsers => users.length > 1; } diff --git a/lib/models/plex/plex_video_playback_data.dart b/lib/models/plex/plex_video_playback_data.dart index 23febc2d..587fb1f3 100644 --- a/lib/models/plex/plex_video_playback_data.dart +++ b/lib/models/plex/plex_video_playback_data.dart @@ -26,6 +26,4 @@ class PlexVideoPlaybackData { }); bool get hasValidVideoUrl => videoUrl != null && videoUrl!.isNotEmpty; - - bool get hasMediaInfo => mediaInfo != null; } diff --git a/lib/models/seerr/seerr_details.dart b/lib/models/seerr/seerr_details.dart index 19a0c504..f6ff6f26 100644 --- a/lib/models/seerr/seerr_details.dart +++ b/lib/models/seerr/seerr_details.dart @@ -5,45 +5,13 @@ import 'seerr_media.dart'; part 'seerr_details.g.dart'; /// Full movie detail from `GET /movie/{tmdbId}` — the subset the catalog -/// surfaces need (credits, external ids, availability, air status). +/// surfaces need (credits, availability). @JsonSerializable(createToJson: false) class SeerrMovieDetails { - final int id; - final String? title; - final String? overview; - final String? posterPath; - final String? backdropPath; - final String? releaseDate; - - /// Minutes. - final int? runtime; - - /// `Released` / `In Production` / `Post Production` / `Planned` / - /// `Canceled` / `Rumored`. - final String? status; - final double? voteAverage; - final int? voteCount; - final List? genres; final SeerrCredits? credits; - final SeerrExternalIds? externalIds; final SeerrMediaInfo? mediaInfo; - const SeerrMovieDetails({ - required this.id, - this.title, - this.overview, - this.posterPath, - this.backdropPath, - this.releaseDate, - this.runtime, - this.status, - this.voteAverage, - this.voteCount, - this.genres, - this.credits, - this.externalIds, - this.mediaInfo, - }); + const SeerrMovieDetails({this.credits, this.mediaInfo}); factory SeerrMovieDetails.fromJson(Map json) => _$SeerrMovieDetailsFromJson(json); } @@ -51,66 +19,15 @@ class SeerrMovieDetails { /// Full TV detail from `GET /tv/{tmdbId}`. @JsonSerializable(createToJson: false) class SeerrTvDetails { - final int id; - final String? name; - final String? overview; - final String? posterPath; - final String? backdropPath; - final String? firstAirDate; - final List? episodeRunTime; - - /// `Returning Series` / `Ended` / `Canceled` / `In Production` / - /// `Planned` / `Pilot`. - final String? status; - final double? voteAverage; - final int? voteCount; - final List? genres; - final List? networks; - final int? numberOfEpisodes; - final int? numberOfSeasons; final List? seasons; final SeerrCredits? credits; - final SeerrExternalIds? externalIds; final SeerrMediaInfo? mediaInfo; - const SeerrTvDetails({ - required this.id, - this.name, - this.overview, - this.posterPath, - this.backdropPath, - this.firstAirDate, - this.episodeRunTime, - this.status, - this.voteAverage, - this.voteCount, - this.genres, - this.networks, - this.numberOfEpisodes, - this.numberOfSeasons, - this.seasons, - this.credits, - this.externalIds, - this.mediaInfo, - }); + const SeerrTvDetails({this.seasons, this.credits, this.mediaInfo}); factory SeerrTvDetails.fromJson(Map json) => _$SeerrTvDetailsFromJson(json); } -@JsonSerializable(createToJson: false) -class SeerrGenre { - final String? name; - const SeerrGenre({this.name}); - factory SeerrGenre.fromJson(Map json) => _$SeerrGenreFromJson(json); -} - -@JsonSerializable(createToJson: false) -class SeerrNetwork { - final String? name; - const SeerrNetwork({this.name}); - factory SeerrNetwork.fromJson(Map json) => _$SeerrNetworkFromJson(json); -} - /// One TMDB season entry (`TvDetails.seasons[]`). Season 0 is specials. @JsonSerializable(createToJson: false) class SeerrSeason { @@ -141,13 +58,3 @@ class SeerrCastMember { factory SeerrCastMember.fromJson(Map json) => _$SeerrCastMemberFromJson(json); } - -@JsonSerializable(createToJson: false) -class SeerrExternalIds { - final String? imdbId; - final int? tvdbId; - - const SeerrExternalIds({this.imdbId, this.tvdbId}); - - factory SeerrExternalIds.fromJson(Map json) => _$SeerrExternalIdsFromJson(json); -} diff --git a/lib/models/seerr/seerr_details.g.dart b/lib/models/seerr/seerr_details.g.dart index 133bb02b..d0f9afe4 100644 --- a/lib/models/seerr/seerr_details.g.dart +++ b/lib/models/seerr/seerr_details.g.dart @@ -8,27 +8,9 @@ part of 'seerr_details.dart'; SeerrMovieDetails _$SeerrMovieDetailsFromJson(Map json) => SeerrMovieDetails( - id: (json['id'] as num).toInt(), - title: json['title'] as String?, - overview: json['overview'] as String?, - posterPath: json['posterPath'] as String?, - backdropPath: json['backdropPath'] as String?, - releaseDate: json['releaseDate'] as String?, - runtime: (json['runtime'] as num?)?.toInt(), - status: json['status'] as String?, - voteAverage: (json['voteAverage'] as num?)?.toDouble(), - voteCount: (json['voteCount'] as num?)?.toInt(), - genres: (json['genres'] as List?) - ?.map((e) => SeerrGenre.fromJson(e as Map)) - .toList(), credits: json['credits'] == null ? null : SeerrCredits.fromJson(json['credits'] as Map), - externalIds: json['externalIds'] == null - ? null - : SeerrExternalIds.fromJson( - json['externalIds'] as Map, - ), mediaInfo: json['mediaInfo'] == null ? null : SeerrMediaInfo.fromJson(json['mediaInfo'] as Map), @@ -36,48 +18,17 @@ SeerrMovieDetails _$SeerrMovieDetailsFromJson(Map json) => SeerrTvDetails _$SeerrTvDetailsFromJson(Map json) => SeerrTvDetails( - id: (json['id'] as num).toInt(), - name: json['name'] as String?, - overview: json['overview'] as String?, - posterPath: json['posterPath'] as String?, - backdropPath: json['backdropPath'] as String?, - firstAirDate: json['firstAirDate'] as String?, - episodeRunTime: (json['episodeRunTime'] as List?) - ?.map((e) => (e as num).toInt()) - .toList(), - status: json['status'] as String?, - voteAverage: (json['voteAverage'] as num?)?.toDouble(), - voteCount: (json['voteCount'] as num?)?.toInt(), - genres: (json['genres'] as List?) - ?.map((e) => SeerrGenre.fromJson(e as Map)) - .toList(), - networks: (json['networks'] as List?) - ?.map((e) => SeerrNetwork.fromJson(e as Map)) - .toList(), - numberOfEpisodes: (json['numberOfEpisodes'] as num?)?.toInt(), - numberOfSeasons: (json['numberOfSeasons'] as num?)?.toInt(), seasons: (json['seasons'] as List?) ?.map((e) => SeerrSeason.fromJson(e as Map)) .toList(), credits: json['credits'] == null ? null : SeerrCredits.fromJson(json['credits'] as Map), - externalIds: json['externalIds'] == null - ? null - : SeerrExternalIds.fromJson( - json['externalIds'] as Map, - ), mediaInfo: json['mediaInfo'] == null ? null : SeerrMediaInfo.fromJson(json['mediaInfo'] as Map), ); -SeerrGenre _$SeerrGenreFromJson(Map json) => - SeerrGenre(name: json['name'] as String?); - -SeerrNetwork _$SeerrNetworkFromJson(Map json) => - SeerrNetwork(name: json['name'] as String?); - SeerrSeason _$SeerrSeasonFromJson(Map json) => SeerrSeason( seasonNumber: (json['seasonNumber'] as num).toInt(), name: json['name'] as String?, @@ -97,9 +48,3 @@ SeerrCastMember _$SeerrCastMemberFromJson(Map json) => character: json['character'] as String?, profilePath: json['profilePath'] as String?, ); - -SeerrExternalIds _$SeerrExternalIdsFromJson(Map json) => - SeerrExternalIds( - imdbId: json['imdbId'] as String?, - tvdbId: (json['tvdbId'] as num?)?.toInt(), - ); diff --git a/lib/models/user_switch_response.dart b/lib/models/user_switch_response.dart index 25f8ad83..03149da3 100644 --- a/lib/models/user_switch_response.dart +++ b/lib/models/user_switch_response.dart @@ -117,44 +117,4 @@ class UserSwitchResponse { attributionPartner: optString('attributionPartner'), ); } - - Map toJson() { - return { - 'id': id, - 'uuid': uuid, - 'username': username, - 'title': title, - 'email': email, - 'friendlyName': friendlyName, - 'locale': locale, - 'confirmed': confirmed, - 'joinedAt': joinedAt, - 'emailOnlyAuth': emailOnlyAuth, - 'hasPassword': hasPassword, - 'protected': protected, - 'thumb': thumb, - 'authToken': authToken, - 'mailingListActive': mailingListActive, - 'scrobbleTypes': scrobbleTypes, - 'country': country, - 'restricted': restricted, - 'anonymous': anonymous, - 'home': home, - 'guest': guest, - 'homeSize': homeSize, - 'homeAdmin': homeAdmin, - 'maxHomeSize': maxHomeSize, - 'profile': profile.toJson()['profile'], - 'twoFactorEnabled': twoFactorEnabled, - 'backupCodesCreated': backupCodesCreated, - 'attributionPartner': attributionPartner, - }; - } - - String get displayName => friendlyName ?? title; - - bool get isAdminUser => homeAdmin; - bool get isRestrictedUser => restricted; - bool get isGuestUser => guest; - bool get requiresPassword => hasPassword; } diff --git a/lib/navigation/navigation_tabs.dart b/lib/navigation/navigation_tabs.dart index e4a70954..e1653b17 100644 --- a/lib/navigation/navigation_tabs.dart +++ b/lib/navigation/navigation_tabs.dart @@ -21,12 +21,6 @@ class NavigationTab { return NavigationDestination(icon: AppIcon(icon, fill: 1), selectedIcon: AppIcon(icon, fill: 1), label: getLabel()); } - /// Get the index for a tab ID in the visible tabs list - static int indexFor(NavigationTabId id, {required bool isOffline, bool hasLiveTv = false, bool hasExplore = false}) { - final tabs = getVisibleTabs(isOffline: isOffline, hasLiveTv: hasLiveTv, hasExplore: hasExplore); - return tabs.indexWhere((tab) => tab.id == id); - } - /// Get tabs filtered by offline mode and feature availability static List getVisibleTabs({ required bool isOffline, diff --git a/lib/profiles/active_profile_binder.dart b/lib/profiles/active_profile_binder.dart index 32fda426..43c1edd8 100644 --- a/lib/profiles/active_profile_binder.dart +++ b/lib/profiles/active_profile_binder.dart @@ -113,8 +113,6 @@ class ActiveProfileBinder { final Set _plexHomePreVerified = {}; final Set _userInitiatedActivations = {}; - bool get isSwitching => _isSwitching; - @visibleForTesting String? get debugLastBoundProfileId => _lastBoundProfileId; diff --git a/lib/screens/base_media_list_detail_screen.dart b/lib/screens/base_media_list_detail_screen.dart index 63d7a30b..edae8c09 100644 --- a/lib/screens/base_media_list_detail_screen.dart +++ b/lib/screens/base_media_list_detail_screen.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; import '../media/ids.dart'; -import 'package:plezy/widgets/app_icon.dart'; -import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import '../media/media_item.dart'; import '../media/media_playlist.dart'; @@ -133,40 +131,6 @@ abstract class BaseMediaListDetailScreen extends State return []; } - - /// Build standard app bar actions (play, shuffle, delete) - /// Subclasses can override to customize actions - List buildAppBarActions({ - VoidCallback? onDelete, - String? deleteTooltip, - Color? deleteColor, - bool showDelete = true, - }) { - return [ - // Play button - if (items.isNotEmpty) - IconButton( - icon: const AppIcon(Symbols.play_arrow_rounded, fill: 1), - tooltip: t.common.play, - onPressed: playItems, - ), - // Shuffle button - if (items.isNotEmpty) - IconButton( - icon: const AppIcon(Symbols.shuffle_rounded, fill: 1), - tooltip: t.common.shuffle, - onPressed: shufflePlayItems, - ), - // Delete button - if (showDelete && onDelete != null) - IconButton( - icon: const AppIcon(Symbols.delete_rounded, fill: 1), - tooltip: deleteTooltip ?? t.common.delete, - onPressed: onDelete, - color: deleteColor ?? Colors.red, - ), - ]; - } } /// Mixin that provides standard loadItems implementation for media lists diff --git a/lib/screens/playlist/playlist_detail_screen.dart b/lib/screens/playlist/playlist_detail_screen.dart index 3f944684..612b86b5 100644 --- a/lib/screens/playlist/playlist_detail_screen.dart +++ b/lib/screens/playlist/playlist_detail_screen.dart @@ -48,10 +48,7 @@ class PlaylistDetailScreen extends StatefulWidget { } class _PlaylistDetailScreenState extends BaseMediaListDetailScreen - with - StandardItemLoader, - GridFocusNodeMixin, - FocusableDetailScreenMixin { + with GridFocusNodeMixin, FocusableDetailScreenMixin { static const int _pageSize = playlistItemsPageSize; @override @@ -231,11 +228,6 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen> fetchItems() async { - return fetchAllPlaylistItems(mediaClient, widget.playlist.id); - } - @override Future loadItems() async { if (mounted) { @@ -323,11 +315,6 @@ class _PlaylistDetailScreenState extends BaseMediaListDetailScreen t.pinned.equals(false))).go(); } - /// Pin every row whose `cacheKey` matches the SQL `LIKE` [pattern]. Used by - /// backend subclasses that pin by item-shape rather than a single endpoint - /// (e.g. Jellyfin's per-user item rows where the user segment is a - /// wildcard). - Future pinByKeyPattern(String pattern) async { - await (_db.update( - _db.apiCache, - )..where((t) => t.cacheKey.like(pattern))).write(const ApiCacheCompanion(pinned: Value(true))); - } - - Future unpinByKeyPattern(String pattern) async { - await (_db.update( - _db.apiCache, - )..where((t) => t.cacheKey.like(pattern))).write(const ApiCacheCompanion(pinned: Value(false))); - } - - Future hasPinnedMatching(String pattern) async { - final rows = await (_db.select(_db.apiCache)..where((t) => t.cacheKey.like(pattern) & t.pinned.equals(true))).get(); - return rows.isNotEmpty; - } - /// Pull pinned rows for [serverId] and extract the first capture group of /// [keyPattern] from each `cacheKey`. Returns the unique set of captured /// ids — backend subclasses use this to enumerate their pinned items diff --git a/lib/services/companion_remote/remote_auth_service.dart b/lib/services/companion_remote/remote_auth_service.dart index 507f60a2..f4bf1d6d 100644 --- a/lib/services/companion_remote/remote_auth_service.dart +++ b/lib/services/companion_remote/remote_auth_service.dart @@ -388,10 +388,6 @@ class RemoteAuthService { _cachedSecret = null; _cachedSecretKey = null; } - - // Static direction constants for external use - static int get directionHost => _directionHost; - static int get directionClient => _directionClient; } /// Helper for building byte arrays. diff --git a/lib/services/download_artwork_service.dart b/lib/services/download_artwork_service.dart index b807779c..103991a5 100644 --- a/lib/services/download_artwork_service.dart +++ b/lib/services/download_artwork_service.dart @@ -44,13 +44,6 @@ class DownloadArtworkService { return isUsableArtworkFile(file); } - Future hasMissingArtwork(ServerId serverId, Iterable specs) async { - for (final spec in specs) { - if (!await existsUsable(serverId, spec.localKey)) return true; - } - return false; - } - Future ensureArtworkForMetadata(MediaItem metadata, MediaServerClient client) async { final serverId = metadata.serverId; if (serverId == null) return false; diff --git a/lib/services/episode_navigation_service.dart b/lib/services/episode_navigation_service.dart index f455c8d1..322a6e06 100644 --- a/lib/services/episode_navigation_service.dart +++ b/lib/services/episode_navigation_service.dart @@ -36,7 +36,6 @@ class AdjacentEpisodes { bool get hasNext => next != null; bool get hasPrevious => previous != null; bool get isEndConfirmed => nextStatus == QueueNavigationStatus.boundary; - bool get nextLoadFailed => nextStatus == QueueNavigationStatus.failed; } enum _EpisodeQueueAvailability { active, unavailable, failed } diff --git a/lib/services/jellyfin_auth_service.dart b/lib/services/jellyfin_auth_service.dart index 5dac5ee2..03387049 100644 --- a/lib/services/jellyfin_auth_service.dart +++ b/lib/services/jellyfin_auth_service.dart @@ -5,7 +5,6 @@ import 'package:flutter/foundation.dart' show visibleForTesting; import 'package:http/http.dart' as http; import '../connection/connection.dart'; -import '../connection/connection_auth_service.dart'; import '../exceptions/media_server_exceptions.dart'; import '../utils/app_logger.dart'; import '../utils/media_server_http_client.dart'; @@ -45,9 +44,9 @@ class _JellyfinAuthenticationResponse { /// 2. [authenticateByName] (or future Quick Connect equivalent) — exchanges /// credentials for a long-lived access token and returns a built /// [JellyfinConnection] ready to insert into [ConnectionRegistry]. -/// 3. (later) [validate] / [refresh] / [signOut] for the [ConnectionAuthService] -/// contract. -class JellyfinConnectionAuthService implements ConnectionAuthService { +/// 3. (later) [validate] / [refresh] / [signOut] to keep the stored +/// connection current. +class JellyfinConnectionAuthService { JellyfinConnectionAuthService({ required this.clientName, required this.clientVersion, @@ -320,7 +319,8 @@ class JellyfinConnectionAuthService implements ConnectionAuthService { } } - @override + /// Best-effort check that an existing token still works. Returns false on + /// 401/403; throws on transport failures the caller should retry. Future validate(Connection connection) async { if (connection is! JellyfinConnection) return false; final client = _authenticatedClient(connection); @@ -335,7 +335,8 @@ class JellyfinConnectionAuthService implements ConnectionAuthService { } } - @override + /// Re-check the stored token and return the connection with its status + /// updated accordingly. Future refresh(Connection connection) async { if (connection is! JellyfinConnection) return connection; final ok = await validate(connection); @@ -345,7 +346,8 @@ class JellyfinConnectionAuthService implements ConnectionAuthService { return connection.copyWith(status: ConnectionStatus.online, lastAuthenticatedAt: DateTime.now()); } - @override + /// Revoke the token server-side and forget local credentials. The caller + /// is responsible for removing the row from [ConnectionRegistry]. Future signOut(Connection connection) async { if (connection is! JellyfinConnection) return; final client = _authenticatedClient(connection); diff --git a/lib/services/jellyfin_client/parts/browse.dart b/lib/services/jellyfin_client/parts/browse.dart index ecbda98a..f53ddc61 100644 --- a/lib/services/jellyfin_client/parts/browse.dart +++ b/lib/services/jellyfin_client/parts/browse.dart @@ -1189,27 +1189,6 @@ mixin _JellyfinBrowseMethods on MediaServerCacheMixin { return _pagedMediaItems(response.data, offset: offset, requestedSize: pageSize); } - @override - Future> fetchRecentlyAdded({int limit = 50}) async { - // Matches userLibraryApi.getLatestMedia in the Jellyfin SDK. - final response = await _http.get( - '/Users/${_segment(connection.userId)}/Items/Latest', - queryParameters: { - 'Limit': limit.toString(), - 'Fields': _browseFields, - 'IncludeItemTypes': 'Movie,Series,Episode', - ...jellyfinImageQueryParameters, - }, - ); - throwIfHttpError(response); - final data = response.data; - // Latest returns a bare array, not an Items wrapper. - if (data is List) { - return _mapItems(data.whereType>()); - } - return _mapItems(_itemsArray(data)); - } - @override Future> fetchContinueWatching({int? count = 20}) async { final results = await Future.wait([ diff --git a/lib/services/jellyfin_client/parts/metadata_edit.dart b/lib/services/jellyfin_client/parts/metadata_edit.dart index 5afd2356..926cf395 100644 --- a/lib/services/jellyfin_client/parts/metadata_edit.dart +++ b/lib/services/jellyfin_client/parts/metadata_edit.dart @@ -43,13 +43,6 @@ mixin _JellyfinMetadataEditMethods on MediaServerCacheMixin { return data is Map ? data : const {}; } - Future>> getItemImageInfos(String itemId) async { - final response = await _http.get('/Items/${_segment(itemId)}/Images'); - throwIfHttpError(response); - final data = response.data; - return data is List ? data.whereType>().toList() : const >[]; - } - Future downloadRemoteImage(String itemId, {required String imageType, required String imageUrl}) async { final response = await _http.post( '/Items/${_segment(itemId)}/RemoteImages/Download', diff --git a/lib/services/macos_window_service.dart b/lib/services/macos_window_service.dart index 701525eb..9f7f413a 100644 --- a/lib/services/macos_window_service.dart +++ b/lib/services/macos_window_service.dart @@ -110,10 +110,6 @@ class MacOSWindowService { } } - static void removeWindowDelegate(MacOSWindowDelegate delegate) { - _delegates.remove(delegate); - } - static Future setTrafficLightsVisible(bool visible) => _invoke('setTrafficLightsVisible', {'visible': visible}); static Future syncWindowChrome() => _invoke('syncWindowChrome'); diff --git a/lib/services/multi_server_manager.dart b/lib/services/multi_server_manager.dart index 5714618d..38b065f5 100644 --- a/lib/services/multi_server_manager.dart +++ b/lib/services/multi_server_manager.dart @@ -97,10 +97,10 @@ class MultiServerManager { /// Map of serverId to active optimization futures final Map> _activeOptimizations = {}; - /// Per-server clientIdentifier. Plex servers added via [addPlexAccount] - /// register their owning account's clientIdentifier here so reconnects + - /// endpoint optimization use the right identity (each account has its own - /// device row on plex.tv). + /// Per-server clientIdentifier. Plex servers added via + /// [refreshTokensForProfile] register their owning account's + /// clientIdentifier here so reconnects + endpoint optimization use the + /// right identity (each account has its own device row on plex.tv). final Map _clientIdByServer = {}; final Map _plexScopeByServer = {}; @@ -499,57 +499,6 @@ class MultiServerManager { } } - /// Connect every server attached to a Plex account in parallel. Each - /// account has its own `clientIdentifier` (registered as a separate - /// device on plex.tv), and we keep that mapping per-server in - /// [_clientIdByServer] so subsequent reconnects + endpoint optimization - /// race connections from the right identity. - Future addPlexAccount( - PlexAccountConnection connection, { - required String profileId, - Duration timeout = MediaServerTimeouts.perServerConnect, - Function(ServerId serverId, bool success)? onServerStatus, - }) async { - if (connection.servers.isEmpty) return 0; - appLogger.i( - 'Connecting Plex account ${connection.accountLabel} ' - '(${connection.servers.length} server${connection.servers.length == 1 ? '' : 's'})', - ); - - int connected = 0; - final futures = connection.servers.map((server) async { - final serverId = server.clientIdentifier; - final profileScopeId = buildPlexProfileScopeId(serverId: ServerId(serverId), profileId: profileId); - _clientIdByServer[serverId] = connection.clientIdentifier; - _plexServers[serverId] = server; - _plexScopeByServer[serverId] = profileScopeId; - try { - final client = await _createClientForServer( - server: server, - clientIdentifier: connection.clientIdentifier, - profileScopeId: profileScopeId, - ).namedTimeout(timeout, operation: 'connect to ${server.name}'); - final oldClient = _clients[serverId]; - if (oldClient != null) _closeClient(oldClient); - _clients[serverId] = client; - _serverStatus[serverId] = true; - onServerStatus?.call(ServerId(serverId), true); - connected++; - } catch (e, stackTrace) { - appLogger.e('Failed to connect ${server.name}', error: e, stackTrace: stackTrace); - _serverStatus[serverId] = false; - onServerStatus?.call(ServerId(serverId), false); - } - }); - - await Future.wait(futures); - _statusController.add(Map.from(_serverStatus)); - if (connected > 0 && _connectivitySubscription == null) { - _startNetworkMonitoring(); - } - return connected; - } - /// Apply a freshly-fetched [PlexAccountConnection] to the manager, /// rotating per-server access tokens in place when possible. /// diff --git a/lib/services/music/music_playback_service.dart b/lib/services/music/music_playback_service.dart index 6b28526b..02f45b1f 100644 --- a/lib/services/music/music_playback_service.dart +++ b/lib/services/music/music_playback_service.dart @@ -37,9 +37,6 @@ class MusicPlayContext { /// profile switch tears the session down. [notifyListeners] fires only on /// discrete changes (track, status, queue shape, modes) — progress bars /// subscribe to [positionStream] instead. -/// -/// [StubMusicPlaybackService] is registered until the playback engine lands; -/// UI gates transport affordances on [isAvailable]. abstract class MusicPlaybackService extends ChangeNotifier { /// False on the stub — playback affordances should render disabled or /// fall back to a "not supported yet" notice. @@ -155,9 +152,8 @@ abstract class MusicPlaybackService extends ChangeNotifier { Future fetchLyrics(MediaItem track); } -/// No-op placeholder bound while the playback engine is not wired yet (or -/// on platforms where it failed to initialize). Keeps every UI consumer -/// null-safe without per-call-site feature checks. +/// No-op base for test doubles, which override only the members under test. +/// Production always binds `MusicPlaybackServiceImpl`. class StubMusicPlaybackService extends MusicPlaybackService { final ValueNotifier _volumeNotifier = ValueNotifier(100); int _playIntentGeneration = 0; diff --git a/lib/services/music/music_source_resolver.dart b/lib/services/music/music_source_resolver.dart index 4481321e..764a1b48 100644 --- a/lib/services/music/music_source_resolver.dart +++ b/lib/services/music/music_source_resolver.dart @@ -24,9 +24,6 @@ class MusicSource { /// `DirectPlay` / `Transcode` for progress reports. final String? playMethod; - final int selectedMediaIndex; - final String? selectedMediaSourceId; - /// True when [url] points at a downloaded/local copy. final bool isOffline; @@ -41,8 +38,6 @@ class MusicSource { this.headers, this.playSessionId, this.playMethod, - this.selectedMediaIndex = 0, - this.selectedMediaSourceId, this.isOffline = false, this.mediaInfo, this.reportingClient, @@ -93,8 +88,6 @@ class ServerMusicSourceResolver implements MusicSourceResolver { headers: context.streamHeaders, playSessionId: result.playSessionId, playMethod: result.playMethod ?? (result.isTranscoding ? 'Transcode' : 'DirectPlay'), - selectedMediaIndex: result.selectedMediaIndex, - selectedMediaSourceId: result.selectedMediaSourceId, isOffline: result.isOffline, mediaInfo: result.mediaInfo, reportingClient: context.reportingClient, diff --git a/lib/services/play_queue_launcher.dart b/lib/services/play_queue_launcher.dart index 637391fd..21fe222c 100644 --- a/lib/services/play_queue_launcher.dart +++ b/lib/services/play_queue_launcher.dart @@ -28,9 +28,8 @@ export 'media_list_playback_launcher.dart' /// 4. Handling errors with appropriate feedback /// /// Implements [MediaListPlaybackLauncher.launchFromCollectionOrPlaylist] for -/// the backend-neutral entry point. Plex-only flows such as -/// [launchFromPlaylistItem] live directly on this class because they have no -/// Jellyfin equivalent. +/// the backend-neutral entry point. Flows outside that abstraction, such as +/// [launchFromFolder], live directly on this class. class PlexPlayQueueLauncher extends MediaListPlaybackLauncher { final BuildContext context; final PlexClient client; @@ -153,17 +152,7 @@ class PlexPlayQueueLauncher extends MediaListPlaybackLauncher { ); } - // If the queue is empty, try fetching it again with getPlayQueue - if (playQueue != null && (playQueue.items == null || playQueue.items!.isEmpty)) { - final fetchedQueue = await client.getPlayQueue( - playQueue.playQueueID, - librarySectionID: sourceLibraryId, - librarySectionTitle: sourceLibraryTitle, - ); - if (fetchedQueue != null && fetchedQueue.items != null && fetchedQueue.items!.isNotEmpty) { - playQueue = fetchedQueue; - } - } + playQueue = await _refetchIfEmpty(playQueue, libraryId: sourceLibraryId, libraryTitle: sourceLibraryTitle); // Close loading dialog before navigating to the player await dismissLoading(); @@ -181,40 +170,6 @@ class PlexPlayQueueLauncher extends MediaListPlaybackLauncher { ); } - /// Launch playback from a playlist starting at a specific item. - Future launchFromPlaylistItem({ - required MediaPlaylist playlist, - required MediaItem selectedItem, - bool showLoadingIndicator = true, - }) async { - return executeWithLoading( - context: context, - showLoading: showLoadingIndicator, - actionLabel: t.common.play, - execute: (dismissLoading) async { - // Plex's createPlayQueue takes the metadata `key` (`/library/metadata/{id}`), - // not the bare ratingKey. Construct it from the MediaItem id. - final selectedKey = '/library/metadata/${selectedItem.id}'; - final playQueue = await client.createPlayQueue( - playlistID: int.parse(playlist.id), - type: 'video', - key: selectedKey, - ); - - // Close loading dialog before navigating to the player - await dismissLoading(); - - return _launchFromQueue( - playQueue: playQueue, - ratingKey: playlist.id, - serverId: serverIdOrNull(serverId), - serverName: serverName, - selectedItem: _resolveSelectedMediaItem(playQueue), - ); - }, - ); - } - /// Launch shuffled playback for a show or season. @override Future launchShuffledShow({required MediaItem metadata, bool showLoadingIndicator = true}) async { @@ -287,16 +242,7 @@ class PlexPlayQueueLauncher extends MediaListPlaybackLauncher { librarySectionTitle: libraryTitle, ); - if (playQueue != null && (playQueue.items == null || playQueue.items!.isEmpty)) { - final fetchedQueue = await client.getPlayQueue( - playQueue.playQueueID, - librarySectionID: libraryId, - librarySectionTitle: libraryTitle, - ); - if (fetchedQueue != null && fetchedQueue.items != null && fetchedQueue.items!.isNotEmpty) { - playQueue = fetchedQueue; - } - } + playQueue = await _refetchIfEmpty(playQueue, libraryId: libraryId, libraryTitle: libraryTitle); await dismissLoading(); @@ -312,6 +258,27 @@ class PlexPlayQueueLauncher extends MediaListPlaybackLauncher { ); } + /// Creation sometimes returns a queue without its items; re-read it by ID + /// and keep the refetched copy only when it actually carries items. + Future _refetchIfEmpty( + PlayQueueResponse? playQueue, { + String? libraryId, + String? libraryTitle, + }) async { + if (playQueue == null || (playQueue.items != null && playQueue.items!.isNotEmpty)) { + return playQueue; + } + final fetchedQueue = await client.getPlayQueue( + playQueue.playQueueID, + librarySectionID: libraryId, + librarySectionTitle: libraryTitle, + ); + if (fetchedQueue != null && fetchedQueue.items != null && fetchedQueue.items!.isNotEmpty) { + return fetchedQueue; + } + return playQueue; + } + /// Core method to launch playback from a play queue. Future _launchFromQueue({ required PlayQueueResponse? playQueue, diff --git a/lib/services/playback_context.dart b/lib/services/playback_context.dart index 37fce46e..63666d23 100644 --- a/lib/services/playback_context.dart +++ b/lib/services/playback_context.dart @@ -27,5 +27,4 @@ class PlaybackContext { bool get usesLocalMedia => sourceKind == PlaybackSourceKind.localFile; bool get shouldQueueOnReportFailure => reportingMode == PlaybackReportingMode.onlineWithOfflineFallback; - bool get shouldQueueOnly => reportingMode == PlaybackReportingMode.offlineQueue; } diff --git a/lib/services/plex_api_cache.dart b/lib/services/plex_api_cache.dart index d9dee915..05f4754c 100644 --- a/lib/services/plex_api_cache.dart +++ b/lib/services/plex_api_cache.dart @@ -1,6 +1,7 @@ import '../media/ids.dart'; import 'package:drift/drift.dart'; +import 'package:flutter/foundation.dart'; import '../database/app_database.dart'; import '../database/plex_metadata_recovery.dart'; @@ -73,6 +74,7 @@ class PlexApiCache extends ApiCache { // Rating keys can be alphanumeric, not just numeric. static final RegExp _metadataKeyPattern = RegExp(r'/library/metadata/([^/]+)$'); + @visibleForTesting Future> getPinnedKeys(ServerId serverId) => extractPinnedIds(serverId, _metadataKeyPattern); /// Copy one pinned item between cache namespaces. diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index b6e2adc7..207e2319 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -34,13 +34,8 @@ import '../models/livetv_capture_buffer.dart'; import '../models/livetv_channel.dart'; import '../models/livetv_dvr.dart'; import '../models/livetv_hub_result.dart'; -import '../models/livetv_lineup.dart'; import '../models/livetv_program.dart'; -import '../models/livetv_server_status.dart'; -import '../models/livetv_session.dart'; import '../models/media_grab_operation.dart'; -import '../models/media_grabber_device.dart'; -import '../models/media_provider_info.dart'; import '../models/media_subscription.dart'; import '../models/plex/plex_activity.dart'; import '../models/plex/plex_config.dart'; @@ -1420,18 +1415,6 @@ class PlexClient return results; } - /// Get recently added media (filtered to video content only) - Future> _getRecentlyAdded({int limit = 50}) async { - final response = await _getWithFailover( - '/library/recentlyAdded', - queryParameters: {'X-Plex-Container-Size': limit, 'includeGuids': 1}, - ); - final allItems = _extractMetadataList(response); - - // Filter out music content (artists, albums, tracks) - return allItems.where((item) => !ContentTypes.musicTypes.contains(item.type?.toLowerCase())).toList(); - } - /// Get continue watching items via the hubs system. /// Prefer the provider's dedicated Continue Watching feature key when /// advertised; fall back to Plex Web's legacy hubs query. Both respect the @@ -3463,12 +3446,6 @@ class PlexClient return results.map((m) => PlexMappers.mediaItem(m)).toList(); } - @override - Future> fetchRecentlyAdded({int limit = 50}) async { - final items = await _getRecentlyAdded(limit: limit); - return items.map((m) => PlexMappers.mediaItem(m)).toList(); - } - @override Future> fetchContinueWatching({int? count = 20}) async { final items = await _getContinueWatching(count: count); diff --git a/lib/services/plex_client/parts/live_tv.dart b/lib/services/plex_client/parts/live_tv.dart index 3a183601..d3aa59d2 100644 --- a/lib/services/plex_client/parts/live_tv.dart +++ b/lib/services/plex_client/parts/live_tv.dart @@ -20,6 +20,7 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport Map? queryParameters, // ignore: unused_element_parameter Map? headers, + // ignore: unused_element_parameter Duration? timeout, // ignore: unused_element_parameter AbortController? abort, @@ -197,73 +198,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport return dvrs.isNotEmpty; } - @override - Future fetchLiveTvServerStatus() async { - final response = await _getWithFailover('/'); - final container = _getMediaContainer(response); - return LiveTvServerStatus.fromJson(container ?? const {}); - } - - @override - Future fetchDvr(String dvrId) async { - final response = await _getWithFailover('/livetv/dvrs/$dvrId'); - final container = _getMediaContainer(response); - final rootMappings = container?['ChannelMapping']; - return _extractFirst(response, const ['Dvr'], (json) { - final map = Map.from(json)..putIfAbsent('ChannelMapping', () => rootMappings); - return LiveTvDvr.fromJson(map); - }); - } - - @override - Future> createDvr({ - required List devices, - required List lineups, - String? language, - String? country, - String? postalCode, - }) async { - final response = await _http.post( - '/livetv/dvrs', - queryParameters: { - 'device': devices, - 'lineup': lineups, - ...?(language == null ? null : {'language': language}), - ...?(country == null ? null : {'country': country}), - ...?(postalCode == null ? null : {'postalCode': postalCode}), - }, - timeout: MediaServerTimeouts.receive, - ); - _throwIfFailed(response); - return LiveTvActivityResult( - value: _extractFirst(response, const ['Dvr'], LiveTvDvr.fromJson), - activityUuid: _activityUuid(response), - ); - } - - @override - Future deleteDvr(String dvrId) => _expectOk(() => _http.delete('/livetv/dvrs/$dvrId')); - - @override - Future updateDvrPrefs(String dvrId, Map prefs) => - _expectOk(() => _http.put('/livetv/dvrs/$dvrId/prefs', queryParameters: prefs)); - - @override - Future attachDeviceToDvr(String dvrId, String deviceId) => - _expectOk(() => _http.put('/livetv/dvrs/$dvrId/devices/$deviceId')); - - @override - Future detachDeviceFromDvr(String dvrId, String deviceId) => - _expectOk(() => _http.delete('/livetv/dvrs/$dvrId/devices/$deviceId')); - - @override - Future addLineupToDvr(String dvrId, String lineupUri) => - _expectOk(() => _http.put('/livetv/dvrs/$dvrId/lineups', queryParameters: {'lineup': lineupUri})); - - @override - Future removeLineupFromDvr(String dvrId, String lineupUri) => - _expectOk(() => _http.delete('/livetv/dvrs/$dvrId/lineups', queryParameters: {'lineup': lineupUri})); - @override Future> reloadGuide(String dvrId) async { final response = await _http.post('/livetv/dvrs/$dvrId/reloadGuide', timeout: MediaServerTimeouts.receive); @@ -271,200 +205,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport return LiveTvActivityResult(value: null, activityUuid: _activityUuid(response)); } - @override - Future cancelGuideReload(String dvrId) => _expectOk(() => _http.delete('/livetv/dvrs/$dvrId/reloadGuide')); - - @override - Future> fetchGrabbers({String? protocol}) async { - final response = await _getWithFailover( - '/media/grabbers', - queryParameters: { - ...?(protocol == null ? null : {'protocol': protocol}), - }, - ); - return _extractContainerList(response, const ['MediaGrabber'], MediaGrabber.fromJson); - } - - @override - Future> fetchGrabberDevices() async { - final response = await _getWithFailover('/media/grabbers/devices'); - return _extractContainerList(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson); - } - - @override - Future>> discoverGrabberDevices() async { - final response = await _http.post('/media/grabbers/devices/discover', timeout: MediaServerTimeouts.receive); - _throwIfFailed(response); - return LiveTvActivityResult( - value: _extractContainerList(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson), - activityUuid: _activityUuid(response), - ); - } - - @override - Future fetchGrabberDevice(String deviceId) async { - final response = await _getWithFailover('/media/grabbers/devices/$deviceId'); - return _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson); - } - - @override - Future addGrabberDevice(String uri, {String? grabberId}) async { - final path = grabberId == null ? '/media/grabbers/devices' : '/media/grabbers/$grabberId/devices'; - final response = await _http.post(path, queryParameters: {'uri': uri}); - _throwIfFailed(response); - return _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson); - } - - @override - Future updateGrabberDevice(String deviceId, {bool? enabled, String? title}) => _expectOk( - () => _http.put( - '/media/grabbers/devices/$deviceId', - queryParameters: { - ...?(enabled == null ? null : {'enabled': enabled ? 1 : 0}), - ...?(title == null ? null : {'title': title}), - }, - ), - ); - - @override - Future deleteGrabberDevice(String deviceId) => - _expectOk(() => _http.delete('/media/grabbers/devices/$deviceId')); - - @override - Future> fetchGrabberDeviceChannels(String deviceId) async { - final response = await _getWithFailover('/media/grabbers/devices/$deviceId/channels'); - return _extractContainerList(response, const ['DeviceChannel'], MediaGrabberDeviceChannel.fromJson); - } - - @override - Future> scanGrabberDevice( - String deviceId, { - String? source, - Map prefs = const {}, - String? network, - String? country, - }) async { - final response = await _http.post( - '/media/grabbers/devices/$deviceId/scan', - queryParameters: { - ...?(source == null ? null : {'source': source}), - for (final entry in prefs.entries) 'prefs[${entry.key}]': entry.value, - ...?(network == null ? null : {'network': network}), - ...?(country == null ? null : {'country': country}), - }, - timeout: MediaServerTimeouts.receive, - ); - _throwIfFailed(response); - return LiveTvActivityResult( - value: _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson), - activityUuid: _activityUuid(response), - ); - } - - @override - Future cancelGrabberDeviceScan(String deviceId) async { - final response = await _http.delete('/media/grabbers/devices/$deviceId/scan'); - _throwIfFailed(response); - return _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson); - } - - @override - Future saveGrabberDeviceChannelMap( - String deviceId, - MediaGrabberChannelMapRequest request, - ) async { - final response = await _http.put( - '/media/grabbers/devices/$deviceId/channelmap', - queryParameters: { - if (request.channelsEnabled.isNotEmpty) 'channelsEnabled': request.channelsEnabled.join(','), - for (final entry in request.channelMapping.entries) 'channelMapping[${entry.key}]': entry.value, - for (final entry in request.channelMappingByKey.entries) 'channelMappingByKey[${entry.key}]': entry.value, - }, - ); - _throwIfFailed(response); - return _extractFirst(response, const ['Device', 'Devices'], MediaGrabberDevice.fromJson); - } - - @override - Future updateGrabberDevicePrefs(String deviceId, Map prefs) => - _expectOk(() => _http.put('/media/grabbers/devices/$deviceId/prefs', queryParameters: prefs)); - - @override - String buildGrabberDeviceThumbUrl(String deviceId, int version) => - '${config.baseUrl}/media/grabbers/devices/$deviceId/thumb/$version'.withPlexToken(config.token); - - @override - Future> fetchEpgCountries() async { - final response = await _getWithFailover('/livetv/epg/countries'); - return _extractContainerList(response, const ['Country'], LiveTvCountry.fromJson); - } - - @override - Future> fetchEpgLanguages() async { - final response = await _getWithFailover('/livetv/epg/languages'); - return _extractContainerList(response, const ['Language'], LiveTvLanguage.fromJson); - } - - @override - Future> fetchEpgRegions(String country, String epgId) async { - final response = await _getWithFailover('/livetv/epg/countries/$country/$epgId/regions'); - return _extractContainerList(response, const ['Region'], LiveTvRegion.fromJson); - } - - @override - Future fetchEpgLineups(String country, String epgId, {String? postalCode, String? region}) async { - final path = region == null - ? '/livetv/epg/countries/$country/$epgId/lineups' - : '/livetv/epg/countries/$country/$epgId/regions/$region/lineups'; - final response = await _getWithFailover( - path, - queryParameters: { - ...?(postalCode == null ? null : {'postalCode': postalCode}), - }, - ); - final container = _getMediaContainer(response); - return LiveTvLineupResult( - lineupGroupUuid: container?['uuid'] as String?, - lineups: _extractContainerList(response, const ['Lineup'], LiveTvLineup.fromJson), - ); - } - - @override - Future> fetchEpgChannelsForLineup(String lineupUri) async { - final response = await _getWithFailover('/livetv/epg/channels', queryParameters: {'lineup': lineupUri}); - return _extractContainerList(response, const [ - 'Channel', - ], (json) => LiveTvChannel.fromJson(json).copyWith(serverId: serverId, serverName: serverName)); - } - - @override - Future> fetchEpgChannelsForLineups(List lineupUris) async { - final response = await _getWithFailover('/livetv/epg/lineupchannels', queryParameters: {'lineup': lineupUris}); - return _extractContainerList(response, const ['Lineup'], LiveTvLineup.fromJson); - } - - @override - Future> computeEpgChannelMap({required String deviceUri, required String lineupUri}) async { - final response = await _getWithFailover( - '/livetv/epg/channelmap', - queryParameters: {'device': deviceUri, 'lineup': lineupUri}, - ); - return _extractContainerList(response, const ['ChannelMapping'], ChannelMapping.fromJson); - } - - @override - Future?>> findBestLineup({ - required String deviceUri, - required String lineupGroupUri, - }) async { - final response = await _getWithFailover( - '/livetv/epg/lineup', - queryParameters: {'device': deviceUri, 'lineupGroup': lineupGroupUri}, - timeout: MediaServerTimeouts.receive, - ); - return LiveTvActivityResult(value: _getMediaContainer(response), activityUuid: _activityUuid(response)); - } - /// Get EPG channels using provider lineup endpoints (matches official Plex web client) Future> getEpgChannels({String? lineup}) async { List parseChannels(MediaServerResponse response) { @@ -744,19 +484,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport return _extractContainerList(response, const ['MediaSubscription'], MediaSubscription.fromJson); } - @override - Future fetchRecordingRule( - String subscriptionId, { - bool includeGrabs = true, - bool includeStorage = true, - }) async { - final response = await _getWithFailover( - '/media/subscriptions/$subscriptionId', - queryParameters: {'includeGrabs': includeGrabs ? 1 : 0, 'includeStorage': includeStorage ? 1 : 0}, - ); - return _extractFirst(response, const ['MediaSubscription'], MediaSubscription.fromJson); - } - @override Future createRecordingRule(MediaSubscriptionCreateRequest request) async { final response = await _http.post(_withQuery('/media/subscriptions', _subscriptionCreateQuery(request))); @@ -778,18 +505,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport Future deleteRecordingRule(String subscriptionId) => _expectOk(() => _http.delete('/media/subscriptions/$subscriptionId')); - @override - Future moveRecordingRule(String subscriptionId, {String? afterSubscriptionId}) async { - final response = await _http.put( - '/media/subscriptions/$subscriptionId/move', - queryParameters: { - ...?(afterSubscriptionId == null ? null : {'after': afterSubscriptionId}), - }, - ); - _throwIfFailed(response); - return _extractFirst(response, const ['MediaSubscription'], MediaSubscription.fromJson); - } - @override Future processRecordingRules() => _expectOk(() => _http.post('/media/subscriptions/process')); @@ -824,23 +539,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport return _extractContainerList(response, const ['MediaSubscription'], MediaSubscription.fromJson); } - @override - Future> fetchMediaProviders() async { - final response = await _getWithFailover('/media/providers'); - return _extractContainerList(response, const ['MediaProvider'], MediaProviderInfo.fromJson); - } - - @override - Future registerMediaProvider(String url) => - _expectOk(() => _http.post('/media/providers', queryParameters: {'url': url})); - - @override - Future refreshMediaProviders() => _expectOk(() => _http.post('/media/providers/refresh')); - - @override - Future unregisterMediaProvider(String providerId) => - _expectOk(() => _http.delete('/media/providers/$providerId')); - /// Tune to a live TV channel. /// /// POSTs to the tune endpoint and extracts metadata, session info, and @@ -1105,53 +803,6 @@ mixin _PlexLiveTvClientMethods on MediaServerCacheMixin implements LiveTvSupport return '${config.baseUrl}$streamPath'.withPlexToken(config.token); } - @override - Future> fetchLiveTvSessionsDetailed() async { - final response = await _getWithFailover('/livetv/sessions'); - return _extractContainerList(response, const [ - 'LiveTVSession', - 'LiveTvSession', - 'Session', - 'Metadata', - ], LiveTvSession.fromJson); - } - - @override - Future fetchLiveTvSession(String sessionId) async { - final response = await _getWithFailover('/livetv/sessions/$sessionId'); - return _extractFirst(response, const [ - 'LiveTVSession', - 'LiveTvSession', - 'Session', - 'Metadata', - ], LiveTvSession.fromJson); - } - - @override - Uri buildNotificationWebSocketUri({List? filters}) { - final base = Uri.parse(config.baseUrl); - return base.replace( - scheme: base.scheme == 'https' ? 'wss' : 'ws', - path: '/:/websocket/notifications', - queryParameters: { - if (config.token != null) 'X-Plex-Token': config.token!, - if (filters != null) 'filters': filters.join(','), - }, - ); - } - - @override - Uri buildNotificationEventSourceUri({List? filters}) { - final base = Uri.parse(config.baseUrl); - return base.replace( - path: '/:/eventsource/notifications', - queryParameters: { - if (config.token != null) 'X-Plex-Token': config.token!, - if (filters != null) 'filters': filters.join(','), - }, - ); - } - /// Build the source URI for favorite channels: `server://{machineIdentifier}/{providerIdentifier}` @override Future buildFavoriteChannelSource({String? lineup}) async { diff --git a/lib/services/seerr/seerr_client.dart b/lib/services/seerr/seerr_client.dart index bf5cd27e..144475c5 100644 --- a/lib/services/seerr/seerr_client.dart +++ b/lib/services/seerr/seerr_client.dart @@ -145,10 +145,6 @@ class SeerrClient { return SeerrRequest.fromJson(data as Map); } - Future deleteRequest(int requestId) async { - await _request('DELETE', '/request/$requestId'); - } - // ---------- Sonarr / Radarr options (request sheet advanced pickers) ---------- Future> getRadarrServices() => _serviceList('/service/radarr'); diff --git a/lib/services/seerr/seerr_constants.dart b/lib/services/seerr/seerr_constants.dart index 830a2b5c..c7bd7be1 100644 --- a/lib/services/seerr/seerr_constants.dart +++ b/lib/services/seerr/seerr_constants.dart @@ -23,9 +23,7 @@ abstract final class SeerrMediaServerType { /// app checks are named; the full mask is stored on the session untouched. abstract final class SeerrPermission { static const int admin = 2; - static const int manageRequests = 16; static const int request = 32; - static const int autoApprove = 128; static const int request4k = 1024; static const int request4kMovie = 2048; static const int request4kTv = 4096; diff --git a/lib/services/sync_rule_executor.dart b/lib/services/sync_rule_executor.dart index 1551297b..0362abd3 100644 --- a/lib/services/sync_rule_executor.dart +++ b/lib/services/sync_rule_executor.dart @@ -50,8 +50,6 @@ class SyncRuleExecutor { SyncRuleExecutor({required this._database}); - bool get isExecuting => _isExecuting; - /// Execute every enabled sync rule. /// /// The adaptive cooldown (30 min on WiFi/Ethernet, 3 h on cellular) only diff --git a/lib/services/trakt/trakt_constants.dart b/lib/services/trakt/trakt_constants.dart index c758c62c..6afc0304 100644 --- a/lib/services/trakt/trakt_constants.dart +++ b/lib/services/trakt/trakt_constants.dart @@ -59,12 +59,6 @@ enum TraktMediaKind { movie, episode; - static TraktMediaKind? tryFromMediaKindId(String type) => switch (type) { - 'movie' => movie, - 'episode' => episode, - _ => null, - }; - static TraktMediaKind fromName(String name) => values.firstWhere((v) => v.name == name, orElse: () => throw ArgumentError('Unknown TraktMediaKind: $name')); } diff --git a/lib/utils/app_logger.dart b/lib/utils/app_logger.dart index 2a82fee9..c4ee9877 100644 --- a/lib/utils/app_logger.dart +++ b/lib/utils/app_logger.dart @@ -51,10 +51,6 @@ class MemoryLogOutput extends LogOutput { _currentSize = 0; } - static int getCurrentSize() => _currentSize; - - static double getCurrentSizeMB() => _currentSize / (1024 * 1024); - @override void output(OutputEvent event) { // Only print to console — storage is done in MemoryAwareLogPrinter.log() diff --git a/lib/utils/content_utils.dart b/lib/utils/content_utils.dart index ff1b7d3a..4f8e7f66 100644 --- a/lib/utils/content_utils.dart +++ b/lib/utils/content_utils.dart @@ -17,7 +17,6 @@ class ContentTypes { static const Set musicTypes = {artist, album, track}; static const Set videoTypes = {movie, show, season, episode}; - static const Set playableTypes = {movie, episode, clip, track}; } class ContentTypeHelper { diff --git a/lib/utils/continuation_pagination_coordinator.dart b/lib/utils/continuation_pagination_coordinator.dart index b98ddae6..a56f22cc 100644 --- a/lib/utils/continuation_pagination_coordinator.dart +++ b/lib/utils/continuation_pagination_coordinator.dart @@ -37,14 +37,12 @@ class ContinuationPaginationCoordinator { bool _disposed = false; bool _isLoading = false; Object? _error; - StackTrace? _errorStackTrace; int? get nextStartIndex => _nextStartIndex; int? get totalCount => _totalCount; bool get hasMore => _nextStartIndex != null; bool get isLoading => _isLoading; Object? get error => _error; - StackTrace? get errorStackTrace => _errorStackTrace; /// Invalidates all prior work, runs [request], and reports whether its result /// still belongs to the current generation. @@ -65,7 +63,6 @@ class ContinuationPaginationCoordinator { _totalCount = totalCount; _nextStartIndex = startIndex < totalCount ? startIndex : null; _error = null; - _errorStackTrace = null; onStateChanged?.call(); } @@ -99,7 +96,6 @@ class ContinuationPaginationCoordinator { _inFlightGeneration = null; _isLoading = false; _error = null; - _errorStackTrace = null; } int _beginGeneration() { @@ -110,7 +106,6 @@ class ContinuationPaginationCoordinator { _inFlightGeneration = null; _isLoading = false; _error = null; - _errorStackTrace = null; if (!_disposed) onStateChanged?.call(); return _generation; } @@ -120,7 +115,6 @@ class ContinuationPaginationCoordinator { Future _loadRemaining(int generation) async { _isLoading = true; _error = null; - _errorStackTrace = null; onStateChanged?.call(); try { @@ -148,7 +142,6 @@ class ContinuationPaginationCoordinator { } catch (exception, stackTrace) { if (!_isCurrent(generation)) return ContinuationLoadStatus.stale; _error = exception; - _errorStackTrace = stackTrace; onError?.call(exception, stackTrace); return ContinuationLoadStatus.failed; } finally { diff --git a/lib/utils/hierarchical_event_mixin.dart b/lib/utils/hierarchical_event_mixin.dart index 3d3a47f9..d2bc0b85 100644 --- a/lib/utils/hierarchical_event_mixin.dart +++ b/lib/utils/hierarchical_event_mixin.dart @@ -23,10 +23,6 @@ mixin HierarchicalEventMixin { /// Check if this event affects a specific item by id. bool affectsItem(String itemId) => this.itemId == itemId || parentChain.contains(itemId); - /// Check if this event affects a specific globalKey. - bool affectsGlobalKey(String globalKey) => - this.globalKey == globalKey || parentChain.any((pk) => buildGlobalKey(serverId, pk) == globalKey); - /// Check if this event affects any item in a collection. bool affectsAnyOf(Iterable itemIds) { if (itemIds.contains(itemId)) return true; diff --git a/lib/utils/layout_constants.dart b/lib/utils/layout_constants.dart index ebfc3ba3..002a101b 100644 --- a/lib/utils/layout_constants.dart +++ b/lib/utils/layout_constants.dart @@ -31,7 +31,6 @@ class ScreenBreakpoints { /// Animation and notification durations. class AppDurations { - static const Duration animFast = Duration(milliseconds: 200); static const Duration animMedium = Duration(milliseconds: 300); static const Duration animSlow = Duration(milliseconds: 500); static const Duration snackBarDefault = Duration(seconds: 3); diff --git a/lib/utils/media_image_helper.dart b/lib/utils/media_image_helper.dart index b430d4da..d7d30b8e 100644 --- a/lib/utils/media_image_helper.dart +++ b/lib/utils/media_image_helper.dart @@ -334,31 +334,4 @@ class MediaImageHelper { return true; } - - /// Optimized URL for clear-logo overlays ([ImageType.logo]). - static String logoUrl({ - required MediaServerClient? client, - required String? thumbPath, - required BuildContext context, - required double containerWidth, - required double containerHeight, - }) => _typedUrl(client, thumbPath, context, containerWidth, containerHeight, ImageType.logo); - - static String _typedUrl( - MediaServerClient? client, - String? thumbPath, - BuildContext context, - double containerWidth, - double containerHeight, - ImageType type, - ) { - return getOptimizedImageUrl( - client: client, - thumbPath: thumbPath, - maxWidth: containerWidth, - maxHeight: containerHeight, - devicePixelRatio: effectiveDevicePixelRatio(context), - imageType: type, - ); - } } diff --git a/lib/utils/platform_detector.dart b/lib/utils/platform_detector.dart index 663dbff8..17320db9 100644 --- a/lib/utils/platform_detector.dart +++ b/lib/utils/platform_detector.dart @@ -112,8 +112,6 @@ class TvDetectionService { bool get isTV => _isTV; - List get tvDetectionReasons => _effectiveDetectionReasons; - List get _effectiveDetectionReasons { final reasons = [..._detectionReasons]; if (_forceTv && !reasons.contains('force_tv')) reasons.add('force_tv'); diff --git a/lib/watch_together/providers/watch_together_provider.dart b/lib/watch_together/providers/watch_together_provider.dart index 644d898e..79df6f7c 100644 --- a/lib/watch_together/providers/watch_together_provider.dart +++ b/lib/watch_together/providers/watch_together_provider.dart @@ -155,11 +155,6 @@ class WatchTogetherProvider with ChangeNotifier { bool get hasCurrentPlayback => currentMediaRatingKey != null && currentMediaServerId != null && currentMediaTitle != null; - /// Set the display name for this user - void setDisplayName(String name) { - _displayName = name; - } - String? _buildPlaybackKey(String? ratingKey, ServerId? serverId) { if (ratingKey == null || serverId == null) return null; return '$serverId:$ratingKey'; diff --git a/lib/watch_together/services/guest_playback_reconciler.dart b/lib/watch_together/services/guest_playback_reconciler.dart index da907683..189f07be 100644 --- a/lib/watch_together/services/guest_playback_reconciler.dart +++ b/lib/watch_together/services/guest_playback_reconciler.dart @@ -120,7 +120,6 @@ class GuestPlaybackReconciler { Timer? _statusRefreshTimer; PlaybackState? get latestState => _latestState; - bool get isCorrecting => _correcting; // --------------------------------------------------------------------- // Public inputs diff --git a/lib/watch_together/services/host_playback_coordinator.dart b/lib/watch_together/services/host_playback_coordinator.dart index e9c905d2..18bfe170 100644 --- a/lib/watch_together/services/host_playback_coordinator.dart +++ b/lib/watch_together/services/host_playback_coordinator.dart @@ -117,7 +117,6 @@ class HostPlaybackCoordinator { bool _disposed = false; PlaybackPhase get phase => _phase; - Set get incompatiblePeers => Set.unmodifiable(_incompatiblePeers); // --------------------------------------------------------------------- // Public inputs diff --git a/lib/widgets/hub_section.dart b/lib/widgets/hub_section.dart index 1475720c..2acf8646 100644 --- a/lib/widgets/hub_section.dart +++ b/lib/widgets/hub_section.dart @@ -230,9 +230,6 @@ class HubSectionState extends State with MountedSetStateMixin, Skele }); } - /// Check if this hub currently has focus - bool get hasFocusedItem => _hubFocusNode.hasFocus; - /// Get the number of items in this hub int get itemCount => _totalItemCount; diff --git a/lib/widgets/video_controls/helpers/track_selection_helper.dart b/lib/widgets/video_controls/helpers/track_selection_helper.dart index e541cedf..afe63949 100644 --- a/lib/widgets/video_controls/helpers/track_selection_helper.dart +++ b/lib/widgets/video_controls/helpers/track_selection_helper.dart @@ -2,40 +2,11 @@ import 'package:flutter/material.dart'; import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../i18n/strings.g.dart'; -import '../../../mpv/mpv.dart'; import '../../../theme/mono_tokens.dart'; import '../../../utils/track_label_builder.dart'; import '../../../widgets/focusable_list_tile.dart'; class TrackSelectionHelper { - /// Get the appropriate empty message based on track type - static String getEmptyMessage() { - if (T == SubtitleTrack) { - return t.videoControls.noSubtitlesAvailable; - } else if (T == AudioTrack) { - return t.videoControls.noAudioTracksAvailable; - } - return t.videoControls.noTracksAvailable; - } - - static Widget buildEmptyState() { - return Center(child: Text(getEmptyMessage())); - } - - /// Check if "Off" is selected for a track - static bool isOffSelected(T? selectedTrack, bool Function(T track)? isOffTrack) { - return selectedTrack == null || (isOffTrack?.call(selectedTrack) ?? false); - } - - static String getTrackId(T track) { - if (track is AudioTrack) { - return track.id; - } else if (track is SubtitleTrack) { - return track.id; - } - return ''; - } - static Widget buildOffTile({ required BuildContext context, required bool isSelected, diff --git a/lib/widgets/video_controls/player_chrome_controller.dart b/lib/widgets/video_controls/player_chrome_controller.dart index 6e400647..1e6529e2 100644 --- a/lib/widgets/video_controls/player_chrome_controller.dart +++ b/lib/widgets/video_controls/player_chrome_controller.dart @@ -34,7 +34,6 @@ class PlayerChromeController extends ChangeNotifier implements ValueListenable _controlsPresented; bool get contentStripVisible => _contentStripVisible; - bool get hasVisibleHold => _holds.isNotEmpty; bool isHeld(PlayerChromeHold hold) => _holds.contains(hold); PlayerChromeFocusTarget? get pendingFocusTarget => _pendingFocusTarget; diff --git a/test/models/livetv_flexible_parsing_test.dart b/test/models/livetv_flexible_parsing_test.dart index 39e21c6f..6412ab24 100644 --- a/test/models/livetv_flexible_parsing_test.dart +++ b/test/models/livetv_flexible_parsing_test.dart @@ -1,8 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/models/livetv_dvr.dart'; -import 'package:plezy/models/livetv_lineup.dart'; -import 'package:plezy/models/media_grabber_device.dart'; -import 'package:plezy/models/media_provider_info.dart'; import 'package:plezy/models/media_subscription.dart'; void main() { @@ -28,48 +25,6 @@ void main() { {'uuid': 'device-1'}, ]); - final grabber = MediaGrabberDevice.fromJson({ - 'key': 'device-1', - 'uuid': 'device-1', - 'ChannelMapping': [ - {'channelKey': 7}, - {'channelKey': 'channel-1'}, - ], - 'Setting': [ - {'id': 7}, - {'id': 'setting-1'}, - ], - }); - expect(grabber.channelMappings.map((entry) => entry.channelKey), ['channel-1']); - expect(grabber.settings.map((entry) => entry.id), ['setting-1']); - - final lineup = LiveTvLineup.fromJson({ - 'uuid': 'lineup-1', - 'Channel': [ - {'callSign': 7}, - {'key': 'channel-1', 'callSign': 'ONE'}, - ], - }); - expect(lineup.channels.map((entry) => entry.callSign), ['ONE']); - - final provider = MediaProviderInfo.fromJson({ - 'identifier': 'provider-1', - 'Feature': [ - {'type': 7}, - { - 'type': 'livetv', - 'Directory': [ - 'invalid', - {'key': 'guide'}, - ], - }, - ], - }); - expect(provider.features.map((entry) => entry.type), ['livetv']); - expect(provider.features.single.directories, [ - {'key': 'guide'}, - ]); - final template = SubscriptionTemplate.fromJson({ 'MediaSubscription': [ {'title': 7}, diff --git a/test/services/plex_live_tv_support_test.dart b/test/services/plex_live_tv_support_test.dart index 370d3078..cea40263 100644 --- a/test/services/plex_live_tv_support_test.dart +++ b/test/services/plex_live_tv_support_test.dart @@ -51,8 +51,8 @@ void main() { ); } - http.Response jsonResponse(Map body, {Map? headers}) { - return http.Response(jsonEncode(body), 200, headers: {'content-type': 'application/json', ...?headers}); + http.Response jsonResponse(Map body) { + return http.Response(jsonEncode(body), 200, headers: const {'content-type': 'application/json'}); } test('favorite source follows requested lineup provider', () async { @@ -155,37 +155,6 @@ void main() { expect(dvrs.single.channelMappings.single.enabled, isTrue); }); - test('createDvr sends repeated device and lineup query params and exposes activity id', () async { - late http.Request captured; - final client = makeClient((request) async { - captured = request; - return jsonResponse( - { - 'MediaContainer': { - 'Dvr': [ - {'key': '42', 'uuid': 'dvr-42'}, - ], - }, - }, - headers: {'x-plex-activity': 'activity-1'}, - ); - }); - addTearDown(client.close); - - final result = await client.liveTvDvr!.createDvr( - devices: const ['dev-a', 'dev-b'], - lineups: const ['lineup-a', 'lineup-b'], - language: 'eng', - ); - - expect(captured.url.path, '/livetv/dvrs'); - expect(captured.url.queryParametersAll['device'], ['dev-a', 'dev-b']); - expect(captured.url.queryParametersAll['lineup'], ['lineup-a', 'lineup-b']); - expect(captured.url.queryParameters['language'], 'eng'); - expect(result.activityUuid, 'activity-1'); - expect(result.value?.key, '42'); - }); - test('subscription template parses settings and URL-encoded enum labels', () async { final client = makeClient((request) async { expect(request.url.path, '/media/subscriptions/template');