feat: replace YouTube API with piped API

This commit is contained in:
Kingkor Roy Tirtho 2023-05-14 14:26:13 +06:00
parent edb3d47a53
commit 1ecc36da57
10 changed files with 289 additions and 181 deletions

View File

@ -3,13 +3,13 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
class SiblingTracksSheet extends HookConsumerWidget { class SiblingTracksSheet extends HookConsumerWidget {
final bool floating; final bool floating;
@ -26,7 +26,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
final siblings = playlist.isFetching == false final siblings = playlist.isFetching == false
? (playlist.activeTrack as SpotubeTrack).siblings ? (playlist.activeTrack as SpotubeTrack).siblings
: <Video>[]; : <PipedSearchItemStream>[];
final borderRadius = floating final borderRadius = floating
? BorderRadius.circular(10) ? BorderRadius.circular(10)
@ -77,7 +77,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
leading: Padding( leading: Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: UniversalImage( child: UniversalImage(
path: video.thumbnails.lowResUrl, path: video.thumbnail,
height: 60, height: 60,
width: 60, width: 60,
), ),
@ -86,26 +86,18 @@ class SiblingTracksSheet extends HookConsumerWidget {
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
), ),
trailing: Text( trailing: Text(
PrimitiveUtils.toReadableDuration( PrimitiveUtils.toReadableDuration(video.duration),
video.duration ?? Duration.zero,
), ),
), subtitle: Text(video.uploaderName),
subtitle: Text(video.author),
enabled: playlist.isFetching != true, enabled: playlist.isFetching != true,
selected: playlist.isFetching != true && selected: playlist.isFetching != true &&
video.id.value == video.id ==
(playlist.activeTrack as SpotubeTrack) (playlist.activeTrack as SpotubeTrack).ytTrack.id,
.ytTrack
.id
.value,
selectedTileColor: theme.popupMenuTheme.color, selectedTileColor: theme.popupMenuTheme.color,
onTap: () async { onTap: () async {
if (playlist.isFetching == false && if (playlist.isFetching == false &&
video.id.value != video.id !=
(playlist.activeTrack as SpotubeTrack) (playlist.activeTrack as SpotubeTrack).ytTrack.id) {
.ytTrack
.id
.value) {
await playlistNotifier.swapSibling(video); await playlistNotifier.swapSibling(video);
} }
}, },

41
lib/extensions/piped.dart Normal file
View File

@ -0,0 +1,41 @@
import 'dart:convert';
import 'package:catcher/catcher.dart';
import 'package:http/http.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotube/entities/cache_track.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/models/track.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/youtube.dart';
import 'package:spotube/utils/duration.dart';
extension PipedSearchItemExtension on PipedSearchItem {
static PipedSearchItemStream fromCacheTrack(CacheTrack cacheTrack) {
return PipedSearchItemStream(
type: PipedSearchItemType.stream,
url: "watch?v=${cacheTrack.id}",
title: cacheTrack.title,
uploaderName: cacheTrack.author,
uploaderUrl: "/channel/${cacheTrack.channelId}",
uploaded: -1,
uploadedDate: cacheTrack.uploadDate ?? "",
shortDescription: cacheTrack.description,
isShort: false,
thumbnail: "",
duration: cacheTrack.duration != null
? tryParseDuration(cacheTrack.duration!) ?? Duration.zero
: Duration.zero,
uploaderAvatar: "",
uploaderVerified: false,
views: cacheTrack.engagement.viewCount,
);
}
}
extension PipedStreamResponseExtension on PipedStreamResponse {
static Future<PipedStreamResponse> fromBackendTrack(
BackendTrack track) async {
return await PipedSpotube.client.streams(track.youtubeId);
}
}

View File

@ -72,6 +72,7 @@ Future<void> main(List<String> rawArgs) async {
exit(0); exit(0);
} }
await PipedSpotube.initialize();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
@ -134,7 +135,6 @@ Future<void> main(List<String> rawArgs) async {
return Downloader( return Downloader(
ref, ref,
queueInstance, queueInstance,
yt: youtube,
downloadPath: ref.watch( downloadPath: ref.watch(
userPreferencesProvider.select( userPreferencesProvider.select(
(s) => s.downloadLocation, (s) => s.downloadLocation,
@ -211,7 +211,7 @@ class SpotubeState extends ConsumerState<Spotube> {
/// For enabling hot reload for audio player /// For enabling hot reload for audio player
if (!kDebugMode) return; if (!kDebugMode) return;
audioPlayer.dispose(); audioPlayer.dispose();
youtube.close(); // youtube.close();
}; };
}, []); }, []);

View File

@ -1,21 +1,22 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:catcher/catcher.dart'; import 'package:catcher/catcher.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart';
import 'package:http/http.dart';
import 'package:piped_client/piped_client.dart';
import 'package:pocketbase/pocketbase.dart'; import 'package:pocketbase/pocketbase.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/video.dart'; import 'package:spotube/extensions/piped.dart';
import 'package:spotube/extensions/album_simple.dart'; import 'package:spotube/extensions/album_simple.dart';
import 'package:spotube/extensions/artist_simple.dart'; import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/models/track.dart'; import 'package:spotube/models/track.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/pocketbase.dart'; import 'package:spotube/services/pocketbase.dart';
import 'package:spotube/services/youtube.dart'; import 'package:spotube/services/youtube.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
enum SpotubeTrackMatchAlgorithm { enum SpotubeTrackMatchAlgorithm {
@ -28,10 +29,10 @@ enum SpotubeTrackMatchAlgorithm {
} }
class SpotubeTrack extends Track { class SpotubeTrack extends Track {
final Video ytTrack; final PipedStreamResponse ytTrack;
final String ytUri; final String ytUri;
final List<Map<String, int>> skipSegments; final List<Map<String, int>> skipSegments;
final List<Video> siblings; final List<PipedSearchItemStream> siblings;
SpotubeTrack( SpotubeTrack(
this.ytTrack, this.ytTrack,
@ -67,6 +68,65 @@ class SpotubeTrack extends Track {
uri = track.uri; uri = track.uri;
} }
static PipedAudioStream getStreamInfo(
PipedStreamResponse item,
AudioQuality audioQuality,
) {
final streamFormat =
kIsLinux ? PipedAudioStreamFormat.webm : PipedAudioStreamFormat.m4a;
if (audioQuality == AudioQuality.high) {
return item.highestBitrateAudioStreamOfFormat(streamFormat)!;
} else {
return item.lowestBitrateAudioStreamOfFormat(streamFormat)!;
}
}
static Future<List<Map<String, int>>> getSkipSegments(
String id,
UserPreferences preferences,
) async {
if (!preferences.skipSponsorSegments) return [];
try {
final res = await get(Uri(
scheme: "https",
host: "sponsor.ajay.app",
path: "/api/skipSegments",
queryParameters: {
"videoID": id,
"category": [
'sponsor',
'selfpromo',
'interaction',
'intro',
'outro',
'music_offtopic'
],
"actionType": 'skip'
},
));
if (res.body == "Not Found") {
return List.castFrom<dynamic, Map<String, int>>([]);
}
final data = jsonDecode(res.body) as List;
final segments = data.map((obj) {
return Map.castFrom<String, dynamic, String, int>({
"start": obj["segment"].first.toInt(),
"end": obj["segment"].last.toInt(),
});
}).toList();
getLogger(SpotubeTrack).v(
"[SponsorBlock] successfully fetched skip segments for $id",
);
return List.castFrom<dynamic, Map<String, int>>(segments);
} catch (e, stack) {
Catcher.reportCheckedError(e, stack);
return List.castFrom<dynamic, Map<String, int>>([]);
}
}
static Future<SpotubeTrack> fetchFromTrack( static Future<SpotubeTrack> fetchFromTrack(
Track track, UserPreferences preferences) async { Track track, UserPreferences preferences) async {
final artists = (track.artists ?? []) final artists = (track.artists ?? [])
@ -92,48 +152,51 @@ class SpotubeTrack extends Track {
final cachedTrack = final cachedTrack =
cachedTracks != null ? BackendTrack.fromRecord(cachedTracks) : null; cachedTracks != null ? BackendTrack.fromRecord(cachedTracks) : null;
Video ytVideo; PipedStreamResponse ytVideo;
List<Video> siblings = []; PipedAudioStream ytStream;
List<PipedSearchItemStream> siblings = [];
List<Map<String, int>> skipSegments = [];
if (cachedTrack != null) { if (cachedTrack != null) {
ytVideo = await VideoFromCacheTrackExtension.fromBackendTrack( final responses = await Future.wait(
cachedTrack, [
youtube, PipedStreamResponseExtension.fromBackendTrack(cachedTrack),
if (preferences.skipSponsorSegments)
getSkipSegments(cachedTrack.youtubeId, preferences)
else
Future.value([])
],
); );
ytVideo = responses.first as PipedStreamResponse;
skipSegments = responses.last as List<Map<String, int>>;
ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
} else { } else {
VideoSearchList videos = await PrimitiveUtils.raceMultiple( final videos = await PipedSpotube.client
() => youtube.search.search("${artists.join(", ")} - $title"), .search("${artists.join(", ")} - $title", PipedFilter.musicSongs);
// await PrimitiveUtils.raceMultiple(
// () => youtube.search.search("${artists.join(", ")} - $title"),
// );
siblings =
videos.items.whereType<PipedSearchItemStream>().take(10).toList();
final responses = await Future.wait(
[
PipedSpotube.client.streams(siblings.first.id),
if (preferences.skipSponsorSegments)
getSkipSegments(siblings.first.id, preferences)
else
Future.value([])
],
); );
siblings = videos.where((video) => !video.isLive).take(10).toList(); ytVideo = responses.first as PipedStreamResponse;
ytVideo = siblings.first; skipSegments = responses.last as List<Map<String, int>>;
ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
} }
StreamManifest trackManifest = await PrimitiveUtils.raceMultiple(
() => youtube.videos.streams.getManifest(ytVideo.id),
);
final audioManifest = trackManifest.audioOnly.where((info) {
final isMp4a = info.codec.mimeType == "audio/mp4";
if (kIsLinux) {
return !isMp4a;
} else if (kIsMacOS || kIsIOS) {
return isMp4a;
} else {
return true;
}
});
final chosenStreamInfo = preferences.audioQuality == AudioQuality.high
? audioManifest.withHighestBitrate()
: audioManifest.sortByBitrate().last;
final ytUri = chosenStreamInfo.url.toString();
if (cachedTrack == null) { if (cachedTrack == null) {
await Future<RecordModel?>.value( await Future<RecordModel?>.value(
pb.collection(BackendTrack.collection).create( pb.collection(BackendTrack.collection).create(
body: BackendTrack( body: BackendTrack(
spotifyId: track.id!, spotifyId: track.id!,
youtubeId: ytVideo.id.value, youtubeId: ytVideo.id,
votes: 0, votes: 0,
).toJson(), ).toJson(),
)).catchError((e, stack) { )).catchError((e, stack) {
@ -143,29 +206,17 @@ class SpotubeTrack extends Track {
} }
if (preferences.predownload && if (preferences.predownload &&
ytVideo.duration! < const Duration(minutes: 15)) { ytVideo.duration < const Duration(minutes: 15)) {
await DefaultCacheManager().getFileFromCache(track.id!).then( await DefaultCacheManager().getFileFromCache(track.id!).then(
(file) async { (file) async {
if (file != null) return file.file; if (file != null) return file.file;
final List<int> bytesStore = [];
final bytesFuture = Completer<Uint8List>();
youtube.videos.streams.get(chosenStreamInfo).listen( final res = await get(Uri.parse(ytStream.url));
(data) {
bytesStore.addAll(data);
},
onDone: () {
bytesFuture.complete(Uint8List.fromList(bytesStore));
},
onError: (e) {
bytesFuture.completeError(e);
},
);
final cached = await DefaultCacheManager().putFile( final cached = await DefaultCacheManager().putFile(
track.id!, track.id!,
await bytesFuture.future, res.bodyBytes,
fileExtension: chosenStreamInfo.codec.mimeType.split("/").last, fileExtension: ytStream.mimeType.split("/").last,
); );
return cached; return cached;
@ -176,44 +227,41 @@ class SpotubeTrack extends Track {
return SpotubeTrack.fromTrack( return SpotubeTrack.fromTrack(
track: track, track: track,
ytTrack: ytVideo, ytTrack: ytVideo,
ytUri: ytUri, ytUri: ytStream.url,
skipSegments: preferences.skipSponsorSegments skipSegments: skipSegments,
? await ytVideo.getSkipSegments(preferences)
: [],
siblings: siblings, siblings: siblings,
); );
} }
Future<SpotubeTrack?> swappedCopy( Future<SpotubeTrack?> swappedCopy(
Video video, PipedSearchItemStream video,
UserPreferences preferences, UserPreferences preferences,
) async { ) async {
if (siblings.none((element) => element.id == video.id)) return null; if (siblings.none((element) => element.id == video.id)) return null;
StreamManifest trackManifest = await PrimitiveUtils.raceMultiple( final [PipedStreamResponse ytVideo, List<Map<String, int>> skipSegments] =
() => youtube.videos.streams.getManifest(video.id), await Future.wait<dynamic>(
[
PipedSpotube.client.streams(video.id),
if (preferences.skipSponsorSegments)
getSkipSegments(video.id, preferences)
else
Future.value(<Map<String, int>>[])
],
); );
final audioManifest = trackManifest.audioOnly.where((info) { // await PrimitiveUtils.raceMultiple(
final isMp4a = info.codec.mimeType == "audio/mp4"; // () => youtube.videos.streams.getManifest(video.id),
if (kIsLinux) { // );
return !isMp4a;
} else if (kIsMacOS || kIsIOS) {
return isMp4a;
} else {
return true;
}
});
final chosenStreamInfo = preferences.audioQuality == AudioQuality.high final ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
? audioManifest.withHighestBitrate()
: audioManifest.sortByBitrate().last;
final ytUri = chosenStreamInfo.url.toString(); final ytUri = ytStream.url;
final cachedTracks = await Future<RecordModel?>.value( final cachedTracks = await Future<RecordModel?>.value(
pb.collection(BackendTrack.collection).getFirstListItem( pb.collection(BackendTrack.collection).getFirstListItem(
"spotify_id = '$id' && youtube_id = '${video.id.value}'"), "spotify_id = '$id' && youtube_id = '${video.id}'",
),
).catchError((e, stack) { ).catchError((e, stack) {
Catcher.reportCheckedError(e, stack); Catcher.reportCheckedError(e, stack);
return null; return null;
@ -227,7 +275,7 @@ class SpotubeTrack extends Track {
pb.collection(BackendTrack.collection).create( pb.collection(BackendTrack.collection).create(
body: BackendTrack( body: BackendTrack(
spotifyId: id!, spotifyId: id!,
youtubeId: video.id.value, youtubeId: video.id,
votes: 1, votes: 1,
).toJson(), ).toJson(),
)).catchError((e, stack) { )).catchError((e, stack) {
@ -246,29 +294,17 @@ class SpotubeTrack extends Track {
} }
if (preferences.predownload && if (preferences.predownload &&
video.duration! < const Duration(minutes: 15)) { video.duration < const Duration(minutes: 15)) {
await DefaultCacheManager().getFileFromCache(id!).then( await DefaultCacheManager().getFileFromCache(id!).then(
(file) async { (file) async {
if (file != null) return file.file; if (file != null) return file.file;
final List<int> bytesStore = [];
final bytesFuture = Completer<Uint8List>();
youtube.videos.streams.get(chosenStreamInfo).listen( final res = await get(Uri.parse(ytStream.url));
(data) {
bytesStore.addAll(data);
},
onDone: () {
bytesFuture.complete(Uint8List.fromList(bytesStore));
},
onError: (e) {
bytesFuture.completeError(e);
},
);
final cached = await DefaultCacheManager().putFile( final cached = await DefaultCacheManager().putFile(
id!, id!,
await bytesFuture.future, res.bodyBytes,
fileExtension: chosenStreamInfo.codec.mimeType.split("/").last, fileExtension: ytStream.mimeType.split("/").last,
); );
return cached; return cached;
@ -278,11 +314,9 @@ class SpotubeTrack extends Track {
return SpotubeTrack.fromTrack( return SpotubeTrack.fromTrack(
track: this, track: this,
ytTrack: video, ytTrack: ytVideo,
ytUri: ytUri, ytUri: ytUri,
skipSegments: preferences.skipSponsorSegments skipSegments: skipSegments,
? await video.getSkipSegments(preferences)
: [],
siblings: [ siblings: [
video, video,
...siblings.where((element) => element.id != video.id), ...siblings.where((element) => element.id != video.id),
@ -293,12 +327,12 @@ class SpotubeTrack extends Track {
static SpotubeTrack fromJson(Map<String, dynamic> map) { static SpotubeTrack fromJson(Map<String, dynamic> map) {
return SpotubeTrack.fromTrack( return SpotubeTrack.fromTrack(
track: Track.fromJson(map), track: Track.fromJson(map),
ytTrack: VideoToJson.fromJson(map["ytTrack"]), ytTrack: PipedStreamResponse.fromJson(map["ytTrack"]),
ytUri: map["ytUri"], ytUri: map["ytUri"],
skipSegments: skipSegments:
List.castFrom<dynamic, Map<String, int>>(map["skipSegments"]), List.castFrom<dynamic, Map<String, int>>(map["skipSegments"]),
siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"]) siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"])
.map((sibling) => VideoToJson.fromJson(sibling)) .map((sibling) => PipedSearchItemStream.fromJson(sibling))
.toList(), .toList(),
); );
} }
@ -316,11 +350,13 @@ class SpotubeTrack extends Track {
artists: artists, artists: artists,
onlyCleanArtist: true, onlyCleanArtist: true,
).trim(); ).trim();
VideoSearchList videos = await PrimitiveUtils.raceMultiple( final videos = await PipedSpotube.client.search(
() => youtube.search.search("${artists.join(", ")} - $title"), "${artists.join(", ")} - $title",
PipedFilter.musicSongs,
); );
final siblings = videos.where((video) => !video.isLive).take(10).toList(); final siblings =
videos.items.whereType<PipedSearchItemStream>().take(10).toList();
return SpotubeTrack.fromTrack( return SpotubeTrack.fromTrack(
track: this, track: this,

View File

@ -9,13 +9,10 @@ import 'package:queue/queue.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:spotify/spotify.dart' hide Image, Queue; import 'package:spotify/spotify.dart' hide Image, Queue;
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart'; import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/provider/user_preferences_provider.dart'; import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/services/youtube.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart' hide Comment;
Queue queueInstance = Queue(delay: const Duration(seconds: 5)); Queue queueInstance = Queue(delay: const Duration(seconds: 5));
Queue grabberQueue = Queue(delay: const Duration(seconds: 5)); Queue grabberQueue = Queue(delay: const Duration(seconds: 5));
@ -23,14 +20,13 @@ Queue grabberQueue = Queue(delay: const Duration(seconds: 5));
class Downloader with ChangeNotifier { class Downloader with ChangeNotifier {
Ref ref; Ref ref;
Queue _queue; Queue _queue;
YoutubeExplode yt;
String downloadPath; String downloadPath;
FutureOr<bool> Function(SpotubeTrack track)? onFileExists; FutureOr<bool> Function(SpotubeTrack track)? onFileExists;
Downloader( Downloader(
this.ref, this.ref,
this._queue, { this._queue, {
required this.downloadPath, required this.downloadPath,
required this.yt,
this.onFileExists, this.onFileExists,
}); });
@ -71,27 +67,23 @@ class Downloader with ChangeNotifier {
return; return;
} }
file.createSync(recursive: true); file.createSync(recursive: true);
StreamManifest manifest =
await yt.videos.streamsClient.getManifest(track.ytTrack.url);
logger.v( logger.v(
"[addToQueue] Getting download information for ${file.path}", "[addToQueue] Getting download information for ${file.path}",
); );
final audioStream = yt.videos.streamsClient final audioStream = await get(
.get( Uri.parse(
manifest.audioOnly SpotubeTrack.getStreamInfo(
.where( track.ytTrack,
(audio) => audio.codec.mimeType == "audio/mp4", ref.read(userPreferencesProvider).audioQuality,
) ).url,
.withHighestBitrate(), ),
) );
.asBroadcastStream();
logger.v( logger.v(
"[addToQueue] ${file.path} download started", "[addToQueue] ${file.path} download started",
); );
IOSink outputFileStream = file.openWrite(); IOSink outputFileStream = file.openWrite();
await audioStream.pipe(outputFileStream); outputFileStream.write(audioStream.bodyBytes);
await outputFileStream.flush(); await outputFileStream.flush();
logger.v( logger.v(
"[addToQueue] Download of ${file.path} is done successfully", "[addToQueue] Download of ${file.path} is done successfully",
@ -161,7 +153,6 @@ final downloaderProvider = ChangeNotifierProvider(
return Downloader( return Downloader(
ref, ref,
queueInstance, queueInstance,
yt: youtube,
downloadPath: ref.watch( downloadPath: ref.watch(
userPreferencesProvider.select( userPreferencesProvider.select(
(s) => s.downloadLocation, (s) => s.downloadLocation,

View File

@ -1,6 +1,7 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:palette_generator/palette_generator.dart'; import 'package:palette_generator/palette_generator.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
@ -262,7 +263,7 @@ class ProxyPlaylistNotifier extends StateNotifier<ProxyPlaylist>
Future<void> addTracksAtFirst(Iterable<Track> track) async {} Future<void> addTracksAtFirst(Iterable<Track> track) async {}
Future<void> populateSibling() async {} Future<void> populateSibling() async {}
Future<void> swapSibling(Video video) async {} Future<void> swapSibling(PipedSearchItem video) async {}
Future<void> next() async { Future<void> next() async {
final track = await ensureNthSourcePlayable(audioPlayer.currentIndex + 1); final track = await ensureNthSourcePlayable(audioPlayer.currentIndex + 1);

View File

@ -1,3 +1,26 @@
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:piped_client/piped_client.dart';
final youtube = YoutubeExplode(); PipedClient _defaultClient = PipedClient();
class PipedSpotube {
/// Checks for a working instance of piped.video
///
/// To distribute the load, in each startup it randomizes public instances
/// and selects a working instance and uses that throughout the session
static Future<void> initialize() async {
final pipedInstances = await _defaultClient.instanceList();
pipedInstances.shuffle();
for (final instance in pipedInstances) {
final client = PipedClient(instance: instance.apiUrl);
try {
await client.streams("dQw4w9WgXcQ");
_defaultClient = client;
break;
} catch (e) {
continue;
}
}
}
static PipedClient get client => _defaultClient;
}

View File

@ -3,15 +3,15 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/widgets.dart' hide Image; import 'package:flutter/widgets.dart' hide Image;
import 'package:metadata_god/metadata_god.dart' hide Image; import 'package:metadata_god/metadata_god.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:piped_client/piped_client.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/shared/links/anchor_button.dart'; import 'package:spotube/components/shared/links/anchor_button.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/models/spotube_track.dart'; import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
enum ImagePlaceholder { enum ImagePlaceholder {
albumArt, albumArt,
@ -127,22 +127,28 @@ abstract class TypeConversionUtils {
String? art, String? art,
}) { }) {
final track = SpotubeTrack( final track = SpotubeTrack(
Video( PipedStreamResponse(
VideoId("dQw4w9WgXcQ"), id: "dQw4w9WgXcQ",
basenameWithoutExtension(file.path), title: basenameWithoutExtension(file.path),
metadata?.artist ?? "", dash: null,
ChannelId( description: "",
"https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw", dislikes: -1,
), duration: Duration(milliseconds: metadata?.durationMs?.toInt() ?? 0),
DateTime.now(), hls: null,
"", lbryId: "",
DateTime.now(), likes: -1,
"", livestream: false,
Duration(milliseconds: metadata?.durationMs?.toInt() ?? 0), proxyUrl: "",
ThumbnailSet(metadata?.title ?? ""), thumbnailUrl: art ?? "",
[], uploadedDate: DateTime.now().toUtc().toString(),
const Engagement(0, 0, 0), uploader: metadata?.albumArtist ?? "",
false, uploaderUrl: "",
uploaderVerified: false,
views: -1,
audioStreams: [],
videoStreams: [],
relatedStreams: [],
subtitles: [],
), ),
file.path, file.path,
[], [],

View File

@ -278,10 +278,10 @@ packages:
description: description:
path: "." path: "."
ref: HEAD ref: HEAD
resolved-ref: "5ccf7c0d8bbb07329667cff561aede1a9bee4934" resolved-ref: "5c91db2578abd0c1609dc409ee3daee168d8b20e"
url: "https://github.com/ThexXTURBOXx/catcher" url: "https://github.com/ThexXTURBOXx/catcher"
source: git source: git
version: "0.7.1" version: "0.8.0"
change_case: change_case:
dependency: transitive dependency: transitive
description: description:
@ -422,10 +422,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: device_info_plus name: device_info_plus
sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" sha256: "9b1a0c32b2a503f8fe9f8764fac7b5fcd4f6bd35d8f49de5350bccf9e2a33b8a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.0" version: "9.0.0"
device_info_plus_platform_interface: device_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@ -438,10 +438,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: dio name: dio
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" sha256: "347d56c26d63519552ef9a569f2a593dda99a81fdbdff13c584b7197cfe05059"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.6" version: "5.1.2"
dots_indicator: dots_indicator:
dependency: transitive dependency: transitive
description: description:
@ -494,10 +494,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.2.5" version: "5.3.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -670,10 +670,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.7" version: "2.0.14"
flutter_riverpod: flutter_riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@ -938,10 +938,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: json_annotation name: json_annotation
sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.0" version: "4.8.1"
json_serializable: json_serializable:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1219,10 +1219,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.6"
pedantic: pedantic:
dependency: transitive dependency: transitive
description: description:
@ -1279,6 +1279,13 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.1.0" version: "5.1.0"
piped_client:
dependency: "direct main"
description:
path: "../piped_client"
relative: true
source: path
version: "0.1.0"
platform: platform:
dependency: transitive dependency: transitive
description: description:
@ -1291,10 +1298,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.4"
pocketbase: pocketbase:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1427,10 +1434,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: sentry name: sentry
sha256: "81c1f32496ff04476d6ddfe5894215b1034d185301d2e3dffd272853392c5ea7" sha256: "1c5498c8d1754dbf4fa51ca14d31c8c34ea0a0f897ff666ecd516dbd588dad6a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.20.1" version: "7.5.2"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1889,10 +1896,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "4.1.4"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
window_manager: window_manager:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@ -98,7 +98,10 @@ dependencies:
url: https://github.com/google/flutter-desktop-embedding.git url: https://github.com/google/flutter-desktop-embedding.git
ref: a738913c8ce2c9f47515382d40827e794a334274 ref: a738913c8ce2c9f47515382d40827e794a334274
path: plugins/window_size path: plugins/window_size
youtube_explode_dart: ^1.12.1 piped_client:
git:
url: https://github.com/KRTirtho/piped_client
ref: 044d80639d54d3fd4a712d87537d1d03d90e43d2
dev_dependencies: dev_dependencies:
build_runner: ^2.3.2 build_runner: ^2.3.2