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_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
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);
}
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();
};
}, []);

View File

@ -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,

View File

@ -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,

View File

@ -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);

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 '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,
[],

View File

@ -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:

View File

@ -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