diff --git a/lib/provider/proxy_playlist/proxy_playlist_provider.dart b/lib/provider/proxy_playlist/proxy_playlist_provider.dart index ca0fb308..0811fe35 100644 --- a/lib/provider/proxy_playlist/proxy_playlist_provider.dart +++ b/lib/provider/proxy_playlist/proxy_playlist_provider.dart @@ -144,8 +144,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier } } catch (e, stackTrace) { // Removing tracks that were not found to avoid queue interruption - // TODO: Add a flag to enable/disable skip not found tracks - if (e is TrackNotFoundException) { + if (e is TrackNotFoundError) { final oldTrack = mapSourcesToTracks([audioPlayer.nextSource!]).firstOrNull; await removeTrack(oldTrack!.id!); diff --git a/lib/services/sourced_track/exceptions.dart b/lib/services/sourced_track/exceptions.dart index 517d6ba4..85bc5b27 100644 --- a/lib/services/sourced_track/exceptions.dart +++ b/lib/services/sourced_track/exceptions.dart @@ -1,7 +1,12 @@ import 'package:spotify/spotify.dart'; -class TrackNotFoundException implements Exception { - factory TrackNotFoundException(Track track) { - throw Exception("Failed to find any results for ${track.name}"); +class TrackNotFoundError extends Error { + final Track track; + + TrackNotFoundError(this.track); + + @override + String toString() { + return '[TrackNotFoundError] ${track.name} - ${track.artists?.map((e) => e.name).join(", ")}'; } } diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 3ceafbf7..c73f3078 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -1,15 +1,21 @@ +import 'dart:io'; + +import 'package:http/http.dart'; import 'package:collection/collection.dart'; +import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/exceptions.dart'; import 'package:spotube/services/sourced_track/models/source_info.dart'; import 'package:spotube/services/sourced_track/models/source_map.dart'; import 'package:spotube/services/sourced_track/sources/jiosaavn.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/utils/service_utils.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; abstract class SourcedTrack extends Track { final SourceMap source; @@ -101,9 +107,8 @@ abstract class SourcedTrack extends Track { required Track track, required Ref ref, }) async { + final preferences = ref.read(userPreferencesProvider); try { - final preferences = ref.read(userPreferencesProvider); - return switch (preferences.audioSource) { AudioSource.piped => await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref), @@ -112,8 +117,35 @@ abstract class SourcedTrack extends Track { AudioSource.jiosaavn => await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref), }; + } on TrackNotFoundError catch (_) { + return switch (preferences.audioSource) { + AudioSource.piped || + AudioSource.youtube => + await JioSaavnSourcedTrack.fetchFromTrack( + track: track, + ref: ref, + weakMatch: true, + ), + AudioSource.jiosaavn => + await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref), + }; + } on HttpClientClosedException catch (_) { + return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref); } catch (e) { - return YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref); + if (e is DioException || e is ClientException || e is SocketException) { + if (preferences.audioSource == AudioSource.jiosaavn) { + return await JioSaavnSourcedTrack.fetchFromTrack( + track: track, + ref: ref, + weakMatch: true, + ); + } + return await JioSaavnSourcedTrack.fetchFromTrack( + track: track, + ref: ref, + ); + } + rethrow; } } diff --git a/lib/services/sourced_track/sources/jiosaavn.dart b/lib/services/sourced_track/sources/jiosaavn.dart index 7455f4d7..f731de6c 100644 --- a/lib/services/sourced_track/sources/jiosaavn.dart +++ b/lib/services/sourced_track/sources/jiosaavn.dart @@ -37,15 +37,17 @@ class JioSaavnSourcedTrack extends SourcedTrack { static Future fetchFromTrack({ required Track track, required Ref ref, + bool weakMatch = false, }) async { final cachedSource = await SourceMatch.box.get(track.id); if (cachedSource == null || cachedSource.sourceType != SourceType.jiosaavn) { - final siblings = await fetchSiblings(ref: ref, track: track); + final siblings = + await fetchSiblings(ref: ref, track: track, weakMatch: weakMatch); if (siblings.isEmpty) { - throw TrackNotFoundException(track); + throw TrackNotFoundError(track); } await SourceMatch.box.put( @@ -119,6 +121,7 @@ class JioSaavnSourcedTrack extends SourcedTrack { static Future> fetchSiblings({ required Track track, required Ref ref, + bool weakMatch = false, }) async { final query = SourcedTrack.getSearchTerm(track); @@ -126,9 +129,12 @@ class JioSaavnSourcedTrack extends SourcedTrack { await jiosaavnClient.search.songs(query, limit: 20); final trackArtistNames = track.artists?.map((ar) => ar.name).toList(); - return results + + final matchedResults = results .where( (s) { + s.name?.unescapeHtml().contains(track.name!) ?? false; + final sameName = s.name?.unescapeHtml() == track.name; final artistNames = [ s.primaryArtists, @@ -139,12 +145,27 @@ class JioSaavnSourcedTrack extends SourcedTrack { (artist) => trackArtistNames?.any((ar) => artist == ar) ?? false, ); + if (weakMatch) { + final containsName = + s.name?.unescapeHtml().contains(track.name!) ?? false; + final containsPrimaryArtist = s.primaryArtists + .unescapeHtml() + .contains(trackArtistNames?.first ?? ""); + + return containsName && containsPrimaryArtist; + } return sameName && sameArtists; }, ) .map(toSiblingType) .toList(); + + if (weakMatch && matchedResults.isEmpty) { + return results.map(toSiblingType).toList(); + } + + return matchedResults; } @override diff --git a/lib/services/sourced_track/sources/piped.dart b/lib/services/sourced_track/sources/piped.dart index 8a1ec1bc..1eec8549 100644 --- a/lib/services/sourced_track/sources/piped.dart +++ b/lib/services/sourced_track/sources/piped.dart @@ -55,7 +55,7 @@ class PipedSourcedTrack extends SourcedTrack { if (cachedSource == null) { final siblings = await fetchSiblings(ref: ref, track: track); if (siblings.isEmpty) { - throw TrackNotFoundException(track); + throw TrackNotFoundError(track); } await SourceMatch.box.put( @@ -160,13 +160,16 @@ class PipedSourcedTrack extends SourcedTrack { final query = SourcedTrack.getSearchTerm(track); final PipedSearchResult(items: searchResults) = await pipedClient.search( - "$query - Topic", + query, preference.searchMode == SearchMode.youtube ? PipedFilter.video : PipedFilter.musicSongs, ); - final isYouTubeMusic = preference.searchMode == SearchMode.youtubeMusic; + // when falling back to piped API make sure to use the YouTube mode + final isYouTubeMusic = preference.audioSource != AudioSource.piped + ? false + : preference.searchMode == SearchMode.youtubeMusic; if (isYouTubeMusic) { final artists = (track.artists ?? []) diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index f363937c..d27510e4 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -48,7 +48,7 @@ class YoutubeSourcedTrack extends SourcedTrack { if (cachedSource == null || cachedSource.sourceType != SourceType.youtube) { final siblings = await fetchSiblings(ref: ref, track: track); if (siblings.isEmpty) { - throw TrackNotFoundException(track); + throw TrackNotFoundError(track); } await SourceMatch.box.put( diff --git a/pubspec.lock b/pubspec.lock index 0d1b2993..0050a745 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1619,10 +1619,10 @@ packages: dependency: "direct main" description: name: piped_client - sha256: "8b96e1f9d8533c1da7eff7fbbd4bf188256fc76a20900d378b52be09418ea771" + sha256: "87b04b2ebf4e008cfbb0ac85e9920ab3741f5aa697be2dd44919658a3297a4bc" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d3fb5630..21175930 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,7 +76,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.8 permission_handler: ^11.0.1 - piped_client: ^0.1.0 + piped_client: ^0.1.1 popover: ^0.2.6+3 scrobblenaut: git: diff --git a/untranslated_messages.json b/untranslated_messages.json index 4240c8c0..e4e45624 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -77,6 +77,7 @@ ], "nl": [ + "audio_source", "start_a_radio", "how_to_start_radio", "replace_queue_question",