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');