mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-07 15:59:42 +00:00
Compare commits
5 Commits
cfe07d0b66
...
311af069d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
311af069d8 | ||
|
|
2c4cc94985 | ||
|
|
ff252d6b14 | ||
|
|
195cad8f39 | ||
|
|
19f525fa3c |
@ -94,6 +94,7 @@ abstract class FakeData {
|
|||||||
..trackNumber = 1
|
..trackNumber = 1
|
||||||
..type = "type"
|
..type = "type"
|
||||||
..uri = "uri"
|
..uri = "uri"
|
||||||
|
..externalIds = externalIds
|
||||||
..isPlayable = true
|
..isPlayable = true
|
||||||
..explicit = false
|
..explicit = false
|
||||||
..linkedFrom = trackLink;
|
..linkedFrom = trackLink;
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import 'dart:typed_data';
|
|||||||
import 'package:metadata_god/metadata_god.dart';
|
import 'package:metadata_god/metadata_god.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
import 'package:spotify/spotify.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/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
extension TrackExtensions on Track {
|
extension TrackExtensions on Track {
|
||||||
Track fromFile(
|
Track fromFile(
|
||||||
@ -67,27 +69,40 @@ extension TrackExtensions on Track {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TrackSimpleExtensions on TrackSimple {
|
extension IterableTrackSimpleExtensions on Iterable<TrackSimple> {
|
||||||
Track asTrack(AlbumSimple album) {
|
Future<List<Track>> asTracks(AlbumSimple album, ref) async {
|
||||||
Track track = Track();
|
try {
|
||||||
track.name = name;
|
final spotify = ref.read(spotifyProvider);
|
||||||
track.album = album;
|
final tracks = await spotify.invoke(
|
||||||
track.artists = artists;
|
(api) => api.tracks.list(map((trackSimple) => trackSimple.id!).toList()));
|
||||||
track.availableMarkets = availableMarkets;
|
return tracks.toList();
|
||||||
track.discNumber = discNumber;
|
} catch (e, stack) {
|
||||||
track.durationMs = durationMs;
|
// Ignore errors and create the track locally
|
||||||
track.explicit = explicit;
|
AppLogger.reportError(e, stack);
|
||||||
track.externalUrls = externalUrls;
|
|
||||||
track.href = href;
|
List<Track> tracks = [];
|
||||||
track.id = id;
|
for (final trackSimple in this) {
|
||||||
track.isPlayable = isPlayable;
|
Track track = Track();
|
||||||
track.linkedFrom = linkedFrom;
|
track.album = album;
|
||||||
track.name = name;
|
track.name = trackSimple.name;
|
||||||
track.previewUrl = previewUrl;
|
track.artists = trackSimple.artists;
|
||||||
track.trackNumber = trackNumber;
|
track.availableMarkets = trackSimple.availableMarkets;
|
||||||
track.type = type;
|
track.discNumber = trackSimple.discNumber;
|
||||||
track.uri = uri;
|
track.durationMs = trackSimple.durationMs;
|
||||||
return track;
|
track.explicit = trackSimple.explicit;
|
||||||
|
track.externalUrls = trackSimple.externalUrls;
|
||||||
|
track.href = trackSimple.href;
|
||||||
|
track.id = trackSimple.id;
|
||||||
|
track.isPlayable = trackSimple.isPlayable;
|
||||||
|
track.linkedFrom = trackSimple.linkedFrom;
|
||||||
|
track.previewUrl = trackSimple.previewUrl;
|
||||||
|
track.trackNumber = trackSimple.trackNumber;
|
||||||
|
track.type = trackSimple.type;
|
||||||
|
track.uri = trackSimple.uri;
|
||||||
|
tracks.add(track);
|
||||||
|
}
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -137,16 +137,16 @@
|
|||||||
"pre_download_play_description": "Anzi che effettuare lo stream dell'audio, scarica invece i byte e li riproduce (raccomandato per gli utenti con banda più alta)",
|
"pre_download_play_description": "Anzi che effettuare lo stream dell'audio, scarica invece i byte e li riproduce (raccomandato per gli utenti con banda più alta)",
|
||||||
"skip_non_music": "Salta i segmenti non di musica (SponsorBlock)",
|
"skip_non_music": "Salta i segmenti non di musica (SponsorBlock)",
|
||||||
"blacklist_description": "Tracce e artisti in blacklist",
|
"blacklist_description": "Tracce e artisti in blacklist",
|
||||||
"wait_for_download_to_finish": "Prego attendere che lo scaricamento corrente finisca",
|
"wait_for_download_to_finish": "Prego attendere che il download corrente finisca",
|
||||||
"desktop": "Desktop",
|
"desktop": "Desktop",
|
||||||
"close_behavior": "Comportamento Chiusura",
|
"close_behavior": "Comportamento Chiusura",
|
||||||
"close": "Chiudi",
|
"close": "Chiudi",
|
||||||
"minimize_to_tray": "Minimizza in tray",
|
"minimize_to_tray": "Minimizza in tray",
|
||||||
"show_tray_icon": "Mostra icona in tray di sistema",
|
"show_tray_icon": "Mostra icona in tray di sistema",
|
||||||
"about": "A proposito di",
|
"about": "Informazioni su",
|
||||||
"u_love_spotube": "Sappiamo che ami Spotube",
|
"u_love_spotube": "Sappiamo che ami Spotube",
|
||||||
"check_for_updates": "Controlla aggiornamenti",
|
"check_for_updates": "Controlla aggiornamenti",
|
||||||
"about_spotube": "A proposito di Spotube",
|
"about_spotube": "Informazioni su Spotube",
|
||||||
"blacklist": "Blacklist",
|
"blacklist": "Blacklist",
|
||||||
"please_sponsor": "Per favore sponsorizza/dona",
|
"please_sponsor": "Per favore sponsorizza/dona",
|
||||||
"spotube_description": "Spotube, un client spotify gratis per tutti, multipiattaforma e leggero",
|
"spotube_description": "Spotube, un client spotify gratis per tutti, multipiattaforma e leggero",
|
||||||
@ -187,7 +187,7 @@
|
|||||||
"generate_playlist": "Genera Playlist",
|
"generate_playlist": "Genera Playlist",
|
||||||
"track_exists": "La traccia {track} esiste già",
|
"track_exists": "La traccia {track} esiste già",
|
||||||
"replace_downloaded_tracks": "Sostituisci tutte le tracce scaricate",
|
"replace_downloaded_tracks": "Sostituisci tutte le tracce scaricate",
|
||||||
"skip_download_tracks": "Salta lo scaricamento di tutte le tracce scaricate",
|
"skip_download_tracks": "Salta il download di tutte le tracce scaricate",
|
||||||
"do_you_want_to_replace": "Vuoi sovrascrivere la traccia esistente??",
|
"do_you_want_to_replace": "Vuoi sovrascrivere la traccia esistente??",
|
||||||
"replace": "Sovrascrivi",
|
"replace": "Sovrascrivi",
|
||||||
"skip": "Salta",
|
"skip": "Salta",
|
||||||
@ -256,7 +256,7 @@
|
|||||||
"querying_info": "Richiesta informazioni...",
|
"querying_info": "Richiesta informazioni...",
|
||||||
"piped_api_down": "Le Piped API non funzionano",
|
"piped_api_down": "Le Piped API non funzionano",
|
||||||
"piped_down_error_instructions": "L'istanza di Piped {pipedInstance} è correntemente offline\n\nCambia istanza o cambia 'Tipo API' alle API ufficiali YouTube\n\nAssicurati di riavviare l'app dopo il cambio",
|
"piped_down_error_instructions": "L'istanza di Piped {pipedInstance} è correntemente offline\n\nCambia istanza o cambia 'Tipo API' alle API ufficiali YouTube\n\nAssicurati di riavviare l'app dopo il cambio",
|
||||||
"you_are_offline": "Sei correntemente offline",
|
"you_are_offline": "Al momento sei offline",
|
||||||
"connection_restored": "Connessione ad internet ripristinata",
|
"connection_restored": "Connessione ad internet ripristinata",
|
||||||
"use_system_title_bar": "Usa la barra del titolo di sistema",
|
"use_system_title_bar": "Usa la barra del titolo di sistema",
|
||||||
"crunching_results": "Elaborazione risultati...",
|
"crunching_results": "Elaborazione risultati...",
|
||||||
@ -267,15 +267,15 @@
|
|||||||
"change_cover": "Cambia copertina",
|
"change_cover": "Cambia copertina",
|
||||||
"add_cover": "Aggiungi copertina",
|
"add_cover": "Aggiungi copertina",
|
||||||
"restore_defaults": "Ripristina default",
|
"restore_defaults": "Ripristina default",
|
||||||
"download_music_codec": "Codec musicale scaricamento",
|
"download_music_codec": "Codec download musica",
|
||||||
"streaming_music_codec": "Codec musicale streaming",
|
"streaming_music_codec": "Codec streaming musica",
|
||||||
"login_with_lastfm": "Accesso a Last.fm",
|
"login_with_lastfm": "Accedi con Last.fm",
|
||||||
"connect": "Connetti",
|
"connect": "Connettiti",
|
||||||
"disconnect_lastfm": "Disconnetti Last.fm",
|
"disconnect_lastfm": "Disconnettiti da Last.fm",
|
||||||
"disconnect": "Disconnetti",
|
"disconnect": "Disconnetti",
|
||||||
"username": "Nome utente",
|
"username": "Nome utente",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"login": "Accesso",
|
"login": "Accedi",
|
||||||
"login_with_your_lastfm": "Accedi con il tuo account Last.fm",
|
"login_with_your_lastfm": "Accedi con il tuo account Last.fm",
|
||||||
"scrobble_to_lastfm": "Invia a Last.fm",
|
"scrobble_to_lastfm": "Invia a Last.fm",
|
||||||
"audio_source": "Fonte audio",
|
"audio_source": "Fonte audio",
|
||||||
@ -299,7 +299,7 @@
|
|||||||
"song_link": "Link della Canzone",
|
"song_link": "Link della Canzone",
|
||||||
"skip_this_nonsense": "Salta questa sciocchezza",
|
"skip_this_nonsense": "Salta questa sciocchezza",
|
||||||
"freedom_of_music": "“Libertà della Musica”",
|
"freedom_of_music": "“Libertà della Musica”",
|
||||||
"freedom_of_music_palm": "“Libertà della Musica nel palmo della tua mano”",
|
"freedom_of_music_palm": "“Libertà della Musica nelle tue mani”",
|
||||||
"get_started": "Cominciamo",
|
"get_started": "Cominciamo",
|
||||||
"youtube_source_description": "Consigliato e funziona meglio.",
|
"youtube_source_description": "Consigliato e funziona meglio.",
|
||||||
"piped_source_description": "Ti senti libero? Come YouTube ma molto più gratuito.",
|
"piped_source_description": "Ti senti libero? Come YouTube ma molto più gratuito.",
|
||||||
|
|||||||
@ -54,7 +54,7 @@ class AlbumCard extends HookConsumerWidget {
|
|||||||
|
|
||||||
Future<List<Track>> fetchAllTrack() async {
|
Future<List<Track>> fetchAllTrack() async {
|
||||||
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
if (album.tracks != null && album.tracks!.isNotEmpty) {
|
||||||
return album.tracks!.map((track) => track.asTrack(album)).toList();
|
return album.tracks!.asTracks(album, ref);
|
||||||
}
|
}
|
||||||
await ref.read(albumTracksProvider(album).future);
|
await ref.read(albumTracksProvider(album).future);
|
||||||
return ref.read(albumTracksProvider(album).notifier).fetchAll();
|
return ref.read(albumTracksProvider(album).notifier).fetchAll();
|
||||||
|
|||||||
@ -33,7 +33,7 @@ class AlbumTracksNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<Track,
|
|||||||
final tracks = await spotify.invoke(
|
final tracks = await spotify.invoke(
|
||||||
(api) => api.albums.tracks(arg.id!).getPage(limit, offset),
|
(api) => api.albums.tracks(arg.id!).getPage(limit, offset),
|
||||||
);
|
);
|
||||||
final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? [];
|
final items = await tracks.items!.asTracks(arg, ref);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
items: items,
|
items: items,
|
||||||
|
|||||||
@ -236,29 +236,67 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<List<YoutubeVideoInfo>> fetchFromIsrc({
|
||||||
|
required Track track,
|
||||||
|
required Provider provider,
|
||||||
|
required Ref ref,
|
||||||
|
}) async {
|
||||||
|
final isrcResults = <YoutubeVideoInfo>[];
|
||||||
|
final isrc = track.externalIds?.isrc;
|
||||||
|
if (isrc != null && isrc.isNotEmpty) {
|
||||||
|
final searchedVideos = await ref
|
||||||
|
.read(provider)
|
||||||
|
.searchVideos(isrc.toString());
|
||||||
|
if (searchedVideos.isNotEmpty) {
|
||||||
|
isrcResults.addAll(searchedVideos
|
||||||
|
.map<YoutubeVideoInfo>(YoutubeVideoInfo.fromVideo)
|
||||||
|
.map((YoutubeVideoInfo videoInfo) {
|
||||||
|
final ytWords =
|
||||||
|
videoInfo.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '')
|
||||||
|
.split(RegExp(r'\s+'))
|
||||||
|
.where((item) => item.isNotEmpty);
|
||||||
|
final spWords =
|
||||||
|
track.name!
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(RegExp(r'\((.*)\)'), '')
|
||||||
|
.replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '')
|
||||||
|
.split(RegExp(r'\s+'))
|
||||||
|
.where((item) => item.isNotEmpty);
|
||||||
|
// Word match to filter out unrelated results
|
||||||
|
final matchCount =
|
||||||
|
ytWords.where((word) => spWords.contains(word)).length;
|
||||||
|
if (matchCount > spWords.length ~/ 2) {
|
||||||
|
return videoInfo;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
).whereType<YoutubeVideoInfo>().toList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isrcResults;
|
||||||
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
required Track track,
|
required Track track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
|
final videoResults = <YoutubeVideoInfo>[];
|
||||||
|
|
||||||
|
final isrcResults = await fetchFromIsrc(track: track, provider: youtubeEngineProvider, ref: ref);
|
||||||
|
videoResults.addAll(isrcResults);
|
||||||
|
|
||||||
final links = await SongLinkService.links(track.id!);
|
final links = await SongLinkService.links(track.id!);
|
||||||
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
|
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
|
||||||
|
|
||||||
if (ytLink?.url != null
|
if (isrcResults.isEmpty && ytLink?.url != null) {
|
||||||
// allows to fetch siblings more results for already sourced track
|
|
||||||
&&
|
|
||||||
track is! SourcedTrack) {
|
|
||||||
try {
|
try {
|
||||||
return [
|
videoResults.add(
|
||||||
await toSiblingType(
|
|
||||||
0,
|
|
||||||
YoutubeVideoInfo.fromVideo(
|
YoutubeVideoInfo.fromVideo(
|
||||||
await ref.read(youtubeEngineProvider).getVideo(
|
await ref.read(youtubeEngineProvider)
|
||||||
Uri.parse(ytLink!.url!).queryParameters["v"]!,
|
.getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!)
|
||||||
),
|
));
|
||||||
),
|
|
||||||
ref,
|
|
||||||
)
|
|
||||||
];
|
|
||||||
} on VideoUnplayableException catch (e, stack) {
|
} on VideoUnplayableException catch (e, stack) {
|
||||||
// Ignore this error and continue with the search
|
// Ignore this error and continue with the search
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(e, stack);
|
||||||
@ -271,20 +309,28 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
await ref.read(youtubeEngineProvider).searchVideos(query);
|
await ref.read(youtubeEngineProvider).searchVideos(query);
|
||||||
|
|
||||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
if (ServiceUtils.onlyContainsEnglish(query)) {
|
||||||
return await Future.wait(searchResults
|
videoResults.addAll(
|
||||||
.map(YoutubeVideoInfo.fromVideo)
|
searchResults.map(YoutubeVideoInfo.fromVideo).toList()
|
||||||
.mapIndexed((index, info) => toSiblingType(index, info, ref)));
|
);
|
||||||
|
} else {
|
||||||
|
videoResults.addAll(rankResults(
|
||||||
|
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
||||||
|
track,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
final rankedSiblings = rankResults(
|
final seenIds = <String>{};
|
||||||
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
int index = 0;
|
||||||
track,
|
|
||||||
);
|
|
||||||
|
|
||||||
return await Future.wait(
|
return await Future.wait(
|
||||||
rankedSiblings
|
videoResults.map((videoResult) async {
|
||||||
.mapIndexed((index, info) => toSiblingType(index, info, ref)),
|
// Deduplicate results
|
||||||
);
|
if (!seenIds.contains(videoResult.id)) {
|
||||||
|
seenIds.add(videoResult.id);
|
||||||
|
return await toSiblingType(index++, videoResult, ref);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}),
|
||||||
|
).then((s) => s.whereType<SiblingType>().toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user