fix: make PlexMetadata type/key/title nullable for folder items

Plex API returns Directory entries without type field, causing
TypeError crash in fromJson. Made fields nullable and migrated
callers to use mediaType enum and displayTitle getter instead
of raw string access.
This commit is contained in:
edde746
2026-03-14 05:40:14 +01:00
parent 642c49b1ea
commit 4f736c555b
27 changed files with 134 additions and 140 deletions
+13 -12
View File
@@ -62,11 +62,11 @@ enum PlexMediaType {
class PlexMetadata with MultiServerFields { class PlexMetadata with MultiServerFields {
@JsonKey(readValue: _readRatingKey) @JsonKey(readValue: _readRatingKey)
final String ratingKey; final String ratingKey;
final String key; final String? key;
final String? guid; final String? guid;
final String? studio; final String? studio;
final String type; final String? type;
final String title; final String? title;
final String? titleSort; final String? titleSort;
final String? contentRating; final String? contentRating;
final String? summary; final String? summary;
@@ -135,7 +135,8 @@ class PlexMetadata with MultiServerFields {
/// Parsed media type enum for type-safe comparisons /// Parsed media type enum for type-safe comparisons
PlexMediaType get mediaType { PlexMediaType get mediaType {
return switch (type.toLowerCase()) { if (type == null) return PlexMediaType.unknown;
return switch (type!.toLowerCase()) {
'movie' => PlexMediaType.movie, 'movie' => PlexMediaType.movie,
'show' => PlexMediaType.show, 'show' => PlexMediaType.show,
'season' => PlexMediaType.season, 'season' => PlexMediaType.season,
@@ -153,11 +154,11 @@ class PlexMetadata with MultiServerFields {
PlexMetadata({ PlexMetadata({
required this.ratingKey, required this.ratingKey,
required this.key, this.key,
this.guid, this.guid,
this.studio, this.studio,
required this.type, this.type,
required this.title, this.title,
this.titleSort, this.titleSort,
this.contentRating, this.contentRating,
this.summary, this.summary,
@@ -366,7 +367,7 @@ class PlexMetadata with MultiServerFields {
// Helper to get the display title (show name for episodes/seasons, title otherwise) // Helper to get the display title (show name for episodes/seasons, title otherwise)
String get displayTitle { String get displayTitle {
final itemType = type.toLowerCase(); final itemType = type?.toLowerCase();
// For episodes and seasons, prefer grandparent title (show name) // For episodes and seasons, prefer grandparent title (show name)
if ((itemType == 'episode' || itemType == 'season') && grandparentTitle != null) { if ((itemType == 'episode' || itemType == 'season') && grandparentTitle != null) {
@@ -376,12 +377,12 @@ class PlexMetadata with MultiServerFields {
if (itemType == 'season' && parentTitle != null) { if (itemType == 'season' && parentTitle != null) {
return parentTitle!; return parentTitle!;
} }
return title; return title ?? '';
} }
// Helper to get the subtitle (episode/season title) // Helper to get the subtitle (episode/season title)
String? get displaySubtitle { String? get displaySubtitle {
final itemType = type.toLowerCase(); final itemType = type?.toLowerCase();
if (itemType == 'episode' || itemType == 'season') { if (itemType == 'episode' || itemType == 'season') {
// If we showed grandparent/parent as title, show this item's title as subtitle // If we showed grandparent/parent as title, show this item's title as subtitle
@@ -401,7 +402,7 @@ class PlexMetadata with MultiServerFields {
/// For movies/shows/seasons in mixed hub context: returns art (16:9 background) /// For movies/shows/seasons in mixed hub context: returns art (16:9 background)
/// For other types: returns thumb /// For other types: returns thumb
String? posterThumb({EpisodePosterMode mode = EpisodePosterMode.seriesPoster, bool mixedHubContext = false}) { String? posterThumb({EpisodePosterMode mode = EpisodePosterMode.seriesPoster, bool mixedHubContext = false}) {
final itemType = type.toLowerCase(); final itemType = type?.toLowerCase();
if (itemType == 'episode') { if (itemType == 'episode') {
switch (mode) { switch (mode) {
@@ -436,7 +437,7 @@ class PlexMetadata with MultiServerFields {
/// Clips (trailers, extras) always use 16:9. /// Clips (trailers, extras) always use 16:9.
/// Movies, shows, and seasons use 16:9 in mixed hub context with episodeThumbnail mode. /// Movies, shows, and seasons use 16:9 in mixed hub context with episodeThumbnail mode.
bool usesWideAspectRatio(EpisodePosterMode mode, {bool mixedHubContext = false}) { bool usesWideAspectRatio(EpisodePosterMode mode, {bool mixedHubContext = false}) {
final itemType = type.toLowerCase(); final itemType = type?.toLowerCase();
// Clips (trailers, extras) are always 16:9 // Clips (trailers, extras) are always 16:9
if (itemType == 'clip') return true; if (itemType == 'clip') return true;
if (itemType == 'episode' && mode == EpisodePosterMode.episodeThumbnail) { if (itemType == 'episode' && mode == EpisodePosterMode.episodeThumbnail) {
+3 -3
View File
@@ -8,11 +8,11 @@ part of 'plex_metadata.dart';
PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata( PlexMetadata _$PlexMetadataFromJson(Map<String, dynamic> json) => PlexMetadata(
ratingKey: _readRatingKey(json, 'ratingKey') as String, ratingKey: _readRatingKey(json, 'ratingKey') as String,
key: json['key'] as String, key: json['key'] as String?,
guid: json['guid'] as String?, guid: json['guid'] as String?,
studio: json['studio'] as String?, studio: json['studio'] as String?,
type: json['type'] as String, type: json['type'] as String?,
title: json['title'] as String, title: json['title'] as String?,
titleSort: json['titleSort'] as String?, titleSort: json['titleSort'] as String?,
contentRating: json['contentRating'] as String?, contentRating: json['contentRating'] as String?,
summary: json['summary'] as String?, summary: json['summary'] as String?,
+10 -10
View File
@@ -515,10 +515,10 @@ class DownloadProvider extends ChangeNotifier {
} }
// We have metadata, check type // We have metadata, check type
final type = meta.type.toLowerCase(); final mt = meta.mediaType;
if (type == 'show') { if (mt == PlexMediaType.show) {
return getAggregateProgressForShow(serverId, ratingKey); return getAggregateProgressForShow(serverId, ratingKey);
} else if (type == 'season') { } else if (mt == PlexMediaType.season) {
return getAggregateProgressForSeason(serverId, ratingKey); return getAggregateProgressForSeason(serverId, ratingKey);
} }
@@ -606,19 +606,19 @@ class DownloadProvider extends ChangeNotifier {
_queueing.add(globalKey); _queueing.add(globalKey);
notifyListeners(); notifyListeners();
final type = metadata.type.toLowerCase(); final mt = metadata.mediaType;
if (type == 'movie' || type == 'episode') { if (mt == PlexMediaType.movie || mt == PlexMediaType.episode) {
// Direct download of a single item // Direct download of a single item
await _queueSingleDownload(metadata, client); await _queueSingleDownload(metadata, client);
return 1; return 1;
} else if (type == 'show') { } else if (mt == PlexMediaType.show) {
// Store show metadata so getProgress() can identify it as a show // Store show metadata so getProgress() can identify it as a show
_metadata[globalKey] = metadata; _metadata[globalKey] = metadata;
// Download all episodes from all seasons // Download all episodes from all seasons
return await _queueShowDownload(metadata, client); return await _queueShowDownload(metadata, client);
} else if (type == 'season') { } else if (mt == PlexMediaType.season) {
// Store season metadata so getProgress() can identify it as a season // Store season metadata so getProgress() can identify it as a season
_metadata[globalKey] = metadata; _metadata[globalKey] = metadata;
@@ -765,11 +765,11 @@ class DownloadProvider extends ChangeNotifier {
/// Used for resuming partial downloads /// Used for resuming partial downloads
/// Returns the number of episodes queued /// Returns the number of episodes queued
Future<int> queueMissingEpisodes(PlexMetadata metadata, PlexClient client) async { Future<int> queueMissingEpisodes(PlexMetadata metadata, PlexClient client) async {
final type = metadata.type.toLowerCase(); final mt = metadata.mediaType;
if (type == 'show') { if (mt == PlexMediaType.show) {
return await _queueMissingShowEpisodes(metadata, client); return await _queueMissingShowEpisodes(metadata, client);
} else if (type == 'season') { } else if (mt == PlexMediaType.season) {
return await _queueMissingSeasonEpisodes(metadata, client); return await _queueMissingSeasonEpisodes(metadata, client);
} else { } else {
throw Exception('queueMissingEpisodes only supports shows/seasons'); throw Exception('queueMissingEpisodes only supports shows/seasons');
+3 -3
View File
@@ -31,7 +31,7 @@ class _CollectionDetailScreenState extends BaseMediaListDetailScreen<CollectionD
PlexMetadata get mediaItem => widget.collection; PlexMetadata get mediaItem => widget.collection;
@override @override
String get title => widget.collection.title; String get title => widget.collection.title!;
@override @override
String get emptyMessage => t.collections.empty; String get emptyMessage => t.collections.empty;
@@ -93,7 +93,7 @@ class _CollectionDetailScreenState extends BaseMediaListDetailScreen<CollectionD
final confirmed = await showDeleteConfirmation( final confirmed = await showDeleteConfirmation(
context, context,
title: t.collections.deleteCollection, title: t.collections.deleteCollection,
message: t.collections.deleteConfirm(title: widget.collection.title), message: t.collections.deleteConfirm(title: widget.collection.displayTitle),
); );
if (!confirmed) return; if (!confirmed) return;
@@ -134,7 +134,7 @@ class _CollectionDetailScreenState extends BaseMediaListDetailScreen<CollectionD
body: CustomScrollView( body: CustomScrollView(
controller: scrollController, controller: scrollController,
slivers: [ slivers: [
CustomAppBar(title: Text(widget.collection.title), actions: buildFocusableAppBarActions()), CustomAppBar(title: Text(widget.collection.title!), actions: buildFocusableAppBarActions()),
...buildStateSlivers(), ...buildStateSlivers(),
if (items.isNotEmpty) if (items.isNotEmpty)
buildFocusableGrid( buildFocusableGrid(
+1 -1
View File
@@ -1263,7 +1263,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
Widget _buildHeroItem(PlexMetadata heroItem, double heroHeight) { Widget _buildHeroItem(PlexMetadata heroItem, double heroHeight) {
final isEpisode = heroItem.isEpisode; final isEpisode = heroItem.isEpisode;
final showName = heroItem.grandparentTitle ?? heroItem.title; final showName = heroItem.grandparentTitle ?? heroItem.displayTitle;
final screenWidth = MediaQuery.of(context).size.width; final screenWidth = MediaQuery.of(context).size.width;
final isLargeScreen = ScreenBreakpoints.isWideTabletOrLarger(screenWidth); final isLargeScreen = ScreenBreakpoints.isWideTabletOrLarger(screenWidth);
+2 -2
View File
@@ -180,7 +180,7 @@ class _HubDetailScreenState extends State<HubDetailScreen>
switch (sortKey) { switch (sortKey) {
case 'titleSort': case 'titleSort':
case 'title': case 'title':
comparison = a.title.compareTo(b.title); comparison = (a.title ?? '').compareTo(b.title ?? '');
break; break;
case 'addedAt': case 'addedAt':
comparison = (a.addedAt ?? 0).compareTo(b.addedAt ?? 0); comparison = (a.addedAt ?? 0).compareTo(b.addedAt ?? 0);
@@ -193,7 +193,7 @@ class _HubDetailScreenState extends State<HubDetailScreen>
comparison = (a.rating ?? 0).compareTo(b.rating ?? 0); comparison = (a.rating ?? 0).compareTo(b.rating ?? 0);
break; break;
default: default:
comparison = a.title.compareTo(b.title); comparison = (a.title ?? '').compareTo(b.title ?? '');
} }
return _isSortDescending ? -comparison : comparison; return _isSortDescending ? -comparison : comparison;
+9 -16
View File
@@ -42,21 +42,14 @@ class FolderTreeItem extends StatelessWidget {
} }
// File icons based on type // File icons based on type
final type = item.type.toLowerCase(); return switch (item.mediaType) {
switch (type) { PlexMediaType.movie => Symbols.movie_rounded,
case 'movie': PlexMediaType.show => Symbols.tv_rounded,
return Symbols.movie_rounded; PlexMediaType.season => Symbols.video_library_rounded,
case 'show': PlexMediaType.episode => Symbols.play_circle_rounded,
return Symbols.tv_rounded; PlexMediaType.collection => Symbols.collections_rounded,
case 'season': _ => Symbols.insert_drive_file_rounded,
return Symbols.video_library_rounded; };
case 'episode':
return Symbols.play_circle_rounded;
case 'collection':
return Symbols.collections_rounded;
default:
return Symbols.insert_drive_file_rounded;
}
} }
void _handleTap() { void _handleTap() {
@@ -104,7 +97,7 @@ class FolderTreeItem extends StatelessWidget {
// Item title // Item title
Expanded( Expanded(
child: Text( child: Text(
item.title, item.displayTitle,
style: TextStyle(fontSize: 14, fontWeight: isFolder ? FontWeight.w500 : FontWeight.w400), style: TextStyle(fontSize: 14, fontWeight: isFolder ? FontWeight.w500 : FontWeight.w400),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
+14 -14
View File
@@ -87,32 +87,32 @@ class _FolderTreeViewState extends State<FolderTreeView> {
Future<void> _loadFolderChildren(PlexMetadata folder) async { Future<void> _loadFolderChildren(PlexMetadata folder) async {
// Already loading this folder // Already loading this folder
if (_loadingFolders.contains(folder.key)) return; if (_loadingFolders.contains(folder.key!)) return;
// Already loaded and cached // Already loaded and cached
if (_childrenCache.containsKey(folder.key)) { if (_childrenCache.containsKey(folder.key!)) {
setState(() { setState(() {
_expandedFolders.add(folder.key); _expandedFolders.add(folder.key!);
}); });
return; return;
} }
setState(() { setState(() {
_loadingFolders.add(folder.key); _loadingFolders.add(folder.key!);
}); });
try { try {
final client = context.getClientForServer(widget.serverId!); final client = context.getClientForServer(widget.serverId!);
// Items are automatically tagged with server info by PlexClient // Items are automatically tagged with server info by PlexClient
final children = await client.getFolderChildren(folder.key); final children = await client.getFolderChildren(folder.key!);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_childrenCache[folder.key] = children; _childrenCache[folder.key!] = children;
_expandedFolders.add(folder.key); _expandedFolders.add(folder.key!);
_loadingFolders.remove(folder.key); _loadingFolders.remove(folder.key!);
}); });
appLogger.d('Loaded ${children.length} children for folder: ${folder.title}'); appLogger.d('Loaded ${children.length} children for folder: ${folder.title}');
@@ -121,7 +121,7 @@ class _FolderTreeViewState extends State<FolderTreeView> {
appLogger.e('Failed to load folder children', error: e); appLogger.e('Failed to load folder children', error: e);
setState(() { setState(() {
_loadingFolders.remove(folder.key); _loadingFolders.remove(folder.key!);
}); });
if (mounted) { if (mounted) {
@@ -131,9 +131,9 @@ class _FolderTreeViewState extends State<FolderTreeView> {
} }
void _toggleFolder(PlexMetadata folder) { void _toggleFolder(PlexMetadata folder) {
if (_expandedFolders.contains(folder.key)) { if (_expandedFolders.contains(folder.key!)) {
setState(() { setState(() {
_expandedFolders.remove(folder.key); _expandedFolders.remove(folder.key!);
}); });
} else { } else {
_loadFolderChildren(folder); _loadFolderChildren(folder);
@@ -147,19 +147,19 @@ class _FolderTreeViewState extends State<FolderTreeView> {
Future<void> _handleFolderPlay(PlexMetadata folder) async { Future<void> _handleFolderPlay(PlexMetadata folder) async {
final client = context.getClientForServer(widget.serverId!); final client = context.getClientForServer(widget.serverId!);
final launcher = PlayQueueLauncher(context: context, client: client, serverId: widget.serverId); final launcher = PlayQueueLauncher(context: context, client: client, serverId: widget.serverId);
await launcher.launchFromFolder(folderKey: folder.key, shuffle: false); await launcher.launchFromFolder(folderKey: folder.key!, shuffle: false);
} }
Future<void> _handleFolderShuffle(PlexMetadata folder) async { Future<void> _handleFolderShuffle(PlexMetadata folder) async {
final client = context.getClientForServer(widget.serverId!); final client = context.getClientForServer(widget.serverId!);
final launcher = PlayQueueLauncher(context: context, client: client, serverId: widget.serverId); final launcher = PlayQueueLauncher(context: context, client: client, serverId: widget.serverId);
await launcher.launchFromFolder(folderKey: folder.key, shuffle: true); await launcher.launchFromFolder(folderKey: folder.key!, shuffle: true);
} }
bool _isFolder(PlexMetadata item) { bool _isFolder(PlexMetadata item) {
// Folders typically don't have a specific type or might have special indicators // Folders typically don't have a specific type or might have special indicators
// Check for common folder indicators // Check for common folder indicators
return item.key.contains('/folder') || item.type.isEmpty || item.type.toLowerCase() == 'folder'; return item.key?.contains('/folder') == true || item.type == null || item.type!.isEmpty || item.mediaType == PlexMediaType.unknown;
} }
List<Widget> _buildTreeItems(List<PlexMetadata> items, int depth, [String parentPath = '']) { List<Widget> _buildTreeItems(List<PlexMetadata> items, int depth, [String parentPath = '']) {
+3 -2
View File
@@ -11,6 +11,7 @@ import '../../../focus/locked_hub_controller.dart';
import '../../../i18n/strings.g.dart'; import '../../../i18n/strings.g.dart';
import '../../../models/livetv_channel.dart'; import '../../../models/livetv_channel.dart';
import '../../../models/livetv_hub_result.dart'; import '../../../models/livetv_hub_result.dart';
import '../../../models/plex_metadata.dart';
import '../../../providers/multi_server_provider.dart'; import '../../../providers/multi_server_provider.dart';
import '../../../providers/settings_provider.dart'; import '../../../providers/settings_provider.dart';
import '../../../services/settings_service.dart' show LibraryDensity; import '../../../services/settings_service.dart' show LibraryDensity;
@@ -168,12 +169,12 @@ class WhatsOnTabState extends State<WhatsOnTab> {
if (entry.program.isCurrentlyAiring && channel != null) { if (entry.program.isCurrentlyAiring && channel != null) {
// Live → play directly // Live → play directly
_tuneChannel(channel); _tuneChannel(channel);
} else if (entry.metadata.type.toLowerCase() == 'show') { } else if (entry.metadata.mediaType == PlexMediaType.show) {
// Show with upcoming episodes → show full schedule // Show with upcoming episodes → show full schedule
Navigator.of(context).push( Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
builder: (_) => LiveTvShowScheduleScreen( builder: (_) => LiveTvShowScheduleScreen(
showTitle: entry.metadata.title, showTitle: entry.metadata.displayTitle,
serverId: entry.metadata.serverId ?? '', serverId: entry.metadata.serverId ?? '',
channels: widget.channels, channels: widget.channels,
), ),
+7 -7
View File
@@ -740,7 +740,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
final confirmed = await showDeleteConfirmation( final confirmed = await showDeleteConfirmation(
context, context,
title: t.downloads.deleteDownload, title: t.downloads.deleteDownload,
message: t.downloads.deleteConfirm(title: metadata.title), message: t.downloads.deleteConfirm(title: metadata.displayTitle),
); );
if (confirmed && context.mounted) { if (confirmed && context.mounted) {
@@ -1579,7 +1579,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
onSecondaryTapDown: (details) => tapPosition = details.globalPosition, onSecondaryTapDown: (details) => tapPosition = details.globalPosition,
onSecondaryTap: () => _showSeasonTabContextMenu(index, position: tapPosition), onSecondaryTap: () => _showSeasonTabContextMenu(index, position: tapPosition),
child: FocusableTabChip( child: FocusableTabChip(
label: season.title, label: season.title!,
isSelected: index == _selectedSeasonIndex, isSelected: index == _selectedSeasonIndex,
focusNode: _seasonTabFocusNodes.length > index ? _seasonTabFocusNodes[index] : null, focusNode: _seasonTabFocusNodes.length > index ? _seasonTabFocusNodes[index] : null,
onSelect: () { onSelect: () {
@@ -2251,11 +2251,11 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
fit: BoxFit.contain, fit: BoxFit.contain,
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
errorBuilder: (context, error, stackTrace) => errorBuilder: (context, error, stackTrace) =>
_buildTitleText(context, metadata.title), _buildTitleText(context, metadata.displayTitle),
); );
} }
// Offline but no local file - show title text // Offline but no local file - show title text
return _buildTitleText(context, metadata.title); return _buildTitleText(context, metadata.displayTitle);
} }
// Online - use network image // Online - use network image
@@ -2280,7 +2280,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
placeholder: (context, url) => Align( placeholder: (context, url) => Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
metadata.title, metadata.displayTitle,
style: Theme.of(context).textTheme.displaySmall?.copyWith( style: Theme.of(context).textTheme.displaySmall?.copyWith(
color: Colors.white.withValues(alpha: 0.3), color: Colors.white.withValues(alpha: 0.3),
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -2293,7 +2293,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
), ),
), ),
errorWidget: (context, url, error) { errorWidget: (context, url, error) {
return _buildTitleText(context, metadata.title); return _buildTitleText(context, metadata.displayTitle);
}, },
), ),
sigma: 10, sigma: 10,
@@ -2304,7 +2304,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
) )
else else
Text( Text(
metadata.title, metadata.displayTitle,
style: Theme.of(context).textTheme.displaySmall?.copyWith( style: Theme.of(context).textTheme.displaySmall?.copyWith(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
+4 -4
View File
@@ -237,9 +237,9 @@ class _PlaylistItemCardState extends State<PlaylistItemCard> {
} }
String _buildSubtitle() { String _buildSubtitle() {
final itemType = widget.item.type.toLowerCase(); final itemType = widget.item.mediaType;
if (itemType == 'episode') { if (itemType == PlexMediaType.episode) {
// For episodes, show "S#E# - Episode Title" // For episodes, show "S#E# - Episode Title"
final season = widget.item.parentIndex; final season = widget.item.parentIndex;
final episode = widget.item.index; final episode = widget.item.index;
@@ -247,7 +247,7 @@ class _PlaylistItemCardState extends State<PlaylistItemCard> {
return 'S${season}E$episode${widget.item.displaySubtitle != null ? ' - ${widget.item.displaySubtitle}' : ''}'; return 'S${season}E$episode${widget.item.displaySubtitle != null ? ' - ${widget.item.displaySubtitle}' : ''}';
} }
return widget.item.displaySubtitle ?? t.discover.tvShow; return widget.item.displaySubtitle ?? t.discover.tvShow;
} else if (itemType == 'movie') { } else if (itemType == PlexMediaType.movie) {
// For movies, show year and edition // For movies, show year and edition
final year = widget.item.year?.toString(); final year = widget.item.year?.toString();
if (year != null && widget.item.editionTitle != null) { if (year != null && widget.item.editionTitle != null) {
@@ -257,6 +257,6 @@ class _PlaylistItemCardState extends State<PlaylistItemCard> {
} }
// Default to type // Default to type
return widget.item.type; return widget.item.mediaType.name;
} }
} }
+2 -2
View File
@@ -1485,7 +1485,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
watchTogether.setCurrentMedia( watchTogether.setCurrentMedia(
ratingKey: targetMetadata.ratingKey, ratingKey: targetMetadata.ratingKey,
serverId: targetMetadata.serverId!, serverId: targetMetadata.serverId!,
mediaTitle: targetMetadata.title, mediaTitle: targetMetadata.displayTitle,
); );
} }
} catch (e) { } catch (e) {
@@ -2898,7 +2898,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
) )
else else
Text( Text(
_nextEpisode!.title, _nextEpisode!.title!,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 14, fontSize: 14,
+6 -6
View File
@@ -351,7 +351,7 @@ class DiscordRPCService {
timestamps: _buildTimestamps(), timestamps: _buildTimestamps(),
statusDisplayType: DiscordStatusDisplayType.details, statusDisplayType: DiscordStatusDisplayType.details,
largeAsset: _cachedThumbnailUrl != null largeAsset: _cachedThumbnailUrl != null
? DiscordAsset(url: _cachedThumbnailUrl!, text: metadata.grandparentTitle ?? metadata.title) ? DiscordAsset(url: _cachedThumbnailUrl!, text: metadata.grandparentTitle ?? metadata.title!)
: null, : null,
), ),
); );
@@ -392,14 +392,14 @@ class DiscordRPCService {
switch (metadata.mediaType) { switch (metadata.mediaType) {
case PlexMediaType.movie: case PlexMediaType.movie:
final year = metadata.year != null ? ' (${metadata.year})' : ''; final year = metadata.year != null ? ' (${metadata.year})' : '';
return metadata.title + year; return metadata.title! + year;
case PlexMediaType.episode: case PlexMediaType.episode:
// Show: "Show Name" or just episode title if no show name // Show: "Show Name" or just episode title if no show name
return metadata.grandparentTitle ?? metadata.title; return metadata.grandparentTitle ?? metadata.title!;
default: default:
return metadata.title; return metadata.title!;
} }
} }
@@ -411,9 +411,9 @@ class DiscordRPCService {
final season = metadata.parentIndex; final season = metadata.parentIndex;
final episode = metadata.index; final episode = metadata.index;
if (season != null && episode != null) { if (season != null && episode != null) {
return 'S$season E$episode - ${metadata.title}'; return 'S$season E$episode - ${metadata.title!}';
} }
return metadata.title; return metadata.title!;
case PlexMediaType.movie: case PlexMediaType.movie:
return metadata.studio; return metadata.studio;
+18 -21
View File
@@ -341,7 +341,7 @@ class DownloadManagerService {
serverId: metadata.serverId!, serverId: metadata.serverId!,
ratingKey: metadata.ratingKey, ratingKey: metadata.ratingKey,
globalKey: globalKey, globalKey: globalKey,
type: metadata.type, type: metadata.type ?? '',
parentRatingKey: metadata.parentRatingKey, parentRatingKey: metadata.parentRatingKey,
grandparentRatingKey: metadata.grandparentRatingKey, grandparentRatingKey: metadata.grandparentRatingKey,
status: DownloadStatus.queued.index, status: DownloadStatus.queued.index,
@@ -446,8 +446,8 @@ class DownloadManagerService {
// Build display name for notifications // Build display name for notifications
final displayName = metadata.type == 'episode' final displayName = metadata.type == 'episode'
? '${metadata.grandparentTitle ?? metadata.title} - ${metadata.title}' ? '${metadata.grandparentTitle ?? metadata.displayTitle} - ${metadata.displayTitle}'
: metadata.title; : metadata.displayTitle;
// Get WiFi-only setting for native enforcement // Get WiFi-only setting for native enforcement
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
@@ -1234,7 +1234,7 @@ class DownloadManagerService {
// Emit initial progress // Emit initial progress
_emitDeletionProgress( _emitDeletionProgress(
DeletionProgress(globalKey: globalKey, itemTitle: metadata.title, currentItem: 0, totalItems: totalItems), DeletionProgress(globalKey: globalKey, itemTitle: metadata.displayTitle, currentItem: 0, totalItems: totalItems),
); );
// Delete files from storage (with progress updates) // Delete files from storage (with progress updates)
@@ -1250,7 +1250,7 @@ class DownloadManagerService {
_emitDeletionProgress( _emitDeletionProgress(
DeletionProgress( DeletionProgress(
globalKey: globalKey, globalKey: globalKey,
itemTitle: metadata.title, itemTitle: metadata.displayTitle,
currentItem: totalItems, currentItem: totalItems,
totalItems: totalItems, totalItems: totalItems,
), ),
@@ -1264,17 +1264,14 @@ class DownloadManagerService {
/// Calculate total items to delete (for progress tracking) /// Calculate total items to delete (for progress tracking)
Future<int> _getTotalItemsToDelete(PlexMetadata metadata, String _) async { Future<int> _getTotalItemsToDelete(PlexMetadata metadata, String _) async {
switch (metadata.type.toLowerCase()) { switch (metadata.mediaType) {
case 'episode': case PlexMediaType.episode:
return 1; // Single episode case PlexMediaType.movie:
case 'movie': return 1;
return 1; // Single movie case PlexMediaType.season:
case 'season':
// Count episodes in season
final episodes = await _database.getEpisodesBySeason(metadata.ratingKey); final episodes = await _database.getEpisodesBySeason(metadata.ratingKey);
return episodes.length; return episodes.length;
case 'show': case PlexMediaType.show:
// Count all episodes in show
final episodes = await _database.getEpisodesByShow(metadata.ratingKey); final episodes = await _database.getEpisodesByShow(metadata.ratingKey);
return episodes.length; return episodes.length;
default: default:
@@ -1301,17 +1298,17 @@ class DownloadManagerService {
} }
// Delete based on type // Delete based on type
switch (metadata.type.toLowerCase()) { switch (metadata.mediaType) {
case 'episode': case PlexMediaType.episode:
await _deleteEpisodeFiles(metadata, serverId); await _deleteEpisodeFiles(metadata, serverId);
break; break;
case 'season': case PlexMediaType.season:
await _deleteSeasonFiles(metadata, serverId); await _deleteSeasonFiles(metadata, serverId);
break; break;
case 'show': case PlexMediaType.show:
await _deleteShowFiles(metadata, serverId); await _deleteShowFiles(metadata, serverId);
break; break;
case 'movie': case PlexMediaType.movie:
await _deleteMovieFiles(metadata, serverId); await _deleteMovieFiles(metadata, serverId);
break; break;
default: default:
@@ -1447,7 +1444,7 @@ class DownloadManagerService {
episodes: episodesInSeason, episodes: episodesInSeason,
serverId: serverId, serverId: serverId,
parentKey: season.ratingKey, parentKey: season.ratingKey,
parentTitle: season.title, parentTitle: season.displayTitle,
); );
final seasonDir = await _storageService.getSeasonDirectory(season, showYear: showYear); final seasonDir = await _storageService.getSeasonDirectory(season, showYear: showYear);
@@ -1510,7 +1507,7 @@ class DownloadManagerService {
episodes: episodesInShow, episodes: episodesInShow,
serverId: serverId, serverId: serverId,
parentKey: show.ratingKey, parentKey: show.ratingKey,
parentTitle: show.title, parentTitle: show.displayTitle,
); );
final showDir = await _storageService.getShowDirectory(show); final showDir = await _storageService.getShowDirectory(show);
+3 -3
View File
@@ -65,7 +65,7 @@ class DownloadStorageService {
String _formatEpisodeFileName(PlexMetadata episode) { String _formatEpisodeFileName(PlexMetadata episode) {
final season = padNumber(episode.parentIndex ?? 0, 2); final season = padNumber(episode.parentIndex ?? 0, 2);
final ep = padNumber(episode.index ?? 0, 2); final ep = padNumber(episode.index ?? 0, 2);
final episodeName = _sanitizeFileName(episode.title); final episodeName = _sanitizeFileName(episode.title!);
return 'S${season}E$ep - $episodeName'; return 'S${season}E$ep - $episodeName';
} }
@@ -246,13 +246,13 @@ class DownloadStorageService {
/// Get the folder name for a movie: "Movie Name (YYYY)" /// Get the folder name for a movie: "Movie Name (YYYY)"
String _getMovieFolderName(PlexMetadata movie) { String _getMovieFolderName(PlexMetadata movie) {
return _formatTitleWithYear(movie.title, movie.year); return _formatTitleWithYear(movie.title!, movie.year);
} }
/// Get the folder name for a TV show: "Show Name (YYYY)" /// Get the folder name for a TV show: "Show Name (YYYY)"
/// [showYear]: Pass explicitly for episodes (episode.year may differ from show's year) /// [showYear]: Pass explicitly for episodes (episode.year may differ from show's year)
String _getShowFolderName(PlexMetadata metadata, {int? showYear}) { String _getShowFolderName(PlexMetadata metadata, {int? showYear}) {
final title = metadata.grandparentTitle ?? metadata.title; final title = metadata.grandparentTitle ?? metadata.title!;
final year = showYear ?? metadata.year; final year = showYear ?? metadata.year;
return _formatTitleWithYear(title, year); return _formatTitleWithYear(title, year);
} }
+1 -1
View File
@@ -53,7 +53,7 @@ class MediaControlsManager {
// Update OS media controls // Update OS media controls
await OsMediaControls.setMetadata( await OsMediaControls.setMetadata(
MediaMetadata( MediaMetadata(
title: metadata.title, title: metadata.title!,
artist: _buildArtist(metadata), artist: _buildArtist(metadata),
artworkUrl: artworkUrl, artworkUrl: artworkUrl,
duration: duration, duration: duration,
+2 -2
View File
@@ -134,7 +134,7 @@ class WatchNextService {
title = item.grandparentTitle!; title = item.grandparentTitle!;
episodeTitle = item.title; episodeTitle = item.title;
} else { } else {
title = item.title; title = item.title!;
episodeTitle = null; episodeTitle = null;
} }
@@ -148,7 +148,7 @@ class WatchNextService {
'episodeTitle': episodeTitle, 'episodeTitle': episodeTitle,
'description': item.summary, 'description': item.summary,
'posterUri': posterUri, 'posterUri': posterUri,
'type': item.type.toLowerCase(), 'type': item.mediaType.name,
'duration': item.duration ?? 0, 'duration': item.duration ?? 0,
'lastPlaybackPosition': item.viewOffset ?? 0, 'lastPlaybackPosition': item.viewOffset ?? 0,
'lastEngagementTime': lastEngagementTime, 'lastEngagementTime': lastEngagementTime,
+1 -1
View File
@@ -86,7 +86,7 @@ String formatContentRating(String? contentRating) {
/// Extension on PlexMetadata for type checking convenience methods /// Extension on PlexMetadata for type checking convenience methods
extension PlexMetadataType on PlexMetadata { extension PlexMetadataType on PlexMetadata {
String get _lowerType => type.toLowerCase(); String get _lowerType => type?.toLowerCase() ?? '';
bool get isShow => _lowerType == ContentTypes.show; bool get isShow => _lowerType == ContentTypes.show;
bool get isMovie => _lowerType == ContentTypes.movie; bool get isMovie => _lowerType == ContentTypes.movie;
+1 -1
View File
@@ -80,7 +80,7 @@ class DeletionNotifier extends BaseNotifier<DeletionEvent> {
ratingKey: metadata.ratingKey, ratingKey: metadata.ratingKey,
serverId: metadata.serverId ?? '', serverId: metadata.serverId ?? '',
parentChain: _buildParentChain(metadata), parentChain: _buildParentChain(metadata),
mediaType: metadata.type, mediaType: metadata.type ?? '',
leafCount: metadata.leafCount ?? 1, leafCount: metadata.leafCount ?? 1,
isDownloadOnly: isDownloadOnly, isDownloadOnly: isDownloadOnly,
), ),
+2 -2
View File
@@ -86,7 +86,7 @@ class WatchStateNotifier extends BaseNotifier<WatchStateEvent> {
serverId: metadata.serverId ?? '', serverId: metadata.serverId ?? '',
changeType: isNowWatched ? WatchStateChangeType.watched : WatchStateChangeType.unwatched, changeType: isNowWatched ? WatchStateChangeType.watched : WatchStateChangeType.unwatched,
parentChain: _buildParentChain(metadata), parentChain: _buildParentChain(metadata),
mediaType: metadata.type, mediaType: metadata.type ?? '',
isNowWatched: isNowWatched, isNowWatched: isNowWatched,
), ),
); );
@@ -103,7 +103,7 @@ class WatchStateNotifier extends BaseNotifier<WatchStateEvent> {
serverId: metadata.serverId ?? '', serverId: metadata.serverId ?? '',
changeType: WatchStateChangeType.progressUpdate, changeType: WatchStateChangeType.progressUpdate,
parentChain: _buildParentChain(metadata), parentChain: _buildParentChain(metadata),
mediaType: metadata.type, mediaType: metadata.type ?? '',
viewOffset: viewOffset, viewOffset: viewOffset,
isNowWatched: isNowWatched, isNowWatched: isNowWatched,
), ),
+2 -2
View File
@@ -145,7 +145,7 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
movies.add( movies.add(
DownloadTreeNode( DownloadTreeNode(
key: globalKey, key: globalKey,
title: meta.title, title: meta.displayTitle,
type: DownloadNodeType.movie, type: DownloadNodeType.movie,
progress: download.progressPercent, progress: download.progressPercent,
status: download.status, status: download.status,
@@ -202,7 +202,7 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
if (meta == null) continue; if (meta == null) continue;
final episodeNumber = meta.index; final episodeNumber = meta.index;
final episodeTitle = episodeNumber != null ? 'Episode $episodeNumber - ${meta.title}' : meta.title; final episodeTitle = episodeNumber != null ? 'Episode $episodeNumber - ${meta.title!}' : meta.title!;
episodeNodes.add( episodeNodes.add(
DownloadTreeNode( DownloadTreeNode(
+1 -1
View File
@@ -357,7 +357,7 @@ class _EpisodeCardState extends State<EpisodeCard> {
// Episode title // Episode title
Expanded( Expanded(
child: Text( child: Text(
widget.episode.title, widget.episode.title!,
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
maxLines: 2, maxLines: 2,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
+2 -2
View File
@@ -518,7 +518,7 @@ class _MediaCardList extends StatelessWidget {
color: tokens(context).textMuted.withValues(alpha: 0.85), color: tokens(context).textMuted.withValues(alpha: 0.85),
fontSize: _subtitleFontSize, fontSize: _subtitleFontSize,
); );
final episodeTitle = metadata.displaySubtitle ?? metadata.title; final episodeTitle = metadata.displaySubtitle ?? metadata.displayTitle;
final episodeNum = metadata.index != null ? ' E${metadata.index}' : ''; final episodeNum = metadata.index != null ? ' E${metadata.index}' : '';
return Row( return Row(
children: [ children: [
@@ -783,7 +783,7 @@ class _MediaCardHelpers {
// For episodes, show "S# · Episode Title" with clickable season link // For episodes, show "S# · Episode Title" with clickable season link
if (metadata.isEpisode && metadata.parentIndex != null) { if (metadata.isEpisode && metadata.parentIndex != null) {
final episodeTitle = metadata.displaySubtitle ?? metadata.title; final episodeTitle = metadata.displaySubtitle ?? metadata.displayTitle;
if (metadata.parentRatingKey != null) { if (metadata.parentRatingKey != null) {
return Row( return Row(
children: [ children: [
+16 -14
View File
@@ -327,7 +327,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
context, context,
showDragHandle: true, showDragHandle: true,
builder: (context) => _FocusableContextMenuSheet( builder: (context) => _FocusableContextMenuSheet(
title: widget.item.title, title: widget.item.displayTitle,
actions: menuActions, actions: menuActions,
focusFirstItem: openedFromKeyboard, focusFirstItem: openedFromKeyboard,
), ),
@@ -597,7 +597,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
await OverlaySheetController.showAdaptive( await OverlaySheetController.showAdaptive(
context, context,
isScrollControlled: true, isScrollControlled: true,
builder: (context) => FileInfoBottomSheet(fileInfo: fileInfo, title: metadata.title), builder: (context) => FileInfoBottomSheet(fileInfo: fileInfo, title: metadata.displayTitle),
); );
} else if (context.mounted) { } else if (context.mounted) {
showErrorSnackBar(context, t.messages.fileInfoNotAvailable); showErrorSnackBar(context, t.messages.fileInfoNotAvailable);
@@ -667,7 +667,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
try { try {
final metadata = widget.item as PlexMetadata; final metadata = widget.item as PlexMetadata;
final itemType = metadata.type.toLowerCase(); final itemType = metadata.mediaType.name;
// Load playlists // Load playlists
final playlists = await client.getPlaylists(playlistType: 'video'); final playlists = await client.getPlaylists(playlistType: 'video');
@@ -752,7 +752,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
try { try {
final metadata = widget.item as PlexMetadata; final metadata = widget.item as PlexMetadata;
final itemType = metadata.type.toLowerCase(); final itemType = metadata.mediaType;
// Get the library section ID from the item // Get the library section ID from the item
// First try from the metadata itself // First try from the metadata itself
@@ -776,8 +776,8 @@ class MediaContextMenuState extends State<MediaContextMenu> {
} }
// If still not found, try to extract from the key field // If still not found, try to extract from the key field
if (sectionId == null) { if (sectionId == null && metadata.key != null) {
final keyMatch = RegExp(r'/library/sections/(\d+)').firstMatch(metadata.key); final keyMatch = RegExp(r'/library/sections/(\d+)').firstMatch(metadata.key!);
if (keyMatch != null) { if (keyMatch != null) {
sectionId = int.tryParse(keyMatch.group(1)!); sectionId = int.tryParse(keyMatch.group(1)!);
appLogger.d(' - Extracted from key: $sectionId'); appLogger.d(' - Extracted from key: $sectionId');
@@ -841,18 +841,20 @@ class MediaContextMenuState extends State<MediaContextMenu> {
// Determine the collection type based on the item type // Determine the collection type based on the item type
int? collectionType; int? collectionType;
switch (itemType) { switch (itemType) {
case 'movie': case PlexMediaType.movie:
collectionType = 1; collectionType = 1;
break; break;
case 'show': case PlexMediaType.show:
collectionType = 2; collectionType = 2;
break; break;
case 'season': case PlexMediaType.season:
collectionType = 3; collectionType = 3;
break; break;
case 'episode': case PlexMediaType.episode:
collectionType = 4; collectionType = 4;
break; break;
default:
break;
} }
appLogger.d('Creating collection "$collectionName" with type $collectionType'); appLogger.d('Creating collection "$collectionName" with type $collectionType');
@@ -951,7 +953,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
final confirmed = await showDeleteConfirmation( final confirmed = await showDeleteConfirmation(
context, context,
title: t.collections.removeFromCollection, title: t.collections.removeFromCollection,
message: t.collections.removeFromCollectionConfirm(title: metadata.title), message: t.collections.removeFromCollectionConfirm(title: metadata.displayTitle),
); );
if (!confirmed || !context.mounted) return; if (!confirmed || !context.mounted) return;
@@ -1008,7 +1010,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
Future<void> _handleDelete(BuildContext context, bool isCollection, bool isPlaylist) async { Future<void> _handleDelete(BuildContext context, bool isCollection, bool isPlaylist) async {
final client = _getClientForItem(); final client = _getClientForItem();
final itemTitle = widget.item.title; final itemTitle = widget.item.displayTitle;
final itemTypeLabel = isCollection ? t.collections.collection : t.playlists.playlist; final itemTypeLabel = isCollection ? t.collections.collection : t.playlists.playlist;
// Show confirmation dialog // Show confirmation dialog
@@ -1110,7 +1112,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
final confirmed = await showDeleteConfirmation( final confirmed = await showDeleteConfirmation(
context, context,
title: t.downloads.deleteDownload, title: t.downloads.deleteDownload,
message: t.downloads.deleteConfirm(title: metadata.title), message: t.downloads.deleteConfirm(title: metadata.displayTitle),
); );
if (!confirmed || !context.mounted) return; if (!confirmed || !context.mounted) return;
@@ -1264,7 +1266,7 @@ class _CollectionSelectionDialog extends StatelessWidget {
final collection = collections[index - 1]; final collection = collections[index - 1];
return ListTile( return ListTile(
leading: const AppIcon(Symbols.collections_rounded, fill: 1), leading: const AppIcon(Symbols.collections_rounded, fill: 1),
title: Text(collection.title), title: Text(collection.title!),
subtitle: collection.childCount != null ? Text('${collection.childCount} items') : null, subtitle: collection.childCount != null ? Text('${collection.childCount} items') : null,
onTap: () => Navigator.pop(context, collection.ratingKey), onTap: () => Navigator.pop(context, collection.ratingKey),
); );
@@ -57,7 +57,7 @@ class QueueSheet extends StatelessWidget {
return FocusableListTile( return FocusableListTile(
leading: _buildThumbnail(context, item, isCurrent, isTablet: isTablet), leading: _buildThumbnail(context, item, isCurrent, isTablet: isTablet),
title: Text( title: Text(
item.title, item.title!,
style: TextStyle( style: TextStyle(
color: isCurrent ? primaryColor : null, color: isCurrent ? primaryColor : null,
fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal, fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal,
@@ -139,7 +139,7 @@ class QueueSheet extends StatelessWidget {
if (item.year != null) { if (item.year != null) {
return item.editionTitle != null ? '${item.year} · ${item.editionTitle}' : '${item.year}'; return item.editionTitle != null ? '${item.year} · ${item.editionTitle}' : '${item.year}';
} }
return item.type; return item.mediaType.name;
} }
static dynamic _tryGetClient(BuildContext context, PlexMetadata item) { static dynamic _tryGetClient(BuildContext context, PlexMetadata item) {
@@ -474,7 +474,7 @@ class ContentStripState extends State<ContentStrip> {
const AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: 34), const AppIcon(Symbols.image_rounded, fill: 1, color: Colors.white54, size: 34),
) )
: null, : null,
title: item.title, title: item.title!,
subtitle: _buildQueueSubtitle(item), subtitle: _buildQueueSubtitle(item),
onTap: onTap, onTap: onTap,
); );
@@ -511,7 +511,7 @@ class ContentStripState extends State<ContentStrip> {
} }
if (item.grandparentTitle != null) return item.grandparentTitle!; if (item.grandparentTitle != null) return item.grandparentTitle!;
if (item.year != null) return item.editionTitle != null ? '${item.year} · ${item.editionTitle}' : '${item.year}'; if (item.year != null) return item.editionTitle != null ? '${item.year} · ${item.editionTitle}' : '${item.year}';
return item.type; return item.mediaType.name;
} }
Widget _buildStripItem({ Widget _buildStripItem({
@@ -54,14 +54,14 @@ class VideoControlsHeader extends StatelessWidget {
Widget _buildSingleLineTitle() { Widget _buildSingleLineTitle() {
// Build single-line title combining series and episode info // Build single-line title combining series and episode info
final seriesName = metadata.grandparentTitle ?? metadata.title; final seriesName = metadata.grandparentTitle ?? metadata.title!;
final hasEpisodeInfo = metadata.parentIndex != null && metadata.index != null; final hasEpisodeInfo = metadata.parentIndex != null && metadata.index != null;
List<String> parts = [seriesName]; List<String> parts = [seriesName];
if (hasEpisodeInfo) { if (hasEpisodeInfo) {
parts.add('S${metadata.parentIndex}E${metadata.index}'); parts.add('S${metadata.parentIndex}E${metadata.index}');
parts.add(metadata.title); parts.add(metadata.title!);
} }
return Text( return Text(
@@ -78,7 +78,7 @@ class VideoControlsHeader extends StatelessWidget {
if (metadata.parentIndex != null && metadata.index != null) { if (metadata.parentIndex != null && metadata.index != null) {
secondLineParts.add('S${metadata.parentIndex}'); secondLineParts.add('S${metadata.parentIndex}');
secondLineParts.add('E${metadata.index}'); secondLineParts.add('E${metadata.index}');
secondLineParts.add(metadata.title); secondLineParts.add(metadata.title!);
} }
if (metadata.duration != null) { if (metadata.duration != null) {
@@ -89,7 +89,7 @@ class VideoControlsHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
metadata.grandparentTitle ?? metadata.title, metadata.grandparentTitle ?? metadata.title!,
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold), style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,