mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: add soundcloud sourced track source
This commit is contained in:
parent
edc9636940
commit
325ad2a526
@ -15,7 +15,8 @@ enum AudioSource {
|
|||||||
youtube,
|
youtube,
|
||||||
piped,
|
piped,
|
||||||
jiosaavn,
|
jiosaavn,
|
||||||
invidious;
|
invidious,
|
||||||
|
soundcloud;
|
||||||
|
|
||||||
String get label => name[0].toUpperCase() + name.substring(1);
|
String get label => name[0].toUpperCase() + name.substring(1);
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,8 @@ part of '../database.dart';
|
|||||||
enum SourceType {
|
enum SourceType {
|
||||||
youtube._("YouTube"),
|
youtube._("YouTube"),
|
||||||
youtubeMusic._("YouTube Music"),
|
youtubeMusic._("YouTube Music"),
|
||||||
jiosaavn._("JioSaavn");
|
jiosaavn._("JioSaavn"),
|
||||||
|
soundcloud._("SoundCloud");
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
@ -46,6 +46,27 @@ class ServerPlaybackRoutes {
|
|||||||
|
|
||||||
ServerPlaybackRoutes(this.ref) : dio = Dio();
|
ServerPlaybackRoutes(this.ref) : dio = Dio();
|
||||||
|
|
||||||
|
/// proxy hls playlist file
|
||||||
|
Future<Response> proxyHls(String url, Map<String, dynamic> headers) async {
|
||||||
|
try {
|
||||||
|
final response = await dio.get(
|
||||||
|
url,
|
||||||
|
options: Options(responseType: ResponseType.bytes),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response.ok(
|
||||||
|
response.data as Uint8List,
|
||||||
|
headers: {
|
||||||
|
"content-type": "audio/mpegurl",
|
||||||
|
"content-length": (response.data as Uint8List).length.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
return Response.internalServerError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<({dio_lib.Response<Uint8List> response, Uint8List? bytes})>
|
Future<({dio_lib.Response<Uint8List> response, Uint8List? bytes})>
|
||||||
streamTrack(
|
streamTrack(
|
||||||
SourcedTrack track,
|
SourcedTrack track,
|
||||||
@ -201,8 +222,12 @@ class ServerPlaybackRoutes {
|
|||||||
|
|
||||||
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
||||||
|
|
||||||
|
if (sourcedTrack!.url.contains(".m3u8")) {
|
||||||
|
return await proxyHls(sourcedTrack.url, request.headers);
|
||||||
|
}
|
||||||
|
|
||||||
final (bytes: audioBytes, response: res) =
|
final (bytes: audioBytes, response: res) =
|
||||||
await streamTrack(sourcedTrack!, request.headers);
|
await streamTrack(sourcedTrack, request.headers);
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
res.statusCode!,
|
res.statusCode!,
|
||||||
|
@ -133,4 +133,23 @@ class YoutubeVideoInfo {
|
|||||||
DateTime.fromMillisecondsSinceEpoch(searchResponse.published * 1000),
|
DateTime.fromMillisecondsSinceEpoch(searchResponse.published * 1000),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
factory YoutubeVideoInfo.fromVideoResponse(
|
||||||
|
InvidiousVideoResponse videoResponse,
|
||||||
|
SearchMode searchMode,
|
||||||
|
) {
|
||||||
|
return YoutubeVideoInfo(
|
||||||
|
searchMode: searchMode,
|
||||||
|
title: videoResponse.title,
|
||||||
|
duration: Duration(seconds: videoResponse.lengthSeconds),
|
||||||
|
thumbnailUrl: videoResponse.videoThumbnails.first.url,
|
||||||
|
id: videoResponse.videoId,
|
||||||
|
likes: videoResponse.likeCount,
|
||||||
|
dislikes: videoResponse.dislikeCount,
|
||||||
|
views: videoResponse.viewCount,
|
||||||
|
channelName: videoResponse.author,
|
||||||
|
channelId: videoResponse.authorId,
|
||||||
|
publishedAt: DateTime.fromMillisecondsSinceEpoch(videoResponse.published),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import 'package:spotube/services/sourced_track/models/source_map.dart';
|
|||||||
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.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/piped.dart';
|
||||||
|
import 'package:spotube/services/sourced_track/sources/soundcloud.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
|
|
||||||
@ -86,6 +87,13 @@ abstract class SourcedTrack extends Track {
|
|||||||
sourceInfo: sourceInfo,
|
sourceInfo: sourceInfo,
|
||||||
track: track,
|
track: track,
|
||||||
),
|
),
|
||||||
|
AudioSource.soundcloud => SoundcloudSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
source: source,
|
||||||
|
siblings: siblings,
|
||||||
|
sourceInfo: sourceInfo,
|
||||||
|
track: track,
|
||||||
|
)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,6 +124,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
AudioSource.jiosaavn =>
|
AudioSource.jiosaavn =>
|
||||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
|
AudioSource.soundcloud =>
|
||||||
|
await SoundcloudSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +144,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref),
|
JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||||
AudioSource.invidious =>
|
AudioSource.invidious =>
|
||||||
InvidiousSourcedTrack.fetchSiblings(track: track, ref: ref),
|
InvidiousSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||||
|
AudioSource.soundcloud =>
|
||||||
|
SoundcloudSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,6 +5,8 @@ import 'package:spotify/spotify.dart';
|
|||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
import 'package:spotube/services/song_link/song_link.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.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_info.dart';
|
||||||
@ -180,6 +182,27 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
final invidiousClient = ref.read(invidiousProvider);
|
final invidiousClient = ref.read(invidiousProvider);
|
||||||
final preference = ref.read(userPreferencesProvider);
|
final preference = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
|
final links = await SongLinkService.links(track.id!);
|
||||||
|
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
|
||||||
|
|
||||||
|
if (ytLink != null && track is! SourcedTrack) {
|
||||||
|
try {
|
||||||
|
final videoId = Uri.parse(ytLink.url!).queryParameters["v"]!;
|
||||||
|
|
||||||
|
final manifest = await invidiousClient.videos.get(videoId, local: true);
|
||||||
|
|
||||||
|
return [
|
||||||
|
await toSiblingType(
|
||||||
|
0,
|
||||||
|
YoutubeVideoInfo.fromVideoResponse(manifest, preference.searchMode),
|
||||||
|
invidiousClient,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
} catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
final searchResults = await invidiousClient.search.list(
|
final searchResults = await invidiousClient.search.list(
|
||||||
|
@ -6,6 +6,8 @@ import 'package:spotify/spotify.dart';
|
|||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/database/database.dart';
|
import 'package:spotube/provider/database/database.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
import 'package:spotube/services/song_link/song_link.dart';
|
||||||
|
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||||
@ -181,6 +183,28 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
final pipedClient = ref.read(pipedProvider);
|
final pipedClient = ref.read(pipedProvider);
|
||||||
final preference = ref.read(userPreferencesProvider);
|
final preference = ref.read(userPreferencesProvider);
|
||||||
|
|
||||||
|
final links = await SongLinkService.links(track.id!);
|
||||||
|
final ytLink = links.firstWhereOrNull((link) => link.platform == "youtube");
|
||||||
|
|
||||||
|
if (ytLink != null && track is! SourcedTrack) {
|
||||||
|
try {
|
||||||
|
final videoId = Uri.parse(ytLink.url!).queryParameters["v"]!;
|
||||||
|
|
||||||
|
final manifest = await pipedClient.streams(videoId);
|
||||||
|
|
||||||
|
return [
|
||||||
|
await toSiblingType(
|
||||||
|
0,
|
||||||
|
YoutubeVideoInfo.fromStreamResponse(
|
||||||
|
manifest, preference.searchMode),
|
||||||
|
pipedClient,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
} catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
final PipedSearchResult(items: searchResults) = await pipedClient.search(
|
final PipedSearchResult(items: searchResults) = await pipedClient.search(
|
||||||
|
317
lib/services/sourced_track/sources/soundcloud.dart
Normal file
317
lib/services/sourced_track/sources/soundcloud.dart
Normal file
@ -0,0 +1,317 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/provider/database/database.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
import 'package:spotube/services/song_link/song_link.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/sourced_track.dart';
|
||||||
|
import 'package:soundcloud_explode_dart/soundcloud_explode_dart.dart'
|
||||||
|
as soundcloud;
|
||||||
|
|
||||||
|
final soundcloudProvider = Provider<soundcloud.SoundcloudClient>(
|
||||||
|
(ref) {
|
||||||
|
return soundcloud.SoundcloudClient();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
class SoundcloudSourceInfo extends SourceInfo {
|
||||||
|
SoundcloudSourceInfo({
|
||||||
|
required super.id,
|
||||||
|
required super.title,
|
||||||
|
required super.artist,
|
||||||
|
required super.thumbnail,
|
||||||
|
required super.pageUrl,
|
||||||
|
required super.duration,
|
||||||
|
required super.artistUrl,
|
||||||
|
required super.album,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SoundcloudSourcedTrack extends SourcedTrack {
|
||||||
|
SoundcloudSourcedTrack({
|
||||||
|
required super.ref,
|
||||||
|
required super.source,
|
||||||
|
required super.siblings,
|
||||||
|
required super.sourceInfo,
|
||||||
|
required super.track,
|
||||||
|
});
|
||||||
|
|
||||||
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
|
required Track track,
|
||||||
|
required Ref ref,
|
||||||
|
}) async {
|
||||||
|
// Indicates a stream url refresh
|
||||||
|
if (track is SoundcloudSourcedTrack) {
|
||||||
|
final manifest = await ref
|
||||||
|
.read(soundcloudProvider)
|
||||||
|
.tracks
|
||||||
|
.getStreams(int.parse(track.sourceInfo.id));
|
||||||
|
|
||||||
|
return SoundcloudSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: track.siblings,
|
||||||
|
source: toSourceMap(manifest),
|
||||||
|
sourceInfo: track.sourceInfo,
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||||
|
..where((s) => s.trackId.equals(track.id!))
|
||||||
|
..limit(1)
|
||||||
|
..orderBy([
|
||||||
|
(s) =>
|
||||||
|
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
|
||||||
|
]))
|
||||||
|
.getSingleOrNull();
|
||||||
|
final soundcloudClient = ref.read(soundcloudProvider);
|
||||||
|
|
||||||
|
if (cachedSource == null ||
|
||||||
|
cachedSource.sourceType != SourceType.soundcloud) {
|
||||||
|
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||||
|
if (siblings.isEmpty) {
|
||||||
|
throw TrackNotFoundError(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.into(database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: track.id!,
|
||||||
|
sourceId: siblings.first.info.id,
|
||||||
|
sourceType: const Value(SourceType.soundcloud),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return SoundcloudSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||||
|
source: siblings.first.source as SourceMap,
|
||||||
|
sourceInfo: siblings.first.info,
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final details = await soundcloudClient.tracks.get(
|
||||||
|
int.parse(cachedSource.sourceId),
|
||||||
|
);
|
||||||
|
final streams = await soundcloudClient.tracks.getStreams(
|
||||||
|
int.parse(cachedSource.sourceId),
|
||||||
|
);
|
||||||
|
|
||||||
|
return SoundcloudSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: [],
|
||||||
|
source: toSourceMap(streams),
|
||||||
|
sourceInfo: SoundcloudSourceInfo(
|
||||||
|
id: details.id.toString(),
|
||||||
|
artist: details.user.username,
|
||||||
|
artistUrl: details.user.permalinkUrl.toString(),
|
||||||
|
pageUrl: details.permalinkUrl.toString(),
|
||||||
|
thumbnail: details.artworkUrl.toString(),
|
||||||
|
title: details.title,
|
||||||
|
duration: Duration(seconds: details.duration.toInt()),
|
||||||
|
album: null,
|
||||||
|
),
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SourceMap toSourceMap(List<soundcloud.StreamInfo> manifest) {
|
||||||
|
final m4a = manifest
|
||||||
|
.where((audio) => audio.container == soundcloud.Container.mp3)
|
||||||
|
.sorted((a, b) {
|
||||||
|
return a.quality == soundcloud.Quality.highQuality ? 1 : -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
final weba = manifest
|
||||||
|
.where((audio) => audio.container == soundcloud.Container.ogg)
|
||||||
|
.sorted((a, b) {
|
||||||
|
return a.quality == soundcloud.Quality.highQuality ? 1 : -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return SourceMap(
|
||||||
|
m4a: SourceQualityMap(
|
||||||
|
high: m4a.first.url.toString(),
|
||||||
|
medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(),
|
||||||
|
low: m4a.last.url.toString(),
|
||||||
|
),
|
||||||
|
weba: weba.isNotEmpty
|
||||||
|
? SourceQualityMap(
|
||||||
|
high: weba.first.url.toString(),
|
||||||
|
medium: (weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1])
|
||||||
|
.url
|
||||||
|
.toString(),
|
||||||
|
low: weba.last.url.toString(),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<SiblingType> toSiblingType(
|
||||||
|
int index,
|
||||||
|
soundcloud.Track item,
|
||||||
|
soundcloud.SoundcloudClient soundcloudClient,
|
||||||
|
) async {
|
||||||
|
SourceMap? sourceMap;
|
||||||
|
if (index == 0) {
|
||||||
|
final manifest = await soundcloudClient.tracks.getStreams(item.id);
|
||||||
|
sourceMap = toSourceMap(manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
final SiblingType sibling = (
|
||||||
|
info: SoundcloudSourceInfo(
|
||||||
|
id: item.id.toString(),
|
||||||
|
artist: item.user.username,
|
||||||
|
artistUrl: item.user.permalinkUrl.toString(),
|
||||||
|
pageUrl: item.permalinkUrl.toString(),
|
||||||
|
thumbnail: item.artworkUrl.toString(),
|
||||||
|
title: item.title,
|
||||||
|
duration: Duration(seconds: item.duration.toInt()),
|
||||||
|
album: null,
|
||||||
|
),
|
||||||
|
source: sourceMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
return sibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
|
required Track track,
|
||||||
|
required Ref ref,
|
||||||
|
}) async {
|
||||||
|
final soundcloudClient = ref.read(soundcloudProvider);
|
||||||
|
|
||||||
|
final links = await SongLinkService.links(track.id!);
|
||||||
|
final soundcloudLink =
|
||||||
|
links.firstWhereOrNull((link) => link.platform == "soundcloud");
|
||||||
|
|
||||||
|
if (soundcloudLink != null && track is! SourcedTrack) {
|
||||||
|
try {
|
||||||
|
final details =
|
||||||
|
await soundcloudClient.tracks.getByUrl(soundcloudLink.url!);
|
||||||
|
|
||||||
|
return [
|
||||||
|
await toSiblingType(
|
||||||
|
0,
|
||||||
|
details,
|
||||||
|
soundcloudClient,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
} catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
|
final searchResults = await soundcloudClient.search
|
||||||
|
.getTracks(query, offset: 0, limit: 10)
|
||||||
|
.toList()
|
||||||
|
.then((value) => value.expand((e) => e).toList());
|
||||||
|
|
||||||
|
return await Future.wait(
|
||||||
|
searchResults.mapIndexed(
|
||||||
|
(i, r) => toSiblingType(
|
||||||
|
i,
|
||||||
|
soundcloud.Track(
|
||||||
|
id: r.id,
|
||||||
|
title: r.title,
|
||||||
|
duration: r.duration,
|
||||||
|
user: r.user,
|
||||||
|
artworkUrl: r.artworkUrl,
|
||||||
|
permalinkUrl: r.permalinkUrl,
|
||||||
|
caption: r.caption,
|
||||||
|
commentCount: r.commentCount,
|
||||||
|
createdAt: r.createdAt,
|
||||||
|
description: r.description,
|
||||||
|
downloadCount: r.downloadCount,
|
||||||
|
genre: r.genre,
|
||||||
|
commentable: r.commentable,
|
||||||
|
fullDuration: r.fullDuration,
|
||||||
|
labelName: r.labelName,
|
||||||
|
lastModified: r.lastModified,
|
||||||
|
license: r.license,
|
||||||
|
likesCount: r.likesCount,
|
||||||
|
monetizationModel: r.monetizationModel,
|
||||||
|
playbackCount: r.playbackCount,
|
||||||
|
policy: r.policy,
|
||||||
|
purchaseTitle: r.purchaseTitle,
|
||||||
|
purchaseUrl: r.purchaseUrl,
|
||||||
|
repostsCount: r.repostsCount,
|
||||||
|
tagList: r.tagList,
|
||||||
|
waveformUrl: r.waveformUrl,
|
||||||
|
),
|
||||||
|
soundcloudClient,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SourcedTrack> copyWithSibling() async {
|
||||||
|
if (siblings.isNotEmpty) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
final fetchedSiblings = await fetchSiblings(ref: ref, track: this);
|
||||||
|
|
||||||
|
return SoundcloudSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: fetchedSiblings
|
||||||
|
.where((s) => s.info.id != sourceInfo.id)
|
||||||
|
.map((s) => s.info)
|
||||||
|
.toList(),
|
||||||
|
source: source,
|
||||||
|
sourceInfo: sourceInfo,
|
||||||
|
track: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||||
|
if (sibling.id == sourceInfo.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a sibling source that was fetched from the search results
|
||||||
|
final isStepSibling = siblings.none((s) => s.id == sibling.id);
|
||||||
|
|
||||||
|
final newSourceInfo = isStepSibling
|
||||||
|
? sibling
|
||||||
|
: siblings.firstWhere((s) => s.id == sibling.id);
|
||||||
|
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||||
|
..insert(0, sourceInfo);
|
||||||
|
|
||||||
|
final soundcloudClient = ref.read(soundcloudProvider);
|
||||||
|
|
||||||
|
final manifest = await soundcloudClient.tracks.getStreams(
|
||||||
|
int.parse(newSourceInfo.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
final database = ref.read(databaseProvider);
|
||||||
|
await database.into(database.sourceMatchTable).insert(
|
||||||
|
SourceMatchTableCompanion.insert(
|
||||||
|
trackId: id!,
|
||||||
|
sourceId: newSourceInfo.id,
|
||||||
|
sourceType: const Value(SourceType.soundcloud),
|
||||||
|
// Because we're sorting by createdAt in the query
|
||||||
|
// we have to update it to indicate priority
|
||||||
|
createdAt: Value(DateTime.now()),
|
||||||
|
),
|
||||||
|
mode: InsertMode.replace,
|
||||||
|
);
|
||||||
|
|
||||||
|
return SoundcloudSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: newSiblings,
|
||||||
|
source: toSourceMap(manifest),
|
||||||
|
sourceInfo: newSourceInfo,
|
||||||
|
track: this,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
10
pubspec.lock
10
pubspec.lock
@ -376,7 +376,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.10.1"
|
version: "4.10.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: "direct overridden"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
|
||||||
@ -2165,6 +2165,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
soundcloud_explode_dart:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: soundcloud_explode_dart
|
||||||
|
sha256: "7585f95ed42359895734187f6dfdb5b88140c3ee48e7f23813c2b6301e37882f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.3"
|
||||||
source_gen:
|
source_gen:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -144,6 +144,8 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git
|
url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git
|
||||||
http_parser: ^4.1.2
|
http_parser: ^4.1.2
|
||||||
|
soundcloud_explode_dart: ^1.0.3
|
||||||
|
collection: any
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.13
|
build_runner: ^2.4.13
|
||||||
|
@ -63,13 +63,13 @@ IDI_APP_ICON ICON "resources\\app_icon.ico"
|
|||||||
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
|
||||||
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
|
||||||
#else
|
#else
|
||||||
#define VERSION_AS_NUMBER %{{SPOTUBE_VERSION_AS_NUMBER}}%
|
#define VERSION_AS_NUMBER 1,0,0,0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(FLUTTER_VERSION)
|
#if defined(FLUTTER_VERSION)
|
||||||
#define VERSION_AS_STRING FLUTTER_VERSION
|
#define VERSION_AS_STRING FLUTTER_VERSION
|
||||||
#else
|
#else
|
||||||
#define VERSION_AS_STRING "%{{SPOTUBE_VERSION}}%"
|
#define VERSION_AS_STRING "1.0.0.0"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
VS_VERSION_INFO VERSIONINFO
|
VS_VERSION_INFO VERSIONINFO
|
||||||
|
Loading…
Reference in New Issue
Block a user