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/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
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);
|
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();
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
@ -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 '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,
|
||||||
[],
|
[],
|
||||||
|
55
pubspec.lock
55
pubspec.lock
@ -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:
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user