diff --git a/assets/invidious.jpg b/assets/invidious.jpg new file mode 100644 index 00000000..12c5f107 Binary files /dev/null and b/assets/invidious.jpg differ diff --git a/lib/collections/assets.gen.dart b/lib/collections/assets.gen.dart index cff5b74f..f10242e8 100644 --- a/lib/collections/assets.gen.dart +++ b/lib/collections/assets.gen.dart @@ -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, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c63f8543..6bb5b9bc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/models/connect/connect.g.dart b/lib/models/connect/connect.g.dart index f297024b..10f46c65 100644 --- a/lib/models/connect/connect.g.dart +++ b/lib/models/connect/connect.g.dart @@ -16,7 +16,7 @@ _$WebSocketLoadEventDataPlaylistImpl ? null : PlaylistSimple.fromJson( Map.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.from(json['collection'] as Map)), - initialIndex: json['initialIndex'] as int?, + initialIndex: (json['initialIndex'] as num?)?.toInt(), $type: json['runtimeType'] as String?, ); diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index ae4ec1e8..bca67974 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -14,7 +14,8 @@ enum CloseBehavior { enum AudioSource { youtube, piped, - jiosaavn; + jiosaavn, + invidious; String get label => name[0].toUpperCase() + name.substring(1); } diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index b58a5894..3a31d88e 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -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 { diff --git a/lib/pages/getting_started/sections/playback.dart b/lib/pages/getting_started/sections/playback.dart index e7087afd..dbf0bda2 100644 --- a/lib/pages/getting_started/sections/playback.dart +++ b/lib/pages/getting_started/sections/playback.dart @@ -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( diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 6273c557..771d412d 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -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), diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart index 30322a6f..2c5aa380 100644 --- a/lib/provider/server/routes/playback.dart +++ b/lib/provider/server/routes/playback.dart @@ -20,6 +20,17 @@ class ServerPlaybackRoutes { /// @get('/stream/') Future 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?)?.asBroadcastStream(); diff --git a/lib/provider/server/sourced_track.dart b/lib/provider/server/sourced_track.dart index 53a04023..5a6377b5 100644 --- a/lib/provider/server/sourced_track.dart +++ b/lib/provider/server/sourced_track.dart @@ -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; }); diff --git a/lib/services/sourced_track/models/source_info.g.dart b/lib/services/sourced_track/models/source_info.g.dart index 5fe136ce..54671f63 100644 --- a/lib/services/sourced_track/models/source_info.g.dart +++ b/lib/services/sourced_track/models/source_info.g.dart @@ -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?, ); diff --git a/lib/services/sourced_track/models/video_info.dart b/lib/services/sourced_track/models/video_info.dart index 58dd0280..e3452c61 100644 --- a/lib/services/sourced_track/models/video_info.dart +++ b/lib/services/sourced_track/models/video_info.dart @@ -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), + ); + } } diff --git a/lib/services/sourced_track/sourced_track.dart b/lib/services/sourced_track/sourced_track.dart index 977b980b..6a1b7141 100644 --- a/lib/services/sourced_track/sourced_track.dart +++ b/lib/services/sourced_track/sourced_track.dart @@ -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 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), }; } diff --git a/lib/services/sourced_track/sources/invidious.dart b/lib/services/sourced_track/sources/invidious.dart new file mode 100644 index 00000000..26e19550 --- /dev/null +++ b/lib/services/sourced_track/sources/invidious.dart @@ -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( + (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 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 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> 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() + .map( + (result) => YoutubeVideoInfo.fromSearchResponse( + result, + preference.searchMode, + ), + ) + .mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)), + ); + } + + final rankedSiblings = YoutubeSourcedTrack.rankResults( + searchResults + .whereType() + .map( + (result) => YoutubeVideoInfo.fromSearchResponse( + result, + preference.searchMode, + ), + ) + .toList(), + track, + ); + + return await Future.wait( + rankedSiblings.mapIndexed((i, r) => toSiblingType(i, r, invidiousClient)), + ); + } + + @override + Future 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 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, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 77193ca0..d532dc4b 100644 --- a/pubspec.lock +++ b/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" diff --git a/pubspec.yaml b/pubspec.yaml index df8e668d..490e7f15 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/untranslated_messages.json b/untranslated_messages.json index 9e26dfee..0bff5a73 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1 +1,105 @@ -{} \ No newline at end of file +{ + "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" + ] +}