mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-14 08:25:16 +00:00
feat: add invidious audio source with automatic track switch even on server playback endpoint
This commit is contained in:
parent
6e1cd96903
commit
a8d2210d52
BIN
assets/invidious.jpg
Normal file
BIN
assets/invidious.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
@ -49,6 +49,7 @@ class Assets {
|
||||
AssetGenImage('assets/bengali-patterns-bg.jpg');
|
||||
static const AssetGenImage branding = AssetGenImage('assets/branding.png');
|
||||
static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png');
|
||||
static const AssetGenImage invidious = AssetGenImage('assets/invidious.jpg');
|
||||
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
|
||||
static const AssetGenImage likedTracks =
|
||||
AssetGenImage('assets/liked-tracks.jpg');
|
||||
@ -95,6 +96,7 @@ class Assets {
|
||||
bengaliPatternsBg,
|
||||
branding,
|
||||
emptyBox,
|
||||
invidious,
|
||||
jiosaavn,
|
||||
likedTracks,
|
||||
placeholder,
|
||||
|
@ -307,6 +307,7 @@
|
||||
"youtube_source_description": "Recommended and works best.",
|
||||
"piped_source_description": "Feeling free? Same as YouTube but a lot free.",
|
||||
"jiosaavn_source_description": "Best for South Asian region.",
|
||||
"invidious_source_description": "Similar to Piped but with higher availability.",
|
||||
"highest_quality": "Highest Quality: {quality}",
|
||||
"select_audio_source": "Select Audio Source",
|
||||
"endless_playback_description": "Automatically append new songs\nto the end of the queue",
|
||||
|
@ -16,7 +16,7 @@ _$WebSocketLoadEventDataPlaylistImpl
|
||||
? null
|
||||
: PlaylistSimple.fromJson(
|
||||
Map<String, dynamic>.from(json['collection'] as Map)),
|
||||
initialIndex: json['initialIndex'] as int?,
|
||||
initialIndex: (json['initialIndex'] as num?)?.toInt(),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
@ -39,7 +39,7 @@ _$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson(
|
||||
? null
|
||||
: AlbumSimple.fromJson(
|
||||
Map<String, dynamic>.from(json['collection'] as Map)),
|
||||
initialIndex: json['initialIndex'] as int?,
|
||||
initialIndex: (json['initialIndex'] as num?)?.toInt(),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
|
@ -14,7 +14,8 @@ enum CloseBehavior {
|
||||
enum AudioSource {
|
||||
youtube,
|
||||
piped,
|
||||
jiosaavn;
|
||||
jiosaavn,
|
||||
invidious;
|
||||
|
||||
String get label => name[0].toUpperCase() + name.substring(1);
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
@ -42,6 +43,17 @@ final sourceInfoToIconMap = {
|
||||
),
|
||||
),
|
||||
PipedSourceInfo: const Icon(SpotubeIcons.piped),
|
||||
InvidiousSourceInfo: Container(
|
||||
height: 18,
|
||||
width: 18,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(90),
|
||||
image: DecorationImage(
|
||||
image: Assets.invidious.provider(),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
),
|
||||
};
|
||||
|
||||
class SiblingTracksSheet extends HookConsumerWidget {
|
||||
|
@ -17,6 +17,10 @@ final audioSourceToIconMap = {
|
||||
size: 30,
|
||||
),
|
||||
AudioSource.piped: const Icon(SpotubeIcons.piped, size: 30),
|
||||
AudioSource.invidious: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(48),
|
||||
child: Assets.invidious.image(width: 48, height: 48),
|
||||
),
|
||||
AudioSource.jiosaavn: Assets.jiosaavn.image(width: 48, height: 48),
|
||||
};
|
||||
|
||||
@ -45,6 +49,7 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
||||
AudioSource.jiosaavn:
|
||||
"${context.l10n.jiosaavn_source_description}\n"
|
||||
"${context.l10n.highest_quality("320kbps mp")}",
|
||||
AudioSource.invidious: context.l10n.invidious_source_description,
|
||||
},
|
||||
[]);
|
||||
|
||||
@ -104,7 +109,9 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
|
||||
title: Align(
|
||||
alignment: switch (preferences.audioSource) {
|
||||
AudioSource.youtube => Alignment.centerLeft,
|
||||
AudioSource.piped => Alignment.center,
|
||||
AudioSource.piped ||
|
||||
AudioSource.invidious =>
|
||||
Alignment.center,
|
||||
AudioSource.jiosaavn => Alignment.centerRight,
|
||||
},
|
||||
child: Text(
|
||||
|
@ -159,7 +159,8 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: preferences.searchMode == SearchMode.youtube &&
|
||||
(preferences.audioSource == AudioSource.piped ||
|
||||
preferences.audioSource == AudioSource.youtube)
|
||||
preferences.audioSource == AudioSource.youtube ||
|
||||
preferences.audioSource == AudioSource.invidious)
|
||||
? SwitchListTile(
|
||||
secondary: const Icon(SpotubeIcons.skip),
|
||||
title: Text(context.l10n.skip_non_music),
|
||||
|
@ -20,6 +20,17 @@ class ServerPlaybackRoutes {
|
||||
|
||||
/// @get('/stream/<trackId>')
|
||||
Future<Response> getStreamTrackId(Request request, String trackId) async {
|
||||
final options = Options(
|
||||
headers: {
|
||||
...request.headers,
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||
"Cache-Control": "max-age=0",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
responseType: ResponseType.stream,
|
||||
validateStatus: (status) => status! < 400,
|
||||
);
|
||||
try {
|
||||
final track =
|
||||
playlist.tracks.firstWhere((element) => element.id == trackId);
|
||||
@ -30,22 +41,32 @@ class ServerPlaybackRoutes {
|
||||
: await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future);
|
||||
|
||||
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
||||
|
||||
final res = await dio.get(
|
||||
final res = await dio
|
||||
.get(
|
||||
sourcedTrack!.url,
|
||||
options: Options(
|
||||
options: options.copyWith(
|
||||
headers: {
|
||||
...request.headers,
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||
...options.headers!,
|
||||
"host": Uri.parse(sourcedTrack.url).host,
|
||||
"Cache-Control": "max-age=0",
|
||||
"Connection": "keep-alive",
|
||||
},
|
||||
responseType: ResponseType.stream,
|
||||
validateStatus: (status) => status! < 500,
|
||||
),
|
||||
);
|
||||
)
|
||||
.catchError((e, stack) async {
|
||||
final sourcedTrack = await ref.read(
|
||||
sourcedTrackProvider(SpotubeMedia(track, extras: {"switch": true}))
|
||||
.future,
|
||||
);
|
||||
|
||||
return await dio.get(
|
||||
sourcedTrack!.url,
|
||||
options: options.copyWith(
|
||||
headers: {
|
||||
...options.headers!,
|
||||
"host": Uri.parse(sourcedTrack.url).host,
|
||||
},
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
final audioStream =
|
||||
(res.data?.stream as Stream<Uint8List>?)?.asBroadcastStream();
|
||||
|
@ -21,8 +21,9 @@ final sourcedTrackProvider =
|
||||
},
|
||||
);
|
||||
|
||||
final sourcedTrack =
|
||||
await SourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
final sourcedTrack = media?.extras?["switch"] == true
|
||||
? await SourcedTrack.fetchFromTrackAltSource(track: track, ref: ref)
|
||||
: await SourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
|
||||
return sourcedTrack;
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo(
|
||||
artist: json['artist'] as String,
|
||||
thumbnail: json['thumbnail'] as String,
|
||||
pageUrl: json['pageUrl'] as String,
|
||||
duration: Duration(microseconds: json['duration'] as int),
|
||||
duration: Duration(microseconds: (json['duration'] as num).toInt()),
|
||||
artistUrl: json['artistUrl'] as String,
|
||||
album: json['album'] as String?,
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import 'package:invidious/invidious.dart';
|
||||
import 'package:piped_client/piped_client.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
|
||||
@ -112,4 +113,24 @@ class YoutubeVideoInfo {
|
||||
channelId: stream.uploaderUrl,
|
||||
);
|
||||
}
|
||||
|
||||
factory YoutubeVideoInfo.fromSearchResponse(
|
||||
InvidiousSearchResponseVideo searchResponse,
|
||||
SearchMode searchMode,
|
||||
) {
|
||||
return YoutubeVideoInfo(
|
||||
searchMode: searchMode,
|
||||
title: searchResponse.title,
|
||||
duration: Duration(seconds: searchResponse.lengthSeconds),
|
||||
thumbnailUrl: searchResponse.videoThumbnails.first.url,
|
||||
id: searchResponse.videoId,
|
||||
likes: 0,
|
||||
dislikes: 0,
|
||||
views: searchResponse.viewCount,
|
||||
channelName: searchResponse.author,
|
||||
channelId: searchResponse.authorId,
|
||||
publishedAt:
|
||||
DateTime.fromMillisecondsSinceEpoch(searchResponse.published * 1000),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
@ -85,6 +86,13 @@ abstract class SourcedTrack extends Track {
|
||||
sourceInfo: sourceInfo,
|
||||
track: track,
|
||||
),
|
||||
AudioSource.invidious => InvidiousSourcedTrack(
|
||||
ref: ref,
|
||||
source: source,
|
||||
siblings: siblings,
|
||||
sourceInfo: sourceInfo,
|
||||
track: track,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@ -104,6 +112,49 @@ abstract class SourcedTrack extends Track {
|
||||
return "$title - ${artists.join(", ")}";
|
||||
}
|
||||
|
||||
static fetchFromTrackAltSource({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
try {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.invidious ||
|
||||
AudioSource.jiosaavn =>
|
||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.youtube =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on TrackNotFoundError catch (_) {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.youtube ||
|
||||
AudioSource.invidious =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: true,
|
||||
),
|
||||
AudioSource.jiosaavn =>
|
||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on HttpClientClosedException catch (_) {
|
||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
} on VideoUnplayableException catch (_) {
|
||||
return await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
} catch (e) {
|
||||
if (e is DioException || e is ClientException || e is SocketException) {
|
||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: preferences.audioSource == AudioSource.jiosaavn,
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<SourcedTrack> fetchFromTrack({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
@ -117,11 +168,14 @@ abstract class SourcedTrack extends Track {
|
||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.jiosaavn =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.invidious =>
|
||||
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on TrackNotFoundError catch (_) {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.youtube =>
|
||||
AudioSource.youtube ||
|
||||
AudioSource.invidious =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
@ -159,6 +213,8 @@ abstract class SourcedTrack extends Track {
|
||||
YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||
AudioSource.jiosaavn =>
|
||||
JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||
AudioSource.invidious =>
|
||||
InvidiousSourcedTrack.fetchSiblings(track: track, ref: ref),
|
||||
};
|
||||
}
|
||||
|
||||
|
263
lib/services/sourced_track/sources/invidious.dart
Normal file
263
lib/services/sourced_track/sources/invidious.dart
Normal file
@ -0,0 +1,263 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
import 'package:invidious/invidious.dart';
|
||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
final invidiousProvider = Provider<InvidiousClient>(
|
||||
(ref) {
|
||||
return InvidiousClient(server: "https://inv.nadeko.net");
|
||||
},
|
||||
);
|
||||
|
||||
class InvidiousSourceInfo extends SourceInfo {
|
||||
InvidiousSourceInfo({
|
||||
required super.id,
|
||||
required super.title,
|
||||
required super.artist,
|
||||
required super.thumbnail,
|
||||
required super.pageUrl,
|
||||
required super.duration,
|
||||
required super.artistUrl,
|
||||
required super.album,
|
||||
});
|
||||
}
|
||||
|
||||
class InvidiousSourcedTrack extends SourcedTrack {
|
||||
InvidiousSourcedTrack({
|
||||
required super.ref,
|
||||
required super.source,
|
||||
required super.siblings,
|
||||
required super.sourceInfo,
|
||||
required super.track,
|
||||
});
|
||||
|
||||
static Future<SourcedTrack> fetchFromTrack({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final database = ref.read(databaseProvider);
|
||||
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||
..where((s) => s.trackId.equals(track.id!))
|
||||
..limit(1)
|
||||
..orderBy([
|
||||
(s) =>
|
||||
OrderingTerm(expression: s.createdAt, mode: OrderingMode.desc),
|
||||
]))
|
||||
.getSingleOrNull();
|
||||
final invidiousClient = ref.read(invidiousProvider);
|
||||
|
||||
if (cachedSource == null) {
|
||||
final siblings = await fetchSiblings(ref: ref, track: track);
|
||||
if (siblings.isEmpty) {
|
||||
throw TrackNotFoundError(track);
|
||||
}
|
||||
|
||||
await database.into(database.sourceMatchTable).insert(
|
||||
SourceMatchTableCompanion.insert(
|
||||
trackId: track.id!,
|
||||
sourceId: siblings.first.info.id,
|
||||
sourceType: const Value(SourceType.youtube),
|
||||
),
|
||||
);
|
||||
|
||||
return InvidiousSourcedTrack(
|
||||
ref: ref,
|
||||
siblings: siblings.map((s) => s.info).skip(1).toList(),
|
||||
source: siblings.first.source as SourceMap,
|
||||
sourceInfo: siblings.first.info,
|
||||
track: track,
|
||||
);
|
||||
} else {
|
||||
final manifest =
|
||||
await invidiousClient.videos.get(cachedSource.sourceId, local: true);
|
||||
|
||||
return InvidiousSourcedTrack(
|
||||
ref: ref,
|
||||
siblings: [],
|
||||
source: toSourceMap(manifest),
|
||||
sourceInfo: InvidiousSourceInfo(
|
||||
id: manifest.videoId,
|
||||
artist: manifest.author,
|
||||
artistUrl: manifest.authorUrl,
|
||||
pageUrl: "https://www.youtube.com/watch?v=${manifest.videoId}",
|
||||
thumbnail: manifest.videoThumbnails.first.url,
|
||||
title: manifest.title,
|
||||
duration: Duration(seconds: manifest.lengthSeconds),
|
||||
album: null,
|
||||
),
|
||||
track: track,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static SourceMap toSourceMap(InvidiousVideoResponse manifest) {
|
||||
final m4a = manifest.adaptiveFormats
|
||||
.where((audio) => audio.type.contains("audio/mp4"))
|
||||
.sorted((a, b) => int.parse(a.bitrate).compareTo(int.parse(b.bitrate)));
|
||||
|
||||
final weba = manifest.adaptiveFormats
|
||||
.where((audio) => audio.type.contains("audio/webm"))
|
||||
.sorted((a, b) => int.parse(a.bitrate).compareTo(int.parse(b.bitrate)));
|
||||
|
||||
return SourceMap(
|
||||
m4a: SourceQualityMap(
|
||||
high: m4a.first.url.toString(),
|
||||
medium: (m4a.elementAtOrNull(m4a.length ~/ 2) ?? m4a[1]).url.toString(),
|
||||
low: m4a.last.url.toString(),
|
||||
),
|
||||
weba: SourceQualityMap(
|
||||
high: weba.first.url.toString(),
|
||||
medium:
|
||||
(weba.elementAtOrNull(weba.length ~/ 2) ?? weba[1]).url.toString(),
|
||||
low: weba.last.url.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<SiblingType> toSiblingType(
|
||||
int index,
|
||||
YoutubeVideoInfo item,
|
||||
InvidiousClient invidiousClient,
|
||||
) async {
|
||||
SourceMap? sourceMap;
|
||||
if (index == 0) {
|
||||
final manifest = await invidiousClient.videos.get(item.id, local: true);
|
||||
sourceMap = toSourceMap(manifest);
|
||||
}
|
||||
|
||||
final SiblingType sibling = (
|
||||
info: InvidiousSourceInfo(
|
||||
id: item.id,
|
||||
artist: item.channelName,
|
||||
artistUrl: "https://www.youtube.com/${item.channelId}",
|
||||
pageUrl: "https://www.youtube.com/watch?v=${item.id}",
|
||||
thumbnail: item.thumbnailUrl,
|
||||
title: item.title,
|
||||
duration: item.duration,
|
||||
album: null,
|
||||
),
|
||||
source: sourceMap,
|
||||
);
|
||||
|
||||
return sibling;
|
||||
}
|
||||
|
||||
static Future<List<SiblingType>> fetchSiblings({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final invidiousClient = ref.read(invidiousProvider);
|
||||
final preference = ref.read(userPreferencesProvider);
|
||||
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
final searchResults = await invidiousClient.search.list(
|
||||
query,
|
||||
type: InvidiousSearchType.video,
|
||||
);
|
||||
|
||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
||||
return await Future.wait(
|
||||
searchResults
|
||||
.whereType<InvidiousSearchResponseVideo>()
|
||||
.map(
|
||||
(result) => YoutubeVideoInfo.fromSearchResponse(
|
||||
result,
|
||||
preference.searchMode,
|
||||
),
|
||||
)
|
||||
.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)),
|
||||
);
|
||||
}
|
||||
|
||||
final rankedSiblings = YoutubeSourcedTrack.rankResults(
|
||||
searchResults
|
||||
.whereType<InvidiousSearchResponseVideo>()
|
||||
.map(
|
||||
(result) => YoutubeVideoInfo.fromSearchResponse(
|
||||
result,
|
||||
preference.searchMode,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
track,
|
||||
);
|
||||
|
||||
return await Future.wait(
|
||||
rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SourcedTrack> copyWithSibling() async {
|
||||
if (siblings.isNotEmpty) {
|
||||
return this;
|
||||
}
|
||||
final fetchedSiblings = await fetchSiblings(ref: ref, track: this);
|
||||
|
||||
return InvidiousSourcedTrack(
|
||||
ref: ref,
|
||||
siblings: fetchedSiblings
|
||||
.where((s) => s.info.id != sourceInfo.id)
|
||||
.map((s) => s.info)
|
||||
.toList(),
|
||||
source: source,
|
||||
sourceInfo: sourceInfo,
|
||||
track: this,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SourcedTrack?> swapWithSibling(SourceInfo sibling) async {
|
||||
if (sibling.id == sourceInfo.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// a sibling source that was fetched from the search results
|
||||
final isStepSibling = siblings.none((s) => s.id == sibling.id);
|
||||
|
||||
final newSourceInfo = isStepSibling
|
||||
? sibling
|
||||
: siblings.firstWhere((s) => s.id == sibling.id);
|
||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||
..insert(0, sourceInfo);
|
||||
|
||||
final pipedClient = ref.read(invidiousProvider);
|
||||
|
||||
final manifest =
|
||||
await pipedClient.videos.get(newSourceInfo.id, local: true);
|
||||
|
||||
final database = ref.read(databaseProvider);
|
||||
await database.into(database.sourceMatchTable).insert(
|
||||
SourceMatchTableCompanion.insert(
|
||||
trackId: id!,
|
||||
sourceId: newSourceInfo.id,
|
||||
sourceType: const Value(SourceType.youtube),
|
||||
// Because we're sorting by createdAt in the query
|
||||
// we have to update it to indicate priority
|
||||
createdAt: Value(DateTime.now()),
|
||||
),
|
||||
mode: InsertMode.replace,
|
||||
);
|
||||
|
||||
return InvidiousSourcedTrack(
|
||||
ref: ref,
|
||||
siblings: newSiblings,
|
||||
source: toSourceMap(manifest),
|
||||
sourceInfo: newSourceInfo,
|
||||
track: this,
|
||||
);
|
||||
}
|
||||
}
|
33
pubspec.lock
33
pubspec.lock
@ -518,10 +518,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5"
|
||||
sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.3+1"
|
||||
version: "5.7.0"
|
||||
dio_web_adapter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dio_web_adapter
|
||||
sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
disable_battery_optimization:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -993,10 +1001,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: freezed_annotation
|
||||
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
|
||||
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.4"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1247,6 +1255,13 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
invidious:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../invidious"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.1.0"
|
||||
io:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -1275,18 +1290,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.8.1"
|
||||
version: "4.9.0"
|
||||
json_serializable:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: json_serializable
|
||||
sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969
|
||||
sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.7.1"
|
||||
version: "6.8.0"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2432,5 +2447,5 @@ packages:
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
sdks:
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
dart: ">=3.5.3 <4.0.0"
|
||||
flutter: ">=3.24.0"
|
||||
|
@ -58,6 +58,10 @@ dependencies:
|
||||
html: ^0.15.1
|
||||
image_picker: ^1.1.0
|
||||
intl: any
|
||||
invidious:
|
||||
# git:
|
||||
# url: https://github.com/KRTirtho/invidious_dart.git
|
||||
path: ../invidious
|
||||
json_annotation: ^4.8.1
|
||||
logger: ^2.0.2
|
||||
media_kit: ^1.1.10+1
|
||||
|
@ -1 +1,105 @@
|
||||
{}
|
||||
{
|
||||
"ar": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"de": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"es": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"id": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"ka": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"ne": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"th": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
"invidious_source_description"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
"invidious_source_description"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user