From 31d2d9dc98ffa5b6e9f75c6260a433191b68f053 Mon Sep 17 00:00:00 2001
From: edde746 <86283021+edde746@users.noreply.github.com>
Date: Sun, 26 Apr 2026 02:34:09 +0200
Subject: [PATCH] feat: jellyfin
---
assets/jellyfin_icon.svg | 4 +
assets/plex_chevron.svg | 3 +
lib/connection/connection.dart | 328 +
lib/connection/connection_auth_service.dart | 20 +
lib/connection/connection_bootstrap.dart | 327 +
lib/connection/connection_registry.dart | 188 +
lib/database/app_database.dart | 344 +-
lib/database/app_database.g.dart | 5325 +++++++++++++++--
lib/database/download_operations.dart | 124 +-
lib/database/tables.dart | 148 +-
lib/exceptions/media_server_exceptions.dart | 105 +
lib/i18n/da.i18n.json | 144 +-
lib/i18n/de.i18n.json | 144 +-
lib/i18n/en.i18n.json | 144 +-
lib/i18n/es.i18n.json | 144 +-
lib/i18n/fr.i18n.json | 144 +-
lib/i18n/it.i18n.json | 144 +-
lib/i18n/ja.i18n.json | 144 +-
lib/i18n/ko.i18n.json | 144 +-
lib/i18n/nb.i18n.json | 144 +-
lib/i18n/nl.i18n.json | 144 +-
lib/i18n/pl.i18n.json | 144 +-
lib/i18n/pt.i18n.json | 144 +-
lib/i18n/ru.i18n.json | 144 +-
lib/i18n/strings.g.dart | 4 +-
lib/i18n/strings_da.g.dart | 528 +-
lib/i18n/strings_de.g.dart | 528 +-
lib/i18n/strings_en.g.dart | 764 ++-
lib/i18n/strings_es.g.dart | 528 +-
lib/i18n/strings_fr.g.dart | 528 +-
lib/i18n/strings_it.g.dart | 528 +-
lib/i18n/strings_ja.g.dart | 528 +-
lib/i18n/strings_ko.g.dart | 528 +-
lib/i18n/strings_nb.g.dart | 528 +-
lib/i18n/strings_nl.g.dart | 528 +-
lib/i18n/strings_pl.g.dart | 528 +-
lib/i18n/strings_pt.g.dart | 528 +-
lib/i18n/strings_ru.g.dart | 528 +-
lib/i18n/strings_sv.g.dart | 528 +-
lib/i18n/strings_zh.g.dart | 528 +-
lib/i18n/sv.i18n.json | 144 +-
lib/i18n/zh.i18n.json | 144 +-
lib/main.dart | 382 +-
lib/media/download_resolution.dart | 53 +
lib/media/library_filter_result.dart | 13 +
lib/media/library_first_character.dart | 16 +
lib/media/library_query.dart | 114 +
lib/media/live_tv_support.dart | 84 +
lib/media/media_backend.dart | 38 +
.../media_file_info.dart} | 88 +-
.../media_filter.dart} | 18 +-
lib/media/media_filter.g.dart | 35 +
lib/media/media_hub.dart | 72 +
lib/media/media_item.dart | 1487 +++++
lib/media/media_item_types.dart | 30 +
lib/media/media_kind.dart | 63 +
lib/media/media_library.dart | 77 +
lib/media/media_part.dart | 37 +
lib/media/media_playlist.dart | 109 +
lib/media/media_role.dart | 9 +
lib/media/media_server_client.dart | 572 ++
lib/media/media_server_user_profile.dart | 53 +
.../plex_sort.dart => media/media_sort.dart} | 14 +-
.../media_sort.g.dart} | 6 +-
.../media_source_info.dart} | 194 +-
lib/media/media_stream.dart | 49 +
lib/media/media_version.dart | 117 +
lib/media/play_queue.dart | 122 +
lib/media/server_capabilities.dart | 219 +
lib/mixins/deletion_aware.dart | 22 +-
lib/mixins/event_aware.dart | 6 +-
lib/mixins/item_updatable.dart | 29 +-
lib/mixins/library_tab_state.dart | 18 +-
lib/mixins/paginated_item_loader.dart | 26 +-
lib/mixins/server_bound_media_mixin.dart | 21 +-
lib/mixins/tab_navigation_mixin.dart | 6 +-
lib/mixins/watch_state_aware.dart | 22 +-
.../jellyfin/jellyfin_user_profile.dart | 50 +
lib/models/livetv_channel.dart | 27 +-
lib/models/livetv_channel.g.dart | 42 +-
lib/models/livetv_dvr.g.dart | 17 +-
lib/models/livetv_hub_result.dart | 4 +-
lib/models/play_queue_response.dart | 79 -
lib/models/play_queue_response.g.dart | 26 -
lib/models/plex/play_queue_response.dart | 46 +
lib/models/{ => plex}/plex_activity.dart | 0
lib/models/{ => plex}/plex_activity.g.dart | 0
lib/models/{ => plex}/plex_config.dart | 0
lib/models/{ => plex}/plex_home.dart | 0
lib/models/{ => plex}/plex_home.g.dart | 0
lib/models/{ => plex}/plex_home_user.dart | 0
lib/models/{ => plex}/plex_home_user.g.dart | 0
lib/models/{ => plex}/plex_match_result.dart | 2 +-
lib/models/plex/plex_match_result.g.dart | 29 +
.../plex_subtitle_search_result.dart | 2 +-
.../plex_subtitle_search_result.g.dart | 20 +-
lib/models/{ => plex}/plex_user_profile.dart | 12 +-
lib/models/plex/plex_user_profile.g.dart | 37 +
.../{ => plex}/plex_video_playback_data.dart | 10 +-
lib/models/plex_filter.g.dart | 38 -
lib/models/plex_first_character.dart | 19 -
lib/models/plex_first_character.g.dart | 21 -
lib/models/plex_hub.dart | 82 -
lib/models/plex_library.dart | 90 -
lib/models/plex_library.g.dart | 34 -
lib/models/plex_match_result.g.dart | 31 -
lib/models/plex_media_version.dart | 151 -
lib/models/plex_media_version.g.dart | 23 -
lib/models/plex_metadata.dart | 580 --
lib/models/plex_metadata.g.dart | 134 -
lib/models/plex_playlist.dart | 158 -
lib/models/plex_playlist.g.dart | 48 -
lib/models/plex_role.dart | 24 -
lib/models/plex_role.g.dart | 27 -
lib/models/plex_user_profile.g.dart | 49 -
lib/models/trackers/tracker_context.dart | 8 +-
lib/models/trakt/trakt_ids.dart | 4 +-
lib/models/user_switch_response.dart | 2 +-
lib/profiles/active_plex_identity.dart | 71 +
lib/profiles/active_profile_binder.dart | 518 ++
lib/profiles/active_profile_provider.dart | 280 +
lib/profiles/plex_home_service.dart | 256 +
lib/profiles/plex_home_switch.dart | 81 +
lib/profiles/profile.dart | 239 +
lib/profiles/profile_activation.dart | 126 +
lib/profiles/profile_avatar.dart | 88 +
lib/profiles/profile_connection.dart | 55 +
lib/profiles/profile_connection_registry.dart | 211 +
lib/profiles/profile_merge.dart | 33 +
lib/profiles/profile_registry.dart | 108 +
lib/profiles/profiles_view.dart | 158 +
lib/providers/companion_remote_provider.dart | 409 +-
lib/providers/download_provider.dart | 838 ++-
lib/providers/libraries_provider.dart | 18 +-
lib/providers/multi_server_provider.dart | 181 +-
lib/providers/offline_mode_provider.dart | 26 +-
lib/providers/offline_watch_provider.dart | 52 +-
lib/providers/playback_state_provider.dart | 137 +-
lib/providers/user_profile_provider.dart | 601 +-
lib/screens/actor_media_screen.dart | 37 +-
lib/screens/auth/plex_pin_auth_flow.dart | 299 +
lib/screens/auth_screen.dart | 608 +-
.../base_media_list_detail_screen.dart | 78 +-
lib/screens/collection_detail_screen.dart | 63 +-
lib/screens/discover_screen.dart | 374 +-
lib/screens/downloads/downloads_screen.dart | 10 +-
lib/screens/downloads/sync_rules_screen.dart | 128 +-
.../focusable_detail_screen_mixin.dart | 23 +-
lib/screens/hub_detail_screen.dart | 85 +-
lib/screens/libraries/alpha_jump_bar.dart | 8 +-
lib/screens/libraries/alpha_jump_helper.dart | 13 +-
.../libraries/alpha_scroll_handle.dart | 4 +-
.../libraries/filters_bottom_sheet.dart | 43 +-
lib/screens/libraries/folder_tree_item.dart | 44 +-
lib/screens/libraries/folder_tree_view.dart | 122 +-
lib/screens/libraries/libraries_screen.dart | 143 +-
.../libraries/library_alpha_bar_strategy.dart | 212 +
.../libraries/library_filter_sort_loader.dart | 43 +
lib/screens/libraries/sort_bottom_sheet.dart | 18 +-
.../libraries/tabs/base_library_tab.dart | 6 +-
.../libraries/tabs/library_browse_tab.dart | 288 +-
.../tabs/library_collections_tab.dart | 19 +-
.../libraries/tabs/library_playlists_tab.dart | 21 +-
.../tabs/library_recommended_tab.dart | 44 +-
lib/screens/livetv/live_tv_actions_mixin.dart | 29 +-
lib/screens/livetv/live_tv_screen.dart | 179 +-
.../livetv/live_tv_show_schedule_screen.dart | 8 +-
lib/screens/livetv/program_details_sheet.dart | 2 +-
.../livetv/reorder_favorites_sheet.dart | 8 +-
lib/screens/livetv/tabs/guide_tab.dart | 46 +-
lib/screens/livetv/tabs/whats_on_tab.dart | 19 +-
lib/screens/main_screen.dart | 386 +-
lib/screens/media_detail_screen.dart | 945 +--
.../playlist/playlist_detail_screen.dart | 247 +-
lib/screens/playlist/playlist_item_card.dart | 46 +-
...tch_screen.dart => plex_match_screen.dart} | 58 +-
...en.dart => plex_metadata_edit_screen.dart} | 224 +-
.../profile/add_local_profile_screen.dart | 132 +
.../profile/borrow_connection_screen.dart | 425 ++
lib/screens/profile/pin_entry_dialog.dart | 19 +
lib/screens/profile/pin_status_row.dart | 40 +
.../profile/profile_detail_screen.dart | 347 ++
lib/screens/profile/profile_list_tile.dart | 125 -
lib/screens/profile/profile_name_field.dart | 31 +
.../profile/profile_switch_screen.dart | 544 +-
lib/screens/profile/user_avatar_widget.dart | 213 -
lib/screens/search_screen.dart | 10 +-
.../settings/add_connection_screen.dart | 165 +
lib/screens/settings/add_jellyfin_screen.dart | 502 ++
.../settings/add_plex_account_screen.dart | 209 +
.../settings/appearance_settings_screen.dart | 8 +-
.../settings/async_form_state_mixin.dart | 57 +
.../settings/connection_persistence.dart | 53 +
lib/screens/settings/logs_screen.dart | 2 +-
lib/screens/settings/settings_screen.dart | 233 +-
.../tracker_library_filter_screen.dart | 8 +-
lib/screens/video_player_screen.dart | 1024 ++--
lib/services/api_cache.dart | 234 +
lib/services/bif_thumbnail_service.dart | 12 +-
.../cached_playback_metadata_service.dart | 112 +
.../companion_remote_peer_service.dart | 246 +-
.../lan_discovery_service.dart | 127 +-
.../companion_remote/remote_auth_context.dart | 21 +
.../companion_remote/remote_auth_service.dart | 69 +-
lib/services/credential_vault.dart | 132 +
lib/services/data_aggregation_service.dart | 479 +-
lib/services/discord_rpc_service.dart | 78 +-
lib/services/download_artwork_helpers.dart | 38 +
lib/services/download_manager_service.dart | 861 ++-
lib/services/download_storage_service.dart | 54 +-
lib/services/episode_navigation_service.dart | 132 +-
lib/services/external_player_service.dart | 22 +-
.../favorite_channels_repository.dart | 55 +
lib/services/file_info_parser.dart | 210 +
lib/services/image_cache_service.dart | 6 +-
lib/services/jellyfin_api_cache.dart | 256 +
lib/services/jellyfin_auth_header.dart | 19 +
lib/services/jellyfin_auth_service.dart | 462 ++
lib/services/jellyfin_client.dart | 2255 +++++++
lib/services/jellyfin_mappers.dart | 525 ++
lib/services/jellyfin_media_info_mapper.dart | 243 +
lib/services/jellyfin_playback_bundle.dart | 45 +
.../jellyfin_sequential_launcher.dart | 197 +
lib/services/jellyfin_trickplay_service.dart | 201 +
lib/services/library_query_translator.dart | 282 +
lib/services/live_session_tracker.dart | 38 +
lib/services/media_controls_manager.dart | 22 +-
.../media_list_playback_launcher.dart | 226 +
lib/services/multi_server_manager.dart | 701 ++-
lib/services/offline_watch_sync_service.dart | 527 +-
lib/services/play_queue_launcher.dart | 261 +-
.../playback_initialization_service.dart | 436 +-
.../playback_initialization_types.dart | 98 +
lib/services/playback_progress_tracker.dart | 194 +-
lib/services/playback_report_session.dart | 253 +
lib/services/playlist_items_loader.dart | 16 +
lib/services/plex_api_cache.dart | 217 +-
lib/services/plex_auth_service.dart | 60 +-
lib/services/plex_client.dart | 1757 ++++--
lib/services/plex_constants.dart | 32 +
lib/services/plex_mappers.dart | 1123 ++++
lib/services/scrub_preview_source.dart | 59 +
.../server_connection_orchestrator.dart | 80 -
lib/services/server_registry.dart | 127 +-
lib/services/settings_export_service.dart | 12 +-
lib/services/storage_service.dart | 258 +-
lib/services/sync_rule_executor.dart | 193 +-
lib/services/track_manager.dart | 83 +-
lib/services/track_selection_service.dart | 193 +-
.../trackers/tracker_coordinator.dart | 27 +-
.../trackers/tracker_id_resolver.dart | 54 +-
lib/services/trakt/trakt_constants.dart | 5 +-
.../trakt/trakt_scrobble_service.dart | 30 +-
lib/services/trakt/trakt_sync_service.dart | 36 +-
lib/services/update_service.dart | 2 +-
lib/services/video_filter_manager.dart | 4 +-
lib/services/watch_next_service.dart | 31 +-
lib/utils/app_logger.dart | 6 +-
lib/utils/connection_constants.dart | 29 -
lib/utils/content_utils.dart | 34 +-
lib/utils/deletion_notifier.dart | 32 +-
lib/utils/download_utils.dart | 53 +-
lib/utils/download_version_utils.dart | 54 +-
lib/utils/episode_collection.dart | 71 +-
lib/utils/error_message_utils.dart | 13 +-
lib/utils/external_ids.dart | 60 +
lib/utils/global_key_utils.dart | 19 +
lib/utils/hierarchical_event_mixin.dart | 16 +-
lib/utils/initials_palette.dart | 32 +
lib/utils/jellyfin_time.dart | 37 +
lib/utils/json_utils.dart | 18 +
lib/utils/live_tv_player_navigation.dart | 114 +-
lib/utils/log_redaction_manager.dart | 55 +-
...ge_helper.dart => media_image_helper.dart} | 140 +-
lib/utils/media_navigation_helper.dart | 122 +-
...ent.dart => media_server_http_client.dart} | 94 +-
lib/utils/media_server_timeouts.dart | 45 +
lib/utils/obfuscation_utils.dart | 15 +
lib/utils/platform_http_client_stub.dart | 2 +-
lib/utils/plex_external_ids.dart | 33 -
lib/utils/plex_http_exception.dart | 56 -
lib/utils/plex_library_section_helpers.dart | 31 +
lib/utils/poll_with_backoff.dart | 39 +
lib/utils/provider_extensions.dart | 128 +-
lib/utils/quality_preset_labels.dart | 32 +-
lib/utils/resolution_label.dart | 20 +
lib/utils/session_identifier.dart | 15 +
lib/utils/url_utils.dart | 15 +
lib/utils/video_player_navigation.dart | 37 +-
lib/utils/watch_state_notifier.dart | 64 +-
.../screens/watch_together_screen.dart | 21 +-
lib/widgets/artwork_picker_dialog.dart | 41 +-
lib/widgets/auth_error_banner.dart | 74 +
lib/widgets/backend_badge.dart | 35 +
.../companion_remote/discovery_view.dart | 43 +-
.../remote_session_dialog.dart | 40 +-
lib/widgets/device_code_dialog.dart | 3 +-
lib/widgets/download_tree_view.dart | 62 +-
lib/widgets/episode_card.dart | 36 +-
lib/widgets/file_info_bottom_sheet.dart | 90 +-
lib/widgets/focusable_media_card.dart | 6 +-
lib/widgets/hub_section.dart | 16 +-
lib/widgets/loading_indicator_box.dart | 18 +
lib/widgets/media_card.dart | 353 +-
lib/widgets/media_context_menu.dart | 759 +--
lib/widgets/oauth_proxy_dialog.dart | 3 +-
..._image.dart => optimized_media_image.dart} | 58 +-
lib/widgets/server_activities_button.dart | 8 +-
lib/widgets/side_navigation_rail.dart | 43 +-
.../desktop_video_controls.dart | 13 +-
.../video_controls/mobile_video_controls.dart | 15 +-
.../models/track_controls_state.dart | 19 +-
.../painters/buffer_range_painter.dart | 4 +-
.../video_controls/sheets/chapter_sheet.dart | 18 +-
.../video_controls/sheets/queue_sheet.dart | 29 +-
.../sheets/subtitle_search_sheet.dart | 29 +-
.../video_controls/sheets/track_sheet.dart | 18 +-
.../sheets/version_quality_sheet.dart | 64 +-
.../video_controls/video_controls.dart | 303 +-
.../video_controls/widgets/content_strip.dart | 50 +-
.../widgets/timeline_slider.dart | 80 +-
.../widgets/track_chapter_controls.dart | 26 +-
.../widgets/video_controls_header.dart | 8 +-
.../widgets/video_timeline_bar.dart | 9 +-
pubspec.lock | 30 +-
pubspec.yaml | 5 +-
scripts/ci_checks.sh | 12 +-
scripts/generate_ducet_ranks.dart | 1 +
slang.yaml | 5 +
.../connection/connection_bootstrap_test.dart | 236 +
test/connection/connection_models_test.dart | 131 +
test/connection/connection_registry_test.dart | 190 +
test/database/app_database_test.dart | 382 +-
test/database/download_operations_test.dart | 119 +
test/media/media_item_test.dart | 204 +
test/media/media_playlist_test.dart | 174 +
test/media/media_sort_test.dart | 80 +
test/mixins/deletion_aware_test.dart | 64 +-
test/mixins/event_aware_test.dart | 104 +-
test/mixins/item_updatable_test.dart | 54 +-
test/mixins/library_tab_state_test.dart | 16 +-
test/mixins/paginated_item_loader_test.dart | 64 +-
.../mixins/server_bound_media_mixin_test.dart | 16 +-
test/mixins/tab_navigation_mixin_test.dart | 26 +
test/mixins/watch_state_aware_test.dart | 75 +-
test/models/livetv_channel_test.dart | 19 +
test/models/plex_media_version_test.dart | 53 +-
test/models/plex_metadata_test.dart | 396 --
test/models/plex_sort_test.dart | 80 -
test/profiles/active_profile_binder_test.dart | 146 +
.../active_profile_provider_test.dart | 174 +
test/profiles/plex_home_service_test.dart | 213 +
.../profile_connection_registry_test.dart | 179 +
test/profiles/profile_registry_test.dart | 125 +
test/profiles/profile_test.dart | 127 +
test/profiles/profiles_view_test.dart | 42 +
.../companion_remote_provider_test.dart | 378 ++
test/providers/download_provider_test.dart | 664 +-
test/providers/libraries_provider_test.dart | 13 +-
.../providers/multi_server_provider_test.dart | 101 +
.../providers/offline_mode_provider_test.dart | 35 +-
.../offline_watch_provider_test.dart | 10 +-
.../playback_state_provider_test.dart | 61 +-
.../providers/user_profile_provider_test.dart | 398 +-
test/screens/auth_screen_test.dart | 64 +
.../downloads/sync_rules_screen_test.dart | 195 +
.../settings/add_jellyfin_screen_test.dart | 49 +
.../data_aggregation_bridge_test.dart | 38 +
.../download_manager_service_test.dart | 161 +
.../download_storage_service_test.dart | 44 +-
.../episode_navigation_service_test.dart | 119 +-
.../favorite_channels_repository_test.dart | 118 +
test/services/file_info_parser_test.dart | 181 +
test/services/jellyfin_api_cache_test.dart | 317 +
test/services/jellyfin_auth_service_test.dart | 553 ++
.../jellyfin_client_failures_test.dart | 139 +
test/services/jellyfin_client_urls_test.dart | 892 +++
.../jellyfin_favorites_isolation_test.dart | 73 +
test/services/jellyfin_mappers_test.dart | 403 ++
test/services/jellyfin_media_info_test.dart | 376 ++
.../jellyfin_playback_bundle_test.dart | 160 +
.../jellyfin_sequential_launcher_test.dart | 534 ++
.../jellyfin_trickplay_service_test.dart | 260 +
.../library_query_translator_test.dart | 276 +
test/services/live_session_tracker_test.dart | 92 +
test/services/multi_server_manager_test.dart | 240 +-
.../offline_watch_sync_service_test.dart | 574 +-
test/services/play_queue_launcher_test.dart | 14 +-
...ack_initialization_offline_cache_test.dart | 320 +
.../playback_progress_tracker_test.dart | 424 +-
.../playback_report_session_test.dart | 175 +
test/services/plex_api_cache_test.dart | 23 +-
test/services/plex_live_tv_support_test.dart | 67 +
test/services/plex_mappers_test.dart | 526 ++
test/services/server_registry_test.dart | 161 +-
.../settings_export_service_test.dart | 20 +-
test/services/storage_service_test.dart | 247 +-
test/services/sync_rule_executor_test.dart | 392 ++
test/services/track_manager_test.dart | 34 +-
.../track_selection_service_test.dart | 187 +-
test/utils/content_utils_test.dart | 38 +-
test/utils/external_ids_test.dart | 55 +
test/utils/log_redaction_manager_test.dart | 51 +-
.../media_server_http_exception_test.dart | 232 +
test/utils/plex_http_exception_test.dart | 111 -
test/widgets/download_tree_view_test.dart | 125 +
test/widgets/media_context_menu_test.dart | 58 +
test/widgets/video_controls_test.dart | 50 +
408 files changed, 56022 insertions(+), 13747 deletions(-)
create mode 100644 assets/jellyfin_icon.svg
create mode 100644 assets/plex_chevron.svg
create mode 100644 lib/connection/connection.dart
create mode 100644 lib/connection/connection_auth_service.dart
create mode 100644 lib/connection/connection_bootstrap.dart
create mode 100644 lib/connection/connection_registry.dart
create mode 100644 lib/exceptions/media_server_exceptions.dart
create mode 100644 lib/media/download_resolution.dart
create mode 100644 lib/media/library_filter_result.dart
create mode 100644 lib/media/library_first_character.dart
create mode 100644 lib/media/library_query.dart
create mode 100644 lib/media/live_tv_support.dart
create mode 100644 lib/media/media_backend.dart
rename lib/{models/plex_file_info.dart => media/media_file_info.dart} (52%)
rename lib/{models/plex_filter.dart => media/media_filter.dart} (57%)
create mode 100644 lib/media/media_filter.g.dart
create mode 100644 lib/media/media_hub.dart
create mode 100644 lib/media/media_item.dart
create mode 100644 lib/media/media_item_types.dart
create mode 100644 lib/media/media_kind.dart
create mode 100644 lib/media/media_library.dart
create mode 100644 lib/media/media_part.dart
create mode 100644 lib/media/media_playlist.dart
create mode 100644 lib/media/media_role.dart
create mode 100644 lib/media/media_server_client.dart
create mode 100644 lib/media/media_server_user_profile.dart
rename lib/{models/plex_sort.dart => media/media_sort.dart} (67%)
rename lib/{models/plex_sort.g.dart => media/media_sort.g.dart} (74%)
rename lib/{models/plex_media_info.dart => media/media_source_info.dart} (57%)
create mode 100644 lib/media/media_stream.dart
create mode 100644 lib/media/media_version.dart
create mode 100644 lib/media/play_queue.dart
create mode 100644 lib/media/server_capabilities.dart
create mode 100644 lib/models/jellyfin/jellyfin_user_profile.dart
delete mode 100644 lib/models/play_queue_response.dart
delete mode 100644 lib/models/play_queue_response.g.dart
create mode 100644 lib/models/plex/play_queue_response.dart
rename lib/models/{ => plex}/plex_activity.dart (100%)
rename lib/models/{ => plex}/plex_activity.g.dart (100%)
rename lib/models/{ => plex}/plex_config.dart (100%)
rename lib/models/{ => plex}/plex_home.dart (100%)
rename lib/models/{ => plex}/plex_home.g.dart (100%)
rename lib/models/{ => plex}/plex_home_user.dart (100%)
rename lib/models/{ => plex}/plex_home_user.g.dart (100%)
rename lib/models/{ => plex}/plex_match_result.dart (96%)
create mode 100644 lib/models/plex/plex_match_result.g.dart
rename lib/models/{ => plex}/plex_subtitle_search_result.dart (97%)
rename lib/models/{ => plex}/plex_subtitle_search_result.g.dart (70%)
rename lib/models/{ => plex}/plex_user_profile.dart (87%)
create mode 100644 lib/models/plex/plex_user_profile.g.dart
rename lib/models/{ => plex}/plex_video_playback_data.dart (80%)
delete mode 100644 lib/models/plex_filter.g.dart
delete mode 100644 lib/models/plex_first_character.dart
delete mode 100644 lib/models/plex_first_character.g.dart
delete mode 100644 lib/models/plex_hub.dart
delete mode 100644 lib/models/plex_library.dart
delete mode 100644 lib/models/plex_library.g.dart
delete mode 100644 lib/models/plex_match_result.g.dart
delete mode 100644 lib/models/plex_media_version.dart
delete mode 100644 lib/models/plex_media_version.g.dart
delete mode 100644 lib/models/plex_metadata.dart
delete mode 100644 lib/models/plex_metadata.g.dart
delete mode 100644 lib/models/plex_playlist.dart
delete mode 100644 lib/models/plex_playlist.g.dart
delete mode 100644 lib/models/plex_role.dart
delete mode 100644 lib/models/plex_role.g.dart
delete mode 100644 lib/models/plex_user_profile.g.dart
create mode 100644 lib/profiles/active_plex_identity.dart
create mode 100644 lib/profiles/active_profile_binder.dart
create mode 100644 lib/profiles/active_profile_provider.dart
create mode 100644 lib/profiles/plex_home_service.dart
create mode 100644 lib/profiles/plex_home_switch.dart
create mode 100644 lib/profiles/profile.dart
create mode 100644 lib/profiles/profile_activation.dart
create mode 100644 lib/profiles/profile_avatar.dart
create mode 100644 lib/profiles/profile_connection.dart
create mode 100644 lib/profiles/profile_connection_registry.dart
create mode 100644 lib/profiles/profile_merge.dart
create mode 100644 lib/profiles/profile_registry.dart
create mode 100644 lib/profiles/profiles_view.dart
create mode 100644 lib/screens/auth/plex_pin_auth_flow.dart
create mode 100644 lib/screens/libraries/library_alpha_bar_strategy.dart
create mode 100644 lib/screens/libraries/library_filter_sort_loader.dart
rename lib/screens/{match_screen.dart => plex_match_screen.dart} (82%)
rename lib/screens/{metadata_edit_screen.dart => plex_metadata_edit_screen.dart} (76%)
create mode 100644 lib/screens/profile/add_local_profile_screen.dart
create mode 100644 lib/screens/profile/borrow_connection_screen.dart
create mode 100644 lib/screens/profile/pin_status_row.dart
create mode 100644 lib/screens/profile/profile_detail_screen.dart
delete mode 100644 lib/screens/profile/profile_list_tile.dart
create mode 100644 lib/screens/profile/profile_name_field.dart
delete mode 100644 lib/screens/profile/user_avatar_widget.dart
create mode 100644 lib/screens/settings/add_connection_screen.dart
create mode 100644 lib/screens/settings/add_jellyfin_screen.dart
create mode 100644 lib/screens/settings/add_plex_account_screen.dart
create mode 100644 lib/screens/settings/async_form_state_mixin.dart
create mode 100644 lib/screens/settings/connection_persistence.dart
create mode 100644 lib/services/api_cache.dart
create mode 100644 lib/services/cached_playback_metadata_service.dart
create mode 100644 lib/services/companion_remote/remote_auth_context.dart
create mode 100644 lib/services/credential_vault.dart
create mode 100644 lib/services/download_artwork_helpers.dart
create mode 100644 lib/services/favorite_channels_repository.dart
create mode 100644 lib/services/file_info_parser.dart
create mode 100644 lib/services/jellyfin_api_cache.dart
create mode 100644 lib/services/jellyfin_auth_header.dart
create mode 100644 lib/services/jellyfin_auth_service.dart
create mode 100644 lib/services/jellyfin_client.dart
create mode 100644 lib/services/jellyfin_mappers.dart
create mode 100644 lib/services/jellyfin_media_info_mapper.dart
create mode 100644 lib/services/jellyfin_playback_bundle.dart
create mode 100644 lib/services/jellyfin_sequential_launcher.dart
create mode 100644 lib/services/jellyfin_trickplay_service.dart
create mode 100644 lib/services/library_query_translator.dart
create mode 100644 lib/services/live_session_tracker.dart
create mode 100644 lib/services/media_list_playback_launcher.dart
create mode 100644 lib/services/playback_initialization_types.dart
create mode 100644 lib/services/playback_report_session.dart
create mode 100644 lib/services/playlist_items_loader.dart
create mode 100644 lib/services/plex_constants.dart
create mode 100644 lib/services/plex_mappers.dart
create mode 100644 lib/services/scrub_preview_source.dart
delete mode 100644 lib/services/server_connection_orchestrator.dart
delete mode 100644 lib/utils/connection_constants.dart
create mode 100644 lib/utils/external_ids.dart
create mode 100644 lib/utils/initials_palette.dart
create mode 100644 lib/utils/jellyfin_time.dart
rename lib/utils/{plex_image_helper.dart => media_image_helper.dart} (69%)
rename lib/utils/{plex_http_client.dart => media_server_http_client.dart} (80%)
create mode 100644 lib/utils/media_server_timeouts.dart
create mode 100644 lib/utils/obfuscation_utils.dart
delete mode 100644 lib/utils/plex_external_ids.dart
delete mode 100644 lib/utils/plex_http_exception.dart
create mode 100644 lib/utils/plex_library_section_helpers.dart
create mode 100644 lib/utils/poll_with_backoff.dart
create mode 100644 lib/utils/resolution_label.dart
create mode 100644 lib/utils/session_identifier.dart
create mode 100644 lib/utils/url_utils.dart
create mode 100644 lib/widgets/auth_error_banner.dart
create mode 100644 lib/widgets/backend_badge.dart
create mode 100644 lib/widgets/loading_indicator_box.dart
rename lib/widgets/{plex_optimized_image.dart => optimized_media_image.dart} (89%)
create mode 100644 test/connection/connection_bootstrap_test.dart
create mode 100644 test/connection/connection_models_test.dart
create mode 100644 test/connection/connection_registry_test.dart
create mode 100644 test/media/media_item_test.dart
create mode 100644 test/media/media_playlist_test.dart
create mode 100644 test/media/media_sort_test.dart
create mode 100644 test/models/livetv_channel_test.dart
delete mode 100644 test/models/plex_metadata_test.dart
delete mode 100644 test/models/plex_sort_test.dart
create mode 100644 test/profiles/active_profile_binder_test.dart
create mode 100644 test/profiles/active_profile_provider_test.dart
create mode 100644 test/profiles/plex_home_service_test.dart
create mode 100644 test/profiles/profile_connection_registry_test.dart
create mode 100644 test/profiles/profile_registry_test.dart
create mode 100644 test/profiles/profile_test.dart
create mode 100644 test/profiles/profiles_view_test.dart
create mode 100644 test/screens/auth_screen_test.dart
create mode 100644 test/screens/downloads/sync_rules_screen_test.dart
create mode 100644 test/screens/settings/add_jellyfin_screen_test.dart
create mode 100644 test/services/data_aggregation_bridge_test.dart
create mode 100644 test/services/download_manager_service_test.dart
create mode 100644 test/services/favorite_channels_repository_test.dart
create mode 100644 test/services/file_info_parser_test.dart
create mode 100644 test/services/jellyfin_api_cache_test.dart
create mode 100644 test/services/jellyfin_auth_service_test.dart
create mode 100644 test/services/jellyfin_client_failures_test.dart
create mode 100644 test/services/jellyfin_client_urls_test.dart
create mode 100644 test/services/jellyfin_favorites_isolation_test.dart
create mode 100644 test/services/jellyfin_mappers_test.dart
create mode 100644 test/services/jellyfin_media_info_test.dart
create mode 100644 test/services/jellyfin_playback_bundle_test.dart
create mode 100644 test/services/jellyfin_sequential_launcher_test.dart
create mode 100644 test/services/jellyfin_trickplay_service_test.dart
create mode 100644 test/services/library_query_translator_test.dart
create mode 100644 test/services/live_session_tracker_test.dart
create mode 100644 test/services/playback_initialization_offline_cache_test.dart
create mode 100644 test/services/playback_report_session_test.dart
create mode 100644 test/services/plex_live_tv_support_test.dart
create mode 100644 test/services/plex_mappers_test.dart
create mode 100644 test/services/sync_rule_executor_test.dart
create mode 100644 test/utils/external_ids_test.dart
create mode 100644 test/utils/media_server_http_exception_test.dart
delete mode 100644 test/utils/plex_http_exception_test.dart
create mode 100644 test/widgets/download_tree_view_test.dart
create mode 100644 test/widgets/media_context_menu_test.dart
create mode 100644 test/widgets/video_controls_test.dart
diff --git a/assets/jellyfin_icon.svg b/assets/jellyfin_icon.svg
new file mode 100644
index 00000000..a5689016
--- /dev/null
+++ b/assets/jellyfin_icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/plex_chevron.svg b/assets/plex_chevron.svg
new file mode 100644
index 00000000..c100a24a
--- /dev/null
+++ b/assets/plex_chevron.svg
@@ -0,0 +1,3 @@
+
diff --git a/lib/connection/connection.dart b/lib/connection/connection.dart
new file mode 100644
index 00000000..2fa709af
--- /dev/null
+++ b/lib/connection/connection.dart
@@ -0,0 +1,328 @@
+import '../media/media_backend.dart';
+import '../models/plex/plex_home_user.dart';
+import '../services/plex_auth_service.dart';
+
+/// Identifier of a backend kind a [Connection] points at. Lighter-weight than
+/// [MediaBackend] for places that only care about persistence/auth shape
+/// (e.g. database column values).
+enum ConnectionKind {
+ plex,
+ jellyfin;
+
+ String get id => switch (this) {
+ ConnectionKind.plex => 'plex',
+ ConnectionKind.jellyfin => 'jellyfin',
+ };
+
+ static ConnectionKind fromId(String id) => switch (id) {
+ 'plex' => ConnectionKind.plex,
+ 'jellyfin' => ConnectionKind.jellyfin,
+ _ => throw ArgumentError('Unknown ConnectionKind id: $id'),
+ };
+
+ MediaBackend get backend => switch (this) {
+ ConnectionKind.plex => MediaBackend.plex,
+ ConnectionKind.jellyfin => MediaBackend.jellyfin,
+ };
+}
+
+/// Health snapshot for a connection. Updated by the orchestrator each time a
+/// session is established or refreshed.
+enum ConnectionStatus { unknown, online, offline, authError, disabled }
+
+/// A media server connection — a unit of authentication the user added.
+///
+/// A `PlexAccountConnection` carries one Plex account + its discovered servers + an
+/// optional active Home profile. A `JellyfinConnection` is a single server +
+/// user. Most users only ever add one connection.
+sealed class Connection {
+ String get id;
+ ConnectionKind get kind;
+ String get displayName;
+ ConnectionStatus get status;
+ DateTime get createdAt;
+ DateTime? get lastAuthenticatedAt;
+
+ /// Backend kind as a [MediaBackend] — for UI that branches on backend
+ /// (badges, etc.). Just a passthrough to [kind.backend].
+ MediaBackend get backend => kind.backend;
+
+ /// Primary label shown in connection-list UIs. Plex shows the active
+ /// profile/account name; Jellyfin shows the server name.
+ String get displayLabel;
+
+ /// Secondary line shown beneath [displayLabel] in connection-list UIs.
+ /// Plex: server count; Jellyfin: `userName · baseUrl`. May be null when
+ /// no useful subtitle exists.
+ String? get displaySubtitle;
+
+ /// Backend-specific config payload, persisted as JSON. Each subclass
+ /// defines the schema.
+ Map toConfigJson();
+}
+
+/// A Plex account connection.
+///
+/// Fields here mirror what [PlexAuthService] gathers during PIN OAuth: an
+/// account token (long-lived), the per-device client identifier (so plex.tv
+/// doesn't see a "new device" each launch), and the optional Home user the
+/// user has switched into.
+class PlexAccountConnection extends Connection {
+ @override
+ final String id;
+
+ @override
+ final ConnectionStatus status;
+
+ @override
+ final DateTime createdAt;
+
+ @override
+ final DateTime? lastAuthenticatedAt;
+
+ /// plex.tv account access token.
+ final String accountToken;
+
+ /// Per-device client identifier. Stable across launches.
+ final String clientIdentifier;
+
+ /// Display name shown for this connection (typically the Plex account email
+ /// or username, fallback "Plex").
+ final String accountLabel;
+
+ /// Active Home user, or `null` for the main account.
+ final PlexHomeUser? activeProfile;
+
+ /// Servers discovered for this account (cached). Populated by the auth
+ /// flow and refreshed periodically.
+ final List servers;
+
+ PlexAccountConnection({
+ required this.id,
+ required this.accountToken,
+ required this.clientIdentifier,
+ required this.accountLabel,
+ this.activeProfile,
+ this.servers = const [],
+ this.status = ConnectionStatus.unknown,
+ required this.createdAt,
+ this.lastAuthenticatedAt,
+ });
+
+ @override
+ ConnectionKind get kind => ConnectionKind.plex;
+
+ @override
+ String get displayName => activeProfile != null && activeProfile!.title.isNotEmpty
+ ? '${activeProfile!.title} · $accountLabel'
+ : accountLabel;
+
+ @override
+ String get displayLabel => displayName;
+
+ @override
+ String? get displaySubtitle => servers.length == 1 ? '1 Plex server' : '${servers.length} Plex servers';
+
+ PlexAccountConnection copyWith({
+ String? id,
+ String? accountToken,
+ String? clientIdentifier,
+ String? accountLabel,
+ PlexHomeUser? activeProfile,
+ bool clearActiveProfile = false,
+ List? servers,
+ ConnectionStatus? status,
+ DateTime? createdAt,
+ DateTime? lastAuthenticatedAt,
+ }) {
+ return PlexAccountConnection(
+ id: id ?? this.id,
+ accountToken: accountToken ?? this.accountToken,
+ clientIdentifier: clientIdentifier ?? this.clientIdentifier,
+ accountLabel: accountLabel ?? this.accountLabel,
+ activeProfile: clearActiveProfile ? null : (activeProfile ?? this.activeProfile),
+ servers: servers ?? this.servers,
+ status: status ?? this.status,
+ createdAt: createdAt ?? this.createdAt,
+ lastAuthenticatedAt: lastAuthenticatedAt ?? this.lastAuthenticatedAt,
+ );
+ }
+
+ @override
+ Map toConfigJson() {
+ return {
+ 'accountToken': accountToken,
+ 'clientIdentifier': clientIdentifier,
+ 'accountLabel': accountLabel,
+ 'activeProfile': activeProfile?.toJson(),
+ 'servers': servers.map((s) => s.toJson()).toList(),
+ };
+ }
+
+ factory PlexAccountConnection.fromConfigJson({
+ required String id,
+ required Map json,
+ required ConnectionStatus status,
+ required DateTime createdAt,
+ DateTime? lastAuthenticatedAt,
+ }) {
+ final profileJson = json['activeProfile'];
+ final activeProfile = profileJson is Map ? PlexHomeUser.fromJson(profileJson) : null;
+ final serversJson = json['servers'];
+ final servers = serversJson is List
+ ? serversJson.whereType