mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-09 08:47:31 +00:00
Compare commits
2 Commits
da3957beb3
...
2341e5ca06
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2341e5ca06 | ||
|
|
50d729f8a0 |
@ -94,7 +94,6 @@ 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,9 +4,7 @@ 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(
|
||||||
@ -70,38 +68,26 @@ extension TrackExtensions on Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension TrackSimpleExtensions on TrackSimple {
|
extension TrackSimpleExtensions on TrackSimple {
|
||||||
Future<Track> asTrack(AlbumSimple album, ref) async {
|
Track asTrack(AlbumSimple album) {
|
||||||
try {
|
Track track = Track();
|
||||||
final spotify = ref.read(spotifyProvider);
|
track.name = name;
|
||||||
return await spotify.invoke((api) => api.tracks.get(id!));
|
track.album = album;
|
||||||
} catch (e, stack) {
|
track.artists = artists;
|
||||||
if ((e is AuthorizationException && e.error != 'invalid_token') ||
|
track.availableMarkets = availableMarkets;
|
||||||
e is SpotifyException) {
|
track.discNumber = discNumber;
|
||||||
// Ignore this error and create the Track locally
|
track.durationMs = durationMs;
|
||||||
AppLogger.reportError(e, stack);
|
track.explicit = explicit;
|
||||||
|
track.externalUrls = externalUrls;
|
||||||
Track track = Track();
|
track.href = href;
|
||||||
track.name = name;
|
track.id = id;
|
||||||
track.album = album;
|
track.isPlayable = isPlayable;
|
||||||
track.artists = artists;
|
track.linkedFrom = linkedFrom;
|
||||||
track.availableMarkets = availableMarkets;
|
track.name = name;
|
||||||
track.discNumber = discNumber;
|
track.previewUrl = previewUrl;
|
||||||
track.durationMs = durationMs;
|
track.trackNumber = trackNumber;
|
||||||
track.explicit = explicit;
|
track.type = type;
|
||||||
track.externalUrls = externalUrls;
|
track.uri = uri;
|
||||||
track.href = href;
|
return track;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,8 +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 Future.wait(
|
return album.tracks!.map((track) => track.asTrack(album)).toList();
|
||||||
album.tracks!.map((track) => track.asTrack(album, ref)).toList());
|
|
||||||
}
|
}
|
||||||
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 List<Track> items = await Future.wait(tracks.items?.map((e) => e.asTrack(arg, ref)).toList() ?? []);
|
final items = tracks.items?.map((e) => e.asTrack(arg)).toList() ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
items: items,
|
items: items,
|
||||||
|
|||||||
@ -236,64 +236,69 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<YoutubeVideoInfo>> fetchFromIsrc({
|
|
||||||
required Track track,
|
|
||||||
required Provider provider,
|
|
||||||
required Ref ref,
|
|
||||||
}) async {
|
|
||||||
final matchingResults = <YoutubeVideoInfo>[];
|
|
||||||
final isrc = track.externalIds?.isrc;
|
|
||||||
if (isrc != null && isrc.isNotEmpty) {
|
|
||||||
final isrcResults = await ref
|
|
||||||
.read(provider)
|
|
||||||
.searchVideos(isrc.toString());
|
|
||||||
if (isrcResults.isNotEmpty) {
|
|
||||||
for (final videoInfo in isrcResults.map(YoutubeVideoInfo.fromVideo).toList()) {
|
|
||||||
final titleWords =
|
|
||||||
videoInfo.title
|
|
||||||
.toLowerCase()
|
|
||||||
.replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '')
|
|
||||||
.split(RegExp(r'\s+'))
|
|
||||||
.where((item) => item.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
final nameLower =
|
|
||||||
track.name!
|
|
||||||
.toLowerCase()
|
|
||||||
.replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '')
|
|
||||||
.split(RegExp(r'\s+'))
|
|
||||||
.where((item) => item.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
final matchCount =
|
|
||||||
titleWords.where((word) => nameLower.contains(word)).length;
|
|
||||||
if (matchCount > nameLower.length / 2) {
|
|
||||||
matchingResults.add(videoInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return matchingResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
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>[];
|
List<SiblingType> siblings = [];
|
||||||
|
|
||||||
videoResults.addAll(await fetchFromIsrc(track: track, provider: youtubeEngineProvider, ref: ref));
|
final isrc = track.externalIds?.isrc;
|
||||||
|
if (isrc != null && isrc.isNotEmpty) {
|
||||||
|
final isrcResults =
|
||||||
|
await ref.read(youtubeEngineProvider).searchVideos(isrc.toString());
|
||||||
|
if (isrcResults.isNotEmpty) {
|
||||||
|
final rankedResults = rankResults(
|
||||||
|
isrcResults.map(YoutubeVideoInfo.fromVideo).toList(), track);
|
||||||
|
final matchingResults = <YoutubeVideoInfo>[];
|
||||||
|
for (final video in rankedResults) {
|
||||||
|
final titleWords = video.title
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '')
|
||||||
|
.split(RegExp(r'\s+'))
|
||||||
|
.where((item) => item.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
final nameLower = track.name!
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(RegExp(r'[^a-zA-Z0-9\s]+'), '')
|
||||||
|
.split(RegExp(r'\s+'))
|
||||||
|
.where((item) => item.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
final matchCount =
|
||||||
|
titleWords.where((word) => nameLower.contains(word)).length;
|
||||||
|
if (matchCount > nameLower.length / 2) {
|
||||||
|
matchingResults.add(video);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchingResults.isNotEmpty) {
|
||||||
|
final matchingSiblings =
|
||||||
|
await Future.wait(matchingResults.map((matchingResult) async {
|
||||||
|
try {
|
||||||
|
return await toSiblingType(0, matchingResult, ref);
|
||||||
|
} on VideoUnplayableException catch (e, stack) {
|
||||||
|
// Ignore this error and continue with the search
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
siblings.addAll(matchingSiblings.whereType<SiblingType>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 (ytLink?.url != null) {
|
||||||
try {
|
try {
|
||||||
videoResults.add(
|
siblings.add(await toSiblingType(
|
||||||
YoutubeVideoInfo.fromVideo(
|
0,
|
||||||
await ref
|
YoutubeVideoInfo.fromVideo(
|
||||||
.read(youtubeEngineProvider)
|
await ref.read(youtubeEngineProvider).getVideo(
|
||||||
.getVideo(Uri.parse(ytLink!.url!).queryParameters["v"]!),
|
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);
|
||||||
@ -302,33 +307,33 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
final searchResults = await ref
|
final searchResults =
|
||||||
.read(youtubeEngineProvider)
|
await ref.read(youtubeEngineProvider).searchVideos(query);
|
||||||
.searchVideos(query);
|
|
||||||
|
|
||||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
if (ServiceUtils.onlyContainsEnglish(query)) {
|
||||||
videoResults.addAll(
|
siblings.addAll(await Future.wait(searchResults
|
||||||
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
.map(YoutubeVideoInfo.fromVideo)
|
||||||
);
|
.mapIndexed((index, info) => toSiblingType(index, info, ref))));
|
||||||
} else {
|
} else {
|
||||||
videoResults.addAll(rankResults(
|
final rankedSiblings = rankResults(
|
||||||
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
searchResults.map(YoutubeVideoInfo.fromVideo).toList(),
|
||||||
track,
|
track,
|
||||||
|
);
|
||||||
|
siblings.addAll(await Future.wait(
|
||||||
|
rankedSiblings
|
||||||
|
.mapIndexed((index, info) => toSiblingType(index, info, ref)),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
final seenIds = <String>{};
|
final seenIds = <String>{};
|
||||||
int index = 0;
|
// Deduplicate siblings by info.id, keeping the first occurrence
|
||||||
return await Future.wait(
|
return await Future.wait(siblings.map((sibling) async {
|
||||||
videoResults.map((videoResult) async {
|
if (!seenIds.contains(sibling.info.id)) {
|
||||||
// Deduplicate results
|
seenIds.add(sibling.info.id);
|
||||||
if (!seenIds.contains(videoResult.id)) {
|
return sibling;
|
||||||
seenIds.add(videoResult.id);
|
}
|
||||||
return await toSiblingType(index++, videoResult, ref);
|
return null;
|
||||||
}
|
})).then((s) => s.whereType<SiblingType>().toList());
|
||||||
return null;
|
|
||||||
}),
|
|
||||||
).then((s) => s.whereType<SiblingType>().toList());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user