mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat: re-introduce youtube API along with piped
This commit is contained in:
parent
b47ef98197
commit
b54ee96233
@ -3,7 +3,6 @@ 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:spotify/spotify.dart' hide Offset;
|
||||
import 'package:spotube/collections/spotube_icons.dart';
|
||||
|
||||
@ -11,10 +10,12 @@ import 'package:spotube/components/shared/image/universal_image.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/hooks/use_debounce.dart';
|
||||
import 'package:spotube/models/matched_track.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/piped_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/youtube_provider.dart';
|
||||
import 'package:spotube/services/youtube/youtube.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
@ -31,12 +32,11 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
final theme = Theme.of(context);
|
||||
final playlist = ref.watch(ProxyPlaylistNotifier.provider);
|
||||
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
|
||||
final preferencesSearchMode =
|
||||
ref.watch(userPreferencesProvider.select((value) => value.searchMode));
|
||||
final pipedClient = ref.watch(pipedClientProvider);
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
final youtube = ref.watch(youtubeProvider);
|
||||
|
||||
final isSearching = useState(false);
|
||||
final searchMode = useState(preferencesSearchMode);
|
||||
final searchMode = useState(preferences.searchMode);
|
||||
|
||||
final title = ServiceUtils.getTitle(
|
||||
playlist.activeTrack?.name ?? "",
|
||||
@ -57,21 +57,10 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
|
||||
final searchRequest = useMemoized(() async {
|
||||
if (searchTerm.trim().isEmpty) {
|
||||
return <PipedSearchItemStream>[];
|
||||
return <YoutubeVideoInfo>[];
|
||||
}
|
||||
|
||||
return pipedClient
|
||||
.search(
|
||||
searchTerm.trim(),
|
||||
switch (searchMode.value) {
|
||||
SearchMode.youtube => PipedFilter.video,
|
||||
SearchMode.youtubeMusic => PipedFilter.musicSongs,
|
||||
},
|
||||
)
|
||||
.then(
|
||||
(result) =>
|
||||
result.items.whereType<PipedSearchItemStream>().toList(),
|
||||
);
|
||||
return youtube.search(searchTerm.trim());
|
||||
}, [
|
||||
searchTerm,
|
||||
searchMode.value,
|
||||
@ -79,7 +68,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
|
||||
final siblings = playlist.isFetching == false
|
||||
? (playlist.activeTrack as SpotubeTrack).siblings
|
||||
: <PipedSearchItemStream>[];
|
||||
: <YoutubeVideoInfo>[];
|
||||
|
||||
final borderRadius = floating
|
||||
? BorderRadius.circular(10)
|
||||
@ -96,13 +85,13 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
return null;
|
||||
}, [playlist.activeTrack]);
|
||||
|
||||
final itemBuilder = useCallback((PipedSearchItemStream video) {
|
||||
final itemBuilder = useCallback((YoutubeVideoInfo video) {
|
||||
return ListTile(
|
||||
title: Text(video.title),
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: UniversalImage(
|
||||
path: video.thumbnail,
|
||||
path: video.thumbnailUrl,
|
||||
height: 60,
|
||||
width: 60,
|
||||
),
|
||||
@ -113,7 +102,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
trailing: Text(
|
||||
PrimitiveUtils.toReadableDuration(video.duration),
|
||||
),
|
||||
subtitle: Text(video.uploaderName),
|
||||
subtitle: Text(video.channelName),
|
||||
enabled: playlist.isFetching != true,
|
||||
selected: playlist.isFetching != true &&
|
||||
video.id == (playlist.activeTrack as SpotubeTrack).ytTrack.id,
|
||||
@ -182,6 +171,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
},
|
||||
)
|
||||
else ...[
|
||||
if (preferences.youtubeApiType == YoutubeApiType.piped)
|
||||
PopupMenuButton(
|
||||
icon: const Icon(SpotubeIcons.filter, size: 18),
|
||||
onSelected: (SearchMode mode) {
|
||||
|
@ -59,8 +59,8 @@ class TrackDetailsDialog extends HookWidget {
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
context.l10n.channel: Hyperlink(
|
||||
ytTrack.uploader,
|
||||
"https://youtube.com${ytTrack.uploaderUrl}",
|
||||
ytTrack.channelName,
|
||||
"https://youtube.com${ytTrack.channelName}",
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
@ -248,5 +248,6 @@
|
||||
"logs": "Logs",
|
||||
"developers": "Developers",
|
||||
"not_logged_in": "You're not logged in",
|
||||
"search_mode": "Search Mode"
|
||||
"search_mode": "Search Mode",
|
||||
"youtube_api_type": "YouTube API Type"
|
||||
}
|
@ -103,6 +103,7 @@ Future<void> main(List<String> rawArgs) async {
|
||||
);
|
||||
Hive.registerAdapter(MatchedTrackAdapter());
|
||||
Hive.registerAdapter(SkipSegmentAdapter());
|
||||
Hive.registerAdapter(SearchModeAdapter());
|
||||
|
||||
await Hive.openLazyBox<MatchedTrack>(
|
||||
MatchedTrack.boxName,
|
||||
|
@ -1,5 +1,4 @@
|
||||
import "package:hive/hive.dart";
|
||||
|
||||
part "matched_track.g.dart";
|
||||
|
||||
@HiveType(typeId: 1)
|
||||
@ -8,6 +7,8 @@ class MatchedTrack {
|
||||
String youtubeId;
|
||||
@HiveField(1)
|
||||
String spotifyId;
|
||||
@HiveField(2)
|
||||
SearchMode searchMode;
|
||||
|
||||
String? id;
|
||||
DateTime? createdAt;
|
||||
@ -21,12 +22,14 @@ class MatchedTrack {
|
||||
MatchedTrack({
|
||||
required this.youtubeId,
|
||||
required this.spotifyId,
|
||||
required this.searchMode,
|
||||
this.id,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
factory MatchedTrack.fromJson(Map<String, dynamic> json) {
|
||||
return MatchedTrack(
|
||||
searchMode: SearchMode.fromString(json["searchMode"]),
|
||||
youtubeId: json["youtube_id"],
|
||||
spotifyId: json["spotify_id"],
|
||||
id: json["id"],
|
||||
@ -39,7 +42,27 @@ class MatchedTrack {
|
||||
"youtube_id": youtubeId,
|
||||
"spotify_id": spotifyId,
|
||||
"id": id,
|
||||
"searchMode": searchMode.name,
|
||||
"created_at": createdAt?.toString()
|
||||
}..removeWhere((key, value) => value == null);
|
||||
}
|
||||
}
|
||||
|
||||
@HiveType(typeId: 4)
|
||||
enum SearchMode {
|
||||
@HiveField(0)
|
||||
youtube._internal('YouTube'),
|
||||
@HiveField(1)
|
||||
youtubeMusic._internal('YouTube Music');
|
||||
|
||||
final String label;
|
||||
|
||||
const SearchMode._internal(this.label);
|
||||
|
||||
factory SearchMode.fromString(String value) {
|
||||
return SearchMode.values.firstWhere(
|
||||
(element) => element.name == value,
|
||||
orElse: () => SearchMode.youtube,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -19,17 +19,20 @@ class MatchedTrackAdapter extends TypeAdapter<MatchedTrack> {
|
||||
return MatchedTrack(
|
||||
youtubeId: fields[0] as String,
|
||||
spotifyId: fields[1] as String,
|
||||
searchMode: fields[2] as SearchMode,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, MatchedTrack obj) {
|
||||
writer
|
||||
..writeByte(2)
|
||||
..writeByte(3)
|
||||
..writeByte(0)
|
||||
..write(obj.youtubeId)
|
||||
..writeByte(1)
|
||||
..write(obj.spotifyId);
|
||||
..write(obj.spotifyId)
|
||||
..writeByte(2)
|
||||
..write(obj.searchMode);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -42,3 +45,42 @@ class MatchedTrackAdapter extends TypeAdapter<MatchedTrack> {
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class SearchModeAdapter extends TypeAdapter<SearchMode> {
|
||||
@override
|
||||
final int typeId = 4;
|
||||
|
||||
@override
|
||||
SearchMode read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return SearchMode.youtube;
|
||||
case 1:
|
||||
return SearchMode.youtubeMusic;
|
||||
default:
|
||||
return SearchMode.youtube;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SearchMode obj) {
|
||||
switch (obj) {
|
||||
case SearchMode.youtube:
|
||||
writer.writeByte(0);
|
||||
break;
|
||||
case SearchMode.youtubeMusic:
|
||||
writer.writeByte(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SearchModeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
@ -1,20 +1,18 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/extensions/album_simple.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/models/matched_track.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/services/youtube/youtube.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
class SpotubeTrack extends Track {
|
||||
final PipedStreamResponse ytTrack;
|
||||
final YoutubeVideoInfo ytTrack;
|
||||
final String ytUri;
|
||||
|
||||
final List<PipedSearchItemStream> siblings;
|
||||
final List<YoutubeVideoInfo> siblings;
|
||||
|
||||
SpotubeTrack(
|
||||
this.ytTrack,
|
||||
@ -48,25 +46,10 @@ 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<PipedSearchItemStream>> fetchSiblings(
|
||||
static Future<List<YoutubeVideoInfo>> fetchSiblings(
|
||||
Track track,
|
||||
PipedClient client, [
|
||||
PipedFilter filter = PipedFilter.musicSongs,
|
||||
]) async {
|
||||
YoutubeEndpoints client,
|
||||
) async {
|
||||
final artists = (track.artists ?? [])
|
||||
.map((ar) => ar.name)
|
||||
.toList()
|
||||
@ -79,22 +62,21 @@ class SpotubeTrack extends Track {
|
||||
onlyCleanArtist: true,
|
||||
).trim();
|
||||
|
||||
final List<PipedSearchItemStream> siblings =
|
||||
await client.search("$title - ${artists.join(", ")}", filter).then(
|
||||
final List<YoutubeVideoInfo> siblings =
|
||||
await client.search("$title - ${artists.join(", ")}").then(
|
||||
(res) {
|
||||
final siblings = res.items
|
||||
.whereType<PipedSearchItemStream>()
|
||||
final siblings = res
|
||||
.where((item) {
|
||||
return artists.any(
|
||||
(artist) =>
|
||||
artist.toLowerCase() == item.uploaderName.toLowerCase(),
|
||||
artist.toLowerCase() == item.channelName.toLowerCase(),
|
||||
);
|
||||
})
|
||||
.take(10)
|
||||
.toList();
|
||||
|
||||
if (siblings.isEmpty) {
|
||||
return res.items.whereType<PipedSearchItemStream>().take(10).toList();
|
||||
return res.take(10).toList();
|
||||
}
|
||||
|
||||
return siblings;
|
||||
@ -106,61 +88,53 @@ class SpotubeTrack extends Track {
|
||||
|
||||
static Future<SpotubeTrack> fetchFromTrack(
|
||||
Track track,
|
||||
UserPreferences preferences,
|
||||
PipedClient client,
|
||||
YoutubeEndpoints client,
|
||||
) async {
|
||||
final matchedCachedTrack = await MatchedTrack.box.get(track.id!);
|
||||
var siblings = <PipedSearchItemStream>[];
|
||||
PipedStreamResponse ytVideo;
|
||||
if (matchedCachedTrack != null) {
|
||||
ytVideo = await client.streams(matchedCachedTrack.youtubeId);
|
||||
} else {
|
||||
siblings = await fetchSiblings(
|
||||
track,
|
||||
client,
|
||||
switch (preferences.searchMode) {
|
||||
SearchMode.youtube => PipedFilter.video,
|
||||
SearchMode.youtubeMusic => PipedFilter.musicSongs,
|
||||
},
|
||||
var siblings = <YoutubeVideoInfo>[];
|
||||
YoutubeVideoInfo ytVideo;
|
||||
String ytStreamUrl;
|
||||
if (matchedCachedTrack != null &&
|
||||
matchedCachedTrack.searchMode == client.preferences.searchMode) {
|
||||
(ytVideo, ytStreamUrl) = await client.video(
|
||||
matchedCachedTrack.youtubeId,
|
||||
matchedCachedTrack.searchMode,
|
||||
);
|
||||
} else {
|
||||
siblings = await fetchSiblings(track, client);
|
||||
if (siblings.isEmpty) {
|
||||
throw Exception("Failed to find any results for ${track.name}");
|
||||
}
|
||||
ytVideo = await client.streams(siblings.first.id);
|
||||
(ytVideo, ytStreamUrl) =
|
||||
await client.video(siblings.first.id, siblings.first.searchMode);
|
||||
|
||||
await MatchedTrack.box.put(
|
||||
track.id!,
|
||||
MatchedTrack(
|
||||
youtubeId: ytVideo.id,
|
||||
spotifyId: track.id!,
|
||||
searchMode: siblings.first.searchMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final PipedAudioStream ytStream =
|
||||
getStreamInfo(ytVideo, preferences.audioQuality);
|
||||
|
||||
return SpotubeTrack.fromTrack(
|
||||
track: track,
|
||||
ytTrack: ytVideo,
|
||||
ytUri: ytStream.url,
|
||||
ytUri: ytStreamUrl,
|
||||
siblings: siblings,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SpotubeTrack?> swappedCopy(
|
||||
PipedSearchItemStream video,
|
||||
UserPreferences preferences,
|
||||
PipedClient client,
|
||||
YoutubeVideoInfo video,
|
||||
YoutubeEndpoints client,
|
||||
) async {
|
||||
// sibling tracks that were manually searched and swapped
|
||||
final isStepSibling = siblings.none((element) => element.id == video.id);
|
||||
|
||||
final ytVideo = await client.streams(video.id);
|
||||
|
||||
final ytStream = getStreamInfo(ytVideo, preferences.audioQuality);
|
||||
|
||||
final ytUri = ytStream.url;
|
||||
final (ytVideo, ytStreamUrl) =
|
||||
await client.video(video.id, siblings.first.searchMode);
|
||||
|
||||
if (!isStepSibling) {
|
||||
await MatchedTrack.box.put(
|
||||
@ -168,6 +142,7 @@ class SpotubeTrack extends Track {
|
||||
MatchedTrack(
|
||||
youtubeId: video.id,
|
||||
spotifyId: id!,
|
||||
searchMode: siblings.first.searchMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -175,7 +150,7 @@ class SpotubeTrack extends Track {
|
||||
return SpotubeTrack.fromTrack(
|
||||
track: this,
|
||||
ytTrack: ytVideo,
|
||||
ytUri: ytUri,
|
||||
ytUri: ytStreamUrl,
|
||||
siblings: [
|
||||
video,
|
||||
...siblings.where((element) => element.id != video.id),
|
||||
@ -186,24 +161,20 @@ class SpotubeTrack extends Track {
|
||||
static SpotubeTrack fromJson(Map<String, dynamic> map) {
|
||||
return SpotubeTrack.fromTrack(
|
||||
track: Track.fromJson(map),
|
||||
ytTrack: PipedStreamResponse.fromJson(map["ytTrack"]),
|
||||
ytTrack: YoutubeVideoInfo.fromJson(map["ytTrack"]),
|
||||
ytUri: map["ytUri"],
|
||||
siblings: List.castFrom<dynamic, Map<String, dynamic>>(map["siblings"])
|
||||
.map((sibling) => PipedSearchItemStream.fromJson(sibling))
|
||||
.map((sibling) => YoutubeVideoInfo.fromJson(sibling))
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<SpotubeTrack> populatedCopy(
|
||||
PipedClient client,
|
||||
PipedFilter filter,
|
||||
) async {
|
||||
Future<SpotubeTrack> populatedCopy(YoutubeEndpoints client) async {
|
||||
if (this.siblings.isNotEmpty) return this;
|
||||
|
||||
final siblings = await fetchSiblings(
|
||||
this,
|
||||
client,
|
||||
filter,
|
||||
);
|
||||
|
||||
return SpotubeTrack.fromTrack(
|
||||
|
@ -20,10 +20,11 @@ import 'package:spotube/collections/spotify_markets.dart';
|
||||
import 'package:spotube/extensions/constrains.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/l10n/l10n.dart';
|
||||
import 'package:spotube/models/matched_track.dart';
|
||||
import 'package:spotube/provider/download_manager_provider.dart';
|
||||
import 'package:spotube/provider/authentication_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/piped_provider.dart';
|
||||
import 'package:spotube/provider/piped_instances_provider.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class SettingsPage extends HookConsumerWidget {
|
||||
@ -290,16 +291,39 @@ class SettingsPage extends HookConsumerWidget {
|
||||
}
|
||||
},
|
||||
),
|
||||
Consumer(builder: (context, ref, child) {
|
||||
AdaptiveSelectTile<YoutubeApiType>(
|
||||
secondary: const Icon(SpotubeIcons.youtube),
|
||||
title: Text(context.l10n.youtube_api_type),
|
||||
value: preferences.youtubeApiType,
|
||||
options: YoutubeApiType.values
|
||||
.map((e) => DropdownMenuItem(
|
||||
value: e,
|
||||
child: Text(e.label),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
preferences.setYoutubeApiType(value);
|
||||
},
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: preferences.youtubeApiType ==
|
||||
YoutubeApiType.youtube
|
||||
? const SizedBox.shrink()
|
||||
: Consumer(builder: (context, ref, child) {
|
||||
final instanceList =
|
||||
ref.watch(pipedInstancesFutureProvider);
|
||||
|
||||
return instanceList.when(
|
||||
data: (data) {
|
||||
return AdaptiveSelectTile<String>(
|
||||
secondary: const Icon(SpotubeIcons.piped),
|
||||
title: Text(context.l10n.piped_instance),
|
||||
subtitle: Text(context.l10n.piped_description),
|
||||
secondary:
|
||||
const Icon(SpotubeIcons.piped),
|
||||
title:
|
||||
Text(context.l10n.piped_instance),
|
||||
subtitle: Text(
|
||||
context.l10n.piped_description),
|
||||
value: preferences.pipedInstance,
|
||||
showValueWhenUnfolded: false,
|
||||
options: data
|
||||
@ -328,7 +352,13 @@ class SettingsPage extends HookConsumerWidget {
|
||||
Text(error.toString()),
|
||||
);
|
||||
}),
|
||||
AdaptiveSelectTile<SearchMode>(
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: preferences.youtubeApiType ==
|
||||
YoutubeApiType.youtube
|
||||
? const SizedBox.shrink()
|
||||
: AdaptiveSelectTile<SearchMode>(
|
||||
secondary: const Icon(SpotubeIcons.search),
|
||||
title: Text(context.l10n.search_mode),
|
||||
value: preferences.searchMode,
|
||||
@ -343,20 +373,15 @@ class SettingsPage extends HookConsumerWidget {
|
||||
preferences.setSearchMode(value);
|
||||
},
|
||||
),
|
||||
AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity:
|
||||
preferences.searchMode == SearchMode.youtubeMusic
|
||||
? 0
|
||||
: 1,
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: SizedBox(
|
||||
height: preferences.searchMode ==
|
||||
SearchMode.youtubeMusic
|
||||
? 0
|
||||
: 50,
|
||||
child: SwitchListTile(
|
||||
),
|
||||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: preferences.searchMode ==
|
||||
SearchMode.youtubeMusic &&
|
||||
preferences.youtubeApiType ==
|
||||
YoutubeApiType.piped
|
||||
? const SizedBox.shrink()
|
||||
: SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.skip),
|
||||
title: Text(context.l10n.skip_non_music),
|
||||
value: preferences.skipNonMusic,
|
||||
@ -365,8 +390,6 @@ class SettingsPage extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(SpotubeIcons.playlistRemove),
|
||||
title: Text(context.l10n.blacklist),
|
||||
|
@ -7,13 +7,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/collections/routes.dart';
|
||||
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/piped_provider.dart';
|
||||
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/youtube_provider.dart';
|
||||
import 'package:spotube/services/youtube/youtube.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class DownloadManagerProvider extends StateNotifier<List<SpotubeTrack>> {
|
||||
@ -104,7 +105,7 @@ class DownloadManagerProvider extends StateNotifier<List<SpotubeTrack>> {
|
||||
}
|
||||
|
||||
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
||||
PipedClient get pipedClient => ref.read(pipedClientProvider);
|
||||
YoutubeEndpoints get youtube => ref.read(youtubeProvider);
|
||||
|
||||
int get totalDownloads => state.length;
|
||||
List<Track> get items => state;
|
||||
@ -129,8 +130,7 @@ class DownloadManagerProvider extends StateNotifier<List<SpotubeTrack>> {
|
||||
? track
|
||||
: await SpotubeTrack.fetchFromTrack(
|
||||
track,
|
||||
preferences,
|
||||
pipedClient,
|
||||
youtube,
|
||||
);
|
||||
state = [...state, spotubeTrack];
|
||||
final task = DownloadTask(
|
||||
|
10
lib/provider/piped_instances_provider.dart
Normal file
10
lib/provider/piped_instances_provider.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotube/provider/youtube_provider.dart';
|
||||
|
||||
final pipedInstancesFutureProvider = FutureProvider<List<PipedInstance>>(
|
||||
(ref) async {
|
||||
final youtube = ref.watch(youtubeProvider);
|
||||
return await youtube.piped?.instanceList() ?? [];
|
||||
},
|
||||
);
|
@ -1,18 +0,0 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
|
||||
PipedClient _defaultClient = PipedClient();
|
||||
|
||||
final pipedClientProvider = Provider((ref) {
|
||||
final instanceUrl =
|
||||
ref.watch(userPreferencesProvider.select((s) => s.pipedInstance));
|
||||
|
||||
if (instanceUrl == "https://pipedapi.kavin.rocks") return _defaultClient;
|
||||
|
||||
return PipedClient(instance: instanceUrl);
|
||||
});
|
||||
|
||||
final pipedInstancesFutureProvider = FutureProvider<List<PipedInstance>>(
|
||||
(ref) async => _defaultClient.instanceList(),
|
||||
);
|
@ -1,7 +1,6 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/local_track.dart';
|
||||
import 'package:spotube/models/matched_track.dart';
|
||||
@ -9,11 +8,12 @@ import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/supabase.dart';
|
||||
import 'package:spotube/services/youtube/youtube.dart';
|
||||
|
||||
mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
||||
Future<List<SpotubeTrack>> fetchTracks(
|
||||
UserPreferences preferences,
|
||||
PipedClient pipedClient, {
|
||||
YoutubeEndpoints youtube, {
|
||||
int count = 3,
|
||||
int offset = 0,
|
||||
}) async {
|
||||
@ -29,8 +29,7 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
||||
bareTracks.mapIndexed((i, track) async {
|
||||
final future = SpotubeTrack.fetchFromTrack(
|
||||
track,
|
||||
preferences,
|
||||
pipedClient,
|
||||
youtube,
|
||||
);
|
||||
if (i == 0) {
|
||||
return await future;
|
||||
@ -117,6 +116,7 @@ mixin NextFetcher on StateNotifier<ProxyPlaylist> {
|
||||
MatchedTrack(
|
||||
youtubeId: spotubeTrack.ytTrack.id,
|
||||
spotifyId: spotubeTrack.id!,
|
||||
searchMode: spotubeTrack.ytTrack.searchMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -7,11 +7,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:http/http.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';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/models/matched_track.dart';
|
||||
import 'package:spotube/models/skip_segment.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/provider/blacklist_provider.dart';
|
||||
@ -19,9 +19,10 @@ import 'package:spotube/provider/palette_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/next_fetcher_mixin.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/youtube_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
import 'package:spotube/services/audio_services/audio_services.dart';
|
||||
import 'package:spotube/provider/piped_provider.dart';
|
||||
import 'package:spotube/services/youtube/youtube.dart';
|
||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
@ -51,7 +52,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
late final AudioServices notificationService;
|
||||
|
||||
UserPreferences get preferences => ref.read(userPreferencesProvider);
|
||||
PipedClient get pipedClient => ref.read(pipedClientProvider);
|
||||
YoutubeEndpoints get youtube => ref.read(youtubeProvider);
|
||||
ProxyPlaylist get playlist => state;
|
||||
BlackListNotifier get blacklist =>
|
||||
ref.read(BlackListNotifier.provider.notifier);
|
||||
@ -213,7 +214,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
|
||||
final nthFetchedTrack = switch (track.runtimeType) {
|
||||
SpotubeTrack => track as SpotubeTrack,
|
||||
_ => await SpotubeTrack.fetchFromTrack(track, preferences, pipedClient),
|
||||
_ => await SpotubeTrack.fetchFromTrack(track, youtube),
|
||||
};
|
||||
|
||||
await audioPlayer.replaceSource(
|
||||
@ -298,8 +299,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
} else {
|
||||
final addableTrack = await SpotubeTrack.fetchFromTrack(
|
||||
tracks.elementAtOrNull(initialIndex) ?? tracks.first,
|
||||
preferences,
|
||||
pipedClient,
|
||||
youtube,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
@ -387,13 +387,7 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
Future<void> populateSibling() async {
|
||||
if (state.activeTrack is SpotubeTrack) {
|
||||
final activeTrackWithSiblingsForSure =
|
||||
await (state.activeTrack as SpotubeTrack).populatedCopy(
|
||||
pipedClient,
|
||||
switch (preferences.searchMode) {
|
||||
SearchMode.youtube => PipedFilter.video,
|
||||
SearchMode.youtubeMusic => PipedFilter.musicSongs,
|
||||
},
|
||||
);
|
||||
await (state.activeTrack as SpotubeTrack).populatedCopy(youtube);
|
||||
|
||||
state = state.copyWith(
|
||||
tracks: mergeTracks([activeTrackWithSiblingsForSure], state.tracks),
|
||||
@ -403,11 +397,11 @@ class ProxyPlaylistNotifier extends PersistedStateNotifier<ProxyPlaylist>
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> swapSibling(PipedSearchItem video) async {
|
||||
if (state.activeTrack is SpotubeTrack && video is PipedSearchItemStream) {
|
||||
Future<void> swapSibling(YoutubeVideoInfo video) async {
|
||||
if (state.activeTrack is SpotubeTrack) {
|
||||
await populateSibling();
|
||||
final newTrack = await (state.activeTrack as SpotubeTrack)
|
||||
.swappedCopy(video, preferences, pipedClient);
|
||||
final newTrack =
|
||||
await (state.activeTrack as SpotubeTrack).swappedCopy(video, youtube);
|
||||
if (newTrack == null) return;
|
||||
state = state.copyWith(
|
||||
tracks: mergeTracks([newTrack], state.tracks),
|
||||
|
@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
|
||||
import 'package:spotube/models/matched_track.dart';
|
||||
import 'package:spotube/provider/palette_provider.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
|
||||
@ -28,13 +29,11 @@ enum CloseBehavior {
|
||||
close,
|
||||
}
|
||||
|
||||
enum SearchMode {
|
||||
youtube._internal('YouTube'),
|
||||
youtubeMusic._internal('YouTube Music');
|
||||
enum YoutubeApiType {
|
||||
youtube,
|
||||
piped;
|
||||
|
||||
final String label;
|
||||
|
||||
const SearchMode._internal(this.label);
|
||||
String get label => name[0].toUpperCase() + name.substring(1);
|
||||
}
|
||||
|
||||
class UserPreferences extends PersistedChangeNotifier {
|
||||
@ -63,6 +62,8 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
|
||||
bool skipNonMusic;
|
||||
|
||||
YoutubeApiType youtubeApiType;
|
||||
|
||||
final Ref ref;
|
||||
|
||||
UserPreferences(
|
||||
@ -82,6 +83,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
this.pipedInstance = "https://pipedapi.kavin.rocks",
|
||||
this.searchMode = SearchMode.youtube,
|
||||
this.skipNonMusic = true,
|
||||
this.youtubeApiType = YoutubeApiType.youtube,
|
||||
}) : super() {
|
||||
if (downloadLocation.isEmpty) {
|
||||
_getDefaultDownloadDirectory().then(
|
||||
@ -188,6 +190,12 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
updatePersistence();
|
||||
}
|
||||
|
||||
void setYoutubeApiType(YoutubeApiType type) {
|
||||
youtubeApiType = type;
|
||||
notifyListeners();
|
||||
updatePersistence();
|
||||
}
|
||||
|
||||
Future<String> _getDefaultDownloadDirectory() async {
|
||||
if (kIsAndroid) return "/storage/emulated/0/Download/Spotube";
|
||||
|
||||
@ -240,6 +248,11 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
);
|
||||
|
||||
skipNonMusic = map["skipNonMusic"] ?? skipNonMusic;
|
||||
|
||||
youtubeApiType = YoutubeApiType.values.firstWhere(
|
||||
(type) => type.name == map["youtubeApiType"],
|
||||
orElse: () => YoutubeApiType.youtube,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -261,6 +274,7 @@ class UserPreferences extends PersistedChangeNotifier {
|
||||
"pipedInstance": pipedInstance,
|
||||
"searchMode": searchMode.name,
|
||||
"skipNonMusic": skipNonMusic,
|
||||
"youtubeApiType": youtubeApiType.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
8
lib/provider/youtube_provider.dart
Normal file
8
lib/provider/youtube_provider.dart
Normal file
@ -0,0 +1,8 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/youtube/youtube.dart';
|
||||
|
||||
final youtubeProvider = Provider<YoutubeEndpoints>((ref) {
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
return YoutubeEndpoints(preferences);
|
||||
});
|
221
lib/services/youtube/youtube.dart
Normal file
221
lib/services/youtube/youtube.dart
Normal file
@ -0,0 +1,221 @@
|
||||
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotube/models/matched_track.dart';
|
||||
import 'package:spotube/provider/user_preferences_provider.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
class YoutubeVideoInfo {
|
||||
final SearchMode searchMode;
|
||||
final String title;
|
||||
final Duration duration;
|
||||
final String thumbnailUrl;
|
||||
final String id;
|
||||
final int likes;
|
||||
final int dislikes;
|
||||
final int views;
|
||||
final String channelName;
|
||||
final String channelId;
|
||||
final DateTime publishedAt;
|
||||
|
||||
YoutubeVideoInfo({
|
||||
required this.searchMode,
|
||||
required this.title,
|
||||
required this.duration,
|
||||
required this.thumbnailUrl,
|
||||
required this.id,
|
||||
required this.likes,
|
||||
required this.dislikes,
|
||||
required this.views,
|
||||
required this.channelName,
|
||||
required this.publishedAt,
|
||||
required this.channelId,
|
||||
});
|
||||
|
||||
YoutubeVideoInfo.fromJson(Map<String, dynamic> json)
|
||||
: title = json['title'],
|
||||
searchMode = SearchMode.fromString(json['searchMode']),
|
||||
duration = Duration(seconds: json['duration']),
|
||||
thumbnailUrl = json['thumbnailUrl'],
|
||||
id = json['id'],
|
||||
likes = json['likes'],
|
||||
dislikes = json['dislikes'],
|
||||
views = json['views'],
|
||||
channelName = json['channelName'],
|
||||
channelId = json['channelId'],
|
||||
publishedAt = DateTime.tryParse(json['publishedAt']) ?? DateTime.now();
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'title': title,
|
||||
'duration': duration.inSeconds,
|
||||
'thumbnailUrl': thumbnailUrl,
|
||||
'id': id,
|
||||
'likes': likes,
|
||||
'dislikes': dislikes,
|
||||
'views': views,
|
||||
'channelName': channelName,
|
||||
'channelId': channelId,
|
||||
'publishedAt': publishedAt.toIso8601String(),
|
||||
'searchMode': searchMode.name,
|
||||
};
|
||||
|
||||
factory YoutubeVideoInfo.fromVideo(Video video) {
|
||||
return YoutubeVideoInfo(
|
||||
searchMode: SearchMode.youtube,
|
||||
title: video.title,
|
||||
duration: video.duration ?? Duration.zero,
|
||||
thumbnailUrl: video.thumbnails.mediumResUrl,
|
||||
id: video.id.value,
|
||||
likes: video.engagement.likeCount ?? 0,
|
||||
dislikes: video.engagement.dislikeCount ?? 0,
|
||||
views: video.engagement.viewCount,
|
||||
channelName: video.author,
|
||||
channelId: '/c/${video.channelId.value}',
|
||||
publishedAt: video.uploadDate ?? DateTime(2003, 9, 9),
|
||||
);
|
||||
}
|
||||
|
||||
factory YoutubeVideoInfo.fromSearchItemStream(
|
||||
PipedSearchItemStream searchItem,
|
||||
SearchMode searchMode,
|
||||
) {
|
||||
return YoutubeVideoInfo(
|
||||
searchMode: searchMode,
|
||||
title: searchItem.title,
|
||||
duration: searchItem.duration,
|
||||
thumbnailUrl: searchItem.thumbnail,
|
||||
id: searchItem.id,
|
||||
likes: 0,
|
||||
dislikes: 0,
|
||||
views: searchItem.views,
|
||||
channelName: searchItem.uploaderName,
|
||||
channelId: searchItem.uploaderUrl ?? "",
|
||||
publishedAt: searchItem.uploadedDate != null
|
||||
? DateTime.tryParse(searchItem.uploadedDate!) ?? DateTime(2003, 9, 9)
|
||||
: DateTime(2003, 9, 9),
|
||||
);
|
||||
}
|
||||
|
||||
factory YoutubeVideoInfo.fromStreamResponse(
|
||||
PipedStreamResponse stream, SearchMode searchMode) {
|
||||
return YoutubeVideoInfo(
|
||||
searchMode: searchMode,
|
||||
title: stream.title,
|
||||
duration: stream.duration,
|
||||
thumbnailUrl: stream.thumbnailUrl,
|
||||
id: stream.id,
|
||||
likes: stream.likes,
|
||||
dislikes: stream.dislikes,
|
||||
views: stream.views,
|
||||
channelName: stream.uploader,
|
||||
publishedAt: stream.uploadedDate != null
|
||||
? DateTime.tryParse(stream.uploadedDate!) ?? DateTime(2003, 9, 9)
|
||||
: DateTime(2003, 9, 9),
|
||||
channelId: stream.uploaderUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class YoutubeEndpoints {
|
||||
PipedClient? piped;
|
||||
YoutubeExplode? youtube;
|
||||
|
||||
final UserPreferences preferences;
|
||||
|
||||
YoutubeEndpoints(this.preferences) {
|
||||
switch (preferences.youtubeApiType) {
|
||||
case YoutubeApiType.youtube:
|
||||
youtube = YoutubeExplode();
|
||||
break;
|
||||
case YoutubeApiType.piped:
|
||||
piped = PipedClient(instance: preferences.pipedInstance);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<YoutubeVideoInfo>> search(String query) async {
|
||||
if (youtube != null) {
|
||||
final res = await youtube!.search(
|
||||
query,
|
||||
filter: TypeFilters.video,
|
||||
);
|
||||
|
||||
return res.map(YoutubeVideoInfo.fromVideo).toList();
|
||||
} else {
|
||||
final res = await piped!.search(
|
||||
query,
|
||||
switch (preferences.searchMode) {
|
||||
SearchMode.youtube => PipedFilter.video,
|
||||
SearchMode.youtubeMusic => PipedFilter.musicSongs,
|
||||
},
|
||||
);
|
||||
return res.items
|
||||
.whereType<PipedSearchItemStream>()
|
||||
.map(
|
||||
(e) => YoutubeVideoInfo.fromSearchItemStream(
|
||||
e,
|
||||
preferences.searchMode,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
String _pipedStreamResponseToStreamUrl(PipedStreamResponse stream) {
|
||||
final streamFormat = DesktopTools.platform.isLinux
|
||||
? PipedAudioStreamFormat.webm
|
||||
: PipedAudioStreamFormat.m4a;
|
||||
|
||||
return switch (preferences.audioQuality) {
|
||||
AudioQuality.high =>
|
||||
stream.highestBitrateAudioStreamOfFormat(streamFormat)!.url,
|
||||
AudioQuality.low =>
|
||||
stream.lowestBitrateAudioStreamOfFormat(streamFormat)!.url,
|
||||
};
|
||||
}
|
||||
|
||||
Future<String> streamingUrl(String id) async {
|
||||
if (youtube != null) {
|
||||
final res = await PrimitiveUtils.raceMultiple(
|
||||
() => youtube!.videos.streams.getManifest(id),
|
||||
);
|
||||
final audioOnlyManifests = res.audioOnly.where((info) {
|
||||
final isMp4a = info.codec.mimeType == "audio/mp4";
|
||||
if (DesktopTools.platform.isLinux) {
|
||||
return !isMp4a;
|
||||
} else if (DesktopTools.platform.isMacOS ||
|
||||
DesktopTools.platform.isIOS) {
|
||||
return isMp4a;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return switch (preferences.audioQuality) {
|
||||
AudioQuality.high =>
|
||||
audioOnlyManifests.withHighestBitrate().url.toString(),
|
||||
AudioQuality.low =>
|
||||
audioOnlyManifests.sortByBitrate().last.url.toString(),
|
||||
};
|
||||
} else {
|
||||
return _pipedStreamResponseToStreamUrl(await piped!.streams(id));
|
||||
}
|
||||
}
|
||||
|
||||
Future<(YoutubeVideoInfo info, String streamingUrl)> video(
|
||||
String id, SearchMode searchMode) async {
|
||||
if (youtube != null) {
|
||||
final res = await youtube!.videos.get(id);
|
||||
return (
|
||||
YoutubeVideoInfo.fromVideo(res),
|
||||
await streamingUrl(id),
|
||||
);
|
||||
} else {
|
||||
final res = await piped!.streams(id);
|
||||
return (
|
||||
YoutubeVideoInfo.fromStreamResponse(res, searchMode),
|
||||
_pipedStreamResponseToStreamUrl(res),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,11 +5,12 @@ import 'dart:io';
|
||||
import 'package:flutter/widgets.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/matched_track.dart';
|
||||
import 'package:spotube/models/spotube_track.dart';
|
||||
import 'package:spotube/services/youtube/youtube.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
@ -127,28 +128,19 @@ abstract class TypeConversionUtils {
|
||||
String? art,
|
||||
}) {
|
||||
final track = SpotubeTrack(
|
||||
PipedStreamResponse(
|
||||
YoutubeVideoInfo(
|
||||
searchMode: SearchMode.youtube,
|
||||
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: "",
|
||||
dislikes: 0,
|
||||
likes: 0,
|
||||
thumbnailUrl: art ?? "",
|
||||
uploadedDate: DateTime.now().toUtc().toString(),
|
||||
uploader: metadata?.albumArtist ?? "",
|
||||
uploaderUrl: "",
|
||||
uploaderVerified: false,
|
||||
views: -1,
|
||||
audioStreams: [],
|
||||
videoStreams: [],
|
||||
relatedStreams: [],
|
||||
subtitles: [],
|
||||
views: 0,
|
||||
channelName: metadata?.albumArtist ?? "Spotube",
|
||||
channelId: metadata?.albumArtist ?? "Spotube",
|
||||
publishedAt:
|
||||
metadata?.year != null ? DateTime(metadata!.year!) : DateTime(2003),
|
||||
),
|
||||
file.path,
|
||||
[],
|
||||
|
10
pubspec.lock
10
pubspec.lock
@ -2132,6 +2132,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
youtube_explode_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: youtube_explode_dart
|
||||
sha256: "07889a6229a63e78f8d45a3b852897c2e0fa42e96c4daa38d411be211575bc38"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.12.4"
|
||||
sdks:
|
||||
dart: ">=3.0.2 <4.0.0"
|
||||
dart: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
|
@ -96,6 +96,7 @@ dependencies:
|
||||
background_downloader: ^7.4.0
|
||||
duration: ^3.0.12
|
||||
disable_battery_optimization: ^1.1.0+1
|
||||
youtube_explode_dart: ^1.12.4
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.3.2
|
||||
|
@ -64,12 +64,14 @@
|
||||
"logs",
|
||||
"developers",
|
||||
"not_logged_in",
|
||||
"search_mode"
|
||||
"search_mode",
|
||||
"youtube_api_type"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"not_logged_in",
|
||||
"search_mode"
|
||||
"search_mode",
|
||||
"youtube_api_type"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
@ -137,7 +139,8 @@
|
||||
"logs",
|
||||
"developers",
|
||||
"not_logged_in",
|
||||
"search_mode"
|
||||
"search_mode",
|
||||
"youtube_api_type"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
@ -205,11 +208,13 @@
|
||||
"logs",
|
||||
"developers",
|
||||
"not_logged_in",
|
||||
"search_mode"
|
||||
"search_mode",
|
||||
"youtube_api_type"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"not_logged_in",
|
||||
"search_mode"
|
||||
"search_mode",
|
||||
"youtube_api_type"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user