mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: add youtube engine abstraction and yt-dlp integration
This commit is contained in:
parent
698fb6ba27
commit
d6726bc3b0
1
drift_schemas/app_db/drift_schema_v4.json
Normal file
1
drift_schemas/app_db/drift_schema_v4.json
Normal file
File diff suppressed because one or more lines are too long
@ -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;
|
||||
}
|
||||
|
33
lib/hooks/configurators/use_check_yt_dlp_installed.dart
Normal file
33
lib/hooks/configurators/use_check_yt_dlp_installed.dart
Normal file
@ -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;
|
||||
}, []);
|
||||
}
|
@ -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"
|
||||
}
|
@ -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<void> main(List<String> rawArgs) async {
|
||||
if (rawArgs.contains("web_view_title_bar")) {
|
||||
@ -79,15 +80,15 @@ Future<void> main(List<String> 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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -760,6 +760,17 @@ class $PreferencesTableTable extends PreferencesTable
|
||||
defaultValue: Constant(AudioSource.youtube.name))
|
||||
.withConverter<AudioSource>(
|
||||
$PreferencesTableTable.$converteraudioSource);
|
||||
static const VerificationMeta _youtubeClientEngineMeta =
|
||||
const VerificationMeta('youtubeClientEngine');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<YoutubeClientEngine, String>
|
||||
youtubeClientEngine = GeneratedColumn<String>(
|
||||
'youtube_client_engine', aliasedName, false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name))
|
||||
.withConverter<YoutubeClientEngine>(
|
||||
$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>(ThemeMode.values);
|
||||
static JsonTypeConverter2<AudioSource, String, String> $converteraudioSource =
|
||||
const EnumNameConverter<AudioSource>(AudioSource.values);
|
||||
static JsonTypeConverter2<YoutubeClientEngine, String, String>
|
||||
$converteryoutubeClientEngine =
|
||||
const EnumNameConverter<YoutubeClientEngine>(YoutubeClientEngine.values);
|
||||
static JsonTypeConverter2<SourceCodecs, String, String>
|
||||
$converterstreamMusicCodec =
|
||||
const EnumNameConverter<SourceCodecs>(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<String>(
|
||||
$PreferencesTableTable.$converteraudioSource.toSql(audioSource));
|
||||
}
|
||||
{
|
||||
map['youtube_client_engine'] = Variable<String>($PreferencesTableTable
|
||||
.$converteryoutubeClientEngine
|
||||
.toSql(youtubeClientEngine));
|
||||
}
|
||||
{
|
||||
map['stream_music_codec'] = Variable<String>($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<String>(json['themeMode'])),
|
||||
audioSource: $PreferencesTableTable.$converteraudioSource
|
||||
.fromJson(serializer.fromJson<String>(json['audioSource'])),
|
||||
youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine
|
||||
.fromJson(serializer.fromJson<String>(json['youtubeClientEngine'])),
|
||||
streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec
|
||||
.fromJson(serializer.fromJson<String>(json['streamMusicCodec'])),
|
||||
downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec
|
||||
@ -1316,6 +1346,9 @@ class PreferencesTableData extends DataClass
|
||||
$PreferencesTableTable.$converterthemeMode.toJson(themeMode)),
|
||||
'audioSource': serializer.toJson<String>(
|
||||
$PreferencesTableTable.$converteraudioSource.toJson(audioSource)),
|
||||
'youtubeClientEngine': serializer.toJson<String>($PreferencesTableTable
|
||||
.$converteryoutubeClientEngine
|
||||
.toJson(youtubeClientEngine)),
|
||||
'streamMusicCodec': serializer.toJson<String>($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<PreferencesTableData> {
|
||||
final Value<String> invidiousInstance;
|
||||
final Value<ThemeMode> themeMode;
|
||||
final Value<AudioSource> audioSource;
|
||||
final Value<YoutubeClientEngine> youtubeClientEngine;
|
||||
final Value<SourceCodecs> streamMusicCodec;
|
||||
final Value<SourceCodecs> downloadMusicCodec;
|
||||
final Value<bool> discordPresence;
|
||||
@ -1606,6 +1648,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
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<PreferencesTableData> {
|
||||
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<PreferencesTableData> {
|
||||
Expression<String>? invidiousInstance,
|
||||
Expression<String>? themeMode,
|
||||
Expression<String>? audioSource,
|
||||
Expression<String>? youtubeClientEngine,
|
||||
Expression<String>? streamMusicCodec,
|
||||
Expression<String>? downloadMusicCodec,
|
||||
Expression<bool>? discordPresence,
|
||||
@ -1695,6 +1740,8 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
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<PreferencesTableData> {
|
||||
Value<String>? invidiousInstance,
|
||||
Value<ThemeMode>? themeMode,
|
||||
Value<AudioSource>? audioSource,
|
||||
Value<YoutubeClientEngine>? youtubeClientEngine,
|
||||
Value<SourceCodecs>? streamMusicCodec,
|
||||
Value<SourceCodecs>? downloadMusicCodec,
|
||||
Value<bool>? discordPresence,
|
||||
@ -1755,6 +1803,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
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<PreferencesTableData> {
|
||||
.$converteraudioSource
|
||||
.toSql(audioSource.value));
|
||||
}
|
||||
if (youtubeClientEngine.present) {
|
||||
map['youtube_client_engine'] = Variable<String>($PreferencesTableTable
|
||||
.$converteryoutubeClientEngine
|
||||
.toSql(youtubeClientEngine.value));
|
||||
}
|
||||
if (streamMusicCodec.present) {
|
||||
map['stream_music_codec'] = Variable<String>($PreferencesTableTable
|
||||
.$converterstreamMusicCodec
|
||||
@ -1894,6 +1948,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
..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<String> invidiousInstance,
|
||||
Value<ThemeMode> themeMode,
|
||||
Value<AudioSource> audioSource,
|
||||
Value<YoutubeClientEngine> youtubeClientEngine,
|
||||
Value<SourceCodecs> streamMusicCodec,
|
||||
Value<SourceCodecs> downloadMusicCodec,
|
||||
Value<bool> discordPresence,
|
||||
@ -4595,6 +4651,7 @@ typedef $$PreferencesTableTableUpdateCompanionBuilder
|
||||
Value<String> invidiousInstance,
|
||||
Value<ThemeMode> themeMode,
|
||||
Value<AudioSource> audioSource,
|
||||
Value<YoutubeClientEngine> youtubeClientEngine,
|
||||
Value<SourceCodecs> streamMusicCodec,
|
||||
Value<SourceCodecs> downloadMusicCodec,
|
||||
Value<bool> discordPresence,
|
||||
@ -4702,6 +4759,12 @@ class $$PreferencesTableTableFilterComposer
|
||||
column: $table.audioSource,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<YoutubeClientEngine, YoutubeClientEngine,
|
||||
String>
|
||||
get youtubeClientEngine => $composableBuilder(
|
||||
column: $table.youtubeClientEngine,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<SourceCodecs, SourceCodecs, String>
|
||||
get streamMusicCodec => $composableBuilder(
|
||||
column: $table.streamMusicCodec,
|
||||
@ -4812,6 +4875,10 @@ class $$PreferencesTableTableOrderingComposer
|
||||
ColumnOrderings<String> get audioSource => $composableBuilder(
|
||||
column: $table.audioSource, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get youtubeClientEngine => $composableBuilder(
|
||||
column: $table.youtubeClientEngine,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get streamMusicCodec => $composableBuilder(
|
||||
column: $table.streamMusicCodec,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
@ -4915,6 +4982,10 @@ class $$PreferencesTableTableAnnotationComposer
|
||||
$composableBuilder(
|
||||
column: $table.audioSource, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<YoutubeClientEngine, String>
|
||||
get youtubeClientEngine => $composableBuilder(
|
||||
column: $table.youtubeClientEngine, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<SourceCodecs, String> get streamMusicCodec =>
|
||||
$composableBuilder(
|
||||
column: $table.streamMusicCodec, builder: (column) => column);
|
||||
@ -4985,6 +5056,8 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
||||
Value<String> invidiousInstance = const Value.absent(),
|
||||
Value<ThemeMode> themeMode = const Value.absent(),
|
||||
Value<AudioSource> audioSource = const Value.absent(),
|
||||
Value<YoutubeClientEngine> youtubeClientEngine =
|
||||
const Value.absent(),
|
||||
Value<SourceCodecs> streamMusicCodec = const Value.absent(),
|
||||
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
||||
Value<bool> 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<String> invidiousInstance = const Value.absent(),
|
||||
Value<ThemeMode> themeMode = const Value.absent(),
|
||||
Value<AudioSource> audioSource = const Value.absent(),
|
||||
Value<YoutubeClientEngine> youtubeClientEngine =
|
||||
const Value.absent(),
|
||||
Value<SourceCodecs> streamMusicCodec = const Value.absent(),
|
||||
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
||||
Value<bool> 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,
|
||||
|
@ -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<bool> _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<i1.DatabaseSchemaEntity> 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<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get audioQuality =>
|
||||
columnsByName['audio_quality']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get albumColorSync =>
|
||||
columnsByName['album_color_sync']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get amoledDarkTheme =>
|
||||
columnsByName['amoled_dark_theme']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get checkUpdate =>
|
||||
columnsByName['check_update']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get normalizeAudio =>
|
||||
columnsByName['normalize_audio']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get showSystemTrayIcon =>
|
||||
columnsByName['show_system_tray_icon']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get systemTitleBar =>
|
||||
columnsByName['system_title_bar']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get skipNonMusic =>
|
||||
columnsByName['skip_non_music']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<String> get closeBehavior =>
|
||||
columnsByName['close_behavior']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get accentColorScheme =>
|
||||
columnsByName['accent_color_scheme']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get layoutMode =>
|
||||
columnsByName['layout_mode']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get locale =>
|
||||
columnsByName['locale']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get market =>
|
||||
columnsByName['market']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get searchMode =>
|
||||
columnsByName['search_mode']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get downloadLocation =>
|
||||
columnsByName['download_location']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get localLibraryLocation =>
|
||||
columnsByName['local_library_location']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get pipedInstance =>
|
||||
columnsByName['piped_instance']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get invidiousInstance =>
|
||||
columnsByName['invidious_instance']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get themeMode =>
|
||||
columnsByName['theme_mode']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get audioSource =>
|
||||
columnsByName['audio_source']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get youtubeClientEngine =>
|
||||
columnsByName['youtube_client_engine']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get streamMusicCodec =>
|
||||
columnsByName['stream_music_codec']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get downloadMusicCodec =>
|
||||
columnsByName['download_music_codec']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get discordPresence =>
|
||||
columnsByName['discord_presence']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get endlessPlayback =>
|
||||
columnsByName['endless_playback']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get enableConnect =>
|
||||
columnsByName['enable_connect']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get cacheMusic =>
|
||||
columnsByName['cache_music']! as i1.GeneratedColumn<bool>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_54(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('youtube_client_engine', aliasedName, false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name));
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> 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<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
from2To3: from2To3,
|
||||
from3To4: from3To4,
|
||||
));
|
||||
|
@ -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<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
|
||||
TextColumn get audioSource =>
|
||||
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
|
||||
TextColumn get youtubeClientEngine => textEnum<YoutubeClientEngine>()
|
||||
.withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))();
|
||||
TextColumn get streamMusicCodec =>
|
||||
textEnum<SourceCodecs>().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,
|
||||
|
@ -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) {
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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<SearchMode>(
|
||||
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<YoutubeClientEngine>(
|
||||
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<SearchMode>(
|
||||
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 &&
|
||||
|
@ -207,6 +207,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
||||
setData(PreferencesTableCompanion(audioSource: Value(type)));
|
||||
}
|
||||
|
||||
void setYoutubeClientEngine(YoutubeClientEngine engine) {
|
||||
setData(PreferencesTableCompanion(youtubeClientEngine: Value(engine)));
|
||||
}
|
||||
|
||||
void setSystemTitleBar(bool isSystemTitleBar) {
|
||||
setData(
|
||||
PreferencesTableCompanion(
|
||||
|
20
lib/provider/youtube_engine/youtube_engine.dart
Normal file
20
lib/provider/youtube_engine/youtube_engine.dart
Normal file
@ -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();
|
||||
}
|
||||
});
|
@ -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<StreamManifest> _getStreamManifest(String id) async {
|
||||
return youtubeClient.videos.streamsClient.getManifest(
|
||||
id,
|
||||
requireWatchPage: false,
|
||||
ytClients: [
|
||||
YoutubeApiClient.android,
|
||||
YoutubeApiClient.mweb,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<YoutubeSourcedTrack> 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<SiblingType> 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<YoutubeVideoInfo> rankResults(
|
||||
List<YoutubeVideoInfo> 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);
|
||||
|
||||
|
14
lib/services/youtube_engine/youtube_engine.dart
Normal file
14
lib/services/youtube_engine/youtube_engine.dart
Normal file
@ -0,0 +1,14 @@
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
abstract interface class YouTubeEngine {
|
||||
static bool get isAvailableForPlatform => false;
|
||||
|
||||
static Future<bool> isInstalled() async {
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<Video> getVideo(String videoId);
|
||||
Future<StreamManifest> getStreamManifest(String videoId);
|
||||
Future<(Video, StreamManifest)> getVideoWithStreamInfo(String videoId);
|
||||
Future<List<Video>> searchVideos(String query);
|
||||
}
|
47
lib/services/youtube_engine/youtube_explode_engine.dart
Normal file
47
lib/services/youtube_engine/youtube_explode_engine.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:spotube/services/youtube_engine/youtube_engine.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
class YouTubeExplodeEngine implements YouTubeEngine {
|
||||
static final YoutubeExplode _youtubeExplode = YoutubeExplode();
|
||||
|
||||
static bool get isAvailableForPlatform => true;
|
||||
|
||||
static Future<bool> isInstalled() async {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StreamManifest> getStreamManifest(String videoId) {
|
||||
return _youtubeExplode.videos.streamsClient.getManifest(
|
||||
videoId,
|
||||
requireWatchPage: false,
|
||||
ytClients: [
|
||||
YoutubeApiClient.android,
|
||||
YoutubeApiClient.mweb,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Video> getVideo(String videoId) {
|
||||
return _youtubeExplode.videos.get(videoId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<(Video, StreamManifest)> getVideoWithStreamInfo(String videoId) async {
|
||||
final video = await getVideo(videoId);
|
||||
final streamManifest = await getStreamManifest(videoId);
|
||||
|
||||
return (video, streamManifest);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Video>> searchVideos(String query) {
|
||||
return _youtubeExplode.search
|
||||
.search(
|
||||
query,
|
||||
filter: TypeFilters.video,
|
||||
)
|
||||
.then((searchList) => searchList.toList());
|
||||
}
|
||||
}
|
149
lib/services/youtube_engine/yt_dlp_engine.dart
Normal file
149
lib/services/youtube_engine/yt_dlp_engine.dart
Normal file
@ -0,0 +1,149 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:spotube/services/youtube_engine/youtube_engine.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
import 'package:yt_dlp_dart/yt_dlp_dart.dart';
|
||||
// ignore: depend_on_referenced_packages
|
||||
import 'package:http_parser/http_parser.dart';
|
||||
|
||||
class YtDlpEngine implements YouTubeEngine {
|
||||
StreamManifest _parseFormats(List formats, videoId) {
|
||||
final audioOnlyStreams = formats
|
||||
.where(
|
||||
(f) => f["resolution"] == "audio only" && f["manifest_url"] == null,
|
||||
)
|
||||
.sorted((a, b) => a["quality"] > b["quality"] ? 1 : -1)
|
||||
.map((f) {
|
||||
final filesize = f["filesize"] ?? f["filesize_approx"];
|
||||
return AudioOnlyStreamInfo(
|
||||
VideoId(videoId),
|
||||
0,
|
||||
Uri.parse(f["url"]),
|
||||
StreamContainer.parse(
|
||||
f["container"]?.replaceAll("_dash", "").replaceAll("m4a", "mp4"),
|
||||
),
|
||||
filesize != null ? FileSize(filesize) : FileSize.unknown,
|
||||
Bitrate(
|
||||
(((f["abr"] ?? f["tbr"] ?? 0) * 1000) as num).toInt(),
|
||||
),
|
||||
f["acodec"] ?? "webm",
|
||||
f["format_note"],
|
||||
[],
|
||||
MediaType.parse(
|
||||
"audio/${f["audio_ext"]}",
|
||||
),
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
return StreamManifest(audioOnlyStreams);
|
||||
}
|
||||
|
||||
Video _parseInfo(Map<String, dynamic> info) {
|
||||
final publishDate = info["upload_date"] != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
int.parse(info["upload_date"]) * 1000,
|
||||
)
|
||||
: DateTime.now();
|
||||
return Video(
|
||||
VideoId(info["id"]),
|
||||
info["title"],
|
||||
info["channel"],
|
||||
ChannelId(info["channel_id"]),
|
||||
publishDate,
|
||||
info["upload_date"] as String? ?? DateTime.now().toString(),
|
||||
publishDate,
|
||||
info["description"] ?? "",
|
||||
Duration(seconds: (info["duration"] as num).toInt()),
|
||||
ThumbnailSet(info["id"]),
|
||||
info["tags"]?.cast<String>() ?? <String>[],
|
||||
Engagement(
|
||||
info["view_count"],
|
||||
info["like_count"],
|
||||
null,
|
||||
),
|
||||
info["is_live"] ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
static bool get isAvailableForPlatform => kIsDesktop;
|
||||
|
||||
static Future<bool> isInstalled() async {
|
||||
return isAvailableForPlatform &&
|
||||
await YtDlp.instance.checkAvailableInPath();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<StreamManifest> getStreamManifest(String videoId) async {
|
||||
final formats = await YtDlp.instance.extractInfo(
|
||||
"https://www.youtube.com/watch?v=$videoId",
|
||||
formatSpecifiers: "%(formats)j",
|
||||
extraArgs: [
|
||||
"--no-check-certificate",
|
||||
"--geo-bypass",
|
||||
"--quiet",
|
||||
"--ignore-errors"
|
||||
],
|
||||
) as List;
|
||||
|
||||
return _parseFormats(formats, videoId);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Video> getVideo(String videoId) async {
|
||||
final info = await YtDlp.instance.extractInfo(
|
||||
"https://www.youtube.com/watch?v=$videoId",
|
||||
formatSpecifiers: "%()j",
|
||||
extraArgs: [
|
||||
"--skip-download",
|
||||
"--no-check-certificate",
|
||||
"--geo-bypass",
|
||||
"--quiet",
|
||||
"--ignore-errors",
|
||||
],
|
||||
) as Map<String, dynamic>;
|
||||
|
||||
return _parseInfo(info);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<(Video, StreamManifest)> getVideoWithStreamInfo(String videoId) async {
|
||||
final info = await YtDlp.instance.extractInfo(
|
||||
"https://www.youtube.com/watch?v=$videoId",
|
||||
formatSpecifiers: "%()j",
|
||||
extraArgs: [
|
||||
"--no-check-certificate",
|
||||
"--geo-bypass",
|
||||
"--quiet",
|
||||
"--ignore-errors",
|
||||
],
|
||||
) as Map<String, dynamic>;
|
||||
|
||||
return (_parseInfo(info), _parseFormats(info["formats"], videoId));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Video>> searchVideos(String query) async {
|
||||
final stdout = await YtDlp.instance.extractInfoString(
|
||||
"ytsearch10:$query",
|
||||
formatSpecifiers: "%()j",
|
||||
extraArgs: [
|
||||
"--skip-download",
|
||||
"--no-check-certificate",
|
||||
"--geo-bypass",
|
||||
"--quiet",
|
||||
"--ignore-errors",
|
||||
"--flat-playlist",
|
||||
"--no-playlist",
|
||||
],
|
||||
);
|
||||
|
||||
final json = jsonDecode(
|
||||
"[${stdout.split("\n").where((s) => s.trim().isNotEmpty).join(",")}]",
|
||||
) as List;
|
||||
|
||||
return json.map((e) => _parseInfo(e)).toList();
|
||||
}
|
||||
}
|
@ -2734,6 +2734,13 @@ packages:
|
||||
url: "https://github.com/Hexer10/youtube_explode_dart.git"
|
||||
source: git
|
||||
version: "2.3.7"
|
||||
yt_dlp_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../yt_dlp_dart"
|
||||
relative: true
|
||||
source: path
|
||||
version: "1.0.0"
|
||||
sdks:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
dart: ">=3.6.1 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
@ -140,6 +140,10 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/Hexer10/youtube_explode_dart.git
|
||||
ref: e519db65ad0b0a40b12f69285932f9db509da3cf
|
||||
yt_dlp_dart:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/yt_dlp_dart.git
|
||||
ref: 4199bb019542bae361fbb38b3448b3583fbca022
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.13
|
||||
|
@ -3,6 +3,7 @@
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v4.dart' as v4;
|
||||
import 'schema_v3.dart' as v3;
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v1.dart' as v1;
|
||||
@ -11,6 +12,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 4:
|
||||
return v4.DatabaseAtV4(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
case 2:
|
||||
@ -22,5 +25,5 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2, 3];
|
||||
static const versions = const [1, 2, 3, 4];
|
||||
}
|
||||
|
3433
test/drift/app_db/generated/schema_v4.dart
Normal file
3433
test/drift/app_db/generated/schema_v4.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
@ -34,7 +38,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
@ -53,7 +61,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
@ -72,7 +84,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"de": [
|
||||
@ -91,7 +107,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"es": [
|
||||
@ -110,7 +130,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
@ -129,7 +153,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
@ -148,7 +176,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
@ -167,7 +199,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
@ -186,7 +222,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
@ -205,7 +245,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"id": [
|
||||
@ -224,7 +268,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"it": [
|
||||
@ -243,7 +291,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
@ -262,7 +314,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"ka": [
|
||||
@ -281,7 +337,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
@ -300,7 +360,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"ne": [
|
||||
@ -319,7 +383,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
@ -338,7 +406,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
@ -357,7 +429,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
@ -376,7 +452,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
@ -395,7 +475,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"th": [
|
||||
@ -414,7 +498,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
@ -433,7 +521,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
@ -452,7 +544,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
@ -471,7 +567,11 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
@ -490,6 +590,10 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"download"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user