feat: add invidious audio source with automatic track switch even on server playback endpoint

This commit is contained in:
Kingkor Roy Tirtho 2024-10-16 21:12:58 +06:00
parent 6e1cd96903
commit a8d2210d52
17 changed files with 539 additions and 30 deletions

BIN
assets/invidious.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

View File

@ -49,6 +49,7 @@ class Assets {
AssetGenImage('assets/bengali-patterns-bg.jpg'); AssetGenImage('assets/bengali-patterns-bg.jpg');
static const AssetGenImage branding = AssetGenImage('assets/branding.png'); static const AssetGenImage branding = AssetGenImage('assets/branding.png');
static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.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 jiosaavn = AssetGenImage('assets/jiosaavn.png');
static const AssetGenImage likedTracks = static const AssetGenImage likedTracks =
AssetGenImage('assets/liked-tracks.jpg'); AssetGenImage('assets/liked-tracks.jpg');
@ -95,6 +96,7 @@ class Assets {
bengaliPatternsBg, bengaliPatternsBg,
branding, branding,
emptyBox, emptyBox,
invidious,
jiosaavn, jiosaavn,
likedTracks, likedTracks,
placeholder, placeholder,

View File

@ -307,6 +307,7 @@
"youtube_source_description": "Recommended and works best.", "youtube_source_description": "Recommended and works best.",
"piped_source_description": "Feeling free? Same as YouTube but a lot free.", "piped_source_description": "Feeling free? Same as YouTube but a lot free.",
"jiosaavn_source_description": "Best for South Asian region.", "jiosaavn_source_description": "Best for South Asian region.",
"invidious_source_description": "Similar to Piped but with higher availability.",
"highest_quality": "Highest Quality: {quality}", "highest_quality": "Highest Quality: {quality}",
"select_audio_source": "Select Audio Source", "select_audio_source": "Select Audio Source",
"endless_playback_description": "Automatically append new songs\nto the end of the queue", "endless_playback_description": "Automatically append new songs\nto the end of the queue",

View File

@ -16,7 +16,7 @@ _$WebSocketLoadEventDataPlaylistImpl
? null ? null
: PlaylistSimple.fromJson( : PlaylistSimple.fromJson(
Map<String, dynamic>.from(json['collection'] as Map)), Map<String, dynamic>.from(json['collection'] as Map)),
initialIndex: json['initialIndex'] as int?, initialIndex: (json['initialIndex'] as num?)?.toInt(),
$type: json['runtimeType'] as String?, $type: json['runtimeType'] as String?,
); );
@ -39,7 +39,7 @@ _$WebSocketLoadEventDataAlbumImpl _$$WebSocketLoadEventDataAlbumImplFromJson(
? null ? null
: AlbumSimple.fromJson( : AlbumSimple.fromJson(
Map<String, dynamic>.from(json['collection'] as Map)), Map<String, dynamic>.from(json['collection'] as Map)),
initialIndex: json['initialIndex'] as int?, initialIndex: (json['initialIndex'] as num?)?.toInt(),
$type: json['runtimeType'] as String?, $type: json['runtimeType'] as String?,
); );

View File

