diff --git a/drift_schemas/app_db/drift_schema_v4.json b/drift_schemas/app_db/drift_schema_v4.json new file mode 100644 index 00000000..fc50a6f8 --- /dev/null +++ b/drift_schemas/app_db/drift_schema_v4.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"authentication_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"cookie","getter_name":"cookie","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"access_token","getter_name":"accessToken","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}},{"name":"expiration","getter_name":"expiration","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":1,"references":[],"type":"table","data":{"name":"blacklist_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"element_type","getter_name":"elementType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(BlacklistedType.values)","dart_type_name":"BlacklistedType"}},{"name":"element_id","getter_name":"elementId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":2,"references":[],"type":"table","data":{"name":"preferences_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"audio_quality","getter_name":"audioQuality","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceQualities.high.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceQualities.values)","dart_type_name":"SourceQualities"}},{"name":"album_color_sync","getter_name":"albumColorSync","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"album_color_sync\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"album_color_sync\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"amoled_dark_theme","getter_name":"amoledDarkTheme","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"amoled_dark_theme\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"amoled_dark_theme\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"check_update","getter_name":"checkUpdate","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"check_update\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"check_update\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"normalize_audio","getter_name":"normalizeAudio","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"normalize_audio\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"normalize_audio\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"show_system_tray_icon","getter_name":"showSystemTrayIcon","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"show_system_tray_icon\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"show_system_tray_icon\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"system_title_bar","getter_name":"systemTitleBar","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"system_title_bar\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"system_title_bar\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"skip_non_music","getter_name":"skipNonMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"skip_non_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"skip_non_music\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"close_behavior","getter_name":"closeBehavior","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(CloseBehavior.close.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(CloseBehavior.values)","dart_type_name":"CloseBehavior"}},{"name":"accent_color_scheme","getter_name":"accentColorScheme","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"Blue:0xFF2196F3\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const SpotubeColorConverter()","dart_type_name":"SpotubeColor"}},{"name":"layout_mode","getter_name":"layoutMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(LayoutMode.adaptive.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(LayoutMode.values)","dart_type_name":"LayoutMode"}},{"name":"locale","getter_name":"locale","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant('{\"languageCode\":\"system\",\"countryCode\":\"system\"}')","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const LocaleConverter()","dart_type_name":"Locale"}},{"name":"market","getter_name":"market","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(Market.US.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(Market.values)","dart_type_name":"Market"}},{"name":"search_mode","getter_name":"searchMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SearchMode.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SearchMode.values)","dart_type_name":"SearchMode"}},{"name":"download_location","getter_name":"downloadLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[]},{"name":"local_library_location","getter_name":"localLibraryLocation","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"\")","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}},{"name":"piped_instance","getter_name":"pipedInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://pipedapi.kavin.rocks\")","default_client_dart":null,"dsl_features":[]},{"name":"invidious_instance","getter_name":"invidiousInstance","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"const Constant(\"https://inv.nadeko.net\")","default_client_dart":null,"dsl_features":[]},{"name":"theme_mode","getter_name":"themeMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(ThemeMode.system.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ThemeMode.values)","dart_type_name":"ThemeMode"}},{"name":"audio_source","getter_name":"audioSource","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(AudioSource.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(AudioSource.values)","dart_type_name":"AudioSource"}},{"name":"youtube_client_engine","getter_name":"youtubeClientEngine","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(YoutubeClientEngine.youtubeExplode.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(YoutubeClientEngine.values)","dart_type_name":"YoutubeClientEngine"}},{"name":"stream_music_codec","getter_name":"streamMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.weba.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"download_music_codec","getter_name":"downloadMusicCodec","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceCodecs.m4a.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceCodecs.values)","dart_type_name":"SourceCodecs"}},{"name":"discord_presence","getter_name":"discordPresence","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"discord_presence\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"discord_presence\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"endless_playback","getter_name":"endlessPlayback","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"endless_playback\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"endless_playback\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]},{"name":"enable_connect","getter_name":"enableConnect","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"enable_connect\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"enable_connect\" IN (0, 1))"},"default_dart":"const Constant(false)","default_client_dart":null,"dsl_features":[]},{"name":"cache_music","getter_name":"cacheMusic","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"cache_music\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"cache_music\" IN (0, 1))"},"default_dart":"const Constant(true)","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":3,"references":[],"type":"table","data":{"name":"scrobbler_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"username","getter_name":"username","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"password_hash","getter_name":"passwordHash","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"EncryptedTextConverter()","dart_type_name":"DecryptedText"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":4,"references":[],"type":"table","data":{"name":"skip_segment_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"start","getter_name":"start","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"end","getter_name":"end","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":5,"references":[],"type":"table","data":{"name":"source_match_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_id","getter_name":"sourceId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"source_type","getter_name":"sourceType","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":"Constant(SourceType.youtube.name)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(SourceType.values)","dart_type_name":"SourceType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":6,"references":[],"type":"table","data":{"name":"audio_player_state_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"playing","getter_name":"playing","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"playing\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"playing\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"loop_mode","getter_name":"loopMode","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(PlaylistMode.values)","dart_type_name":"PlaylistMode"}},{"name":"shuffled","getter_name":"shuffled","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"shuffled\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"shuffled\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"collections","getter_name":"collections","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const StringListConverter()","dart_type_name":"List"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":7,"references":[6],"type":"table","data":{"name":"playlist_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"audio_player_state_id","getter_name":"audioPlayerStateId","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES audio_player_state_table (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES audio_player_state_table (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"index","getter_name":"index","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":8,"references":[7],"type":"table","data":{"name":"playlist_media_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"playlist_id","getter_name":"playlistId","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES playlist_table (id)","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES playlist_table (id)"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"uri","getter_name":"uri","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"extras","getter_name":"extras","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}},{"name":"http_headers","getter_name":"httpHeaders","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":9,"references":[],"type":"table","data":{"name":"history_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"currentDateAndTime","default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(HistoryEntryType.values)","dart_type_name":"HistoryEntryType"}},{"name":"item_id","getter_name":"itemId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const MapTypeConverter()","dart_type_name":"Map"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":10,"references":[],"type":"table","data":{"name":"lyrics_table","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"int","nullable":false,"customConstraints":null,"defaultConstraints":"PRIMARY KEY AUTOINCREMENT","dialectAwareDefaultConstraints":{"sqlite":"PRIMARY KEY AUTOINCREMENT"},"default_dart":null,"default_client_dart":null,"dsl_features":["auto-increment"]},{"name":"track_id","getter_name":"trackId","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"SubtitleTypeConverter()","dart_type_name":"SubtitleSimple"}}],"is_virtual":false,"without_rowid":false,"constraints":[]}},{"id":11,"references":[1],"type":"index","data":{"on":1,"name":"unique_blacklist","sql":null,"unique":true,"columns":["element_type","element_id"]}},{"id":12,"references":[5],"type":"index","data":{"on":5,"name":"uniq_track_match","sql":null,"unique":true,"columns":["track_id","source_id","source_type"]}}]} \ No newline at end of file diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index b5fbe5e8..bd9d037c 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -134,4 +134,5 @@ abstract class SpotubeIcons { static const grid = FeatherIcons.grid; static const list = FeatherIcons.list; static const device = FeatherIcons.smartphone; + static const engine = FeatherIcons.server; } diff --git a/lib/hooks/configurators/use_check_yt_dlp_installed.dart b/lib/hooks/configurators/use_check_yt_dlp_installed.dart new file mode 100644 index 00000000..df0bae9f --- /dev/null +++ b/lib/hooks/configurators/use_check_yt_dlp_installed.dart @@ -0,0 +1,33 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; + +void useCheckYtDlpInstalled(WidgetRef ref) { + final context = useContext(); + + useEffect(() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + final youtubeEngine = ref.read( + userPreferencesProvider.select( + (value) => value.youtubeClientEngine, + ), + ); + + if (youtubeEngine == YoutubeClientEngine.ytDlp && + !await YtDlpEngine.isInstalled() && + context.mounted) { + await showDialog( + context: context, + builder: (context) => + YouTubeEngineNotInstalledDialog(engine: youtubeEngine), + ); + } + }); + + return null; + }, []); +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ae7abb01..60cf300e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -415,5 +415,9 @@ "no_tracks_listened_yet": "Looks like you haven't listened to anything yet", "not_following_artists": "You're not following any artists", "no_favorite_albums_yet": "Looks like you haven't added any albums to your favorites yet", - "no_logs_found": "No logs found" + "no_logs_found": "No logs found", + "youtube_engine": "YouTube Engine", + "youtube_engine_not_installed_title": "{engine} is not installed", + "youtube_engine_not_installed_message": "{engine} is not installed in your system.\nPlease install it and make sure it's available in the PATH variable\n\nAfter installing, restart the app", + "download": "Download" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 016c0fea..ec16d401 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,6 +50,7 @@ import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:timezone/data/latest.dart' as tz; import 'package:window_manager/window_manager.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:yt_dlp_dart/yt_dlp_dart.dart'; Future main(List rawArgs) async { if (rawArgs.contains("web_view_title_bar")) { @@ -79,15 +80,15 @@ Future main(List rawArgs) async { await FlutterDisplayMode.setHighRefreshRate(); } - if (kIsDesktop) { - await windowManager.setPreventClose(true); - } - if (!kIsWeb) { MetadataGod.initialize(); } if (kIsDesktop) { + await windowManager.setPreventClose(true); + await YtDlp.instance + .setBinaryLocation("yt-dlp${kIsWindows ? '.exe' : ''}") + .catchError((e, stack) => null); await FlutterDiscordRPC.initialize(Env.discordAppId); } diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index f76d25bc..d6b2786c 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -18,6 +18,8 @@ import 'package:spotube/services/sourced_track/enums.dart'; import 'package:flutter/widgets.dart' hide Table, Key, View; import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart'; import 'package:drift/native.dart'; +import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; +import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart'; @@ -59,7 +61,7 @@ class AppDatabase extends _$AppDatabase { AppDatabase() : super(_openConnection()); @override - int get schemaVersion => 3; + int get schemaVersion => 4; @override MigrationStrategy get migration { @@ -78,6 +80,12 @@ class AppDatabase extends _$AppDatabase { schema.preferencesTable.cacheMusic, ); }, + from3To4: (m, schema) async { + await m.addColumn( + schema.preferencesTable, + schema.preferencesTable.youtubeClientEngine, + ); + }, ), ); } diff --git a/lib/models/database/database.g.dart b/lib/models/database/database.g.dart index 951b2ed5..cd004d69 100644 --- a/lib/models/database/database.g.dart +++ b/lib/models/database/database.g.dart @@ -760,6 +760,17 @@ class $PreferencesTableTable extends PreferencesTable defaultValue: Constant(AudioSource.youtube.name)) .withConverter( $PreferencesTableTable.$converteraudioSource); + static const VerificationMeta _youtubeClientEngineMeta = + const VerificationMeta('youtubeClientEngine'); + @override + late final GeneratedColumnWithTypeConverter + youtubeClientEngine = GeneratedColumn( + 'youtube_client_engine', aliasedName, false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)) + .withConverter( + $PreferencesTableTable.$converteryoutubeClientEngine); static const VerificationMeta _streamMusicCodecMeta = const VerificationMeta('streamMusicCodec'); @override @@ -845,6 +856,7 @@ class $PreferencesTableTable extends PreferencesTable invidiousInstance, themeMode, audioSource, + youtubeClientEngine, streamMusicCodec, downloadMusicCodec, discordPresence, @@ -937,6 +949,8 @@ class $PreferencesTableTable extends PreferencesTable } context.handle(_themeModeMeta, const VerificationResult.success()); context.handle(_audioSourceMeta, const VerificationResult.success()); + context.handle( + _youtubeClientEngineMeta, const VerificationResult.success()); context.handle(_streamMusicCodecMeta, const VerificationResult.success()); context.handle(_downloadMusicCodecMeta, const VerificationResult.success()); if (data.containsKey('discord_presence')) { @@ -1025,6 +1039,9 @@ class $PreferencesTableTable extends PreferencesTable audioSource: $PreferencesTableTable.$converteraudioSource.fromSql( attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}audio_source'])!), + youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine + .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}youtube_client_engine'])!), streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec .fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string, data['${effectivePrefix}stream_music_codec'])!), @@ -1069,6 +1086,9 @@ class $PreferencesTableTable extends PreferencesTable const EnumNameConverter(ThemeMode.values); static JsonTypeConverter2 $converteraudioSource = const EnumNameConverter(AudioSource.values); + static JsonTypeConverter2 + $converteryoutubeClientEngine = + const EnumNameConverter(YoutubeClientEngine.values); static JsonTypeConverter2 $converterstreamMusicCodec = const EnumNameConverter(SourceCodecs.values); @@ -1100,6 +1120,7 @@ class PreferencesTableData extends DataClass final String invidiousInstance; final ThemeMode themeMode; final AudioSource audioSource; + final YoutubeClientEngine youtubeClientEngine; final SourceCodecs streamMusicCodec; final SourceCodecs downloadMusicCodec; final bool discordPresence; @@ -1128,6 +1149,7 @@ class PreferencesTableData extends DataClass required this.invidiousInstance, required this.themeMode, required this.audioSource, + required this.youtubeClientEngine, required this.streamMusicCodec, required this.downloadMusicCodec, required this.discordPresence, @@ -1190,6 +1212,11 @@ class PreferencesTableData extends DataClass map['audio_source'] = Variable( $PreferencesTableTable.$converteraudioSource.toSql(audioSource)); } + { + map['youtube_client_engine'] = Variable($PreferencesTableTable + .$converteryoutubeClientEngine + .toSql(youtubeClientEngine)); + } { map['stream_music_codec'] = Variable($PreferencesTableTable .$converterstreamMusicCodec @@ -1230,6 +1257,7 @@ class PreferencesTableData extends DataClass invidiousInstance: Value(invidiousInstance), themeMode: Value(themeMode), audioSource: Value(audioSource), + youtubeClientEngine: Value(youtubeClientEngine), streamMusicCodec: Value(streamMusicCodec), downloadMusicCodec: Value(downloadMusicCodec), discordPresence: Value(discordPresence), @@ -1273,6 +1301,8 @@ class PreferencesTableData extends DataClass .fromJson(serializer.fromJson(json['themeMode'])), audioSource: $PreferencesTableTable.$converteraudioSource .fromJson(serializer.fromJson(json['audioSource'])), + youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine + .fromJson(serializer.fromJson(json['youtubeClientEngine'])), streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec .fromJson(serializer.fromJson(json['streamMusicCodec'])), downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec @@ -1316,6 +1346,9 @@ class PreferencesTableData extends DataClass $PreferencesTableTable.$converterthemeMode.toJson(themeMode)), 'audioSource': serializer.toJson( $PreferencesTableTable.$converteraudioSource.toJson(audioSource)), + 'youtubeClientEngine': serializer.toJson($PreferencesTableTable + .$converteryoutubeClientEngine + .toJson(youtubeClientEngine)), 'streamMusicCodec': serializer.toJson($PreferencesTableTable .$converterstreamMusicCodec .toJson(streamMusicCodec)), @@ -1351,6 +1384,7 @@ class PreferencesTableData extends DataClass String? invidiousInstance, ThemeMode? themeMode, AudioSource? audioSource, + YoutubeClientEngine? youtubeClientEngine, SourceCodecs? streamMusicCodec, SourceCodecs? downloadMusicCodec, bool? discordPresence, @@ -1379,6 +1413,7 @@ class PreferencesTableData extends DataClass invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, audioSource: audioSource ?? this.audioSource, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, discordPresence: discordPresence ?? this.discordPresence, @@ -1439,6 +1474,9 @@ class PreferencesTableData extends DataClass themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode, audioSource: data.audioSource.present ? data.audioSource.value : this.audioSource, + youtubeClientEngine: data.youtubeClientEngine.present + ? data.youtubeClientEngine.value + : this.youtubeClientEngine, streamMusicCodec: data.streamMusicCodec.present ? data.streamMusicCodec.value : this.streamMusicCodec, @@ -1483,6 +1521,7 @@ class PreferencesTableData extends DataClass ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') ..write('audioSource: $audioSource, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') ..write('streamMusicCodec: $streamMusicCodec, ') ..write('downloadMusicCodec: $downloadMusicCodec, ') ..write('discordPresence: $discordPresence, ') @@ -1516,6 +1555,7 @@ class PreferencesTableData extends DataClass invidiousInstance, themeMode, audioSource, + youtubeClientEngine, streamMusicCodec, downloadMusicCodec, discordPresence, @@ -1548,6 +1588,7 @@ class PreferencesTableData extends DataClass other.invidiousInstance == this.invidiousInstance && other.themeMode == this.themeMode && other.audioSource == this.audioSource && + other.youtubeClientEngine == this.youtubeClientEngine && other.streamMusicCodec == this.streamMusicCodec && other.downloadMusicCodec == this.downloadMusicCodec && other.discordPresence == this.discordPresence && @@ -1578,6 +1619,7 @@ class PreferencesTableCompanion extends UpdateCompanion { final Value invidiousInstance; final Value themeMode; final Value audioSource; + final Value youtubeClientEngine; final Value streamMusicCodec; final Value downloadMusicCodec; final Value discordPresence; @@ -1606,6 +1648,7 @@ class PreferencesTableCompanion extends UpdateCompanion { this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), this.audioSource = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), this.streamMusicCodec = const Value.absent(), this.downloadMusicCodec = const Value.absent(), this.discordPresence = const Value.absent(), @@ -1635,6 +1678,7 @@ class PreferencesTableCompanion extends UpdateCompanion { this.invidiousInstance = const Value.absent(), this.themeMode = const Value.absent(), this.audioSource = const Value.absent(), + this.youtubeClientEngine = const Value.absent(), this.streamMusicCodec = const Value.absent(), this.downloadMusicCodec = const Value.absent(), this.discordPresence = const Value.absent(), @@ -1664,6 +1708,7 @@ class PreferencesTableCompanion extends UpdateCompanion { Expression? invidiousInstance, Expression? themeMode, Expression? audioSource, + Expression? youtubeClientEngine, Expression? streamMusicCodec, Expression? downloadMusicCodec, Expression? discordPresence, @@ -1695,6 +1740,8 @@ class PreferencesTableCompanion extends UpdateCompanion { if (invidiousInstance != null) 'invidious_instance': invidiousInstance, if (themeMode != null) 'theme_mode': themeMode, if (audioSource != null) 'audio_source': audioSource, + if (youtubeClientEngine != null) + 'youtube_client_engine': youtubeClientEngine, if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec, if (downloadMusicCodec != null) 'download_music_codec': downloadMusicCodec, @@ -1727,6 +1774,7 @@ class PreferencesTableCompanion extends UpdateCompanion { Value? invidiousInstance, Value? themeMode, Value? audioSource, + Value? youtubeClientEngine, Value? streamMusicCodec, Value? downloadMusicCodec, Value? discordPresence, @@ -1755,6 +1803,7 @@ class PreferencesTableCompanion extends UpdateCompanion { invidiousInstance: invidiousInstance ?? this.invidiousInstance, themeMode: themeMode ?? this.themeMode, audioSource: audioSource ?? this.audioSource, + youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine, streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec, downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec, discordPresence: discordPresence ?? this.discordPresence, @@ -1845,6 +1894,11 @@ class PreferencesTableCompanion extends UpdateCompanion { .$converteraudioSource .toSql(audioSource.value)); } + if (youtubeClientEngine.present) { + map['youtube_client_engine'] = Variable($PreferencesTableTable + .$converteryoutubeClientEngine + .toSql(youtubeClientEngine.value)); + } if (streamMusicCodec.present) { map['stream_music_codec'] = Variable($PreferencesTableTable .$converterstreamMusicCodec @@ -1894,6 +1948,7 @@ class PreferencesTableCompanion extends UpdateCompanion { ..write('invidiousInstance: $invidiousInstance, ') ..write('themeMode: $themeMode, ') ..write('audioSource: $audioSource, ') + ..write('youtubeClientEngine: $youtubeClientEngine, ') ..write('streamMusicCodec: $streamMusicCodec, ') ..write('downloadMusicCodec: $downloadMusicCodec, ') ..write('discordPresence: $discordPresence, ') @@ -4565,6 +4620,7 @@ typedef $$PreferencesTableTableCreateCompanionBuilder Value invidiousInstance, Value themeMode, Value audioSource, + Value youtubeClientEngine, Value streamMusicCodec, Value downloadMusicCodec, Value discordPresence, @@ -4595,6 +4651,7 @@ typedef $$PreferencesTableTableUpdateCompanionBuilder Value invidiousInstance, Value themeMode, Value audioSource, + Value youtubeClientEngine, Value streamMusicCodec, Value downloadMusicCodec, Value discordPresence, @@ -4702,6 +4759,12 @@ class $$PreferencesTableTableFilterComposer column: $table.audioSource, builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters + get youtubeClientEngine => $composableBuilder( + column: $table.youtubeClientEngine, + builder: (column) => ColumnWithTypeConverterFilters(column)); + ColumnWithTypeConverterFilters get streamMusicCodec => $composableBuilder( column: $table.streamMusicCodec, @@ -4812,6 +4875,10 @@ class $$PreferencesTableTableOrderingComposer ColumnOrderings get audioSource => $composableBuilder( column: $table.audioSource, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get youtubeClientEngine => $composableBuilder( + column: $table.youtubeClientEngine, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get streamMusicCodec => $composableBuilder( column: $table.streamMusicCodec, builder: (column) => ColumnOrderings(column)); @@ -4915,6 +4982,10 @@ class $$PreferencesTableTableAnnotationComposer $composableBuilder( column: $table.audioSource, builder: (column) => column); + GeneratedColumnWithTypeConverter + get youtubeClientEngine => $composableBuilder( + column: $table.youtubeClientEngine, builder: (column) => column); + GeneratedColumnWithTypeConverter get streamMusicCodec => $composableBuilder( column: $table.streamMusicCodec, builder: (column) => column); @@ -4985,6 +5056,8 @@ class $$PreferencesTableTableTableManager extends RootTableManager< Value invidiousInstance = const Value.absent(), Value themeMode = const Value.absent(), Value audioSource = const Value.absent(), + Value youtubeClientEngine = + const Value.absent(), Value streamMusicCodec = const Value.absent(), Value downloadMusicCodec = const Value.absent(), Value discordPresence = const Value.absent(), @@ -5014,6 +5087,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager< invidiousInstance: invidiousInstance, themeMode: themeMode, audioSource: audioSource, + youtubeClientEngine: youtubeClientEngine, streamMusicCodec: streamMusicCodec, downloadMusicCodec: downloadMusicCodec, discordPresence: discordPresence, @@ -5043,6 +5117,8 @@ class $$PreferencesTableTableTableManager extends RootTableManager< Value invidiousInstance = const Value.absent(), Value themeMode = const Value.absent(), Value audioSource = const Value.absent(), + Value youtubeClientEngine = + const Value.absent(), Value streamMusicCodec = const Value.absent(), Value downloadMusicCodec = const Value.absent(), Value discordPresence = const Value.absent(), @@ -5072,6 +5148,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager< invidiousInstance: invidiousInstance, themeMode: themeMode, audioSource: audioSource, + youtubeClientEngine: youtubeClientEngine, streamMusicCodec: streamMusicCodec, downloadMusicCodec: downloadMusicCodec, discordPresence: discordPresence, diff --git a/lib/models/database/database.steps.dart b/lib/models/database/database.steps.dart index 25bf6ad9..8e0f8e3f 100644 --- a/lib/models/database/database.steps.dart +++ b/lib/models/database/database.steps.dart @@ -1,11 +1,11 @@ // dart format width=80 import 'package:drift/internal/versioned_schema.dart' as i0; import 'package:drift/drift.dart' as i1; -import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import +import 'package:drift/drift.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/models/database/database.dart'; -import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/sourced_track/enums.dart'; // ignore_for_file: type=lint,unused_import // GENERATED BY drift_dev, DO NOT MODIFY. final class Schema2 extends i0.VersionedSchema { @@ -907,9 +907,291 @@ i1.GeneratedColumn _column_53(String aliasedName) => defaultConstraints: i1.GeneratedColumn.constraintIsAlways( 'CHECK ("cache_music" IN (0, 1))'), defaultValue: const Constant(true)); + +final class Schema4 extends i0.VersionedSchema { + Schema4({required super.database}) : super(version: 4); + @override + late final List entities = [ + authenticationTable, + blacklistTable, + preferencesTable, + scrobblerTable, + skipSegmentTable, + sourceMatchTable, + audioPlayerStateTable, + playlistTable, + playlistMediaTable, + historyTable, + lyricsTable, + uniqueBlacklist, + uniqTrackMatch, + ]; + late final Shape0 authenticationTable = Shape0( + source: i0.VersionedTable( + entityName: 'authentication_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_1, + _column_2, + _column_3, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape1 blacklistTable = Shape1( + source: i0.VersionedTable( + entityName: 'blacklist_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_4, + _column_5, + _column_6, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape12 preferencesTable = Shape12( + source: i0.VersionedTable( + entityName: 'preferences_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_7, + _column_8, + _column_9, + _column_10, + _column_11, + _column_12, + _column_13, + _column_14, + _column_15, + _column_16, + _column_17, + _column_18, + _column_19, + _column_20, + _column_21, + _column_22, + _column_23, + _column_24, + _column_25, + _column_26, + _column_54, + _column_27, + _column_28, + _column_29, + _column_30, + _column_31, + _column_53, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape3 scrobblerTable = Shape3( + source: i0.VersionedTable( + entityName: 'scrobbler_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_33, + _column_34, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape4 skipSegmentTable = Shape4( + source: i0.VersionedTable( + entityName: 'skip_segment_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_35, + _column_36, + _column_37, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape5 sourceMatchTable = Shape5( + source: i0.VersionedTable( + entityName: 'source_match_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_38, + _column_39, + _column_32, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape6 audioPlayerStateTable = Shape6( + source: i0.VersionedTable( + entityName: 'audio_player_state_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_40, + _column_41, + _column_42, + _column_43, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape7 playlistTable = Shape7( + source: i0.VersionedTable( + entityName: 'playlist_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_44, + _column_45, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape8 playlistMediaTable = Shape8( + source: i0.VersionedTable( + entityName: 'playlist_media_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_46, + _column_47, + _column_48, + _column_49, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape9 historyTable = Shape9( + source: i0.VersionedTable( + entityName: 'history_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_32, + _column_50, + _column_51, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + late final Shape10 lyricsTable = Shape10( + source: i0.VersionedTable( + entityName: 'lyrics_table', + withoutRowId: false, + isStrict: false, + tableConstraints: [], + columns: [ + _column_0, + _column_37, + _column_52, + ], + attachedDatabase: database, + ), + alias: null); + final i1.Index uniqueBlacklist = i1.Index('unique_blacklist', + 'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)'); + final i1.Index uniqTrackMatch = i1.Index('uniq_track_match', + 'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)'); +} + +class Shape12 extends i0.VersionedTable { + Shape12({required super.source, required super.alias}) : super.aliased(); + i1.GeneratedColumn get id => + columnsByName['id']! as i1.GeneratedColumn; + i1.GeneratedColumn get audioQuality => + columnsByName['audio_quality']! as i1.GeneratedColumn; + i1.GeneratedColumn get albumColorSync => + columnsByName['album_color_sync']! as i1.GeneratedColumn; + i1.GeneratedColumn get amoledDarkTheme => + columnsByName['amoled_dark_theme']! as i1.GeneratedColumn; + i1.GeneratedColumn get checkUpdate => + columnsByName['check_update']! as i1.GeneratedColumn; + i1.GeneratedColumn get normalizeAudio => + columnsByName['normalize_audio']! as i1.GeneratedColumn; + i1.GeneratedColumn get showSystemTrayIcon => + columnsByName['show_system_tray_icon']! as i1.GeneratedColumn; + i1.GeneratedColumn get systemTitleBar => + columnsByName['system_title_bar']! as i1.GeneratedColumn; + i1.GeneratedColumn get skipNonMusic => + columnsByName['skip_non_music']! as i1.GeneratedColumn; + i1.GeneratedColumn get closeBehavior => + columnsByName['close_behavior']! as i1.GeneratedColumn; + i1.GeneratedColumn get accentColorScheme => + columnsByName['accent_color_scheme']! as i1.GeneratedColumn; + i1.GeneratedColumn get layoutMode => + columnsByName['layout_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get locale => + columnsByName['locale']! as i1.GeneratedColumn; + i1.GeneratedColumn get market => + columnsByName['market']! as i1.GeneratedColumn; + i1.GeneratedColumn get searchMode => + columnsByName['search_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get downloadLocation => + columnsByName['download_location']! as i1.GeneratedColumn; + i1.GeneratedColumn get localLibraryLocation => + columnsByName['local_library_location']! as i1.GeneratedColumn; + i1.GeneratedColumn get pipedInstance => + columnsByName['piped_instance']! as i1.GeneratedColumn; + i1.GeneratedColumn get invidiousInstance => + columnsByName['invidious_instance']! as i1.GeneratedColumn; + i1.GeneratedColumn get themeMode => + columnsByName['theme_mode']! as i1.GeneratedColumn; + i1.GeneratedColumn get audioSource => + columnsByName['audio_source']! as i1.GeneratedColumn; + i1.GeneratedColumn get youtubeClientEngine => + columnsByName['youtube_client_engine']! as i1.GeneratedColumn; + i1.GeneratedColumn get streamMusicCodec => + columnsByName['stream_music_codec']! as i1.GeneratedColumn; + i1.GeneratedColumn get downloadMusicCodec => + columnsByName['download_music_codec']! as i1.GeneratedColumn; + i1.GeneratedColumn get discordPresence => + columnsByName['discord_presence']! as i1.GeneratedColumn; + i1.GeneratedColumn get endlessPlayback => + columnsByName['endless_playback']! as i1.GeneratedColumn; + i1.GeneratedColumn get enableConnect => + columnsByName['enable_connect']! as i1.GeneratedColumn; + i1.GeneratedColumn get cacheMusic => + columnsByName['cache_music']! as i1.GeneratedColumn; +} + +i1.GeneratedColumn _column_54(String aliasedName) => + i1.GeneratedColumn('youtube_client_engine', aliasedName, false, + type: i1.DriftSqlType.string, + defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name)); i0.MigrationStepWithVersion migrationSteps({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, + required Future Function(i1.Migrator m, Schema4 schema) from3To4, }) { return (currentVersion, database) async { switch (currentVersion) { @@ -923,6 +1205,11 @@ i0.MigrationStepWithVersion migrationSteps({ final migrator = i1.Migrator(database, schema); await from2To3(migrator, schema); return 3; + case 3: + final schema = Schema4(database: database); + final migrator = i1.Migrator(database, schema); + await from3To4(migrator, schema); + return 4; default: throw ArgumentError.value('Unknown migration from $currentVersion'); } @@ -932,9 +1219,11 @@ i0.MigrationStepWithVersion migrationSteps({ i1.OnUpgrade stepByStep({ required Future Function(i1.Migrator m, Schema2 schema) from1To2, required Future Function(i1.Migrator m, Schema3 schema) from2To3, + required Future Function(i1.Migrator m, Schema4 schema) from3To4, }) => i0.VersionedSchema.stepByStepHelper( step: migrationSteps( from1To2: from1To2, from2To3: from2To3, + from3To4: from3To4, )); diff --git a/lib/models/database/tables/preferences.dart b/lib/models/database/tables/preferences.dart index c3904c84..111b2249 100644 --- a/lib/models/database/tables/preferences.dart +++ b/lib/models/database/tables/preferences.dart @@ -20,6 +20,26 @@ enum AudioSource { String get label => name[0].toUpperCase() + name.substring(1); } +enum YoutubeClientEngine { + ytDlp("yt-dlp"), + youtubeExplode("YouTubeExplode"), + newPipe("NewPipe"); + + final String label; + + const YoutubeClientEngine(this.label); + + bool isAvailableForPlatform() { + return switch (this) { + YoutubeClientEngine.youtubeExplode => + YouTubeExplodeEngine.isAvailableForPlatform, + YoutubeClientEngine.ytDlp => YtDlpEngine.isAvailableForPlatform, + // TODO: Implement new pipe support + YoutubeClientEngine.newPipe => false, + }; + } +} + enum MusicCodec { m4a._("M4a (Best for downloaded music)"), weba._("WebA (Best for streamed music)\nDoesn't support audio metadata"); @@ -84,6 +104,8 @@ class PreferencesTable extends Table { textEnum().withDefault(Constant(ThemeMode.system.name))(); TextColumn get audioSource => textEnum().withDefault(Constant(AudioSource.youtube.name))(); + TextColumn get youtubeClientEngine => textEnum() + .withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))(); TextColumn get streamMusicCodec => textEnum().withDefault(Constant(SourceCodecs.weba.name))(); TextColumn get downloadMusicCodec => @@ -120,6 +142,7 @@ class PreferencesTable extends Table { invidiousInstance: "https://inv.nadeko.net", themeMode: ThemeMode.system, audioSource: AudioSource.youtube, + youtubeClientEngine: YoutubeClientEngine.youtubeExplode, streamMusicCodec: SourceCodecs.m4a, downloadMusicCodec: SourceCodecs.m4a, discordPresence: true, diff --git a/lib/modules/player/sibling_tracks_sheet.dart b/lib/modules/player/sibling_tracks_sheet.dart index 6f9763b6..ccc1bfcd 100644 --- a/lib/modules/player/sibling_tracks_sheet.dart +++ b/lib/modules/player/sibling_tracks_sheet.dart @@ -19,6 +19,7 @@ import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/querying_track_info.dart'; import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/provider/youtube_engine/youtube_engine.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'; @@ -68,6 +69,7 @@ class SiblingTracksSheet extends HookConsumerWidget { final playlist = ref.watch(audioPlayerProvider); final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider); final preferences = ref.watch(userPreferencesProvider); + final youtubeEngine = ref.watch(youtubeEngineProvider); final isSearching = useState(false); final searchMode = useState(preferences.searchMode); @@ -115,14 +117,14 @@ class SiblingTracksSheet extends HookConsumerWidget { activeSourceInfo, ); } else { - final resultsYt = await youtubeClient.search.search(searchTerm.trim()); + final resultsYt = await youtubeEngine.searchVideos(searchTerm.trim()); final searchResults = await Future.wait( resultsYt .map(YoutubeVideoInfo.fromVideo) .mapIndexed((i, video) async { final siblingType = - await YoutubeSourcedTrack.toSiblingType(i, video); + await YoutubeSourcedTrack.toSiblingType(i, video, ref); return siblingType.info; }), ); @@ -139,6 +141,7 @@ class SiblingTracksSheet extends HookConsumerWidget { searchMode.value, activeTrack, preferences.audioSource, + youtubeEngine, ]); final siblings = useMemoized( @@ -151,12 +154,15 @@ class SiblingTracksSheet extends HookConsumerWidget { [activeTrack, isFetchingActiveTrack], ); + final previousActiveTrack = usePrevious(activeTrack); useEffect(() { + /// Populate sibling when active track changes + if (previousActiveTrack?.id == activeTrack?.id) return; if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) { activeTrackNotifier.populateSibling(); } return null; - }, [activeTrack]); + }, [activeTrack, previousActiveTrack]); final itemBuilder = useCallback( (SourceInfo sourceInfo) { diff --git a/lib/modules/settings/youtube_engine_not_installed_dialog.dart b/lib/modules/settings/youtube_engine_not_installed_dialog.dart new file mode 100644 index 00000000..7165f56b --- /dev/null +++ b/lib/modules/settings/youtube_engine_not_installed_dialog.dart @@ -0,0 +1,65 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/collections/spotube_icons.dart'; +import 'package:spotube/extensions/context.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:url_launcher/url_launcher.dart'; + +const engineDownloadUrls = { + YoutubeClientEngine.ytDlp: + "https://github.com/yt-dlp/yt-dlp?tab=readme-ov-file#installation", +}; + +class YouTubeEngineNotInstalledDialog extends HookConsumerWidget { + final YoutubeClientEngine engine; + const YouTubeEngineNotInstalledDialog({ + super.key, + required this.engine, + }); + + @override + Widget build(BuildContext context, ref) { + return AlertDialog( + title: Row( + spacing: 8, + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(SpotubeIcons.error, color: Colors.red), + Text( + context.l10n.youtube_engine_not_installed_title(engine.label), + style: const TextStyle(color: Colors.red), + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 10, + children: [ + Text( + context.l10n.youtube_engine_not_installed_message(engine.label), + ), + if (engineDownloadUrls[engine] != null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("${context.l10n.download}:"), + Button.link( + child: Text(engineDownloadUrls[engine]!.split("?").first), + onPressed: () async { + launchUrl(Uri.parse(engineDownloadUrls[engine]!)); + }, + ), + ], + ), + ], + ), + actions: [ + Button.secondary( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.l10n.ok), + ), + ], + ); + } +} diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 43e8fdcb..e2b64b1e 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -3,6 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; +import 'package:spotube/hooks/configurators/use_check_yt_dlp_installed.dart'; import 'package:spotube/modules/root/bottom_player.dart'; import 'package:spotube/modules/root/sidebar/sidebar.dart'; import 'package:spotube/modules/root/spotube_navigation_bar.dart'; @@ -21,9 +22,11 @@ class RootAppPage extends HookConsumerWidget { final brightness = Theme.of(context).brightness; ref.listen(glanceProvider, (_, __) {}); + useGlobalSubscriptions(ref); useDownloaderDialogs(ref); useEndlessPlayback(ref); + useCheckYtDlpInstalled(ref); useEffect(() { SystemChrome.setSystemUIOverlayStyle( diff --git a/lib/pages/settings/sections/playback.dart b/lib/pages/settings/sections/playback.dart index 363e228c..7c35a843 100644 --- a/lib/pages/settings/sections/playback.dart +++ b/lib/pages/settings/sections/playback.dart @@ -13,11 +13,13 @@ import 'package:spotube/models/database/database.dart'; import 'package:spotube/modules/settings/section_card_with_heading.dart'; import 'package:spotube/components/adaptive/adaptive_select_tile.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart'; import 'package:spotube/provider/audio_player/sources/invidious_instances_provider.dart'; import 'package:spotube/provider/audio_player/sources/piped_instances_provider.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/sourced_track/enums.dart'; +import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; import 'package:spotube/utils/platform.dart'; class SettingsPlaybackSection extends HookConsumerWidget { @@ -195,28 +197,52 @@ class SettingsPlaybackSection extends HookConsumerWidget { }, ), ), - AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - crossFadeState: preferences.audioSource == AudioSource.youtube - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: const SizedBox.shrink(), - secondChild: AdaptiveSelectTile( - secondary: const Icon(SpotubeIcons.search), - title: Text(context.l10n.search_mode), - value: preferences.searchMode, - options: SearchMode.values - .map((e) => SelectItemButton( - value: e, - child: Text(e.label), - )) - .toList(), - onChanged: (value) { - if (value == null) return; - preferencesNotifier.setSearchMode(value); - }, - ), - ), + switch (preferences.audioSource) { + AudioSource.youtube => AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.engine), + title: Text(context.l10n.youtube_engine), + value: preferences.youtubeClientEngine, + options: YoutubeClientEngine.values + .where((e) => e.isAvailableForPlatform()) + .map((e) => SelectItemButton( + value: e, + child: Text(e.label), + )) + .toList(), + onChanged: (value) async { + if (value == null) return; + if (value == YoutubeClientEngine.ytDlp && + !await YtDlpEngine.isInstalled() && + context.mounted) { + await showDialog( + context: context, + builder: (context) => + YouTubeEngineNotInstalledDialog(engine: value), + ); + return; + } + preferencesNotifier.setYoutubeClientEngine(value); + }, + ), + AudioSource.piped || + AudioSource.invidious => + AdaptiveSelectTile( + secondary: const Icon(SpotubeIcons.search), + title: Text(context.l10n.search_mode), + value: preferences.searchMode, + options: SearchMode.values + .map((e) => SelectItemButton( + value: e, + child: Text(e.label), + )) + .toList(), + onChanged: (value) { + if (value == null) return; + preferencesNotifier.setSearchMode(value); + }, + ), + _ => const SizedBox.shrink(), + }, AnimatedCrossFade( duration: const Duration(milliseconds: 300), crossFadeState: preferences.searchMode == SearchMode.youtube && diff --git a/lib/provider/user_preferences/user_preferences_provider.dart b/lib/provider/user_preferences/user_preferences_provider.dart index eeb712e1..75234241 100644 --- a/lib/provider/user_preferences/user_preferences_provider.dart +++ b/lib/provider/user_preferences/user_preferences_provider.dart @@ -207,6 +207,10 @@ class UserPreferencesNotifier extends Notifier { setData(PreferencesTableCompanion(audioSource: Value(type))); } + void setYoutubeClientEngine(YoutubeClientEngine engine) { + setData(PreferencesTableCompanion(youtubeClientEngine: Value(engine))); + } + void setSystemTitleBar(bool isSystemTitleBar) { setData( PreferencesTableCompanion( diff --git a/lib/provider/youtube_engine/youtube_engine.dart b/lib/provider/youtube_engine/youtube_engine.dart new file mode 100644 index 00000000..fbac7afe --- /dev/null +++ b/lib/provider/youtube_engine/youtube_engine.dart @@ -0,0 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:spotube/models/database/database.dart'; +import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; +import 'package:spotube/services/youtube_engine/youtube_explode_engine.dart'; +import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart'; + +final youtubeEngineProvider = Provider((ref) { + final engineMode = ref.watch( + userPreferencesProvider.select((value) => value.youtubeClientEngine), + ); + + if (engineMode == YoutubeClientEngine.newPipe) { + throw UnimplementedError(); + } else if (engineMode == YoutubeClientEngine.ytDlp && + YtDlpEngine.isAvailableForPlatform) { + return YtDlpEngine(); + } else { + return YouTubeExplodeEngine(); + } +}); diff --git a/lib/services/sourced_track/sources/youtube.dart b/lib/services/sourced_track/sources/youtube.dart index f54b1772..e9f12bf7 100644 --- a/lib/services/sourced_track/sources/youtube.dart +++ b/lib/services/sourced_track/sources/youtube.dart @@ -4,6 +4,7 @@ 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/youtube_engine/youtube_engine.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:spotube/services/song_link/song_link.dart'; import 'package:spotube/services/sourced_track/enums.dart'; @@ -15,7 +16,6 @@ import 'package:spotube/services/sourced_track/sourced_track.dart'; import 'package:spotube/utils/service_utils.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; -final youtubeClient = YoutubeExplode(); final officialMusicRegex = RegExp( r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)", caseSensitive: false, @@ -43,24 +43,15 @@ class YoutubeSourcedTrack extends SourcedTrack { required super.ref, }); - static Future _getStreamManifest(String id) async { - return youtubeClient.videos.streamsClient.getManifest( - id, - requireWatchPage: false, - ytClients: [ - YoutubeApiClient.android, - YoutubeApiClient.mweb, - ], - ); - } - static Future fetchFromTrack({ required Track track, required Ref ref, }) async { // Indicates the track is requesting a stream refresh if (track is YoutubeSourcedTrack) { - final manifest = await _getStreamManifest(track.sourceInfo.id); + final manifest = await ref + .read(youtubeEngineProvider) + .getStreamManifest(track.sourceInfo.id); final sourcedTrack = YoutubeSourcedTrack( ref: ref, @@ -108,8 +99,10 @@ class YoutubeSourcedTrack extends SourcedTrack { track: track, ); } - final item = await youtubeClient.videos.get(cachedSource.sourceId); - final manifest = await _getStreamManifest(cachedSource.sourceId); + final (item, manifest) = await ref + .read(youtubeEngineProvider) + .getVideoWithStreamInfo(cachedSource.sourceId); + final sourcedTrack = YoutubeSourcedTrack( ref: ref, siblings: [], @@ -162,10 +155,13 @@ class YoutubeSourcedTrack extends SourcedTrack { static Future toSiblingType( int index, YoutubeVideoInfo item, + dynamic ref, ) async { + assert(ref is WidgetRef || ref is Ref, "Invalid ref type"); SourceMap? sourceMap; if (index == 0) { - final manifest = await _getStreamManifest(item.id); + final manifest = + await ref.read(youtubeEngineProvider).getStreamManifest(item.id); sourceMap = toSourceMap(manifest); } @@ -188,11 +184,8 @@ class YoutubeSourcedTrack extends SourcedTrack { static List rankResults( List results, Track track) { - final artists = (track.artists ?? []) - .map((ar) => ar.name) - .toList() - .whereNotNull() - .toList(); + final artists = + (track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList(); return results .sorted((a, b) => b.views.compareTo(a.views)) @@ -259,8 +252,9 @@ class YoutubeSourcedTrack extends SourcedTrack { await toSiblingType( 0, YoutubeVideoInfo.fromVideo( - await youtubeClient.videos.get(ytLink!.url!), + await ref.read(youtubeEngineProvider).getVideo(ytLink!.url!), ), + ref, ) ]; } on VideoUnplayableException catch (e, stack) { @@ -271,15 +265,13 @@ class YoutubeSourcedTrack extends SourcedTrack { final query = SourcedTrack.getSearchTerm(track); - final searchResults = await youtubeClient.search.search( - "$query - Topic", - filter: TypeFilters.video, - ); + final searchResults = + await ref.read(youtubeEngineProvider).searchVideos("$query - Topic"); if (ServiceUtils.onlyContainsEnglish(query)) { return await Future.wait(searchResults .map(YoutubeVideoInfo.fromVideo) - .mapIndexed(toSiblingType)); + .mapIndexed((index, info) => toSiblingType(index, info, ref))); } final rankedSiblings = rankResults( @@ -287,7 +279,10 @@ class YoutubeSourcedTrack extends SourcedTrack { track, ); - return await Future.wait(rankedSiblings.mapIndexed(toSiblingType)); + return await Future.wait( + rankedSiblings + .mapIndexed((index, info) => toSiblingType(index, info, ref)), + ); } @override @@ -305,7 +300,9 @@ class YoutubeSourcedTrack extends SourcedTrack { final newSiblings = siblings.where((s) => s.id != sibling.id).toList() ..insert(0, sourceInfo); - final manifest = await _getStreamManifest(newSourceInfo.id); + final manifest = await ref + .read(youtubeEngineProvider) + .getStreamManifest(newSourceInfo.id); final database = ref.read(databaseProvider); diff --git a/lib/services/youtube_engine/youtube_engine.dart b/lib/services/youtube_engine/youtube_engine.dart new file mode 100644 index 00000000..5a8ffa4e --- /dev/null +++ b/lib/services/youtube_engine/youtube_engine.dart @@ -0,0 +1,14 @@ +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; + +abstract interface class YouTubeEngine { + static bool get isAvailableForPlatform => false; + + static Future isInstalled() async { + return false; + } + + Future