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
+2 -2
View File
@@ -145,7 +145,7 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
movies.add(
DownloadTreeNode(
key: globalKey,
title: meta.title,
title: meta.displayTitle,
type: DownloadNodeType.movie,
progress: download.progressPercent,
status: download.status,
@@ -202,7 +202,7 @@ class _DownloadTreeViewState extends State<DownloadTreeView> {
if (meta == null) continue;
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(
DownloadTreeNode(
+1 -1
View File
@@ -357,7 +357,7 @@ class _EpisodeCardState extends State<EpisodeCard> {
// Episode title
Expanded(
child: Text(
widget.episode.title,
widget.episode.title!,
style: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
maxLines: 2,
overflow: TextOverflow.ellipsis,
+2 -2
View File
@@ -518,7 +518,7 @@ class _MediaCardList extends StatelessWidget {
color: tokens(context).textMuted.withValues(alpha: 0.85),
fontSize: _subtitleFontSize,
);
final episodeTitle = metadata.displaySubtitle ?? metadata.title;
final episodeTitle = metadata.displaySubtitle ?? metadata.displayTitle;
final episodeNum = metadata.index != null ? ' E${metadata.index}' : '';
return Row(
children: [
@@ -783,7 +783,7 @@ class _MediaCardHelpers {
// For episodes, show "S# · Episode Title" with clickable season link
if (metadata.isEpisode && metadata.parentIndex != null) {
final episodeTitle = metadata.displaySubtitle ?? metadata.title;
final episodeTitle = metadata.displaySubtitle ?? metadata.displayTitle;
if (metadata.parentRatingKey != null) {
return Row(
children: [
+16 -14
View File
@@ -327,7 +327,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
context,
showDragHandle: true,
builder: (context) => _FocusableContextMenuSheet(
title: widget.item.title,
title: widget.item.displayTitle,
actions: menuActions,
focusFirstItem: openedFromKeyboard,
),
@@ -597,7 +597,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
await OverlaySheetController.showAdaptive(
context,
isScrollControlled: true,
builder: (context) => FileInfoBottomSheet(fileInfo: fileInfo, title: metadata.title),
builder: (context) => FileInfoBottomSheet(fileInfo: fileInfo, title: metadata.displayTitle),
);
} else if (context.mounted) {
showErrorSnackBar(context, t.messages.fileInfoNotAvailable);
@@ -667,7 +667,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
try {
final metadata = widget.item as PlexMetadata;
final itemType = metadata.type.toLowerCase();
final itemType = metadata.mediaType.name;
// Load playlists
final playlists = await client.getPlaylists(playlistType: 'video');
@@ -752,7 +752,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
try {
final metadata = widget.item as PlexMetadata;
final itemType = metadata.type.toLowerCase();
final itemType = metadata.mediaType;
// Get the library section ID from the item
// 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 (sectionId == null) {
final keyMatch = RegExp(r'/library/sections/(\d+)').firstMatch(metadata.key);
if (sectionId == null && metadata.key != null) {
final keyMatch = RegExp(r'/library/sections/(\d+)').firstMatch(metadata.key!);
if (keyMatch != null) {
sectionId = int.tryParse(keyMatch.group(1)!);
appLogger.d(' - Extracted from key: $sectionId');
@@ -841,18 +841,20 @@ class MediaContextMenuState extends State<MediaContextMenu> {
// Determine the collection type based on the item type
int? collectionType;
switch (itemType) {
case 'movie':
case PlexMediaType.movie:
collectionType = 1;
break;
case 'show':
case PlexMediaType.show:
collectionType = 2;
break;
case 'season':
case PlexMediaType.season:
collectionType = 3;
break;
case 'episode':
case PlexMediaType.episode:
collectionType = 4;
break;
default:
break;
}
appLogger.d('Creating collection "$collectionName" with type $collectionType');
@@ -951,7 +953,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
final confirmed = await showDeleteConfirmation(
context,
title: t.collections.removeFromCollection,
message: t.collections.removeFromCollectionConfirm(title: metadata.title),
message: t.collections.removeFromCollectionConfirm(title: metadata.displayTitle),
);
if (!confirmed || !context.mounted) return;
@@ -1008,7 +1010,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
Future<void> _handleDelete(BuildContext context, bool isCollection, bool isPlaylist) async {
final client = _getClientForItem();
final itemTitle = widget.item.title;
final itemTitle = widget.item.displayTitle;
final itemTypeLabel = isCollection ? t.collections.collection : t.playlists.playlist;
// Show confirmation dialog
@@ -1110,7 +1112,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
final confirmed = await showDeleteConfirmation(
context,
title: t.downloads.deleteDownload,
message: t.downloads.deleteConfirm(title: metadata.title),
message: t.downloads.deleteConfirm(title: metadata.displayTitle),
);
if (!confirmed || !context.mounted) return;
@@ -1264,7 +1266,7 @@ class _CollectionSelectionDialog extends StatelessWidget {
final collection = collections[index - 1];
return ListTile(
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,
onTap: () => Navigator.pop(context, collection.ratingKey),
);
@@ -57,7 +57,7 @@ class QueueSheet extends StatelessWidget {
return FocusableListTile(
leading: _buildThumbnail(context, item, isCurrent, isTablet: isTablet),
title: Text(
item.title,
item.title!,
style: TextStyle(
color: isCurrent ? primaryColor : null,
fontWeight: isCurrent ? FontWeight.bold : FontWeight.normal,
@@ -139,7 +139,7 @@ class QueueSheet extends StatelessWidget {
if (item.year != null) {
return item.editionTitle != null ? '${item.year} · ${item.editionTitle}' : '${item.year}';
}
return item.type;
return item.mediaType.name;
}
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),
)
: null,
title: item.title,
title: item.title!,
subtitle: _buildQueueSubtitle(item),
onTap: onTap,
);
@@ -511,7 +511,7 @@ class ContentStripState extends State<ContentStrip> {
}
if (item.grandparentTitle != null) return item.grandparentTitle!;
if (item.year != null) return item.editionTitle != null ? '${item.year} · ${item.editionTitle}' : '${item.year}';
return item.type;
return item.mediaType.name;
}
Widget _buildStripItem({
@@ -54,14 +54,14 @@ class VideoControlsHeader extends StatelessWidget {
Widget _buildSingleLineTitle() {
// 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;
List<String> parts = [seriesName];
if (hasEpisodeInfo) {
parts.add('S${metadata.parentIndex}E${metadata.index}');
parts.add(metadata.title);
parts.add(metadata.title!);
}
return Text(
@@ -78,7 +78,7 @@ class VideoControlsHeader extends StatelessWidget {
if (metadata.parentIndex != null && metadata.index != null) {
secondLineParts.add('S${metadata.parentIndex}');
secondLineParts.add('E${metadata.index}');
secondLineParts.add(metadata.title);
secondLineParts.add(metadata.title!);
}
if (metadata.duration != null) {
@@ -89,7 +89,7 @@ class VideoControlsHeader extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
metadata.grandparentTitle ?? metadata.title,
metadata.grandparentTitle ?? metadata.title!,
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold),
maxLines: 1,
overflow: TextOverflow.ellipsis,