From 77ef0b5f05fd3baad3c8b50b7bcb9e2efd743c2a Mon Sep 17 00:00:00 2001 From: Seungmin Kim <8457324+ehfd@users.noreply.github.com> Date: Mon, 24 Mar 2025 00:51:33 +0900 Subject: [PATCH] Add ISRC track search for YouTube --- lib/collections/fake.dart | 1 + lib/extensions/track.dart | 54 +++++++++----- lib/modules/album/album_card.dart | 3 +- lib/provider/spotify/album/tracks.dart | 2 +- .../sourced_track/sources/youtube.dart | 73 ++++++++++++------- 5 files changed, 85 insertions(+), 48 deletions(-) diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart index 31f97e0c..8af40e71 100644 --- a/lib/collections/fake.dart +++ b/lib/collections/fake.dart @@ -94,6 +94,7 @@ abstract class FakeData { ..trackNumber = 1 ..type = "type" ..uri = "uri" + ..externalIds = externalIds ..isPlayable = true ..explicit = false ..linkedFrom = trackLink; diff --git a/lib/extensions/track.dart b/lib/extensions/track.dart index 215a5ab2..15ef0375 100644 --- a/lib/extensions/track.dart +++ b/lib/extensions/track.dart @@ -4,7 +4,9 @@ import 'dart:typed_data'; import 'package:metadata_god/metadata_god.dart'; import 'package:path/path.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; +import 'package:spotube/services/logger/logger.dart'; extension TrackExtensions on Track { Track fromFile( @@ -68,26 +70,38 @@ extension TrackExtensions on Track { } extension TrackSimpleExtensions on TrackSimple { - Track asTrack(AlbumSimple album) { - Track track = Track(); - track.name = name; - track.album = album; - track.artists = artists; - track.availableMarkets = availableMarkets; - track.discNumber = discNumber; - track.durationMs = durationMs; - track.explicit = explicit; - track.externalUrls = externalUrls; - track.href = href; - track.id = id; - track.isPlayable = isPlayable; - track.linkedFrom = linkedFrom; - track.name = name; - track.previewUrl = previewUrl; - track.trackNumber = trackNumber; - track.type = type; - track.uri = uri; - return track; + Future asTrack(AlbumSimple album, ref) async { + try { + final spotify = ref.read(spotifyProvider); + return await spotify.invoke((api) => api.tracks.get(id!)); + } catch (e, stack) { + if ((e is AuthorizationException && e.error != 'invalid_token') || + e is SpotifyException) { + // Ignore this error and create the Track locally + AppLogger.reportError(e, stack); + + Track track = Track(); + track.name = name; + track.album = album; + track.artists = artists; + track.availableMarkets = availableMarkets; + track.discNumber = discNumber; + track.durationMs = durationMs; + track.explicit = explicit; + track.externalUrls = externalUrls; + track.href = href; + track.id = id; + track.isPlayable = isPlayable; + track.linkedFrom = linkedFrom; + track.name = name; + track.previewUrl = previewUrl; + track.trackNumber = trackNumber; + track.type = type; + track.uri = uri; + return track; + } + rethrow; + } } } diff --git a/lib/modules/album/album_card.dart b/lib/modules/album/album_card.dart index 84106594..730da82a 100644 --- a/lib/modules/album/album_card.dart +++ b/lib/modules/album/album_card.dart @@ -54,7 +54,8 @@ class AlbumCard extends HookConsumerWidget { Future> fetchAllTrack() async { if (album.tracks != null && album.tracks!.isNotEmpty) { - return album.tracks!.map((track) => track.asTrack(album)).toList(); + return Future.wait( + album.tracks!.map((track) => track.asTrack(album, ref)).toList()); } await ref.read(albumTracksProvider(album).future); return ref.read(albumTracksProvider(album).notifier).fetchAll(); diff --git a/lib/provider/spotify/album/tracks.dart b/lib/provider/spotify/album/tracks.dart index d886d180..038b1837 100644 --- a/lib/provider/spotify/album/tracks.dart +++ b/lib/provider/spotify/album/tracks.dart @@ -33,7 +33,7 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier api.albums.tracks(arg.id!).getPage(limit, offset), ); - final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? []; + final List items = await Future.wait(tracks.items?.map((e) => e.asTrack(arg, ref)).toList() ?? []); return ( items: items, diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index c4881051..17e9b872 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -236,29 +236,42 @@ class YoutubeSourcedTrack extends SourcedTrack { .toList(); } + static Future> fetchFromIsrc({ + required Track track, + required Provider provider, + required Ref ref, + }) async { + final isrcResults = []; + final isrc = track.externalIds?.isrc; + if (isrc != null && isrc.isNotEmpty) { + final searchedVideos = await ref.read(provider) + .searchVideos(isrc.toString()); + isrcResults.addAll(await searchedVideos + .map(YoutubeVideoInfo.fromVideo) + .toList()); + } + return isrcResults; + } + static Future> fetchSiblings({ required Track track, required Ref ref, }) async { + final videoResults = []; + + videoResults.addAll(await fetchFromIsrc( + track: track, provider: youtubeEngineProvider, ref: ref)); + final links = await SongLinkService.links(track.id!); final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube"); - if (ytLink?.url != null - // allows to fetch siblings more results for already sourced track - && - track is! SourcedTrack) { + if (ytLink?.url != null) { try { - return [ - await toSiblingType( - 0, - YoutubeVideoInfo.fromVideo( - await ref.read(youtubeEngineProvider).getVideo( - Uri.parse(ytLink!.url!).queryParameters["v"]!, - ), - ), - ref, - ) - ]; + videoResults.add(YoutubeVideoInfo.fromVideo( + await ref + .read(youtubeEngineProvider) + .getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!), + )); } on VideoUnplayableException catch (e, stack) { // Ignore this error and continue with the search AppLogger.reportError(e, stack); @@ -271,20 +284,28 @@ class YoutubeSourcedTrack extends SourcedTrack { await ref.read(youtubeEngineProvider).searchVideos(query); if (ServiceUtils.onlyContainsEnglish(query)) { - return await Future.wait(searchResults - .map(YoutubeVideoInfo.fromVideo) - .mapIndexed((index, info) => toSiblingType(index, info, ref))); + videoResults.addAll( + searchResults.map(YoutubeVideoInfo.fromVideo).toList(), + ); + } else { + videoResults.addAll(rankResults( + searchResults.map(YoutubeVideoInfo.fromVideo).toList(), + track, + )); } - final rankedSiblings = rankResults( - searchResults.map(YoutubeVideoInfo.fromVideo).toList(), - track, - ); - + final seenIds = {}; + int index = 0; return await Future.wait( - rankedSiblings - .mapIndexed((index, info) => toSiblingType(index, info, ref)), - ); + videoResults.map((videoResult) async { + // Deduplicate results + if (!seenIds.contains(videoResult.id)) { + seenIds.add(videoResult.id); + return await toSiblingType(index++, videoResult, ref); + } + return null; + }), + ).then((s) => s.whereType().toList()); } @override