@@ -97,6 +97,7 @@ class MusifyAudioHandler extends BaseAudioHandler {
9797 static const Duration _errorRetryDelay = Duration (seconds: 2 );
9898 static const Duration _songTransitionTimeout = Duration (seconds: 30 );
9999 static const Duration _debounceInterval = Duration (milliseconds: 150 );
100+ static const String _recentMediaIdPrefix = 'recent:' ;
100101
101102 int _activePreloadCount = 0 ;
102103 final Set <String > _preloadingYtIds = < String > {};
@@ -752,7 +753,11 @@ class MusifyAudioHandler extends BaseAudioHandler {
752753 await _playFromQueue (0 );
753754 }
754755 } catch (e, stackTrace) {
755- logger.log ('Error inserting recommended song' , error: e, stackTrace: stackTrace);
756+ logger.log (
757+ 'Error inserting recommended song' ,
758+ error: e,
759+ stackTrace: stackTrace,
760+ );
756761 }
757762 }
758763
@@ -1256,6 +1261,183 @@ class MusifyAudioHandler extends BaseAudioHandler {
12561261
12571262 bool get hasPrevious => _currentQueueIndex > 0 || _historyList.isNotEmpty;
12581263
1264+ String _recentMediaId (String ytid) => '$_recentMediaIdPrefix $ytid ' ;
1265+
1266+ String ? _ytidFromMediaId (String mediaId) {
1267+ if (mediaId.startsWith (_recentMediaIdPrefix)) {
1268+ return mediaId.substring (_recentMediaIdPrefix.length);
1269+ }
1270+ return mediaId.isEmpty ? null : mediaId;
1271+ }
1272+
1273+ String ? _songYtid (Map song) {
1274+ final ytid = song['ytid' ]? .toString ();
1275+ return ytid == null || ytid.isEmpty ? null : ytid;
1276+ }
1277+
1278+ Map ? _firstPlayableSong (Iterable songs) {
1279+ for (final song in songs.whereType <Map >()) {
1280+ if (_songYtid (song) != null ) {
1281+ return song;
1282+ }
1283+ }
1284+ return null ;
1285+ }
1286+
1287+ Map ? _findSongInList (Iterable songs, String ytid) {
1288+ for (final song in songs.whereType <Map >()) {
1289+ if (_songYtid (song) == ytid) {
1290+ return song;
1291+ }
1292+ }
1293+ return null ;
1294+ }
1295+
1296+ Map ? _findSongByYtid (String ? ytid) {
1297+ if (ytid == null || ytid.isEmpty) return null ;
1298+
1299+ final activeSong = currentSong;
1300+ if (activeSong? ['ytid' ]? .toString () == ytid) {
1301+ return activeSong;
1302+ }
1303+
1304+ for (final source in [
1305+ _queueList,
1306+ userRecentlyPlayed,
1307+ userOfflineSongs,
1308+ userLikedSongsList,
1309+ ]) {
1310+ final song = _findSongInList (source, ytid);
1311+ if (song != null ) return song;
1312+ }
1313+
1314+ return null ;
1315+ }
1316+
1317+ Map ? _latestResumableSong () {
1318+ final activeSong = currentSong;
1319+ if (activeSong != null && _songYtid (activeSong) != null ) {
1320+ return activeSong;
1321+ }
1322+
1323+ final activeMediaItem = mediaItem.valueOrNull;
1324+ final activeYtid = activeMediaItem? .extras? ['ytid' ]? .toString ();
1325+ final activeMediaSong = _findSongByYtid (activeYtid);
1326+ if (activeMediaSong != null ) return activeMediaSong;
1327+ if (activeYtid != null &&
1328+ activeYtid.isNotEmpty &&
1329+ activeMediaItem != null ) {
1330+ return mediaItemToMap (activeMediaItem);
1331+ }
1332+
1333+ return _firstPlayableSong (userRecentlyPlayed) ??
1334+ _firstPlayableSong (userOfflineSongs) ??
1335+ _firstPlayableSong (userLikedSongsList);
1336+ }
1337+
1338+ Map <String , dynamic >? _normaliseResumableSong (Map song) {
1339+ final ytid = _songYtid (song);
1340+ if (ytid == null ) return null ;
1341+
1342+ final normalised = cloneMap (song);
1343+ normalised['id' ] = ytid;
1344+ normalised['ytid' ] = ytid;
1345+ normalised['highResImage' ] ?? =
1346+ normalised['image' ] ?? normalised['lowResImage' ] ?? '' ;
1347+ normalised['lowResImage' ] ?? = normalised['highResImage' ];
1348+ normalised['isLive' ] ?? = false ;
1349+ return normalised;
1350+ }
1351+
1352+ MediaItem ? _mediaItemForResumption (Map song) {
1353+ final normalisedSong = _normaliseResumableSong (song);
1354+ if (normalisedSong == null ) return null ;
1355+
1356+ final ytid = normalisedSong['ytid' ].toString ();
1357+ final artist = normalisedSong['artist' ]? .toString ().trim () ?? '' ;
1358+ return mapToMediaItem (normalisedSong).copyWith (
1359+ id: _recentMediaId (ytid),
1360+ displayTitle: normalisedSong['title' ]? .toString (),
1361+ displaySubtitle: artist.isEmpty ? 'Musify' : artist,
1362+ );
1363+ }
1364+
1365+ Future <void > _playResumableSong (Map song) async {
1366+ final normalisedSong = _normaliseResumableSong (song);
1367+ if (normalisedSong == null ) return ;
1368+
1369+ await playPlaylistSong (
1370+ playlist: {
1371+ 'title' : 'Musify' ,
1372+ 'source' : 'system-recent' ,
1373+ 'list' : [normalisedSong],
1374+ },
1375+ songIndex: 0 ,
1376+ );
1377+ }
1378+
1379+ @override
1380+ Future <List <MediaItem >> getChildren (
1381+ String parentMediaId, [
1382+ Map <String , dynamic >? options,
1383+ ]) async {
1384+ if (parentMediaId != AudioService .recentRootId &&
1385+ parentMediaId != AudioService .browsableRootId) {
1386+ return [];
1387+ }
1388+
1389+ final recentSong = _latestResumableSong ();
1390+ final recentItem = recentSong == null
1391+ ? null
1392+ : _mediaItemForResumption (recentSong);
1393+ return recentItem == null ? [] : [recentItem];
1394+ }
1395+
1396+ @override
1397+ Future <MediaItem ?> getMediaItem (String mediaId) async {
1398+ final song = _findSongByYtid (_ytidFromMediaId (mediaId));
1399+ return song == null ? null : _mediaItemForResumption (song);
1400+ }
1401+
1402+ @override
1403+ Future <void > prepareFromMediaId (
1404+ String mediaId, [
1405+ Map <String , dynamic >? extras,
1406+ ]) async {
1407+ final item = await getMediaItem (mediaId);
1408+ if (item == null ) return ;
1409+
1410+ mediaItem.add (item);
1411+ queue.add ([item]);
1412+ playbackState.add (
1413+ PlaybackState (
1414+ controls: _pausedControls,
1415+ systemActions: const {
1416+ MediaAction .seek,
1417+ MediaAction .seekForward,
1418+ MediaAction .seekBackward,
1419+ },
1420+ androidCompactActionIndices: const [0 , 1 , 3 ],
1421+ processingState: AudioProcessingState .ready,
1422+ queueIndex: 0 ,
1423+ updateTime: DateTime .now (),
1424+ ),
1425+ );
1426+ }
1427+
1428+ @override
1429+ Future <void > playFromMediaId (
1430+ String mediaId, [
1431+ Map <String , dynamic >? extras,
1432+ ]) async {
1433+ final song = _findSongByYtid (_ytidFromMediaId (mediaId));
1434+ if (song == null ) {
1435+ logger.log ('No resumable song found for media id: $mediaId ' );
1436+ return ;
1437+ }
1438+ await _playResumableSong (song);
1439+ }
1440+
12591441 @override
12601442 Future <void > onTaskRemoved () async {
12611443 try {
@@ -1271,6 +1453,13 @@ class MusifyAudioHandler extends BaseAudioHandler {
12711453 @override
12721454 Future <void > play () async {
12731455 try {
1456+ if (audioPlayer.audioSource == null ) {
1457+ final recentSong = _latestResumableSong ();
1458+ if (recentSong != null ) {
1459+ await _playResumableSong (recentSong);
1460+ return ;
1461+ }
1462+ }
12741463 await audioPlayer.play ();
12751464 } catch (e, stackTrace) {
12761465 logger.log ('Error in play()' , error: e, stackTrace: stackTrace);
@@ -1307,10 +1496,12 @@ class MusifyAudioHandler extends BaseAudioHandler {
13071496 /// Returns unplayed manually added songs after the current queue index.
13081497 List <Map > _getUnplayedManualSongs () {
13091498 return _queueList
1310- .skip (_currentQueueIndex >= 0 ? _currentQueueIndex + 1 : 0 )
1311- .where ((song) =>
1312- song['isManuallyAdded' ] == true && song['isAutoPicked' ] != true )
1313- .toList ();
1499+ .skip (_currentQueueIndex >= 0 ? _currentQueueIndex + 1 : 0 )
1500+ .where (
1501+ (song) =>
1502+ song['isManuallyAdded' ] == true && song['isAutoPicked' ] != true ,
1503+ )
1504+ .toList ();
13141505 }
13151506
13161507 void _resetPreloadingState () {
0 commit comments