@ -14,7 +14,8 @@ enum CloseBehavior {
enum AudioSource { enum AudioSource {
youtube, youtube,
piped, piped,
jiosaavn; jiosaavn,
invidious;
String get label => name[0].toUpperCase() + name.substring(1); String get label => name[0].toUpperCase() + name.substring(1);
} }

View File

@ -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/source_info.dart';
import 'package:spotube/services/sourced_track/models/video_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/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/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
@ -42,6 +43,17 @@ final sourceInfoToIconMap = {
), ),
), ),
PipedSourceInfo: const Icon(SpotubeIcons.piped), 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 { class SiblingTracksSheet extends HookConsumerWidget {

View File

@ -17,6 +17,10 @@ final audioSourceToIconMap = {
size: 30, size: 30,
), ),
AudioSource.piped: const Icon(SpotubeIcons.piped, 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), AudioSource.jiosaavn: Assets.jiosaavn.image(width: 48, height: 48),
}; };
@ -45,6 +49,7 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
AudioSource.jiosaavn: AudioSource.jiosaavn:
"${context.l10n.jiosaavn_source_description}\n" "${context.l10n.jiosaavn_source_description}\n"
"${context.l10n.highest_quality("320kbps mp")}", "${context.l10n.highest_quality("320kbps mp")}",
AudioSource.invidious: context.l10n.invidious_source_description,
}, },
[]); []);
@ -104,7 +109,9 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
title: Align( title: Align(
alignment: switch (preferences.audioSource) { alignment: switch (preferences.audioSource) {
AudioSource.youtube => Alignment.centerLeft, AudioSource.youtube => Alignment.centerLeft,
AudioSource.piped => Alignment.center, AudioSource.piped ||
AudioSource.invidious =>
Alignment.center,
AudioSource.jiosaavn => Alignment.centerRight, AudioSource.jiosaavn => Alignment.centerRight,
}, },
child: Text( child: Text(

View File

@ -159,7 +159,8 @@ class SettingsPlaybackSection extends HookConsumerWidget {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: preferences.searchMode == SearchMode.youtube && child: preferences.searchMode == SearchMode.youtube &&
(preferences.audioSource == AudioSource.piped || (preferences.audioSource == AudioSource.piped ||
preferences.audioSource == AudioSource.youtube) preferences.audioSource == AudioSource.youtube ||
preferences.audioSource == AudioSource.invidious)
? SwitchListTile( ? SwitchListTile(
secondary: const Icon(SpotubeIcons.skip), secondary: const Icon(SpotubeIcons.skip),
title: Text(context.l10n.skip_non_music), title: Text(context.l10n.skip_non_music),

View File

@ -20,6 +20,17 @@ class ServerPlaybackRoutes {
/// @get('/stream/<trackId>') /// @get('/stream/<trackId>')
Future<Response> getStreamTrackId(Request request, String trackId) async { 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 { try {
final track = final track =
playlist.tracks.firstWhere((element) => element.id == trackId); playlist.tracks.firstWhere((element) => element.id == trackId);
@ -30,22 +41,32 @@ class ServerPlaybackRoutes {
: await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future); : await ref.read(sourcedTrackProvider(SpotubeMedia(track)).future);
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack); ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
final res = await dio
final res = await dio.get( .get(
sourcedTrack!.url, sourcedTrack!.url,
options: Options( options: options.copyWith(
headers: { headers: {
...request.headers, ...options.headers!,
"User-Agent": "host": Uri.parse(sourcedTrack.url).host,
"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", },
),
)
.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, "host": Uri.parse(sourcedTrack.url).host,
"Cache-Control": "max-age=0",
"Connection": "keep-alive",
}, },
responseType: ResponseType.stream,
validateStatus: (status) => status! < 500,
), ),
); );
});
final audioStream = final audioStream =
(res.data?.stream as Stream<Uint8List>?)?.asBroadcastStream(); (res.data?.stream as Stream<Uint8List>?)?.asBroadcastStream();

View File

@ -21,8 +21,9 @@ final sourcedTrackProvider =
}, },
); );
final sourcedTrack = final sourcedTrack = media?.extras?["switch"] == true
await SourcedTrack.fetchFromTrack(track: track, ref: ref); ? await SourcedTrack.fetchFromTrackAltSource(track: track, ref: ref)
: await SourcedTrack.fetchFromTrack(track: track, ref: ref);
return sourcedTrack; return sourcedTrack;
}); });

View File

@ -12,7 +12,7 @@ SourceInfo _$SourceInfoFromJson(Map json) => SourceInfo(
artist: json['artist'] as String, artist: json['artist'] as String,
thumbnail: json['thumbnail'] as String, thumbnail: json['thumbnail'] as String,
pageUrl: json['pageUrl'] 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, artistUrl: json['artistUrl'] as String,
album: json['album'] as String?, album: json['album'] as String?,
); );

