Skip to content

Commit 2617759

Browse files
committed
fix(playlists): ensure playlist identifiers are sanitized and resolved correctly (closes: #737)
1 parent b769786 commit 2617759

File tree

4 files changed

+135
-14
lines changed

4 files changed

+135
-14
lines changed

lib/screens/playlist_page.dart

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,10 @@ class _PlaylistPageState extends State<PlaylistPage> {
118118
_playlist = widget.playlistData;
119119
final playlistList = _playlist?['list'] as List?;
120120
if (playlistList == null || playlistList.isEmpty) {
121+
final resolvedId =
122+
_playlist?['ytid']?.toString() ?? widget.playlistId;
121123
final fullPlaylist = await getPlaylistInfoForWidget(
122-
widget.playlistId,
124+
resolvedId,
123125
isArtist: widget.isArtist,
124126
);
125127
if (fullPlaylist != null) {
@@ -385,16 +387,31 @@ class _PlaylistPageState extends State<PlaylistPage> {
385387
);
386388

387389
if (result != null) {
388-
final playlistYtid = _playlist['ytid'];
390+
final resolvedPlaylistYtid =
391+
_playlist['ytid']?.toString() ?? widget.playlistId;
392+
if (resolvedPlaylistYtid == null ||
393+
resolvedPlaylistYtid.isEmpty ||
394+
resolvedPlaylistYtid == 'null') {
395+
showToast(context, context.l10n!.error);
396+
return;
397+
}
398+
399+
final updatedPlaylist = {
400+
..._playlist,
401+
...result,
402+
'ytid': resolvedPlaylistYtid,
403+
'source': _playlist['source'] ?? result['source'],
404+
'list': result['list'] ?? _playlist['list'],
405+
};
389406

390407
// Search root list first, then inside folders.
391408
final rootIndex = userCustomPlaylists.value.indexWhere(
392-
(p) => p['ytid'] == playlistYtid,
409+
(p) => p['ytid'] == resolvedPlaylistYtid,
393410
);
394411

395412
if (rootIndex != -1) {
396413
final updatedPlaylists = List<Map>.from(userCustomPlaylists.value);
397-
updatedPlaylists[rootIndex] = result;
414+
updatedPlaylists[rootIndex] = updatedPlaylist;
398415
userCustomPlaylists.value = updatedPlaylists;
399416
unawaited(
400417
addOrUpdateData(
@@ -411,10 +428,10 @@ class _PlaylistPageState extends State<PlaylistPage> {
411428
folder['playlists'] as List? ?? [],
412429
);
413430
final fi = folderPlaylists.indexWhere(
414-
(p) => p['ytid'] == playlistYtid,
431+
(p) => p['ytid'] == resolvedPlaylistYtid,
415432
);
416433
if (fi != -1) {
417-
folderPlaylists[fi] = result;
434+
folderPlaylists[fi] = updatedPlaylist;
418435
folder['playlists'] = folderPlaylists;
419436
break;
420437
}
@@ -429,7 +446,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
429446
);
430447
}
431448

432-
setState(() => _playlist = result);
449+
setState(() => _playlist = updatedPlaylist);
433450
showToast(context, context.l10n!.playlistUpdated);
434451
}
435452
},
@@ -594,7 +611,8 @@ class _PlaylistPageState extends State<PlaylistPage> {
594611
}
595612
_pagingController.refresh();
596613
} else {
597-
final updatedPlaylist = await getPlaylistInfoForWidget(widget.playlistId);
614+
final resolvedId = _playlist['ytid']?.toString() ?? widget.playlistId;
615+
final updatedPlaylist = await getPlaylistInfoForWidget(resolvedId);
598616
if (updatedPlaylist != null && mounted) {
599617
setState(() {
600618
_playlist = updatedPlaylist;

lib/services/playlists_manager.dart

Lines changed: 96 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,92 @@ List onlinePlaylists = [];
5454
final currentLikedPlaylistsLength = ValueNotifier<int>(
5555
userLikedPlaylists.length,
5656
);
57+
bool _didSanitizePlaylistIdentifiers = false;
58+
59+
void _ensurePlaylistIdentifiersSanitized() {
60+
if (_didSanitizePlaylistIdentifiers) return;
61+
_didSanitizePlaylistIdentifiers = true;
62+
_sanitizePlaylistIdentifiers();
63+
}
64+
65+
bool _isMissingPlaylistId(dynamic playlistId) {
66+
final normalized = playlistId?.toString().trim();
67+
return normalized == null || normalized.isEmpty || normalized == 'null';
68+
}
69+
70+
/// Repairs persisted custom playlist entries that have missing/null ids.
71+
///
72+
/// This prevents navigation paths like `/home/playlist/null` from stale,
73+
/// previously corrupted data.
74+
void _sanitizePlaylistIdentifiers() {
75+
var customChanged = false;
76+
final normalizedCustom = userCustomPlaylists.value.map((item) {
77+
if (item is! Map) return item;
78+
final source = item['source']?.toString();
79+
final shouldRepair =
80+
source == null || source == 'user-created' || source == 'custom';
81+
if (!shouldRepair || !_isMissingPlaylistId(item['ytid'])) {
82+
return item;
83+
}
84+
85+
customChanged = true;
86+
return <String, dynamic>{
87+
...Map<String, dynamic>.from(item.cast<dynamic, dynamic>()),
88+
'ytid': generateCustomPlaylistId(),
89+
'source': source ?? 'user-created',
90+
};
91+
}).toList();
92+
93+
if (customChanged) {
94+
userCustomPlaylists.value = normalizedCustom;
95+
unawaited(addOrUpdateData('user', 'customPlaylists', normalizedCustom));
96+
}
97+
98+
var foldersChanged = false;
99+
final normalizedFolders = userPlaylistFolders.value.map((folder) {
100+
if (folder is! Map) return folder;
101+
102+
final folderMap = Map<String, dynamic>.from(
103+
folder.cast<dynamic, dynamic>(),
104+
);
105+
final folderPlaylists = folderMap['playlists'] as List<dynamic>? ?? [];
106+
var folderChanged = false;
107+
108+
final normalizedPlaylists = folderPlaylists.map((playlist) {
109+
if (playlist is! Map) return playlist;
110+
111+
final playlistMap = Map<String, dynamic>.from(
112+
playlist.cast<dynamic, dynamic>(),
113+
);
114+
final source = playlistMap['source']?.toString();
115+
final shouldRepair =
116+
source == null || source == 'user-created' || source == 'custom';
117+
118+
if (!shouldRepair || !_isMissingPlaylistId(playlistMap['ytid'])) {
119+
return playlistMap;
120+
}
121+
122+
folderChanged = true;
123+
return <String, dynamic>{
124+
...playlistMap,
125+
'ytid': generateCustomPlaylistId(),
126+
'source': source ?? 'user-created',
127+
};
128+
}).toList();
129+
130+
if (folderChanged) {
131+
foldersChanged = true;
132+
folderMap['playlists'] = normalizedPlaylists;
133+
}
134+
135+
return folderMap;
136+
}).toList();
137+
138+
if (foldersChanged) {
139+
userPlaylistFolders.value = normalizedFolders;
140+
unawaited(addOrUpdateData('user', 'playlistFolders', normalizedFolders));
141+
}
142+
}
57143

58144
String generateCustomPlaylistId() {
59145
final timestamp = DateTime.now().microsecondsSinceEpoch;
@@ -503,6 +589,7 @@ String deletePlaylistFolder(String folderId, [BuildContext? context]) {
503589
}
504590

505591
List<Map> getPlaylistsInFolder(String folderId) {
592+
_ensurePlaylistIdentifiersSanitized();
506593
try {
507594
final folder = userPlaylistFolders.value.firstWhere(
508595
(folder) => folder['id'] == folderId,
@@ -520,6 +607,7 @@ List<Map> getPlaylistsInFolder(String folderId) {
520607
}
521608

522609
List<Map> getPlaylistsNotInFolders() {
610+
_ensurePlaylistIdentifiersSanitized();
523611
final playlistsInFolders = <String>{};
524612
for (final folder in userPlaylistFolders.value) {
525613
final folderPlaylists = folder['playlists'] as List<dynamic>? ?? [];
@@ -545,6 +633,7 @@ Future<List> getPlaylists({
545633
bool onlyLiked = false,
546634
String type = 'all',
547635
}) async {
636+
_ensurePlaylistIdentifiersSanitized();
548637
if (onlyLiked) {
549638
if (playlistsNum != null) {
550639
return userLikedPlaylists.take(playlistsNum).toList();
@@ -715,12 +804,15 @@ Future<Map?> getPlaylistInfoForWidget(
715804
dynamic id, {
716805
bool isArtist = false,
717806
}) async {
807+
_ensurePlaylistIdentifiersSanitized();
718808
if (id == null) return null;
719-
if (isArtist) return _fetchArtistPlaylist(id.toString());
720-
if (id.toString().startsWith('customId-')) {
721-
return _findCustomPlaylist(id.toString());
809+
final normalizedId = id.toString().trim();
810+
if (normalizedId.isEmpty || normalizedId == 'null') return null;
811+
if (isArtist) return _fetchArtistPlaylist(normalizedId);
812+
if (normalizedId.startsWith('customId-')) {
813+
return _findCustomPlaylist(normalizedId);
722814
}
723-
return _fetchYouTubePlaylist(id.toString());
815+
return _fetchYouTubePlaylist(normalizedId);
724816
}
725817

726818
Future<Map> _fetchArtistPlaylist(String artistName) async {

lib/widgets/edit_playlist_dialog.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,16 @@ class _EditPlaylistDialogState extends State<EditPlaylistDialog> {
131131
FilledButton.icon(
132132
onPressed: () {
133133
final newPlaylist = {
134+
'ytid': widget.playlistData['ytid'],
134135
'title': _titleController.text,
135-
'source': 'user-created',
136+
'source': widget.playlistData['source'] ?? 'user-created',
136137
if (_imageBase64 != null)
137138
'image': _imageBase64
138139
else if (_imageUrlController.text.isNotEmpty)
139140
'image': _imageUrlController.text,
140141
'list': widget.playlistData['list'],
142+
if (widget.playlistData['createdAt'] != null)
143+
'createdAt': widget.playlistData['createdAt'],
141144
};
142145

143146
Navigator.pop(context, newPlaylist);

lib/widgets/playlist_bar.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,15 @@ class PlaylistBar extends StatelessWidget {
366366
};
367367
} else {
368368
return () {
369-
context.push('/home/playlist/$playlistId');
369+
final resolvedPlaylistId =
370+
playlistId ?? playlistData?['ytid']?.toString();
371+
if (resolvedPlaylistId == null ||
372+
resolvedPlaylistId.isEmpty ||
373+
resolvedPlaylistId == 'null') {
374+
showToast(context, context.l10n!.error);
375+
return;
376+
}
377+
context.push('/home/playlist/$resolvedPlaylistId');
370378
};
371379
}
372380
}

0 commit comments

Comments
 (0)