mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: implement yt-dlp for desktop and NewPipeExtractor for Android (#2316)
* feat: add youtube engine abstraction and yt-dlp integration * chore: add yt-dlp as optional dependency * feat: implement custom path support for youtube engines * chore: check for custom path in setting engine select dropdown * chore: update yt_dlp_dart * chore: setting video url instead of video id in fetchSiblings * feat: implement NewPipe engine * chore: update local path to git url for flutter_new_pipe_extractor package * chore: fix android build isn't working * chore: fix routes not working when initially signing in * refactor: drop fallback support to different sources
This commit is contained in:
parent
698fb6ba27
commit
1e6d709e04
2
.github/workflows/pr-lint.yml
vendored
2
.github/workflows/pr-lint.yml
vendored
@ -4,7 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FLUTTER_VERSION: 3.27.0
|
FLUTTER_VERSION: 3.27.3
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
9
Makefile
9
Makefile
@ -45,4 +45,11 @@ gensums:
|
|||||||
sh -c scripts/gensums.sh
|
sh -c scripts/gensums.sh
|
||||||
|
|
||||||
migrate:
|
migrate:
|
||||||
dart run drift_dev make-migrations
|
dart run drift_dev make-migrations
|
||||||
|
|
||||||
|
dmg:
|
||||||
|
flutter build macos &&\
|
||||||
|
if [ -f dist/Spotube-macos-universal.dmg ];\
|
||||||
|
then rm dist/Spotube-macos-universal.dmg;\
|
||||||
|
fi &&\
|
||||||
|
appdmg appdmg.json dist/Spotube-macos-universal.dmg
|
@ -38,6 +38,7 @@ android {
|
|||||||
ndkVersion = "27.0.12077973"
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
coreLibraryDesugaringEnabled true
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_1_8
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@ -120,6 +121,8 @@ flutter {
|
|||||||
|
|
||||||
def glanceVersion = "1.1.1"
|
def glanceVersion = "1.1.1"
|
||||||
dependencies {
|
dependencies {
|
||||||
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||||
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||||
// other deps so just ignore
|
// other deps so just ignore
|
||||||
implementation 'com.android.support:multidex:2.0.1'
|
implementation 'com.android.support:multidex:2.0.1'
|
||||||
|
13
android/app/proguard-rules.pro
vendored
13
android/app/proguard-rules.pro
vendored
@ -6,3 +6,16 @@
|
|||||||
-keepclassmembers class ** {
|
-keepclassmembers class ** {
|
||||||
@kotlinx.serialization.* <fields>;
|
@kotlinx.serialization.* <fields>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## We don't need beans
|
||||||
|
-dontwarn java.beans.BeanDescriptor
|
||||||
|
-dontwarn java.beans.BeanInfo
|
||||||
|
-dontwarn java.beans.IntrospectionException
|
||||||
|
-dontwarn java.beans.Introspector
|
||||||
|
-dontwarn java.beans.PropertyDescriptor
|
||||||
|
|
||||||
|
## Rules for NewPipeExtractor
|
||||||
|
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
|
||||||
|
-keep class org.mozilla.javascript.** { *; }
|
||||||
|
-keep class org.mozilla.classfile.ClassFileWriter
|
||||||
|
-dontwarn org.mozilla.javascript.tools.**
|
@ -12,6 +12,7 @@ depends = jsoncpp
|
|||||||
depends = libnotify
|
depends = libnotify
|
||||||
depends = xdg-user-dirs
|
depends = xdg-user-dirs
|
||||||
depends = webkit2gtk-4.1
|
depends = webkit2gtk-4.1
|
||||||
|
optdepends = yt-dlp-git
|
||||||
source = https://github.com/KRTirtho/spotube/releases/download/v3.7.1/spotube-linux-3.7.1-x86_64.tar.xz
|
source = https://github.com/KRTirtho/spotube/releases/download/v3.7.1/spotube-linux-3.7.1-x86_64.tar.xz
|
||||||
md5sums = 475b1ae9b08f27743a4d4749391ae3db
|
md5sums = 475b1ae9b08f27743a4d4749391ae3db
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ groups=()
|
|||||||
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
||||||
makedepends=()
|
makedepends=()
|
||||||
checkdepends=()
|
checkdepends=()
|
||||||
optdepends=()
|
optdepends=('yt-dlp-git')
|
||||||
provides=()
|
provides=()
|
||||||
conflicts=()
|
conflicts=()
|
||||||
replaces=()
|
replaces=()
|
||||||
|
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
@ -14,21 +14,6 @@ class AppRouter extends RootStackRouter {
|
|||||||
|
|
||||||
AppRouter(this.ref) : super(navigatorKey: rootNavigatorKey);
|
AppRouter(this.ref) : super(navigatorKey: rootNavigatorKey);
|
||||||
|
|
||||||
@override
|
|
||||||
List<AutoRouteGuard> get guards => [
|
|
||||||
AutoRouteGuardCallback(
|
|
||||||
(resolver, router) async {
|
|
||||||
final auth = await ref.read(authenticationProvider.future);
|
|
||||||
|
|
||||||
if (auth == null && !KVStoreService.doneGettingStarted) {
|
|
||||||
resolver.redirect(const GettingStartedRoute());
|
|
||||||
} else {
|
|
||||||
resolver.next(true);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<AutoRoute> get routes => [
|
List<AutoRoute> get routes => [
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
@ -40,6 +25,19 @@ class AppRouter extends RootStackRouter {
|
|||||||
path: "home",
|
path: "home",
|
||||||
page: HomeRoute.page,
|
page: HomeRoute.page,
|
||||||
initial: true,
|
initial: true,
|
||||||
|
guards: [
|
||||||
|
AutoRouteGuardCallback(
|
||||||
|
(resolver, router) async {
|
||||||
|
final auth = await ref.read(authenticationProvider.future);
|
||||||
|
|
||||||
|
if (auth == null && !KVStoreService.doneGettingStarted) {
|
||||||
|
resolver.redirect(const GettingStartedRoute());
|
||||||
|
} else {
|
||||||
|
resolver.next(true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
path: "home/genres",
|
path: "home/genres",
|
||||||
|
@ -134,4 +134,5 @@ abstract class SpotubeIcons {
|
|||||||
static const grid = FeatherIcons.grid;
|
static const grid = FeatherIcons.grid;
|
||||||
static const list = FeatherIcons.list;
|
static const list = FeatherIcons.list;
|
||||||
static const device = FeatherIcons.smartphone;
|
static const device = FeatherIcons.smartphone;
|
||||||
|
static const engine = FeatherIcons.server;
|
||||||
}
|
}
|
||||||
|
40
lib/hooks/configurators/use_check_yt_dlp_installed.dart
Normal file
40
lib/hooks/configurators/use_check_yt_dlp_installed.dart
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
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/kv_store/kv_store.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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final customPath =
|
||||||
|
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp);
|
||||||
|
|
||||||
|
if (youtubeEngine == YoutubeClientEngine.ytDlp &&
|
||||||
|
!await YtDlpEngine.isInstalled() &&
|
||||||
|
(customPath == null || !await File(customPath).exists()) &&
|
||||||
|
context.mounted) {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
YouTubeEngineNotInstalledDialog(engine: youtubeEngine),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, []);
|
||||||
|
}
|
@ -415,5 +415,12 @@
|
|||||||
"no_tracks_listened_yet": "Looks like you haven't listened to anything yet",
|
"no_tracks_listened_yet": "Looks like you haven't listened to anything yet",
|
||||||
"not_following_artists": "You're not following any artists",
|
"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_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.",
|
||||||
|
"youtube_engine_set_path": "Make sure it's available in the PATH variable or\nset the absolute path to the {engine} executable below",
|
||||||
|
"youtube_engine_unix_issue_message": "In macOS/Linux/unix like OS's, setting path on .zshrc/.bashrc/.bash_profile etc. won't work.\nYou need to set the path in the shell configuration file",
|
||||||
|
"download": "Download",
|
||||||
|
"file_not_found": "File not found"
|
||||||
}
|
}
|
@ -50,6 +50,8 @@ import 'package:flutter_displaymode/flutter_displaymode.dart';
|
|||||||
import 'package:timezone/data/latest.dart' as tz;
|
import 'package:timezone/data/latest.dart' as tz;
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:yt_dlp_dart/yt_dlp_dart.dart';
|
||||||
|
import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart';
|
||||||
|
|
||||||
Future<void> main(List<String> rawArgs) async {
|
Future<void> main(List<String> rawArgs) async {
|
||||||
if (rawArgs.contains("web_view_title_bar")) {
|
if (rawArgs.contains("web_view_title_bar")) {
|
||||||
@ -77,17 +79,23 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
// force High Refresh Rate on some Android devices (like One Plus)
|
// force High Refresh Rate on some Android devices (like One Plus)
|
||||||
if (kIsAndroid) {
|
if (kIsAndroid) {
|
||||||
await FlutterDisplayMode.setHighRefreshRate();
|
await FlutterDisplayMode.setHighRefreshRate();
|
||||||
}
|
await NewPipeExtractor.init();
|
||||||
|
|
||||||
if (kIsDesktop) {
|
|
||||||
await windowManager.setPreventClose(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!kIsWeb) {
|
if (!kIsWeb) {
|
||||||
MetadataGod.initialize();
|
MetadataGod.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await KVStoreService.initialize();
|
||||||
|
|
||||||
if (kIsDesktop) {
|
if (kIsDesktop) {
|
||||||
|
await windowManager.setPreventClose(true);
|
||||||
|
await YtDlp.instance
|
||||||
|
.setBinaryLocation(
|
||||||
|
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ??
|
||||||
|
"yt-dlp${kIsWindows ? '.exe' : ''}",
|
||||||
|
)
|
||||||
|
.catchError((e, stack) => null);
|
||||||
await FlutterDiscordRPC.initialize(Env.discordAppId);
|
await FlutterDiscordRPC.initialize(Env.discordAppId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,7 +103,6 @@ Future<void> main(List<String> rawArgs) async {
|
|||||||
await SMTCWindows.initialize();
|
await SMTCWindows.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
await KVStoreService.initialize();
|
|
||||||
await EncryptedKvStoreService.initialize();
|
await EncryptedKvStoreService.initialize();
|
||||||
|
|
||||||
final database = AppDatabase();
|
final database = AppDatabase();
|
||||||
|
@ -18,6 +18,9 @@ import 'package:spotube/services/sourced_track/enums.dart';
|
|||||||
import 'package:flutter/widgets.dart' hide Table, Key, View;
|
import 'package:flutter/widgets.dart' hide Table, Key, View;
|
||||||
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
|
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
|
import 'package:spotube/services/youtube_engine/newpipe_engine.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/sqlite3.dart';
|
||||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||||
|
|
||||||
@ -59,7 +62,7 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
AppDatabase() : super(_openConnection());
|
AppDatabase() : super(_openConnection());
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 3;
|
int get schemaVersion => 4;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
@ -78,6 +81,12 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
schema.preferencesTable.cacheMusic,
|
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))
|
defaultValue: Constant(AudioSource.youtube.name))
|
||||||
.withConverter<AudioSource>(
|
.withConverter<AudioSource>(
|
||||||
$PreferencesTableTable.$converteraudioSource);
|
$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 =
|
static const VerificationMeta _streamMusicCodecMeta =
|
||||||
const VerificationMeta('streamMusicCodec');
|
const VerificationMeta('streamMusicCodec');
|
||||||
@override
|
@override
|
||||||
@ -845,6 +856,7 @@ class $PreferencesTableTable extends PreferencesTable
|
|||||||
invidiousInstance,
|
invidiousInstance,
|
||||||
themeMode,
|
themeMode,
|
||||||
audioSource,
|
audioSource,
|
||||||
|
youtubeClientEngine,
|
||||||
streamMusicCodec,
|
streamMusicCodec,
|
||||||
downloadMusicCodec,
|
downloadMusicCodec,
|
||||||
discordPresence,
|
discordPresence,
|
||||||
@ -937,6 +949,8 @@ class $PreferencesTableTable extends PreferencesTable
|
|||||||
}
|
}
|
||||||
context.handle(_themeModeMeta, const VerificationResult.success());
|
context.handle(_themeModeMeta, const VerificationResult.success());
|
||||||
context.handle(_audioSourceMeta, const VerificationResult.success());
|
context.handle(_audioSourceMeta, const VerificationResult.success());
|
||||||
|
context.handle(
|
||||||
|
_youtubeClientEngineMeta, const VerificationResult.success());
|
||||||
context.handle(_streamMusicCodecMeta, const VerificationResult.success());
|
context.handle(_streamMusicCodecMeta, const VerificationResult.success());
|
||||||
context.handle(_downloadMusicCodecMeta, const VerificationResult.success());
|
context.handle(_downloadMusicCodecMeta, const VerificationResult.success());
|
||||||
if (data.containsKey('discord_presence')) {
|
if (data.containsKey('discord_presence')) {
|
||||||
@ -1025,6 +1039,9 @@ class $PreferencesTableTable extends PreferencesTable
|
|||||||
audioSource: $PreferencesTableTable.$converteraudioSource.fromSql(
|
audioSource: $PreferencesTableTable.$converteraudioSource.fromSql(
|
||||||
attachedDatabase.typeMapping.read(
|
attachedDatabase.typeMapping.read(
|
||||||
DriftSqlType.string, data['${effectivePrefix}audio_source'])!),
|
DriftSqlType.string, data['${effectivePrefix}audio_source'])!),
|
||||||
|
youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine
|
||||||
|
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
|
||||||
|
data['${effectivePrefix}youtube_client_engine'])!),
|
||||||
streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec
|
streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec
|
||||||
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
|
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
|
||||||
data['${effectivePrefix}stream_music_codec'])!),
|
data['${effectivePrefix}stream_music_codec'])!),
|
||||||
@ -1069,6 +1086,9 @@ class $PreferencesTableTable extends PreferencesTable
|
|||||||
const EnumNameConverter<ThemeMode>(ThemeMode.values);
|
const EnumNameConverter<ThemeMode>(ThemeMode.values);
|
||||||
static JsonTypeConverter2<AudioSource, String, String> $converteraudioSource =
|
static JsonTypeConverter2<AudioSource, String, String> $converteraudioSource =
|
||||||
const EnumNameConverter<AudioSource>(AudioSource.values);
|
const EnumNameConverter<AudioSource>(AudioSource.values);
|
||||||
|
static JsonTypeConverter2<YoutubeClientEngine, String, String>
|
||||||
|
$converteryoutubeClientEngine =
|
||||||
|
const EnumNameConverter<YoutubeClientEngine>(YoutubeClientEngine.values);
|
||||||
static JsonTypeConverter2<SourceCodecs, String, String>
|
static JsonTypeConverter2<SourceCodecs, String, String>
|
||||||
$converterstreamMusicCodec =
|
$converterstreamMusicCodec =
|
||||||
const EnumNameConverter<SourceCodecs>(SourceCodecs.values);
|
const EnumNameConverter<SourceCodecs>(SourceCodecs.values);
|
||||||
@ -1100,6 +1120,7 @@ class PreferencesTableData extends DataClass
|
|||||||
final String invidiousInstance;
|
final String invidiousInstance;
|
||||||
final ThemeMode themeMode;
|
final ThemeMode themeMode;
|
||||||
final AudioSource audioSource;
|
final AudioSource audioSource;
|
||||||
|
final YoutubeClientEngine youtubeClientEngine;
|
||||||
final SourceCodecs streamMusicCodec;
|
final SourceCodecs streamMusicCodec;
|
||||||
final SourceCodecs downloadMusicCodec;
|
final SourceCodecs downloadMusicCodec;
|
||||||
final bool discordPresence;
|
final bool discordPresence;
|
||||||
@ -1128,6 +1149,7 @@ class PreferencesTableData extends DataClass
|
|||||||
required this.invidiousInstance,
|
required this.invidiousInstance,
|
||||||
required this.themeMode,
|
required this.themeMode,
|
||||||
required this.audioSource,
|
required this.audioSource,
|
||||||
|
required this.youtubeClientEngine,
|
||||||
required this.streamMusicCodec,
|
required this.streamMusicCodec,
|
||||||
required this.downloadMusicCodec,
|
required this.downloadMusicCodec,
|
||||||
required this.discordPresence,
|
required this.discordPresence,
|
||||||
@ -1190,6 +1212,11 @@ class PreferencesTableData extends DataClass
|
|||||||
map['audio_source'] = Variable<String>(
|
map['audio_source'] = Variable<String>(
|
||||||
$PreferencesTableTable.$converteraudioSource.toSql(audioSource));
|
$PreferencesTableTable.$converteraudioSource.toSql(audioSource));
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
map['youtube_client_engine'] = Variable<String>($PreferencesTableTable
|
||||||
|
.$converteryoutubeClientEngine
|
||||||
|
.toSql(youtubeClientEngine));
|
||||||
|
}
|
||||||
{
|
{
|
||||||
map['stream_music_codec'] = Variable<String>($PreferencesTableTable
|
map['stream_music_codec'] = Variable<String>($PreferencesTableTable
|
||||||
.$converterstreamMusicCodec
|
.$converterstreamMusicCodec
|
||||||
@ -1230,6 +1257,7 @@ class PreferencesTableData extends DataClass
|
|||||||
invidiousInstance: Value(invidiousInstance),
|
invidiousInstance: Value(invidiousInstance),
|
||||||
themeMode: Value(themeMode),
|
themeMode: Value(themeMode),
|
||||||
audioSource: Value(audioSource),
|
audioSource: Value(audioSource),
|
||||||
|
youtubeClientEngine: Value(youtubeClientEngine),
|
||||||
streamMusicCodec: Value(streamMusicCodec),
|
streamMusicCodec: Value(streamMusicCodec),
|
||||||
downloadMusicCodec: Value(downloadMusicCodec),
|
downloadMusicCodec: Value(downloadMusicCodec),
|
||||||
discordPresence: Value(discordPresence),
|
discordPresence: Value(discordPresence),
|
||||||
@ -1273,6 +1301,8 @@ class PreferencesTableData extends DataClass
|
|||||||
.fromJson(serializer.fromJson<String>(json['themeMode'])),
|
.fromJson(serializer.fromJson<String>(json['themeMode'])),
|
||||||
audioSource: $PreferencesTableTable.$converteraudioSource
|
audioSource: $PreferencesTableTable.$converteraudioSource
|
||||||
.fromJson(serializer.fromJson<String>(json['audioSource'])),
|
.fromJson(serializer.fromJson<String>(json['audioSource'])),
|
||||||
|
youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine
|
||||||
|
.fromJson(serializer.fromJson<String>(json['youtubeClientEngine'])),
|
||||||
streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec
|
streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec
|
||||||
.fromJson(serializer.fromJson<String>(json['streamMusicCodec'])),
|
.fromJson(serializer.fromJson<String>(json['streamMusicCodec'])),
|
||||||
downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec
|
downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec
|
||||||
@ -1316,6 +1346,9 @@ class PreferencesTableData extends DataClass
|
|||||||
$PreferencesTableTable.$converterthemeMode.toJson(themeMode)),
|
$PreferencesTableTable.$converterthemeMode.toJson(themeMode)),
|
||||||
'audioSource': serializer.toJson<String>(
|
'audioSource': serializer.toJson<String>(
|
||||||
$PreferencesTableTable.$converteraudioSource.toJson(audioSource)),
|
$PreferencesTableTable.$converteraudioSource.toJson(audioSource)),
|
||||||
|
'youtubeClientEngine': serializer.toJson<String>($PreferencesTableTable
|
||||||
|
.$converteryoutubeClientEngine
|
||||||
|
.toJson(youtubeClientEngine)),
|
||||||
'streamMusicCodec': serializer.toJson<String>($PreferencesTableTable
|
'streamMusicCodec': serializer.toJson<String>($PreferencesTableTable
|
||||||
.$converterstreamMusicCodec
|
.$converterstreamMusicCodec
|
||||||
.toJson(streamMusicCodec)),
|
.toJson(streamMusicCodec)),
|
||||||
@ -1351,6 +1384,7 @@ class PreferencesTableData extends DataClass
|
|||||||
String? invidiousInstance,
|
String? invidiousInstance,
|
||||||
ThemeMode? themeMode,
|
ThemeMode? themeMode,
|
||||||
AudioSource? audioSource,
|
AudioSource? audioSource,
|
||||||
|
YoutubeClientEngine? youtubeClientEngine,
|
||||||
SourceCodecs? streamMusicCodec,
|
SourceCodecs? streamMusicCodec,
|
||||||
SourceCodecs? downloadMusicCodec,
|
SourceCodecs? downloadMusicCodec,
|
||||||
bool? discordPresence,
|
bool? discordPresence,
|
||||||
@ -1379,6 +1413,7 @@ class PreferencesTableData extends DataClass
|
|||||||
invidiousInstance: invidiousInstance ?? this.invidiousInstance,
|
invidiousInstance: invidiousInstance ?? this.invidiousInstance,
|
||||||
themeMode: themeMode ?? this.themeMode,
|
themeMode: themeMode ?? this.themeMode,
|
||||||
audioSource: audioSource ?? this.audioSource,
|
audioSource: audioSource ?? this.audioSource,
|
||||||
|
youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine,
|
||||||
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
||||||
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
||||||
discordPresence: discordPresence ?? this.discordPresence,
|
discordPresence: discordPresence ?? this.discordPresence,
|
||||||
@ -1439,6 +1474,9 @@ class PreferencesTableData extends DataClass
|
|||||||
themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode,
|
themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode,
|
||||||
audioSource:
|
audioSource:
|
||||||
data.audioSource.present ? data.audioSource.value : this.audioSource,
|
data.audioSource.present ? data.audioSource.value : this.audioSource,
|
||||||
|
youtubeClientEngine: data.youtubeClientEngine.present
|
||||||
|
? data.youtubeClientEngine.value
|
||||||
|
: this.youtubeClientEngine,
|
||||||
streamMusicCodec: data.streamMusicCodec.present
|
streamMusicCodec: data.streamMusicCodec.present
|
||||||
? data.streamMusicCodec.value
|
? data.streamMusicCodec.value
|
||||||
: this.streamMusicCodec,
|
: this.streamMusicCodec,
|
||||||
@ -1483,6 +1521,7 @@ class PreferencesTableData extends DataClass
|
|||||||
..write('invidiousInstance: $invidiousInstance, ')
|
..write('invidiousInstance: $invidiousInstance, ')
|
||||||
..write('themeMode: $themeMode, ')
|
..write('themeMode: $themeMode, ')
|
||||||
..write('audioSource: $audioSource, ')
|
..write('audioSource: $audioSource, ')
|
||||||
|
..write('youtubeClientEngine: $youtubeClientEngine, ')
|
||||||
..write('streamMusicCodec: $streamMusicCodec, ')
|
..write('streamMusicCodec: $streamMusicCodec, ')
|
||||||
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
||||||
..write('discordPresence: $discordPresence, ')
|
..write('discordPresence: $discordPresence, ')
|
||||||
@ -1516,6 +1555,7 @@ class PreferencesTableData extends DataClass
|
|||||||
invidiousInstance,
|
invidiousInstance,
|
||||||
themeMode,
|
themeMode,
|
||||||
audioSource,
|
audioSource,
|
||||||
|
youtubeClientEngine,
|
||||||
streamMusicCodec,
|
streamMusicCodec,
|
||||||
downloadMusicCodec,
|
downloadMusicCodec,
|
||||||
discordPresence,
|
discordPresence,
|
||||||
@ -1548,6 +1588,7 @@ class PreferencesTableData extends DataClass
|
|||||||
other.invidiousInstance == this.invidiousInstance &&
|
other.invidiousInstance == this.invidiousInstance &&
|
||||||
other.themeMode == this.themeMode &&
|
other.themeMode == this.themeMode &&
|
||||||
other.audioSource == this.audioSource &&
|
other.audioSource == this.audioSource &&
|
||||||
|
other.youtubeClientEngine == this.youtubeClientEngine &&
|
||||||
other.streamMusicCodec == this.streamMusicCodec &&
|
other.streamMusicCodec == this.streamMusicCodec &&
|
||||||
other.downloadMusicCodec == this.downloadMusicCodec &&
|
other.downloadMusicCodec == this.downloadMusicCodec &&
|
||||||
other.discordPresence == this.discordPresence &&
|
other.discordPresence == this.discordPresence &&
|
||||||
@ -1578,6 +1619,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
final Value<String> invidiousInstance;
|
final Value<String> invidiousInstance;
|
||||||
final Value<ThemeMode> themeMode;
|
final Value<ThemeMode> themeMode;
|
||||||
final Value<AudioSource> audioSource;
|
final Value<AudioSource> audioSource;
|
||||||
|
final Value<YoutubeClientEngine> youtubeClientEngine;
|
||||||
final Value<SourceCodecs> streamMusicCodec;
|
final Value<SourceCodecs> streamMusicCodec;
|
||||||
final Value<SourceCodecs> downloadMusicCodec;
|
final Value<SourceCodecs> downloadMusicCodec;
|
||||||
final Value<bool> discordPresence;
|
final Value<bool> discordPresence;
|
||||||
@ -1606,6 +1648,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
this.invidiousInstance = const Value.absent(),
|
this.invidiousInstance = const Value.absent(),
|
||||||
this.themeMode = const Value.absent(),
|
this.themeMode = const Value.absent(),
|
||||||
this.audioSource = const Value.absent(),
|
this.audioSource = const Value.absent(),
|
||||||
|
this.youtubeClientEngine = const Value.absent(),
|
||||||
this.streamMusicCodec = const Value.absent(),
|
this.streamMusicCodec = const Value.absent(),
|
||||||
this.downloadMusicCodec = const Value.absent(),
|
this.downloadMusicCodec = const Value.absent(),
|
||||||
this.discordPresence = const Value.absent(),
|
this.discordPresence = const Value.absent(),
|
||||||
@ -1635,6 +1678,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
this.invidiousInstance = const Value.absent(),
|
this.invidiousInstance = const Value.absent(),
|
||||||
this.themeMode = const Value.absent(),
|
this.themeMode = const Value.absent(),
|
||||||
this.audioSource = const Value.absent(),
|
this.audioSource = const Value.absent(),
|
||||||
|
this.youtubeClientEngine = const Value.absent(),
|
||||||
this.streamMusicCodec = const Value.absent(),
|
this.streamMusicCodec = const Value.absent(),
|
||||||
this.downloadMusicCodec = const Value.absent(),
|
this.downloadMusicCodec = const Value.absent(),
|
||||||
this.discordPresence = const Value.absent(),
|
this.discordPresence = const Value.absent(),
|
||||||
@ -1664,6 +1708,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
Expression<String>? invidiousInstance,
|
Expression<String>? invidiousInstance,
|
||||||
Expression<String>? themeMode,
|
Expression<String>? themeMode,
|
||||||
Expression<String>? audioSource,
|
Expression<String>? audioSource,
|
||||||
|
Expression<String>? youtubeClientEngine,
|
||||||
Expression<String>? streamMusicCodec,
|
Expression<String>? streamMusicCodec,
|
||||||
Expression<String>? downloadMusicCodec,
|
Expression<String>? downloadMusicCodec,
|
||||||
Expression<bool>? discordPresence,
|
Expression<bool>? discordPresence,
|
||||||
@ -1695,6 +1740,8 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
if (invidiousInstance != null) 'invidious_instance': invidiousInstance,
|
if (invidiousInstance != null) 'invidious_instance': invidiousInstance,
|
||||||
if (themeMode != null) 'theme_mode': themeMode,
|
if (themeMode != null) 'theme_mode': themeMode,
|
||||||
if (audioSource != null) 'audio_source': audioSource,
|
if (audioSource != null) 'audio_source': audioSource,
|
||||||
|
if (youtubeClientEngine != null)
|
||||||
|
'youtube_client_engine': youtubeClientEngine,
|
||||||
if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec,
|
if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec,
|
||||||
if (downloadMusicCodec != null)
|
if (downloadMusicCodec != null)
|
||||||
'download_music_codec': downloadMusicCodec,
|
'download_music_codec': downloadMusicCodec,
|
||||||
@ -1727,6 +1774,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
Value<String>? invidiousInstance,
|
Value<String>? invidiousInstance,
|
||||||
Value<ThemeMode>? themeMode,
|
Value<ThemeMode>? themeMode,
|
||||||
Value<AudioSource>? audioSource,
|
Value<AudioSource>? audioSource,
|
||||||
|
Value<YoutubeClientEngine>? youtubeClientEngine,
|
||||||
Value<SourceCodecs>? streamMusicCodec,
|
Value<SourceCodecs>? streamMusicCodec,
|
||||||
Value<SourceCodecs>? downloadMusicCodec,
|
Value<SourceCodecs>? downloadMusicCodec,
|
||||||
Value<bool>? discordPresence,
|
Value<bool>? discordPresence,
|
||||||
@ -1755,6 +1803,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
invidiousInstance: invidiousInstance ?? this.invidiousInstance,
|
invidiousInstance: invidiousInstance ?? this.invidiousInstance,
|
||||||
themeMode: themeMode ?? this.themeMode,
|
themeMode: themeMode ?? this.themeMode,
|
||||||
audioSource: audioSource ?? this.audioSource,
|
audioSource: audioSource ?? this.audioSource,
|
||||||
|
youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine,
|
||||||
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
||||||
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
||||||
discordPresence: discordPresence ?? this.discordPresence,
|
discordPresence: discordPresence ?? this.discordPresence,
|
||||||
@ -1845,6 +1894,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
.$converteraudioSource
|
.$converteraudioSource
|
||||||
.toSql(audioSource.value));
|
.toSql(audioSource.value));
|
||||||
}
|
}
|
||||||
|
if (youtubeClientEngine.present) {
|
||||||
|
map['youtube_client_engine'] = Variable<String>($PreferencesTableTable
|
||||||
|
.$converteryoutubeClientEngine
|
||||||
|
.toSql(youtubeClientEngine.value));
|
||||||
|
}
|
||||||
if (streamMusicCodec.present) {
|
if (streamMusicCodec.present) {
|
||||||
map['stream_music_codec'] = Variable<String>($PreferencesTableTable
|
map['stream_music_codec'] = Variable<String>($PreferencesTableTable
|
||||||
.$converterstreamMusicCodec
|
.$converterstreamMusicCodec
|
||||||
@ -1894,6 +1948,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
|||||||
..write('invidiousInstance: $invidiousInstance, ')
|
..write('invidiousInstance: $invidiousInstance, ')
|
||||||
..write('themeMode: $themeMode, ')
|
..write('themeMode: $themeMode, ')
|
||||||
..write('audioSource: $audioSource, ')
|
..write('audioSource: $audioSource, ')
|
||||||
|
..write('youtubeClientEngine: $youtubeClientEngine, ')
|
||||||
..write('streamMusicCodec: $streamMusicCodec, ')
|
..write('streamMusicCodec: $streamMusicCodec, ')
|
||||||
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
||||||
..write('discordPresence: $discordPresence, ')
|
..write('discordPresence: $discordPresence, ')
|
||||||
@ -4565,6 +4620,7 @@ typedef $$PreferencesTableTableCreateCompanionBuilder
|
|||||||
Value<String> invidiousInstance,
|
Value<String> invidiousInstance,
|
||||||
Value<ThemeMode> themeMode,
|
Value<ThemeMode> themeMode,
|
||||||
Value<AudioSource> audioSource,
|
Value<AudioSource> audioSource,
|
||||||
|
Value<YoutubeClientEngine> youtubeClientEngine,
|
||||||
Value<SourceCodecs> streamMusicCodec,
|
Value<SourceCodecs> streamMusicCodec,
|
||||||
Value<SourceCodecs> downloadMusicCodec,
|
Value<SourceCodecs> downloadMusicCodec,
|
||||||
Value<bool> discordPresence,
|
Value<bool> discordPresence,
|
||||||
@ -4595,6 +4651,7 @@ typedef $$PreferencesTableTableUpdateCompanionBuilder
|
|||||||
Value<String> invidiousInstance,
|
Value<String> invidiousInstance,
|
||||||
Value<ThemeMode> themeMode,
|
Value<ThemeMode> themeMode,
|
||||||
Value<AudioSource> audioSource,
|
Value<AudioSource> audioSource,
|
||||||
|
Value<YoutubeClientEngine> youtubeClientEngine,
|
||||||
Value<SourceCodecs> streamMusicCodec,
|
Value<SourceCodecs> streamMusicCodec,
|
||||||
Value<SourceCodecs> downloadMusicCodec,
|
Value<SourceCodecs> downloadMusicCodec,
|
||||||
Value<bool> discordPresence,
|
Value<bool> discordPresence,
|
||||||
@ -4702,6 +4759,12 @@ class $$PreferencesTableTableFilterComposer
|
|||||||
column: $table.audioSource,
|
column: $table.audioSource,
|
||||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||||
|
|
||||||
|
ColumnWithTypeConverterFilters<YoutubeClientEngine, YoutubeClientEngine,
|
||||||
|
String>
|
||||||
|
get youtubeClientEngine => $composableBuilder(
|
||||||
|
column: $table.youtubeClientEngine,
|
||||||
|
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||||
|
|
||||||
ColumnWithTypeConverterFilters<SourceCodecs, SourceCodecs, String>
|
ColumnWithTypeConverterFilters<SourceCodecs, SourceCodecs, String>
|
||||||
get streamMusicCodec => $composableBuilder(
|
get streamMusicCodec => $composableBuilder(
|
||||||
column: $table.streamMusicCodec,
|
column: $table.streamMusicCodec,
|
||||||
@ -4812,6 +4875,10 @@ class $$PreferencesTableTableOrderingComposer
|
|||||||
ColumnOrderings<String> get audioSource => $composableBuilder(
|
ColumnOrderings<String> get audioSource => $composableBuilder(
|
||||||
column: $table.audioSource, builder: (column) => ColumnOrderings(column));
|
column: $table.audioSource, builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
|
ColumnOrderings<String> get youtubeClientEngine => $composableBuilder(
|
||||||
|
column: $table.youtubeClientEngine,
|
||||||
|
builder: (column) => ColumnOrderings(column));
|
||||||
|
|
||||||
ColumnOrderings<String> get streamMusicCodec => $composableBuilder(
|
ColumnOrderings<String> get streamMusicCodec => $composableBuilder(
|
||||||
column: $table.streamMusicCodec,
|
column: $table.streamMusicCodec,
|
||||||
builder: (column) => ColumnOrderings(column));
|
builder: (column) => ColumnOrderings(column));
|
||||||
@ -4915,6 +4982,10 @@ class $$PreferencesTableTableAnnotationComposer
|
|||||||
$composableBuilder(
|
$composableBuilder(
|
||||||
column: $table.audioSource, builder: (column) => column);
|
column: $table.audioSource, builder: (column) => column);
|
||||||
|
|
||||||
|
GeneratedColumnWithTypeConverter<YoutubeClientEngine, String>
|
||||||
|
get youtubeClientEngine => $composableBuilder(
|
||||||
|
column: $table.youtubeClientEngine, builder: (column) => column);
|
||||||
|
|
||||||
GeneratedColumnWithTypeConverter<SourceCodecs, String> get streamMusicCodec =>
|
GeneratedColumnWithTypeConverter<SourceCodecs, String> get streamMusicCodec =>
|
||||||
$composableBuilder(
|
$composableBuilder(
|
||||||
column: $table.streamMusicCodec, builder: (column) => column);
|
column: $table.streamMusicCodec, builder: (column) => column);
|
||||||
@ -4985,6 +5056,8 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
|||||||
Value<String> invidiousInstance = const Value.absent(),
|
Value<String> invidiousInstance = const Value.absent(),
|
||||||
Value<ThemeMode> themeMode = const Value.absent(),
|
Value<ThemeMode> themeMode = const Value.absent(),
|
||||||
Value<AudioSource> audioSource = const Value.absent(),
|
Value<AudioSource> audioSource = const Value.absent(),
|
||||||
|
Value<YoutubeClientEngine> youtubeClientEngine =
|
||||||
|
const Value.absent(),
|
||||||
Value<SourceCodecs> streamMusicCodec = const Value.absent(),
|
Value<SourceCodecs> streamMusicCodec = const Value.absent(),
|
||||||
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
||||||
Value<bool> discordPresence = const Value.absent(),
|
Value<bool> discordPresence = const Value.absent(),
|
||||||
@ -5014,6 +5087,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
|||||||
invidiousInstance: invidiousInstance,
|
invidiousInstance: invidiousInstance,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
audioSource: audioSource,
|
audioSource: audioSource,
|
||||||
|
youtubeClientEngine: youtubeClientEngine,
|
||||||
streamMusicCodec: streamMusicCodec,
|
streamMusicCodec: streamMusicCodec,
|
||||||
downloadMusicCodec: downloadMusicCodec,
|
downloadMusicCodec: downloadMusicCodec,
|
||||||
discordPresence: discordPresence,
|
discordPresence: discordPresence,
|
||||||
@ -5043,6 +5117,8 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
|||||||
Value<String> invidiousInstance = const Value.absent(),
|
Value<String> invidiousInstance = const Value.absent(),
|
||||||
Value<ThemeMode> themeMode = const Value.absent(),
|
Value<ThemeMode> themeMode = const Value.absent(),
|
||||||
Value<AudioSource> audioSource = const Value.absent(),
|
Value<AudioSource> audioSource = const Value.absent(),
|
||||||
|
Value<YoutubeClientEngine> youtubeClientEngine =
|
||||||
|
const Value.absent(),
|
||||||
Value<SourceCodecs> streamMusicCodec = const Value.absent(),
|
Value<SourceCodecs> streamMusicCodec = const Value.absent(),
|
||||||
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
||||||
Value<bool> discordPresence = const Value.absent(),
|
Value<bool> discordPresence = const Value.absent(),
|
||||||
@ -5072,6 +5148,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
|||||||
invidiousInstance: invidiousInstance,
|
invidiousInstance: invidiousInstance,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
audioSource: audioSource,
|
audioSource: audioSource,
|
||||||
|
youtubeClientEngine: youtubeClientEngine,
|
||||||
streamMusicCodec: streamMusicCodec,
|
streamMusicCodec: streamMusicCodec,
|
||||||
downloadMusicCodec: downloadMusicCodec,
|
downloadMusicCodec: downloadMusicCodec,
|
||||||
discordPresence: discordPresence,
|
discordPresence: discordPresence,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
// dart format width=80
|
// dart format width=80
|
||||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||||
import 'package:drift/drift.dart' as i1;
|
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:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/database/database.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.
|
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||||
final class Schema2 extends i0.VersionedSchema {
|
final class Schema2 extends i0.VersionedSchema {
|
||||||
@ -907,9 +907,291 @@ i1.GeneratedColumn<bool> _column_53(String aliasedName) =>
|
|||||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||||
'CHECK ("cache_music" IN (0, 1))'),
|
'CHECK ("cache_music" IN (0, 1))'),
|
||||||
defaultValue: const Constant(true));
|
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({
|
i0.MigrationStepWithVersion migrationSteps({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
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, Schema3 schema) from2To3,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
}) {
|
}) {
|
||||||
return (currentVersion, database) async {
|
return (currentVersion, database) async {
|
||||||
switch (currentVersion) {
|
switch (currentVersion) {
|
||||||
@ -923,6 +1205,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
final migrator = i1.Migrator(database, schema);
|
final migrator = i1.Migrator(database, schema);
|
||||||
await from2To3(migrator, schema);
|
await from2To3(migrator, schema);
|
||||||
return 3;
|
return 3;
|
||||||
|
case 3:
|
||||||
|
final schema = Schema4(database: database);
|
||||||
|
final migrator = i1.Migrator(database, schema);
|
||||||
|
await from3To4(migrator, schema);
|
||||||
|
return 4;
|
||||||
default:
|
default:
|
||||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||||
}
|
}
|
||||||
@ -932,9 +1219,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
|||||||
i1.OnUpgrade stepByStep({
|
i1.OnUpgrade stepByStep({
|
||||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
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, Schema3 schema) from2To3,
|
||||||
|
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||||
}) =>
|
}) =>
|
||||||
i0.VersionedSchema.stepByStepHelper(
|
i0.VersionedSchema.stepByStepHelper(
|
||||||
step: migrationSteps(
|
step: migrationSteps(
|
||||||
from1To2: from1To2,
|
from1To2: from1To2,
|
||||||
from2To3: from2To3,
|
from2To3: from2To3,
|
||||||
|
from3To4: from3To4,
|
||||||
));
|
));
|
||||||
|
@ -20,6 +20,25 @@ enum AudioSource {
|
|||||||
String get label => name[0].toUpperCase() + name.substring(1);
|
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,
|
||||||
|
YoutubeClientEngine.newPipe => NewPipeEngine.isAvailableForPlatform,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum MusicCodec {
|
enum MusicCodec {
|
||||||
m4a._("M4a (Best for downloaded music)"),
|
m4a._("M4a (Best for downloaded music)"),
|
||||||
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
|
weba._("WebA (Best for streamed music)\nDoesn't support audio metadata");
|
||||||
@ -84,6 +103,8 @@ class PreferencesTable extends Table {
|
|||||||
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
|
textEnum<ThemeMode>().withDefault(Constant(ThemeMode.system.name))();
|
||||||
TextColumn get audioSource =>
|
TextColumn get audioSource =>
|
||||||
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
|
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
|
||||||
|
TextColumn get youtubeClientEngine => textEnum<YoutubeClientEngine>()
|
||||||
|
.withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))();
|
||||||
TextColumn get streamMusicCodec =>
|
TextColumn get streamMusicCodec =>
|
||||||
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.weba.name))();
|
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.weba.name))();
|
||||||
TextColumn get downloadMusicCodec =>
|
TextColumn get downloadMusicCodec =>
|
||||||
@ -120,6 +141,7 @@ class PreferencesTable extends Table {
|
|||||||
invidiousInstance: "https://inv.nadeko.net",
|
invidiousInstance: "https://inv.nadeko.net",
|
||||||
themeMode: ThemeMode.system,
|
themeMode: ThemeMode.system,
|
||||||
audioSource: AudioSource.youtube,
|
audioSource: AudioSource.youtube,
|
||||||
|
youtubeClientEngine: YoutubeClientEngine.youtubeExplode,
|
||||||
streamMusicCodec: SourceCodecs.m4a,
|
streamMusicCodec: SourceCodecs.m4a,
|
||||||
downloadMusicCodec: SourceCodecs.m4a,
|
downloadMusicCodec: SourceCodecs.m4a,
|
||||||
discordPresence: true,
|
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/audio_player/querying_track_info.dart';
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
import 'package:spotube/provider/server/active_sourced_track.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.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/source_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||||
@ -68,6 +69,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
final playlist = ref.watch(audioPlayerProvider);
|
final playlist = ref.watch(audioPlayerProvider);
|
||||||
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||||
final preferences = ref.watch(userPreferencesProvider);
|
final preferences = ref.watch(userPreferencesProvider);
|
||||||
|
final youtubeEngine = ref.watch(youtubeEngineProvider);
|
||||||
|
|
||||||
final isSearching = useState(false);
|
final isSearching = useState(false);
|
||||||
final searchMode = useState(preferences.searchMode);
|
final searchMode = useState(preferences.searchMode);
|
||||||
@ -115,14 +117,14 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
activeSourceInfo,
|
activeSourceInfo,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final resultsYt = await youtubeClient.search.search(searchTerm.trim());
|
final resultsYt = await youtubeEngine.searchVideos(searchTerm.trim());
|
||||||
|
|
||||||
final searchResults = await Future.wait(
|
final searchResults = await Future.wait(
|
||||||
resultsYt
|
resultsYt
|
||||||
.map(YoutubeVideoInfo.fromVideo)
|
.map(YoutubeVideoInfo.fromVideo)
|
||||||
.mapIndexed((i, video) async {
|
.mapIndexed((i, video) async {
|
||||||
final siblingType =
|
final siblingType =
|
||||||
await YoutubeSourcedTrack.toSiblingType(i, video);
|
await YoutubeSourcedTrack.toSiblingType(i, video, ref);
|
||||||
return siblingType.info;
|
return siblingType.info;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -139,6 +141,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
searchMode.value,
|
searchMode.value,
|
||||||
activeTrack,
|
activeTrack,
|
||||||
preferences.audioSource,
|
preferences.audioSource,
|
||||||
|
youtubeEngine,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
final siblings = useMemoized(
|
final siblings = useMemoized(
|
||||||
@ -151,12 +154,15 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
|||||||
[activeTrack, isFetchingActiveTrack],
|
[activeTrack, isFetchingActiveTrack],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final previousActiveTrack = usePrevious(activeTrack);
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
|
/// Populate sibling when active track changes
|
||||||
|
if (previousActiveTrack?.id == activeTrack?.id) return;
|
||||||
if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) {
|
if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) {
|
||||||
activeTrackNotifier.populateSibling();
|
activeTrackNotifier.populateSibling();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [activeTrack]);
|
}, [activeTrack, previousActiveTrack]);
|
||||||
|
|
||||||
final itemBuilder = useCallback(
|
final itemBuilder = useCallback(
|
||||||
(SourceInfo sourceInfo) {
|
(SourceInfo sourceInfo) {
|
||||||
|
121
lib/modules/settings/youtube_engine_not_installed_dialog.dart
Normal file
121
lib/modules/settings/youtube_engine_not_installed_dialog.dart
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
|
||||||
|
import 'package:spotube/collections/spotube_icons.dart';
|
||||||
|
import 'package:spotube/components/form/text_form_field.dart';
|
||||||
|
import 'package:spotube/extensions/context.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:yt_dlp_dart/yt_dlp_dart.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) {
|
||||||
|
final controller = useTextEditingController();
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
|
||||||
|
|
||||||
|
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: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 400),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 8,
|
||||||
|
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]!));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Text(context.l10n.youtube_engine_set_path(engine.label)),
|
||||||
|
const Gap(8),
|
||||||
|
FormBuilder(
|
||||||
|
key: formKey,
|
||||||
|
child: TextFormBuilderField(
|
||||||
|
name: "path",
|
||||||
|
controller: controller,
|
||||||
|
placeholder: Text(switch (context.theme.platform) {
|
||||||
|
TargetPlatform.macOS => "e.g. /opt/homebrew/bin/yt-dlp",
|
||||||
|
TargetPlatform.windows =>
|
||||||
|
r"e.g. C:\Program Files\yt-dlp\yt-dlp.exe",
|
||||||
|
_ => "e.g. /home/user/.local/bin/yt-dlp",
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (kIsMacOS || kIsLinux)
|
||||||
|
Text(context.l10n.youtube_engine_unix_issue_message),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Button.text(
|
||||||
|
onPressed: () {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
|
},
|
||||||
|
child: Text(context.l10n.cancel),
|
||||||
|
),
|
||||||
|
Button.secondary(
|
||||||
|
onPressed: () async {
|
||||||
|
if (controller.text.isNotEmpty) {
|
||||||
|
if (!await File(controller.text).exists() && context.mounted) {
|
||||||
|
formKey.currentState?.fields["path"]
|
||||||
|
?.invalidate(context.l10n.file_not_found);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await KVStoreService.setYoutubeEnginePath(
|
||||||
|
engine,
|
||||||
|
controller.text,
|
||||||
|
);
|
||||||
|
if (engine == YoutubeClientEngine.ytDlp) {
|
||||||
|
await YtDlp.instance.setBinaryLocation(controller.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
|
},
|
||||||
|
child: Text(context.l10n.save),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_flutter/shadcn_flutter.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/bottom_player.dart';
|
||||||
import 'package:spotube/modules/root/sidebar/sidebar.dart';
|
import 'package:spotube/modules/root/sidebar/sidebar.dart';
|
||||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||||
@ -21,9 +22,11 @@ class RootAppPage extends HookConsumerWidget {
|
|||||||
final brightness = Theme.of(context).brightness;
|
final brightness = Theme.of(context).brightness;
|
||||||
|
|
||||||
ref.listen(glanceProvider, (_, __) {});
|
ref.listen(glanceProvider, (_, __) {});
|
||||||
|
|
||||||
useGlobalSubscriptions(ref);
|
useGlobalSubscriptions(ref);
|
||||||
useDownloaderDialogs(ref);
|
useDownloaderDialogs(ref);
|
||||||
useEndlessPlayback(ref);
|
useEndlessPlayback(ref);
|
||||||
|
useCheckYtDlpInstalled(ref);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
SystemChrome.setSystemUIOverlayStyle(
|
SystemChrome.setSystemUIOverlayStyle(
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
@ -13,11 +15,14 @@ import 'package:spotube/models/database/database.dart';
|
|||||||
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
import 'package:spotube/modules/settings/section_card_with_heading.dart';
|
||||||
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
|
||||||
import 'package:spotube/extensions/context.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/invidious_instances_provider.dart';
|
||||||
import 'package:spotube/provider/audio_player/sources/piped_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/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
|
|
||||||
import 'package:spotube/services/sourced_track/enums.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';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
|
||||||
class SettingsPlaybackSection extends HookConsumerWidget {
|
class SettingsPlaybackSection extends HookConsumerWidget {
|
||||||
@ -195,28 +200,56 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
AnimatedCrossFade(
|
switch (preferences.audioSource) {
|
||||||
duration: const Duration(milliseconds: 300),
|
AudioSource.youtube => AdaptiveSelectTile<YoutubeClientEngine>(
|
||||||
crossFadeState: preferences.audioSource == AudioSource.youtube
|
secondary: const Icon(SpotubeIcons.engine),
|
||||||
? CrossFadeState.showFirst
|
title: Text(context.l10n.youtube_engine),
|
||||||
: CrossFadeState.showSecond,
|
value: preferences.youtubeClientEngine,
|
||||||
firstChild: const SizedBox.shrink(),
|
options: YoutubeClientEngine.values
|
||||||
secondChild: AdaptiveSelectTile<SearchMode>(
|
.where((e) => e.isAvailableForPlatform())
|
||||||
secondary: const Icon(SpotubeIcons.search),
|
.map((e) => SelectItemButton(
|
||||||
title: Text(context.l10n.search_mode),
|
value: e,
|
||||||
value: preferences.searchMode,
|
child: Text(e.label),
|
||||||
options: SearchMode.values
|
))
|
||||||
.map((e) => SelectItemButton(
|
.toList(),
|
||||||
value: e,
|
onChanged: (value) async {
|
||||||
child: Text(e.label),
|
if (value == null) return;
|
||||||
))
|
if (value == YoutubeClientEngine.ytDlp) {
|
||||||
.toList(),
|
final customPath = KVStoreService.getYoutubeEnginePath(value);
|
||||||
onChanged: (value) {
|
if (!await YtDlpEngine.isInstalled() &&
|
||||||
if (value == null) return;
|
(customPath == null ||
|
||||||
preferencesNotifier.setSearchMode(value);
|
!await File(customPath).exists()) &&
|
||||||
},
|
context.mounted) {
|
||||||
),
|
final hasInstalled = await showDialog<bool>(
|
||||||
),
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
YouTubeEngineNotInstalledDialog(engine: value),
|
||||||
|
);
|
||||||
|
if (hasInstalled != true) 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(
|
AnimatedCrossFade(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
crossFadeState: preferences.searchMode == SearchMode.youtube &&
|
crossFadeState: preferences.searchMode == SearchMode.youtube &&
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -110,7 +109,7 @@ final localTracksProvider =
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
).then((value) => value.whereNotNull().toList());
|
).then((value) => value.nonNulls.toList());
|
||||||
|
|
||||||
final tracksFromMetadata = filesWithMetadata
|
final tracksFromMetadata = filesWithMetadata
|
||||||
.map(
|
.map(
|
||||||
|
@ -11,7 +11,6 @@ import 'package:shelf/shelf.dart';
|
|||||||
import 'package:spotube/extensions/artist_simple.dart';
|
import 'package:spotube/extensions/artist_simple.dart';
|
||||||
import 'package:spotube/extensions/image.dart';
|
import 'package:spotube/extensions/image.dart';
|
||||||
import 'package:spotube/extensions/track.dart';
|
import 'package:spotube/extensions/track.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
|
||||||
import 'package:spotube/models/parser/range_headers.dart';
|
import 'package:spotube/models/parser/range_headers.dart';
|
||||||
import 'package:spotube/provider/audio_player/audio_player.dart';
|
import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/provider/audio_player/state.dart';
|
import 'package:spotube/provider/audio_player/state.dart';
|
||||||
@ -125,14 +124,9 @@ class ServerPlaybackRoutes {
|
|||||||
)
|
)
|
||||||
.catchError((e, stack) async {
|
.catchError((e, stack) async {
|
||||||
AppLogger.reportError(e, stack);
|
AppLogger.reportError(e, stack);
|
||||||
final sourcedTrack = userPreferences.audioSource == AudioSource.youtube &&
|
final sourcedTrack = await ref
|
||||||
e is DioException
|
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
||||||
? await ref
|
.refreshStreamingUrl();
|
||||||
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
|
||||||
.refreshStreamingUrl()
|
|
||||||
: await ref
|
|
||||||
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
|
||||||
.switchToAlternativeSources();
|
|
||||||
|
|
||||||
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
||||||
|
|
||||||
|
@ -41,18 +41,6 @@ class SourcedTrackNotifier
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<SourcedTrack?> switchToAlternativeSources() async {
|
|
||||||
if (arg == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return await update((prev) async {
|
|
||||||
return await SourcedTrack.fetchFromTrackAltSource(
|
|
||||||
track: arg!.track,
|
|
||||||
ref: ref,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final sourcedTrackProvider = AsyncNotifierProviderFamily<SourcedTrackNotifier,
|
final sourcedTrackProvider = AsyncNotifierProviderFamily<SourcedTrackNotifier,
|
||||||
|
@ -39,7 +39,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
|||||||
(json) => PlaylistsFeatured.fromJson(json),
|
(json) => PlaylistsFeatured.fromJson(json),
|
||||||
).getPage(limit, offset);
|
).getPage(limit, offset);
|
||||||
|
|
||||||
final items = playlists.items?.whereNotNull().toList() ?? [];
|
final items = playlists.items?.nonNulls.toList() ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
items: items,
|
items: items,
|
||||||
|
@ -207,6 +207,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
|||||||
setData(PreferencesTableCompanion(audioSource: Value(type)));
|
setData(PreferencesTableCompanion(audioSource: Value(type)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setYoutubeClientEngine(YoutubeClientEngine engine) {
|
||||||
|
setData(PreferencesTableCompanion(youtubeClientEngine: Value(engine)));
|
||||||
|
}
|
||||||
|
|
||||||
void setSystemTitleBar(bool isSystemTitleBar) {
|
void setSystemTitleBar(bool isSystemTitleBar) {
|
||||||
setData(
|
setData(
|
||||||
PreferencesTableCompanion(
|
PreferencesTableCompanion(
|
||||||
|
22
lib/provider/youtube_engine/youtube_engine.dart
Normal file
22
lib/provider/youtube_engine/youtube_engine.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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/newpipe_engine.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 &&
|
||||||
|
NewPipeEngine.isAvailableForPlatform) {
|
||||||
|
return NewPipeEngine();
|
||||||
|
} else if (engineMode == YoutubeClientEngine.ytDlp &&
|
||||||
|
YtDlpEngine.isAvailableForPlatform) {
|
||||||
|
return YtDlpEngine();
|
||||||
|
} else {
|
||||||
|
return YouTubeExplodeEngine();
|
||||||
|
}
|
||||||
|
});
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:encrypt/encrypt.dart';
|
import 'package:encrypt/encrypt.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||||
import 'package:uuid/uuid.dart';
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
@ -87,4 +88,31 @@ abstract class KVStoreService {
|
|||||||
sharedPreferences.getBool('hasMigratedToDrift') ?? false;
|
sharedPreferences.getBool('hasMigratedToDrift') ?? false;
|
||||||
static Future<void> setHasMigratedToDrift(bool value) async =>
|
static Future<void> setHasMigratedToDrift(bool value) async =>
|
||||||
await sharedPreferences.setBool('hasMigratedToDrift', value);
|
await sharedPreferences.setBool('hasMigratedToDrift', value);
|
||||||
|
|
||||||
|
static Map<String, dynamic>? get _youtubeEnginePaths {
|
||||||
|
final jsonRaw = sharedPreferences.getString('ytDlpPath');
|
||||||
|
|
||||||
|
if (jsonRaw == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonDecode(jsonRaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String? getYoutubeEnginePath(YoutubeClientEngine engine) {
|
||||||
|
return _youtubeEnginePaths?[engine.name];
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> setYoutubeEnginePath(
|
||||||
|
YoutubeClientEngine engine,
|
||||||
|
String path,
|
||||||
|
) async {
|
||||||
|
await sharedPreferences.setString(
|
||||||
|
'ytDlpPath',
|
||||||
|
jsonEncode({
|
||||||
|
...?_youtubeEnginePaths,
|
||||||
|
engine.name: path,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:http/http.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
|
||||||
import 'package:spotube/services/sourced_track/enums.dart';
|
import 'package:spotube/services/sourced_track/enums.dart';
|
||||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
|
||||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
import 'package:spotube/services/sourced_track/sources/invidious.dart';
|
||||||
@ -17,7 +11,6 @@ import 'package:spotube/services/sourced_track/sources/jiosaavn.dart';
|
|||||||
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
import 'package:spotube/services/sourced_track/sources/piped.dart';
|
||||||
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
import 'package:spotube/services/sourced_track/sources/youtube.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
|
||||||
|
|
||||||
abstract class SourcedTrack extends Track {
|
abstract class SourcedTrack extends Track {
|
||||||
final SourceMap source;
|
final SourceMap source;
|
||||||
@ -97,11 +90,8 @@ abstract class SourcedTrack extends Track {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String getSearchTerm(Track track) {
|
static String getSearchTerm(Track track) {
|
||||||
final artists = (track.artists ?? [])
|
final artists =
|
||||||
.map((ar) => ar.name)
|
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
||||||
.toList()
|
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final title = ServiceUtils.getTitle(
|
final title = ServiceUtils.getTitle(
|
||||||
track.name!,
|
track.name!,
|
||||||
@ -112,100 +102,21 @@ abstract class SourcedTrack extends Track {
|
|||||||
return "$title - ${artists.join(", ")}";
|
return "$title - ${artists.join(", ")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
static fetchFromTrackAltSource({
|
|
||||||
required Track track,
|
|
||||||
required Ref ref,
|
|
||||||
}) async {
|
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
|
||||||
try {
|
|
||||||
return switch (preferences.audioSource) {
|
|
||||||
AudioSource.piped ||
|
|
||||||
AudioSource.invidious ||
|
|
||||||
AudioSource.jiosaavn =>
|
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
|
||||||
AudioSource.youtube =>
|
|
||||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
|
||||||
};
|
|
||||||
} on TrackNotFoundError catch (_) {
|
|
||||||
return switch (preferences.audioSource) {
|
|
||||||
AudioSource.piped ||
|
|
||||||
AudioSource.youtube ||
|
|
||||||
AudioSource.invidious =>
|
|
||||||
await JioSaavnSourcedTrack.fetchFromTrack(
|
|
||||||
track: track,
|
|
||||||
ref: ref,
|
|
||||||
weakMatch: true,
|
|
||||||
),
|
|
||||||
AudioSource.jiosaavn =>
|
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
|
||||||
};
|
|
||||||
} on HttpClientClosedException catch (_) {
|
|
||||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
|
||||||
} on VideoUnplayableException catch (_) {
|
|
||||||
return await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
|
||||||
} catch (e) {
|
|
||||||
if (e is DioException || e is ClientException || e is SocketException) {
|
|
||||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
|
||||||
track: track,
|
|
||||||
ref: ref,
|
|
||||||
weakMatch: preferences.audioSource == AudioSource.jiosaavn,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<SourcedTrack> fetchFromTrack({
|
static Future<SourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
final preferences = ref.read(userPreferencesProvider);
|
final preferences = ref.read(userPreferencesProvider);
|
||||||
try {
|
return switch (preferences.audioSource) {
|
||||||
return switch (preferences.audioSource) {
|
AudioSource.youtube =>
|
||||||
AudioSource.piped =>
|
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
AudioSource.piped =>
|
||||||
AudioSource.youtube =>
|
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
AudioSource.invidious =>
|
||||||
AudioSource.jiosaavn =>
|
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
AudioSource.jiosaavn =>
|
||||||
AudioSource.invidious =>
|
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||||
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
};
|
||||||
};
|
|
||||||
} on TrackNotFoundError catch (_) {
|
|
||||||
return switch (preferences.audioSource) {
|
|
||||||
AudioSource.piped ||
|
|
||||||
AudioSource.youtube ||
|
|
||||||
AudioSource.invidious =>
|
|
||||||
await JioSaavnSourcedTrack.fetchFromTrack(
|
|
||||||
track: track,
|
|
||||||
ref: ref,
|
|
||||||
weakMatch: true,
|
|
||||||
),
|
|
||||||
AudioSource.jiosaavn =>
|
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
|
||||||
};
|
|
||||||
} on HttpClientClosedException catch (_) {
|
|
||||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
|
||||||
} on VideoUnplayableException catch (_) {
|
|
||||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
|
||||||
} catch (e) {
|
|
||||||
if (e is DioException || e is ClientException || e is SocketException) {
|
|
||||||
return switch (preferences.audioSource) {
|
|
||||||
AudioSource.piped ||
|
|
||||||
AudioSource.invidious =>
|
|
||||||
await YoutubeSourcedTrack.fetchFromTrack(
|
|
||||||
track: track,
|
|
||||||
ref: ref,
|
|
||||||
),
|
|
||||||
_ => await JioSaavnSourcedTrack.fetchFromTrack(
|
|
||||||
track: track,
|
|
||||||
ref: ref,
|
|
||||||
weakMatch: preferences.audioSource == AudioSource.jiosaavn,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
rethrow;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<List<SiblingType>> fetchSiblings({
|
static Future<List<SiblingType>> fetchSiblings({
|
||||||
|
@ -50,6 +50,22 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
|||||||
required Track track,
|
required Track track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
|
// Indicates a stream url refresh
|
||||||
|
if (track is InvidiousSourcedTrack) {
|
||||||
|
final manifest = await ref
|
||||||
|
.read(invidiousProvider)
|
||||||
|
.videos
|
||||||
|
.get(track.sourceInfo.id, local: true);
|
||||||
|
|
||||||
|
return InvidiousSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: track.siblings,
|
||||||
|
source: toSourceMap(manifest),
|
||||||
|
sourceInfo: track.sourceInfo,
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
final cachedSource = await (database.select(database.sourceMatchTable)
|
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||||
..where((s) => s.trackId.equals(track.id!))
|
..where((s) => s.trackId.equals(track.id!))
|
||||||
|
@ -50,6 +50,19 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
required Track track,
|
required Track track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
|
// Means it wants a refresh of the stream
|
||||||
|
if (track is PipedSourcedTrack) {
|
||||||
|
final manifest =
|
||||||
|
await ref.read(pipedProvider).streams(track.sourceInfo.id);
|
||||||
|
return PipedSourcedTrack(
|
||||||
|
ref: ref,
|
||||||
|
siblings: track.siblings,
|
||||||
|
sourceInfo: track.sourceInfo,
|
||||||
|
source: toSourceMap(manifest),
|
||||||
|
track: track,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
final cachedSource = await (database.select(database.sourceMatchTable)
|
final cachedSource = await (database.select(database.sourceMatchTable)
|
||||||
..where((s) => s.trackId.equals(track.id!))
|
..where((s) => s.trackId.equals(track.id!))
|
||||||
@ -183,11 +196,8 @@ class PipedSourcedTrack extends SourcedTrack {
|
|||||||
: preference.searchMode == SearchMode.youtubeMusic;
|
: preference.searchMode == SearchMode.youtubeMusic;
|
||||||
|
|
||||||
if (isYouTubeMusic) {
|
if (isYouTubeMusic) {
|
||||||
final artists = (track.artists ?? [])
|
final artists =
|
||||||
.map((ar) => ar.name)
|
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
||||||
.toList()
|
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return await Future.wait(
|
return await Future.wait(
|
||||||
searchResults
|
searchResults
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/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/logger/logger.dart';
|
||||||
import 'package:spotube/services/song_link/song_link.dart';
|
import 'package:spotube/services/song_link/song_link.dart';
|
||||||
import 'package:spotube/services/sourced_track/enums.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:spotube/utils/service_utils.dart';
|
||||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||||
|
|
||||||
final youtubeClient = YoutubeExplode();
|
|
||||||
final officialMusicRegex = RegExp(
|
final officialMusicRegex = RegExp(
|
||||||
r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)",
|
r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)",
|
||||||
caseSensitive: false,
|
caseSensitive: false,
|
||||||
@ -43,24 +43,15 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
required super.ref,
|
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({
|
static Future<YoutubeSourcedTrack> fetchFromTrack({
|
||||||
required Track track,
|
required Track track,
|
||||||
required Ref ref,
|
required Ref ref,
|
||||||
}) async {
|
}) async {
|
||||||
// Indicates the track is requesting a stream refresh
|
// Indicates the track is requesting a stream refresh
|
||||||
if (track is YoutubeSourcedTrack) {
|
if (track is YoutubeSourcedTrack) {
|
||||||
final manifest = await _getStreamManifest(track.sourceInfo.id);
|
final manifest = await ref
|
||||||
|
.read(youtubeEngineProvider)
|
||||||
|
.getStreamManifest(track.sourceInfo.id);
|
||||||
|
|
||||||
final sourcedTrack = YoutubeSourcedTrack(
|
final sourcedTrack = YoutubeSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
@ -108,8 +99,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
track: track,
|
track: track,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
final (item, manifest) = await ref
|
||||||
final manifest = await _getStreamManifest(cachedSource.sourceId);
|
.read(youtubeEngineProvider)
|
||||||
|
.getVideoWithStreamInfo(cachedSource.sourceId);
|
||||||
|
|
||||||
final sourcedTrack = YoutubeSourcedTrack(
|
final sourcedTrack = YoutubeSourcedTrack(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
siblings: [],
|
siblings: [],
|
||||||
@ -162,10 +155,13 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
static Future<SiblingType> toSiblingType(
|
static Future<SiblingType> toSiblingType(
|
||||||
int index,
|
int index,
|
||||||
YoutubeVideoInfo item,
|
YoutubeVideoInfo item,
|
||||||
|
dynamic ref,
|
||||||
) async {
|
) async {
|
||||||
|
assert(ref is WidgetRef || ref is Ref, "Invalid ref type");
|
||||||
SourceMap? sourceMap;
|
SourceMap? sourceMap;
|
||||||
if (index == 0) {
|
if (index == 0) {
|
||||||
final manifest = await _getStreamManifest(item.id);
|
final manifest =
|
||||||
|
await ref.read(youtubeEngineProvider).getStreamManifest(item.id);
|
||||||
sourceMap = toSourceMap(manifest);
|
sourceMap = toSourceMap(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,11 +184,8 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
static List<YoutubeVideoInfo> rankResults(
|
static List<YoutubeVideoInfo> rankResults(
|
||||||
List<YoutubeVideoInfo> results, Track track) {
|
List<YoutubeVideoInfo> results, Track track) {
|
||||||
final artists = (track.artists ?? [])
|
final artists =
|
||||||
.map((ar) => ar.name)
|
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
||||||
.toList()
|
|
||||||
.whereNotNull()
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return results
|
return results
|
||||||
.sorted((a, b) => b.views.compareTo(a.views))
|
.sorted((a, b) => b.views.compareTo(a.views))
|
||||||
@ -259,8 +252,11 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
await toSiblingType(
|
await toSiblingType(
|
||||||
0,
|
0,
|
||||||
YoutubeVideoInfo.fromVideo(
|
YoutubeVideoInfo.fromVideo(
|
||||||
await youtubeClient.videos.get(ytLink!.url!),
|
await ref.read(youtubeEngineProvider).getVideo(
|
||||||
|
Uri.parse(ytLink!.url!).queryParameters["v"]!,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
ref,
|
||||||
)
|
)
|
||||||
];
|
];
|
||||||
} on VideoUnplayableException catch (e, stack) {
|
} on VideoUnplayableException catch (e, stack) {
|
||||||
@ -271,15 +267,13 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
|
|
||||||
final query = SourcedTrack.getSearchTerm(track);
|
final query = SourcedTrack.getSearchTerm(track);
|
||||||
|
|
||||||
final searchResults = await youtubeClient.search.search(
|
final searchResults =
|
||||||
"$query - Topic",
|
await ref.read(youtubeEngineProvider).searchVideos(query);
|
||||||
filter: TypeFilters.video,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
if (ServiceUtils.onlyContainsEnglish(query)) {
|
||||||
return await Future.wait(searchResults
|
return await Future.wait(searchResults
|
||||||
.map(YoutubeVideoInfo.fromVideo)
|
.map(YoutubeVideoInfo.fromVideo)
|
||||||
.mapIndexed(toSiblingType));
|
.mapIndexed((index, info) => toSiblingType(index, info, ref)));
|
||||||
}
|
}
|
||||||
|
|
||||||
final rankedSiblings = rankResults(
|
final rankedSiblings = rankResults(
|
||||||
@ -287,7 +281,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
track,
|
track,
|
||||||
);
|
);
|
||||||
|
|
||||||
return await Future.wait(rankedSiblings.mapIndexed(toSiblingType));
|
return await Future.wait(
|
||||||
|
rankedSiblings
|
||||||
|
.mapIndexed((index, info) => toSiblingType(index, info, ref)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -305,7 +302,9 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
|||||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||||
..insert(0, sourceInfo);
|
..insert(0, sourceInfo);
|
||||||
|
|
||||||
final manifest = await _getStreamManifest(newSourceInfo.id);
|
final manifest = await ref
|
||||||
|
.read(youtubeEngineProvider)
|
||||||
|
.getStreamManifest(newSourceInfo.id);
|
||||||
|
|
||||||
final database = ref.read(databaseProvider);
|
final database = ref.read(databaseProvider);
|
||||||
|
|
||||||
|
109
lib/services/youtube_engine/newpipe_engine.dart
Normal file
109
lib/services/youtube_engine/newpipe_engine.dart
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import 'package:flutter_new_pipe_extractor/flutter_new_pipe_extractor.dart'
|
||||||
|
hide Engagement;
|
||||||
|
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:http_parser/http_parser.dart';
|
||||||
|
|
||||||
|
class NewPipeEngine implements YouTubeEngine {
|
||||||
|
static bool get isAvailableForPlatform => kIsAndroid;
|
||||||
|
|
||||||
|
AudioOnlyStreamInfo _parseAudioStream(AudioStream stream, String videoId) {
|
||||||
|
return AudioOnlyStreamInfo(
|
||||||
|
VideoId(videoId),
|
||||||
|
stream.itag,
|
||||||
|
Uri.parse(stream.content),
|
||||||
|
StreamContainer.parse(stream.mediaFormat!.mimeType.split("/").last),
|
||||||
|
FileSize.unknown,
|
||||||
|
Bitrate(stream.bitrate),
|
||||||
|
stream.codec,
|
||||||
|
stream.quality,
|
||||||
|
[],
|
||||||
|
MediaType.parse(stream.mediaFormat!.mimeType),
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Video _parseVideo(VideoInfo info) {
|
||||||
|
return Video(
|
||||||
|
VideoId(info.id),
|
||||||
|
info.name,
|
||||||
|
info.uploaderName,
|
||||||
|
ChannelId(info.uploaderUrl),
|
||||||
|
info.uploadDate.offsetDateTime,
|
||||||
|
info.uploadDate.offsetDateTime.toString(),
|
||||||
|
info.uploadDate.offsetDateTime,
|
||||||
|
info.description.content ?? "",
|
||||||
|
Duration(seconds: info.duration),
|
||||||
|
ThumbnailSet(info.id),
|
||||||
|
info.tags,
|
||||||
|
Engagement(
|
||||||
|
info.viewCount,
|
||||||
|
info.likeCount,
|
||||||
|
info.dislikeCount,
|
||||||
|
),
|
||||||
|
!info.streamType.name.toLowerCase().contains("live"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Video _parseVideoResult(VideoSearchResultItem info) {
|
||||||
|
final id = Uri.parse(info.url).queryParameters["v"]!;
|
||||||
|
return Video(
|
||||||
|
VideoId(id),
|
||||||
|
info.name,
|
||||||
|
info.uploaderName,
|
||||||
|
ChannelId(info.uploaderUrl),
|
||||||
|
info.uploadDate?.offsetDateTime,
|
||||||
|
info.uploadDate?.offsetDateTime.toString(),
|
||||||
|
info.uploadDate?.offsetDateTime,
|
||||||
|
info.shortDescription ?? "",
|
||||||
|
Duration(seconds: info.duration),
|
||||||
|
ThumbnailSet(id),
|
||||||
|
[],
|
||||||
|
Engagement(info.viewCount, null, null),
|
||||||
|
!info.streamType.name.toLowerCase().contains("live"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<StreamManifest> getStreamManifest(String videoId) async {
|
||||||
|
final video = await NewPipeExtractor.getVideoInfo(videoId);
|
||||||
|
|
||||||
|
final streams =
|
||||||
|
video.audioStreams.map((stream) => _parseAudioStream(stream, videoId));
|
||||||
|
|
||||||
|
return StreamManifest(streams);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Video> getVideo(String videoId) async {
|
||||||
|
final video = await NewPipeExtractor.getVideoInfo(videoId);
|
||||||
|
|
||||||
|
return _parseVideo(video);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<(Video, StreamManifest)> getVideoWithStreamInfo(String videoId) async {
|
||||||
|
final video = await NewPipeExtractor.getVideoInfo(videoId);
|
||||||
|
|
||||||
|
final streams =
|
||||||
|
video.audioStreams.map((stream) => _parseAudioStream(stream, videoId));
|
||||||
|
|
||||||
|
return (_parseVideo(video), StreamManifest(streams));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Video>> searchVideos(String query) async {
|
||||||
|
final results = await NewPipeExtractor.search(
|
||||||
|
query,
|
||||||
|
contentFilters: [SearchContentFilters.videos],
|
||||||
|
);
|
||||||
|
|
||||||
|
final resultsWithVideos = results
|
||||||
|
.whereType<VideoSearchResultItem>()
|
||||||
|
.map((e) => _parseVideoResult(e))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return resultsWithVideos;
|
||||||
|
}
|
||||||
|
}
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -26,6 +26,9 @@ dependencies:
|
|||||||
- libwebkit2gtk-4.1-0 | libwebkit2gtk-4.0-0
|
- libwebkit2gtk-4.1-0 | libwebkit2gtk-4.0-0
|
||||||
- libsoup-3.0-0 | libsoup-2.4-0
|
- libsoup-3.0-0 | libsoup-2.4-0
|
||||||
|
|
||||||
|
suggested_dependencies:
|
||||||
|
- yt-dlp
|
||||||
|
|
||||||
essential: false
|
essential: false
|
||||||
icon: assets/spotube-logo.png
|
icon: assets/spotube-logo.png
|
||||||
|
|
||||||
|
26
pubspec.lock
26
pubspec.lock
@ -914,6 +914,15 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.3"
|
version: "2.4.3"
|
||||||
|
flutter_new_pipe_extractor:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: HEAD
|
||||||
|
resolved-ref: c5162e2c30967b197f20cd42aa30340a58efcd94
|
||||||
|
url: "https://github.com/KRTirtho/flutter_new_pipe_extractor.git"
|
||||||
|
source: git
|
||||||
|
version: "0.1.0"
|
||||||
flutter_plugin_android_lifecycle:
|
flutter_plugin_android_lifecycle:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1210,13 +1219,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.2.1"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http_parser
|
name: http_parser
|
||||||
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
|
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.1"
|
version: "4.1.2"
|
||||||
image:
|
image:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -2734,6 +2743,15 @@ packages:
|
|||||||
url: "https://github.com/Hexer10/youtube_explode_dart.git"
|
url: "https://github.com/Hexer10/youtube_explode_dart.git"
|
||||||
source: git
|
source: git
|
||||||
version: "2.3.7"
|
version: "2.3.7"
|
||||||
|
yt_dlp_dart:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: "."
|
||||||
|
ref: e2d82305fab18566408d6f8758361017d1640c3d
|
||||||
|
resolved-ref: e2d82305fab18566408d6f8758361017d1640c3d
|
||||||
|
url: "https://github.com/KRTirtho/yt_dlp_dart.git"
|
||||||
|
source: git
|
||||||
|
version: "1.0.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.6.0 <4.0.0"
|
dart: ">=3.6.1 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.27.0"
|
||||||
|
@ -140,6 +140,14 @@ dependencies:
|
|||||||
git:
|
git:
|
||||||
url: https://github.com/Hexer10/youtube_explode_dart.git
|
url: https://github.com/Hexer10/youtube_explode_dart.git
|
||||||
ref: e519db65ad0b0a40b12f69285932f9db509da3cf
|
ref: e519db65ad0b0a40b12f69285932f9db509da3cf
|
||||||
|
yt_dlp_dart:
|
||||||
|
git:
|
||||||
|
url: https://github.com/KRTirtho/yt_dlp_dart.git
|
||||||
|
ref: e2d82305fab18566408d6f8758361017d1640c3d
|
||||||
|
flutter_new_pipe_extractor:
|
||||||
|
git:
|
||||||
|
url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git
|
||||||
|
http_parser: ^4.1.2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
build_runner: ^2.4.13
|
build_runner: ^2.4.13
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/internal/migrations.dart';
|
import 'package:drift/internal/migrations.dart';
|
||||||
|
import 'schema_v4.dart' as v4;
|
||||||
import 'schema_v3.dart' as v3;
|
import 'schema_v3.dart' as v3;
|
||||||
import 'schema_v2.dart' as v2;
|
import 'schema_v2.dart' as v2;
|
||||||
import 'schema_v1.dart' as v1;
|
import 'schema_v1.dart' as v1;
|
||||||
@ -11,6 +12,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
|||||||
@override
|
@override
|
||||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||||
switch (version) {
|
switch (version) {
|
||||||
|
case 4:
|
||||||
|
return v4.DatabaseAtV4(db);
|
||||||
case 3:
|
case 3:
|
||||||
return v3.DatabaseAtV3(db);
|
return v3.DatabaseAtV3(db);
|
||||||
case 2:
|
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,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"bn": [
|
"bn": [
|
||||||
@ -34,7 +41,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ca": [
|
"ca": [
|
||||||
@ -53,7 +67,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"cs": [
|
"cs": [
|
||||||
@ -72,7 +93,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"de": [
|
"de": [
|
||||||
@ -91,7 +119,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"es": [
|
"es": [
|
||||||
@ -110,7 +145,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"eu": [
|
"eu": [
|
||||||
@ -129,7 +171,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fa": [
|
"fa": [
|
||||||
@ -148,7 +197,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fi": [
|
"fi": [
|
||||||
@ -167,7 +223,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"fr": [
|
"fr": [
|
||||||
@ -186,7 +249,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"hi": [
|
"hi": [
|
||||||
@ -205,7 +275,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"id": [
|
"id": [
|
||||||
@ -224,7 +301,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
@ -243,7 +327,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ja": [
|
"ja": [
|
||||||
@ -262,7 +353,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ka": [
|
"ka": [
|
||||||
@ -281,7 +379,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ko": [
|
"ko": [
|
||||||
@ -300,7 +405,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ne": [
|
"ne": [
|
||||||
@ -319,7 +431,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"nl": [
|
"nl": [
|
||||||
@ -338,7 +457,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pl": [
|
"pl": [
|
||||||
@ -357,7 +483,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"pt": [
|
"pt": [
|
||||||
@ -376,7 +509,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"ru": [
|
"ru": [
|
||||||
@ -395,7 +535,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"th": [
|
"th": [
|
||||||
@ -414,7 +561,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"tr": [
|
"tr": [
|
||||||
@ -433,7 +587,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"uk": [
|
"uk": [
|
||||||
@ -452,7 +613,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"vi": [
|
"vi": [
|
||||||
@ -471,7 +639,14 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
],
|
],
|
||||||
|
|
||||||
"zh": [
|
"zh": [
|
||||||
@ -490,6 +665,13 @@
|
|||||||
"no_tracks_listened_yet",
|
"no_tracks_listened_yet",
|
||||||
"not_following_artists",
|
"not_following_artists",
|
||||||
"no_favorite_albums_yet",
|
"no_favorite_albums_yet",
|
||||||
"no_logs_found"
|
"no_logs_found",
|
||||||
|
"youtube_engine",
|
||||||
|
"youtube_engine_not_installed_title",
|
||||||
|
"youtube_engine_not_installed_message",
|
||||||
|
"youtube_engine_set_path",
|
||||||
|
"youtube_engine_unix_issue_message",
|
||||||
|
"download",
|
||||||
|
"file_not_found"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,8 @@ SetupIconFile={{SETUP_ICON_FILE}}
|
|||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp"
|
WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp"
|
||||||
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
||||||
ArchitecturesAllowed=x64
|
ArchitecturesAllowed=x64compatible
|
||||||
ArchitecturesInstallIn64BitMode=x64
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
{% for locale in LOCALES %}
|
{% for locale in LOCALES %}
|
||||||
|
Loading…
Reference in New Issue
Block a user