View File

@ -1,3 +1,4 @@
import 'package:invidious/invidious.dart';
import 'package:piped_client/piped_client.dart'; import 'package:piped_client/piped_client.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
@ -112,4 +113,24 @@ class YoutubeVideoInfo {
channelId: stream.uploaderUrl, 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),
);
}
} }

View File

@ -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/exceptions.dart';
import 'package:spotube/services/sourced_track/models/source_info.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/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/jiosaavn.dart';
import 'package:spotube/services/sourced_track/sources/piped.dart'; import 'package:spotube/services/sourced_track/sources/piped.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
@ -85,6 +86,13 @@ abstract class SourcedTrack extends Track {
sourceInfo: sourceInfo, sourceInfo: sourceInfo,
track: track, 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(", ")}"; 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({ static Future<SourcedTrack> fetchFromTrack({
required Track track, required Track track,
required Ref ref, required Ref ref,
@ -117,11 +168,14 @@ abstract class SourcedTrack extends Track {
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref), await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
AudioSource.jiosaavn => AudioSource.jiosaavn =>
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref), await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
AudioSource.invidious =>
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
}; };
} on TrackNotFoundError catch (_) { } on TrackNotFoundError catch (_) {
return switch (preferences.audioSource) { return switch (preferences.audioSource) {
AudioSource.piped || AudioSource.piped ||
AudioSource.youtube => AudioSource.youtube ||
AudioSource.invidious =>
await JioSaavnSourcedTrack.fetchFromTrack( await JioSaavnSourcedTrack.fetchFromTrack(
track: track, track: track,
ref: ref, ref: ref,
@ -159,6 +213,8 @@ abstract class SourcedTrack extends Track {
YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref), YoutubeSourcedTrack.fetchSiblings(track: track, ref: ref),
AudioSource.jiosaavn => AudioSource.jiosaavn =>
JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref), JioSaavnSourcedTrack.fetchSiblings(track: track, ref: ref),
AudioSource.invidious =>
InvidiousSourcedTrack.fetchSiblings(track: track, ref: ref),
}; };
} }

View 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,
);
}
}

View File

@ -518,10 +518,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted 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: disable_battery_optimization:
dependency: "direct main" dependency: "direct main"
description: description:
@ -993,10 +1001,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: freezed_annotation name: freezed_annotation
sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.4"
frontend_server_client: frontend_server_client:
dependency: transitive dependency: transitive
description: description:
@ -1247,6 +1255,13 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.19.0"
invidious:
dependency: "direct main"
description:
path: "../invidious"
relative: true
source: path
version: "0.1.0"
io: io:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -1275,18 +1290,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: json_annotation name: json_annotation
sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.8.1" version: "4.9.0"
json_serializable: json_serializable:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: json_serializable name: json_serializable
sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.7.1" version: "6.8.0"
leak_tracker: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -2432,5 +2447,5 @@ packages:
source: hosted source: hosted
version: "2.2.3" version: "2.2.3"
sdks: sdks:
dart: ">=3.5.0 <4.0.0" dart: ">=3.5.3 <4.0.0"
flutter: ">=3.24.0" flutter: ">=3.24.0"

View File

@ -58,6 +58,10 @@ dependencies:
html: ^0.15.1 html: ^0.15.1
image_picker: ^1.1.0 image_picker: ^1.1.0
intl: any intl: any
invidious:
# git:
# url: https://github.com/KRTirtho/invidious_dart.git
path: ../invidious
json_annotation: ^4.8.1 json_annotation: ^4.8.1
logger: ^2.0.2 logger: ^2.0.2
media_kit: ^1.1.10+1 media_kit: ^1.1.10+1

View File

@ -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"
]
}