mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat: replace YouTube API with piped API
This commit is contained in:
parent
edb3d47a53
commit
1ecc36da57
@ -3,13 +3,13 @@ import 'dart:ui';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.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/extensions/context.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
class SiblingTracksSheet extends HookConsumerWidget {
|
||||
final bool floating;
|
||||
@ -26,7 +26,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
|
||||
final siblings = playlist.isFetching == false
|
||||
? (playlist.activeTrack as SpotubeTrack).siblings
|
||||
: <Video>[];
|
||||
: <PipedSearchItemStream>[];
|
||||
|
||||
final borderRadius = floating
|
||||
? BorderRadius.circular(10)
|
||||
@ -77,7 +77,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: UniversalImage(
|
||||
path: video.thumbnails.lowResUrl,
|
||||
path: video.thumbnail,
|
||||
height: 60,
|
||||
width: 60,
|
||||
),
|
||||
@ -86,26 +86,18 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
trailing: Text(
|
||||
PrimitiveUtils.toReadableDuration(
|
||||
video.duration ?? Duration.zero,
|
||||
),
|
||||
PrimitiveUtils.toReadableDuration(video.duration),
|
||||
),
|
||||
subtitle: Text(video.author),
|
||||
subtitle: Text(video.uploaderName),
|
||||
enabled: playlist.isFetching != true,
|
||||
selected: playlist.isFetching != true &&
|
||||
video.id.value ==
|
||||
(playlist.activeTrack as SpotubeTrack)
|
||||
.ytTrack
|
||||
.id
|
||||
.value,
|
||||
video.id ==
|
||||
(playlist.activeTrack as SpotubeTrack).ytTrack.id,
|
||||
selectedTileColor: theme.popupMenuTheme.color,
|
||||
onTap: () async {
|
||||
if (playlist.isFetching == false &&
|
||||
video.id.value !=
|
||||
(playlist.activeTrack as SpotubeTrack)
|
||||
.ytTrack
|
||||
.id
|
||||
.value) {
|
||||
video.id !=
|
||||
(playlist.activeTrack as SpotubeTrack).ytTrack.id) {
|
||||
await playlistNotifier.swapSibling(video);
|
||||
}
|
||||
},
|
||||
|
41
lib/extensions/piped.dart
Normal file
41
lib/extensions/piped.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -72,6 +72,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
exit(0);
|
||||
}
|
||||
|
||||
await PipedSpotube.initialize();
|
||||
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
|
||||
@ -134,7 +135,6 @@ Future<void> main(List<String> rawArgs) async {
|
||||
return Downloader(
|
||||
ref,
|
||||
queueInstance,
|
||||
yt: youtube,
|
||||
downloadPath: ref.watch(
|
||||
userPreferencesProvider.select(
|
||||
(s) => s.downloadLocation,
|
||||
@ -211,7 +211,7 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
/// For enabling hot reload for audio player
|
||||
if (!kDebugMode) return;
|
||||
audioPlayer.dispose();
|
||||
youtube.close();
|
||||
// youtube.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
@ -1,21 +1,22 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:catcher/catcher.dart';
|
||||
import 'package:flutter/foundation.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: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/artist_simple.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/pocketbase.dart';
|
||||
import 'package:spotube/services/youtube.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
enum SpotubeTrackMatchAlgorithm {
|
||||
@ -28,10 +29,10 @@ enum SpotubeTrackMatchAlgorithm {
|
||||
}
|
||||
|
||||
class SpotubeTrack extends Track {
|
||||
final Video ytTrack;
|
||||
final PipedStreamResponse ytTrack;
|
||||
final String ytUri;
|
||||
final List<Map<String, int>> skipSegments;
|
||||
final List<Video> siblings;
|
||||
final List<PipedSearchItemStream> siblings;
|
||||
|
||||
SpotubeTrack(
|
||||
this.ytTrack,
|
||||
@ -67,6 +68,65 @@ class SpotubeTrack extends Track {
|
||||
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(
|
||||
Track track, UserPreferences preferences) async {
|
||||
final artists = (track.artists ?? [])
|
||||
@ -92,48 +152,51 @@ class SpotubeTrack extends Track {
|
||||
final cachedTrack =
|
||||
cachedTracks != null ? BackendTrack.fromRecord(cachedTracks) : null;
|
||||
|
||||
Video ytVideo;
|
||||
List<Video> siblings = [];
|
||||
PipedStreamResponse ytVideo;
|
||||
PipedAudioStream ytStream;
|
||||
List<PipedSearchItemStream> siblings = [];
|
||||
List<Map<String, int>> skipSegments = [];
|
||||
if (cachedTrack != null) {
|
||||
ytVideo = await VideoFromCacheTrackExtension.fromBackendTrack(
|
||||
cachedTrack,
|
||||
youtube,
|
||||
final responses = await Future.wait(
|
||||
[
|
||||
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 {
|
||||
VideoSearchList videos = await PrimitiveUtils.raceMultiple(
|
||||
() => youtube.search.search("${artists.join(", ")} - $title"),
|
||||
final videos = await PipedSpotube.client
|
||||
.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 = siblings.first;
|
||||
ytVideo = responses.first as PipedStreamResponse;
|
||||
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) {
|
||||
await Future<RecordModel?>.value(
|
||||
pb.collection(BackendTrack.collection).create(
|
||||
body: BackendTrack(
|
||||
spotifyId: track.id!,
|
||||
youtubeId: ytVideo.id.value,
|
||||
youtubeId: ytVideo.id,
|
||||
votes: 0,
|
||||
).toJson(),
|
||||
)).catchError((e, stack) {
|
||||
@ -143,29 +206,17 @@ class SpotubeTrack extends Track {
|
||||
}
|
||||
|
||||
if (preferences.predownload &&
|
||||
ytVideo.duration! < const Duration(minutes: 15)) {
|
||||
ytVideo.duration < const Duration(minutes: 15)) {
|
||||
await DefaultCacheManager().getFileFromCache(track.id!).then(
|
||||
(file) async {
|
||||
if (file != null) return file.file;
|
||||
final List<int> bytesStore = [];
|
||||
final bytesFuture = Completer<Uint8List>();
|
||||
|
||||
youtube.videos.streams.get(chosenStreamInfo).listen(
|
||||
(data) {
|
||||
bytesStore.addAll(data);
|
||||
},
|
||||
onDone: () {
|
||||
bytesFuture.complete(Uint8List.fromList(bytesStore));
|
||||
},
|
||||
onError: (e) {
|
||||
bytesFuture.completeError(e);
|
||||
},
|
||||
);
|
||||
final res = await get(Uri.parse(ytStream.url));
|
||||
|
||||
final cached = await DefaultCacheManager().putFile(
|
||||
track.id!,
|
||||
await bytesFuture.future,
|
||||
fileExtension: chosenStreamInfo.codec.mimeType.split("/").last,
|
||||
res.bodyBytes,
|
||||
fileExtension: ytStream.mimeType.split("/").last,
|
||||
);
|
||||
|
||||
return cached;
|
||||
@ -176,44 +227,41 @@ class SpotubeTrack extends Track {
|
||||
return SpotubeTrack.fromTrack(
|
||||
track: track,
|
||||
ytTrack: ytVideo,
|
||||
ytUri: ytUri,
|
||||
skipSegments: preferences.skipSponsorSegments
|
||||
? await ytVideo.getSkipSegments(preferences)
|
||||
: [],
|
||||
ytUri: ytStream.url,
|
||||
skipSegments: skipSegments,
|
||||
siblings: siblings,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SpotubeTrack?> swappedCopy(
|
||||
Video video,
|
||||
PipedSearchItemStream video,
|
||||
UserPreferences preferences,
|
||||
) async {
|
||||
if (siblings.none((element) => element.id == video.id)) return null;
|
||||
|
||||
StreamManifest trackManifest = await PrimitiveUtils.raceMultiple(
|
||||
() => youtube.videos.streams.getManifest(video.id),
|
||||
final [PipedStreamResponse ytVideo, List<Map<String, int>> skipSegments] =
|
||||
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) {
|
||||
final isMp4a = info.codec.mimeType == "audio/mp4";
|
||||
if (kIsLinux) {
|
||||
return !isMp4a;
|
||||
} else if (kIsMacOS || kIsIOS) {
|
||||
return isMp4a;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// await PrimitiveUtils.raceMultiple(
|
||||
// () => youtube.videos.streams.getManifest(video.id),
|
||||
// );
|
||||
|
||||
final chosenStreamInfo = preferences.audioQuality == AudioQuality.high
|
||||
? audioManifest.withHighestBitrate()
|
||||
: audioManifest.sortByBitrate().last;
|
||||
final ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
|
||||
|
||||
final ytUri = chosenStreamInfo.url.toString();
|
||||
final ytUri = ytStream.url;
|
||||
|
||||
final cachedTracks = await Future<RecordModel?>.value(
|
||||
pb.collection(BackendTrack.collection).getFirstListItem(
|
||||
"spotify_id = '$id' && youtube_id = '${video.id.value}'"),
|
||||
"spotify_id = '$id' && youtube_id = '${video.id}'",
|
||||
),
|
||||
).catchError((e, stack) {
|
||||
Catcher.reportCheckedError(e, stack);
|
||||
return null;
|
||||
@ -227,7 +275,7 @@ class SpotubeTrack extends Track {
|
||||
pb.collection(BackendTrack.collection).create(
|
||||
body: BackendTrack(
|
||||
spotifyId: id!,
|
||||
youtubeId: video.id.value,
|
||||
youtubeId: video.id,
|
||||
votes: 1,
|
||||
).toJson(),
|
||||
)).catchError((e, stack) {
|
||||
@ -246,29 +294,17 @@ class SpotubeTrack extends Track {
|
||||
}
|
||||
|
||||
if (preferences.predownload &&
|
||||
video.duration! < const Duration(minutes: 15)) {
|
||||
video.duration < const Duration(minutes: 15)) {
|
||||
await DefaultCacheManager().getFileFromCache(id!).then(
|
||||
(file) async {
|
||||
if (file != null) return file.file;
|
||||
final List<int> bytesStore = [];
|
||||
final bytesFuture = Completer<Uint8List>();
|
||||
|
||||
youtube.videos.streams.get(chosenStreamInfo).listen(
|
||||
(data) {
|
||||
bytesStore.addAll(data);
|
||||
},
|
||||
onDone: () {
|
||||
bytesFuture.complete(Uint8List.fromList(bytesStore));
|
||||
},
|
||||
onError: (e) {
|
||||
bytesFuture.completeError(e);
|
||||
},
|
||||
);
|
||||
final res = await get(Uri.parse(ytStream.url));
|
||||
|
||||
final cached = await DefaultCacheManager().putFile(
|
||||
id!,
|
||||
await bytesFuture.future,
|
||||
fileExtension: chosenStreamInfo.codec.mimeType.split("/").last,
|
||||
res.bodyBytes,
|
||||
fileExtension: ytStream.mimeType.split("/").last,
|
||||
);
|
||||
|
||||
return cached;
|
||||
@ -278,11 +314,9 @@ class SpotubeTrack extends Track {
|
||||
|
||||
return SpotubeTrack.fromTrack(
|
||||
track: this,
|
||||
ytTrack: video,
|
||||
ytTrack: ytVideo,
|
||||
ytUri: ytUri,
|
||||
skipSegments: preferences.skipSponsorSegments
|
||||
? await video.getSkipSegments(preferences)
|
||||
: [],
|
||||
skipSegments: skipSegments,
|
||||
siblings: [
|
||||
video,
|
||||
...siblings.where((element) => element.id != video.id),
|
||||
@ -293,12 +327,12 @@ class SpotubeTrack extends Track {
|
||||
static SpotubeTrack fromJson(Map<String, dynamic> map) {
|
||||
return SpotubeTrack.fromTrack(
|
||||
track: Track.fromJson(map),
|
||||
ytTrack: VideoToJson.fromJson(map["ytTrack"]),
|
||||
ytTrack: PipedStreamResponse.fromJson(map["ytTrack"]),
|
||||
ytUri: map["ytUri"],
|
||||
skipSegments:
|
||||
List.castFrom<dynamic, Map<String, int>>(map["skipSegments"]),
|
||||
siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"])
|
||||
.map((sibling) => VideoToJson.fromJson(sibling))
|
||||
.map((sibling) => PipedSearchItemStream.fromJson(sibling))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
@ -316,11 +350,13 @@ class SpotubeTrack extends Track {
|
||||
artists: artists,
|
||||
onlyCleanArtist: true,
|
||||
).trim();
|
||||
VideoSearchList videos = await PrimitiveUtils.raceMultiple(
|
||||
() => youtube.search.search("${artists.join(", ")} - $title"),
|
||||
final videos = await PipedSpotube.client.search(
|
||||
"${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(
|
||||
track: this,
|
||||
|
@ -9,13 +9,10 @@ import 'package:queue/queue.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:spotify/spotify.dart' hide Image, Queue;
|
||||
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
|
||||
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/models/spotube_track.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:youtube_explode_dart/youtube_explode_dart.dart' hide Comment;
|
||||
|
||||
Queue queueInstance = 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 {
|
||||
Ref ref;
|
||||
Queue _queue;
|
||||
YoutubeExplode yt;
|
||||
|
||||
String downloadPath;
|
||||
FutureOr<bool> Function(SpotubeTrack track)? onFileExists;
|
||||
Downloader(
|
||||
this.ref,
|
||||
this._queue, {
|
||||
required this.downloadPath,
|
||||
required this.yt,
|
||||
this.onFileExists,
|
||||
});
|
||||
|
||||
@ -71,27 +67,23 @@ class Downloader with ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
file.createSync(recursive: true);
|
||||
StreamManifest manifest =
|
||||
await yt.videos.streamsClient.getManifest(track.ytTrack.url);
|
||||
logger.v(
|
||||
"[addToQueue] Getting download information for ${file.path}",
|
||||
);
|
||||
final audioStream = yt.videos.streamsClient
|
||||
.get(
|
||||
manifest.audioOnly
|
||||
.where(
|
||||
(audio) => audio.codec.mimeType == "audio/mp4",
|
||||
)
|
||||
.withHighestBitrate(),
|
||||
)
|
||||
.asBroadcastStream();
|
||||
|
||||
final audioStream = await get(
|
||||
Uri.parse(
|
||||
SpotubeTrack.getStreamInfo(
|
||||
track.ytTrack,
|
||||
ref.read(userPreferencesProvider).audioQuality,
|
||||
).url,
|
||||
),
|
||||
);
|
||||
logger.v(
|
||||
"[addToQueue] ${file.path} download started",
|
||||
);
|
||||
|
||||
IOSink outputFileStream = file.openWrite();
|
||||
await audioStream.pipe(outputFileStream);
|
||||
outputFileStream.write(audioStream.bodyBytes);
|
||||
await outputFileStream.flush();
|
||||
logger.v(
|
||||
"[addToQueue] Download of ${file.path} is done successfully",
|
||||
@ -161,7 +153,6 @@ final downloaderProvider = ChangeNotifierProvider(
|
||||
return Downloader(
|
||||
ref,
|
||||
queueInstance,
|
||||
yt: youtube,
|
||||
downloadPath: ref.watch(
|
||||
userPreferencesProvider.select(
|
||||
(s) => s.downloadLocation,
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:palette_generator/palette_generator.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/shared/image/universal_image.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> populateSibling() async {}
|
||||
Future<void> swapSibling(Video video) async {}
|
||||
Future<void> swapSibling(PipedSearchItem video) async {}
|
||||
|
||||
Future<void> next() async {
|
||||
final track = await ensureNthSourcePlayable(audioPlayer.currentIndex + 1);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -3,15 +3,15 @@
|
||||
import 'dart:io';
|
||||
|
||||
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:piped_client/piped_client.dart';
|
||||
import 'package:spotube/collections/assets.gen.dart';
|
||||
import 'package:spotube/components/shared/links/anchor_button.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
enum ImagePlaceholder {
|
||||
albumArt,
|
||||
@ -127,22 +127,28 @@ abstract class TypeConversionUtils {
|
||||
String? art,
|
||||
}) {
|
||||
final track = SpotubeTrack(
|
||||
Video(
|
||||
VideoId("dQw4w9WgXcQ"),
|
||||
basenameWithoutExtension(file.path),
|
||||
metadata?.artist ?? "",
|
||||
ChannelId(
|
||||
"https://www.youtube.com/channel/UCuAXFkgsw1L7xaCfnd5JJOw",
|
||||
),
|
||||
DateTime.now(),
|
||||
"",
|
||||
DateTime.now(),
|
||||
"",
|
||||
Duration(milliseconds: metadata?.durationMs?.toInt() ?? 0),
|
||||
ThumbnailSet(metadata?.title ?? ""),
|
||||
[],
|
||||
const Engagement(0, 0, 0),
|
||||
false,
|
||||
PipedStreamResponse(
|
||||
id: "dQw4w9WgXcQ",
|
||||
title: basenameWithoutExtension(file.path),
|
||||
dash: null,
|
||||
description: "",
|
||||
dislikes: -1,
|
||||
duration: Duration(milliseconds: metadata?.durationMs?.toInt() ?? 0),
|
||||
hls: null,
|
||||
lbryId: "",
|
||||
likes: -1,
|
||||
livestream: false,
|
||||
proxyUrl: "",
|
||||
thumbnailUrl: art ?? "",
|
||||
uploadedDate: DateTime.now().toUtc().toString(),
|
||||
uploader: metadata?.albumArtist ?? "",
|
||||
uploaderUrl: "",
|
||||
uploaderVerified: false,
|
||||
views: -1,
|
||||
audioStreams: [],
|
||||
videoStreams: [],
|
||||
relatedStreams: [],
|
||||
subtitles: [],
|
||||
),
|
||||
file.path,
|
||||
[],
|
||||
|
55
pubspec.lock
55
pubspec.lock
@ -278,10 +278,10 @@ packages:
|
||||
description:
|
||||
path: "."
|
||||
ref: HEAD
|
||||
resolved-ref: "5ccf7c0d8bbb07329667cff561aede1a9bee4934"
|
||||
resolved-ref: "5c91db2578abd0c1609dc409ee3daee168d8b20e"
|
||||
url: "https://github.com/ThexXTURBOXx/catcher"
|
||||
source: git
|
||||
version: "0.7.1"
|
||||
version: "0.8.0"
|
||||
change_case:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -422,10 +422,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: device_info_plus
|
||||
sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95"
|
||||
sha256: "9b1a0c32b2a503f8fe9f8764fac7b5fcd4f6bd35d8f49de5350bccf9e2a33b8a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.1.0"
|
||||
version: "9.0.0"
|
||||
device_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -438,10 +438,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio
|
||||
sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8"
|
||||
sha256: "347d56c26d63519552ef9a569f2a593dda99a81fdbdff13c584b7197cfe05059"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.6"
|
||||
version: "5.1.2"
|
||||
dots_indicator:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -494,10 +494,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: file_picker
|
||||
sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9
|
||||
sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.5"
|
||||
version: "5.3.0"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -670,10 +670,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b"
|
||||
sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.14"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -938,10 +938,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317
|
||||
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.0"
|
||||
version: "4.8.1"
|
||||
json_serializable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1219,10 +1219,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
|
||||
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.6"
|
||||
pedantic:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1279,6 +1279,13 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
piped_client:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../piped_client"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.0"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1291,10 +1298,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
|
||||
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.1.4"
|
||||
pocketbase:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1427,10 +1434,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sentry
|
||||
sha256: "81c1f32496ff04476d6ddfe5894215b1034d185301d2e3dffd272853392c5ea7"
|
||||
sha256: "1c5498c8d1754dbf4fa51ca14d31c8c34ea0a0f897ff666ecd516dbd588dad6a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.20.1"
|
||||
version: "7.5.2"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -1889,10 +1896,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46
|
||||
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -98,7 +98,10 @@ dependencies:
|
||||
url: https://github.com/google/flutter-desktop-embedding.git
|
||||
ref: a738913c8ce2c9f47515382d40827e794a334274
|
||||
path: plugins/window_size
|
||||
youtube_explode_dart: ^1.12.1
|
||||
piped_client:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/piped_client
|
||||
ref: 044d80639d54d3fd4a712d87537d1d03d90e43d2
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
|
Loading…
Reference in New Issue
Block a user