mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55: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:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: 3.27.0
|
||||
FLUTTER_VERSION: 3.27.3
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
9
Makefile
9
Makefile
@ -45,4 +45,11 @@ gensums:
|
||||
sh -c scripts/gensums.sh
|
||||
|
||||
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"
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
@ -120,6 +121,8 @@ flutter {
|
||||
|
||||
def glanceVersion = "1.1.1"
|
||||
dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
|
||||
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1'
|
||||
// other deps so just ignore
|
||||
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 ** {
|
||||
@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 = xdg-user-dirs
|
||||
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
|
||||
md5sums = 475b1ae9b08f27743a4d4749391ae3db
|
||||
|
||||
|
@ -11,7 +11,7 @@ groups=()
|
||||
depends=('mpv' 'libappindicator-gtk3' 'libsecret' 'jsoncpp' 'libnotify' 'xdg-user-dirs' 'webkit2gtk-4.1')
|
||||
makedepends=()
|
||||
checkdepends=()
|
||||
optdepends=()
|
||||
optdepends=('yt-dlp-git')
|
||||
provides=()
|
||||
conflicts=()
|
||||
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);
|
||||
|
||||
@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
|
||||
List<AutoRoute> get routes => [
|
||||
AutoRoute(
|
||||
@ -40,6 +25,19 @@ class AppRouter extends RootStackRouter {
|
||||
path: "home",
|
||||
page: HomeRoute.page,
|
||||
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(
|
||||
path: "home/genres",
|
||||
|
@ -134,4 +134,5 @@ abstract class SpotubeIcons {
|
||||
static const grid = FeatherIcons.grid;
|
||||
static const list = FeatherIcons.list;
|
||||
static const device = FeatherIcons.smartphone;
|
||||
static const engine = FeatherIcons.server;
|
||||
}
|
||||
|
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",
|
||||
"not_following_artists": "You're not following any artists",
|
||||
"no_favorite_albums_yet": "Looks like you haven't added any albums to your favorites yet",
|
||||
"no_logs_found": "No logs found"
|
||||
"no_logs_found": "No logs found",
|
||||
"youtube_engine": "YouTube Engine",
|
||||
"youtube_engine_not_installed_title": "{engine} is not installed",
|
||||
"youtube_engine_not_installed_message": "{engine} is not installed in your system.",
|
||||
"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:window_manager/window_manager.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 {
|
||||
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)
|
||||
if (kIsAndroid) {
|
||||
await FlutterDisplayMode.setHighRefreshRate();
|
||||
}
|
||||
|
||||
if (kIsDesktop) {
|
||||
await windowManager.setPreventClose(true);
|
||||
await NewPipeExtractor.init();
|
||||
}
|
||||
|
||||
if (!kIsWeb) {
|
||||
MetadataGod.initialize();
|
||||
}
|
||||
|
||||
await KVStoreService.initialize();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -95,7 +103,6 @@ Future<void> main(List<String> rawArgs) async {
|
||||
await SMTCWindows.initialize();
|
||||
}
|
||||
|
||||
await KVStoreService.initialize();
|
||||
await EncryptedKvStoreService.initialize();
|
||||
|
||||
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:spotube/modules/settings/color_scheme_picker_dialog.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_flutter_libs/sqlite3_flutter_libs.dart';
|
||||
|
||||
@ -59,7 +62,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 3;
|
||||
int get schemaVersion => 4;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
@ -78,6 +81,12 @@ class AppDatabase extends _$AppDatabase {
|
||||
schema.preferencesTable.cacheMusic,
|
||||
);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.addColumn(
|
||||
schema.preferencesTable,
|
||||
schema.preferencesTable.youtubeClientEngine,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -760,6 +760,17 @@ class $PreferencesTableTable extends PreferencesTable
|
||||
defaultValue: Constant(AudioSource.youtube.name))
|
||||
.withConverter<AudioSource>(
|
||||
$PreferencesTableTable.$converteraudioSource);
|
||||
static const VerificationMeta _youtubeClientEngineMeta =
|
||||
const VerificationMeta('youtubeClientEngine');
|
||||
@override
|
||||
late final GeneratedColumnWithTypeConverter<YoutubeClientEngine, String>
|
||||
youtubeClientEngine = GeneratedColumn<String>(
|
||||
'youtube_client_engine', aliasedName, false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name))
|
||||
.withConverter<YoutubeClientEngine>(
|
||||
$PreferencesTableTable.$converteryoutubeClientEngine);
|
||||
static const VerificationMeta _streamMusicCodecMeta =
|
||||
const VerificationMeta('streamMusicCodec');
|
||||
@override
|
||||
@ -845,6 +856,7 @@ class $PreferencesTableTable extends PreferencesTable
|
||||
invidiousInstance,
|
||||
themeMode,
|
||||
audioSource,
|
||||
youtubeClientEngine,
|
||||
streamMusicCodec,
|
||||
downloadMusicCodec,
|
||||
discordPresence,
|
||||
@ -937,6 +949,8 @@ class $PreferencesTableTable extends PreferencesTable
|
||||
}
|
||||
context.handle(_themeModeMeta, const VerificationResult.success());
|
||||
context.handle(_audioSourceMeta, const VerificationResult.success());
|
||||
context.handle(
|
||||
_youtubeClientEngineMeta, const VerificationResult.success());
|
||||
context.handle(_streamMusicCodecMeta, const VerificationResult.success());
|
||||
context.handle(_downloadMusicCodecMeta, const VerificationResult.success());
|
||||
if (data.containsKey('discord_presence')) {
|
||||
@ -1025,6 +1039,9 @@ class $PreferencesTableTable extends PreferencesTable
|
||||
audioSource: $PreferencesTableTable.$converteraudioSource.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
DriftSqlType.string, data['${effectivePrefix}audio_source'])!),
|
||||
youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine
|
||||
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
|
||||
data['${effectivePrefix}youtube_client_engine'])!),
|
||||
streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec
|
||||
.fromSql(attachedDatabase.typeMapping.read(DriftSqlType.string,
|
||||
data['${effectivePrefix}stream_music_codec'])!),
|
||||
@ -1069,6 +1086,9 @@ class $PreferencesTableTable extends PreferencesTable
|
||||
const EnumNameConverter<ThemeMode>(ThemeMode.values);
|
||||
static JsonTypeConverter2<AudioSource, String, String> $converteraudioSource =
|
||||
const EnumNameConverter<AudioSource>(AudioSource.values);
|
||||
static JsonTypeConverter2<YoutubeClientEngine, String, String>
|
||||
$converteryoutubeClientEngine =
|
||||
const EnumNameConverter<YoutubeClientEngine>(YoutubeClientEngine.values);
|
||||
static JsonTypeConverter2<SourceCodecs, String, String>
|
||||
$converterstreamMusicCodec =
|
||||
const EnumNameConverter<SourceCodecs>(SourceCodecs.values);
|
||||
@ -1100,6 +1120,7 @@ class PreferencesTableData extends DataClass
|
||||
final String invidiousInstance;
|
||||
final ThemeMode themeMode;
|
||||
final AudioSource audioSource;
|
||||
final YoutubeClientEngine youtubeClientEngine;
|
||||
final SourceCodecs streamMusicCodec;
|
||||
final SourceCodecs downloadMusicCodec;
|
||||
final bool discordPresence;
|
||||
@ -1128,6 +1149,7 @@ class PreferencesTableData extends DataClass
|
||||
required this.invidiousInstance,
|
||||
required this.themeMode,
|
||||
required this.audioSource,
|
||||
required this.youtubeClientEngine,
|
||||
required this.streamMusicCodec,
|
||||
required this.downloadMusicCodec,
|
||||
required this.discordPresence,
|
||||
@ -1190,6 +1212,11 @@ class PreferencesTableData extends DataClass
|
||||
map['audio_source'] = Variable<String>(
|
||||
$PreferencesTableTable.$converteraudioSource.toSql(audioSource));
|
||||
}
|
||||
{
|
||||
map['youtube_client_engine'] = Variable<String>($PreferencesTableTable
|
||||
.$converteryoutubeClientEngine
|
||||
.toSql(youtubeClientEngine));
|
||||
}
|
||||
{
|
||||
map['stream_music_codec'] = Variable<String>($PreferencesTableTable
|
||||
.$converterstreamMusicCodec
|
||||
@ -1230,6 +1257,7 @@ class PreferencesTableData extends DataClass
|
||||
invidiousInstance: Value(invidiousInstance),
|
||||
themeMode: Value(themeMode),
|
||||
audioSource: Value(audioSource),
|
||||
youtubeClientEngine: Value(youtubeClientEngine),
|
||||
streamMusicCodec: Value(streamMusicCodec),
|
||||
downloadMusicCodec: Value(downloadMusicCodec),
|
||||
discordPresence: Value(discordPresence),
|
||||
@ -1273,6 +1301,8 @@ class PreferencesTableData extends DataClass
|
||||
.fromJson(serializer.fromJson<String>(json['themeMode'])),
|
||||
audioSource: $PreferencesTableTable.$converteraudioSource
|
||||
.fromJson(serializer.fromJson<String>(json['audioSource'])),
|
||||
youtubeClientEngine: $PreferencesTableTable.$converteryoutubeClientEngine
|
||||
.fromJson(serializer.fromJson<String>(json['youtubeClientEngine'])),
|
||||
streamMusicCodec: $PreferencesTableTable.$converterstreamMusicCodec
|
||||
.fromJson(serializer.fromJson<String>(json['streamMusicCodec'])),
|
||||
downloadMusicCodec: $PreferencesTableTable.$converterdownloadMusicCodec
|
||||
@ -1316,6 +1346,9 @@ class PreferencesTableData extends DataClass
|
||||
$PreferencesTableTable.$converterthemeMode.toJson(themeMode)),
|
||||
'audioSource': serializer.toJson<String>(
|
||||
$PreferencesTableTable.$converteraudioSource.toJson(audioSource)),
|
||||
'youtubeClientEngine': serializer.toJson<String>($PreferencesTableTable
|
||||
.$converteryoutubeClientEngine
|
||||
.toJson(youtubeClientEngine)),
|
||||
'streamMusicCodec': serializer.toJson<String>($PreferencesTableTable
|
||||
.$converterstreamMusicCodec
|
||||
.toJson(streamMusicCodec)),
|
||||
@ -1351,6 +1384,7 @@ class PreferencesTableData extends DataClass
|
||||
String? invidiousInstance,
|
||||
ThemeMode? themeMode,
|
||||
AudioSource? audioSource,
|
||||
YoutubeClientEngine? youtubeClientEngine,
|
||||
SourceCodecs? streamMusicCodec,
|
||||
SourceCodecs? downloadMusicCodec,
|
||||
bool? discordPresence,
|
||||
@ -1379,6 +1413,7 @@ class PreferencesTableData extends DataClass
|
||||
invidiousInstance: invidiousInstance ?? this.invidiousInstance,
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
audioSource: audioSource ?? this.audioSource,
|
||||
youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine,
|
||||
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
||||
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
||||
discordPresence: discordPresence ?? this.discordPresence,
|
||||
@ -1439,6 +1474,9 @@ class PreferencesTableData extends DataClass
|
||||
themeMode: data.themeMode.present ? data.themeMode.value : this.themeMode,
|
||||
audioSource:
|
||||
data.audioSource.present ? data.audioSource.value : this.audioSource,
|
||||
youtubeClientEngine: data.youtubeClientEngine.present
|
||||
? data.youtubeClientEngine.value
|
||||
: this.youtubeClientEngine,
|
||||
streamMusicCodec: data.streamMusicCodec.present
|
||||
? data.streamMusicCodec.value
|
||||
: this.streamMusicCodec,
|
||||
@ -1483,6 +1521,7 @@ class PreferencesTableData extends DataClass
|
||||
..write('invidiousInstance: $invidiousInstance, ')
|
||||
..write('themeMode: $themeMode, ')
|
||||
..write('audioSource: $audioSource, ')
|
||||
..write('youtubeClientEngine: $youtubeClientEngine, ')
|
||||
..write('streamMusicCodec: $streamMusicCodec, ')
|
||||
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
||||
..write('discordPresence: $discordPresence, ')
|
||||
@ -1516,6 +1555,7 @@ class PreferencesTableData extends DataClass
|
||||
invidiousInstance,
|
||||
themeMode,
|
||||
audioSource,
|
||||
youtubeClientEngine,
|
||||
streamMusicCodec,
|
||||
downloadMusicCodec,
|
||||
discordPresence,
|
||||
@ -1548,6 +1588,7 @@ class PreferencesTableData extends DataClass
|
||||
other.invidiousInstance == this.invidiousInstance &&
|
||||
other.themeMode == this.themeMode &&
|
||||
other.audioSource == this.audioSource &&
|
||||
other.youtubeClientEngine == this.youtubeClientEngine &&
|
||||
other.streamMusicCodec == this.streamMusicCodec &&
|
||||
other.downloadMusicCodec == this.downloadMusicCodec &&
|
||||
other.discordPresence == this.discordPresence &&
|
||||
@ -1578,6 +1619,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
final Value<String> invidiousInstance;
|
||||
final Value<ThemeMode> themeMode;
|
||||
final Value<AudioSource> audioSource;
|
||||
final Value<YoutubeClientEngine> youtubeClientEngine;
|
||||
final Value<SourceCodecs> streamMusicCodec;
|
||||
final Value<SourceCodecs> downloadMusicCodec;
|
||||
final Value<bool> discordPresence;
|
||||
@ -1606,6 +1648,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
this.invidiousInstance = const Value.absent(),
|
||||
this.themeMode = const Value.absent(),
|
||||
this.audioSource = const Value.absent(),
|
||||
this.youtubeClientEngine = const Value.absent(),
|
||||
this.streamMusicCodec = const Value.absent(),
|
||||
this.downloadMusicCodec = const Value.absent(),
|
||||
this.discordPresence = const Value.absent(),
|
||||
@ -1635,6 +1678,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
this.invidiousInstance = const Value.absent(),
|
||||
this.themeMode = const Value.absent(),
|
||||
this.audioSource = const Value.absent(),
|
||||
this.youtubeClientEngine = const Value.absent(),
|
||||
this.streamMusicCodec = const Value.absent(),
|
||||
this.downloadMusicCodec = const Value.absent(),
|
||||
this.discordPresence = const Value.absent(),
|
||||
@ -1664,6 +1708,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
Expression<String>? invidiousInstance,
|
||||
Expression<String>? themeMode,
|
||||
Expression<String>? audioSource,
|
||||
Expression<String>? youtubeClientEngine,
|
||||
Expression<String>? streamMusicCodec,
|
||||
Expression<String>? downloadMusicCodec,
|
||||
Expression<bool>? discordPresence,
|
||||
@ -1695,6 +1740,8 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
if (invidiousInstance != null) 'invidious_instance': invidiousInstance,
|
||||
if (themeMode != null) 'theme_mode': themeMode,
|
||||
if (audioSource != null) 'audio_source': audioSource,
|
||||
if (youtubeClientEngine != null)
|
||||
'youtube_client_engine': youtubeClientEngine,
|
||||
if (streamMusicCodec != null) 'stream_music_codec': streamMusicCodec,
|
||||
if (downloadMusicCodec != null)
|
||||
'download_music_codec': downloadMusicCodec,
|
||||
@ -1727,6 +1774,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
Value<String>? invidiousInstance,
|
||||
Value<ThemeMode>? themeMode,
|
||||
Value<AudioSource>? audioSource,
|
||||
Value<YoutubeClientEngine>? youtubeClientEngine,
|
||||
Value<SourceCodecs>? streamMusicCodec,
|
||||
Value<SourceCodecs>? downloadMusicCodec,
|
||||
Value<bool>? discordPresence,
|
||||
@ -1755,6 +1803,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
invidiousInstance: invidiousInstance ?? this.invidiousInstance,
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
audioSource: audioSource ?? this.audioSource,
|
||||
youtubeClientEngine: youtubeClientEngine ?? this.youtubeClientEngine,
|
||||
streamMusicCodec: streamMusicCodec ?? this.streamMusicCodec,
|
||||
downloadMusicCodec: downloadMusicCodec ?? this.downloadMusicCodec,
|
||||
discordPresence: discordPresence ?? this.discordPresence,
|
||||
@ -1845,6 +1894,11 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
.$converteraudioSource
|
||||
.toSql(audioSource.value));
|
||||
}
|
||||
if (youtubeClientEngine.present) {
|
||||
map['youtube_client_engine'] = Variable<String>($PreferencesTableTable
|
||||
.$converteryoutubeClientEngine
|
||||
.toSql(youtubeClientEngine.value));
|
||||
}
|
||||
if (streamMusicCodec.present) {
|
||||
map['stream_music_codec'] = Variable<String>($PreferencesTableTable
|
||||
.$converterstreamMusicCodec
|
||||
@ -1894,6 +1948,7 @@ class PreferencesTableCompanion extends UpdateCompanion<PreferencesTableData> {
|
||||
..write('invidiousInstance: $invidiousInstance, ')
|
||||
..write('themeMode: $themeMode, ')
|
||||
..write('audioSource: $audioSource, ')
|
||||
..write('youtubeClientEngine: $youtubeClientEngine, ')
|
||||
..write('streamMusicCodec: $streamMusicCodec, ')
|
||||
..write('downloadMusicCodec: $downloadMusicCodec, ')
|
||||
..write('discordPresence: $discordPresence, ')
|
||||
@ -4565,6 +4620,7 @@ typedef $$PreferencesTableTableCreateCompanionBuilder
|
||||
Value<String> invidiousInstance,
|
||||
Value<ThemeMode> themeMode,
|
||||
Value<AudioSource> audioSource,
|
||||
Value<YoutubeClientEngine> youtubeClientEngine,
|
||||
Value<SourceCodecs> streamMusicCodec,
|
||||
Value<SourceCodecs> downloadMusicCodec,
|
||||
Value<bool> discordPresence,
|
||||
@ -4595,6 +4651,7 @@ typedef $$PreferencesTableTableUpdateCompanionBuilder
|
||||
Value<String> invidiousInstance,
|
||||
Value<ThemeMode> themeMode,
|
||||
Value<AudioSource> audioSource,
|
||||
Value<YoutubeClientEngine> youtubeClientEngine,
|
||||
Value<SourceCodecs> streamMusicCodec,
|
||||
Value<SourceCodecs> downloadMusicCodec,
|
||||
Value<bool> discordPresence,
|
||||
@ -4702,6 +4759,12 @@ class $$PreferencesTableTableFilterComposer
|
||||
column: $table.audioSource,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<YoutubeClientEngine, YoutubeClientEngine,
|
||||
String>
|
||||
get youtubeClientEngine => $composableBuilder(
|
||||
column: $table.youtubeClientEngine,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column));
|
||||
|
||||
ColumnWithTypeConverterFilters<SourceCodecs, SourceCodecs, String>
|
||||
get streamMusicCodec => $composableBuilder(
|
||||
column: $table.streamMusicCodec,
|
||||
@ -4812,6 +4875,10 @@ class $$PreferencesTableTableOrderingComposer
|
||||
ColumnOrderings<String> get audioSource => $composableBuilder(
|
||||
column: $table.audioSource, builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get youtubeClientEngine => $composableBuilder(
|
||||
column: $table.youtubeClientEngine,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
|
||||
ColumnOrderings<String> get streamMusicCodec => $composableBuilder(
|
||||
column: $table.streamMusicCodec,
|
||||
builder: (column) => ColumnOrderings(column));
|
||||
@ -4915,6 +4982,10 @@ class $$PreferencesTableTableAnnotationComposer
|
||||
$composableBuilder(
|
||||
column: $table.audioSource, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<YoutubeClientEngine, String>
|
||||
get youtubeClientEngine => $composableBuilder(
|
||||
column: $table.youtubeClientEngine, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<SourceCodecs, String> get streamMusicCodec =>
|
||||
$composableBuilder(
|
||||
column: $table.streamMusicCodec, builder: (column) => column);
|
||||
@ -4985,6 +5056,8 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
||||
Value<String> invidiousInstance = const Value.absent(),
|
||||
Value<ThemeMode> themeMode = const Value.absent(),
|
||||
Value<AudioSource> audioSource = const Value.absent(),
|
||||
Value<YoutubeClientEngine> youtubeClientEngine =
|
||||
const Value.absent(),
|
||||
Value<SourceCodecs> streamMusicCodec = const Value.absent(),
|
||||
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
||||
Value<bool> discordPresence = const Value.absent(),
|
||||
@ -5014,6 +5087,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
||||
invidiousInstance: invidiousInstance,
|
||||
themeMode: themeMode,
|
||||
audioSource: audioSource,
|
||||
youtubeClientEngine: youtubeClientEngine,
|
||||
streamMusicCodec: streamMusicCodec,
|
||||
downloadMusicCodec: downloadMusicCodec,
|
||||
discordPresence: discordPresence,
|
||||
@ -5043,6 +5117,8 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
||||
Value<String> invidiousInstance = const Value.absent(),
|
||||
Value<ThemeMode> themeMode = const Value.absent(),
|
||||
Value<AudioSource> audioSource = const Value.absent(),
|
||||
Value<YoutubeClientEngine> youtubeClientEngine =
|
||||
const Value.absent(),
|
||||
Value<SourceCodecs> streamMusicCodec = const Value.absent(),
|
||||
Value<SourceCodecs> downloadMusicCodec = const Value.absent(),
|
||||
Value<bool> discordPresence = const Value.absent(),
|
||||
@ -5072,6 +5148,7 @@ class $$PreferencesTableTableTableManager extends RootTableManager<
|
||||
invidiousInstance: invidiousInstance,
|
||||
themeMode: themeMode,
|
||||
audioSource: audioSource,
|
||||
youtubeClientEngine: youtubeClientEngine,
|
||||
streamMusicCodec: streamMusicCodec,
|
||||
downloadMusicCodec: downloadMusicCodec,
|
||||
discordPresence: discordPresence,
|
||||
|
@ -1,11 +1,11 @@
|
||||
// dart format width=80
|
||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||
import 'package:drift/drift.dart' as i1;
|
||||
import 'package:drift/drift.dart'; // ignore_for_file: type=lint,unused_import
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart'; // ignore_for_file: type=lint,unused_import
|
||||
|
||||
// GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
final class Schema2 extends i0.VersionedSchema {
|
||||
@ -907,9 +907,291 @@ i1.GeneratedColumn<bool> _column_53(String aliasedName) =>
|
||||
defaultConstraints: i1.GeneratedColumn.constraintIsAlways(
|
||||
'CHECK ("cache_music" IN (0, 1))'),
|
||||
defaultValue: const Constant(true));
|
||||
|
||||
final class Schema4 extends i0.VersionedSchema {
|
||||
Schema4({required super.database}) : super(version: 4);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
authenticationTable,
|
||||
blacklistTable,
|
||||
preferencesTable,
|
||||
scrobblerTable,
|
||||
skipSegmentTable,
|
||||
sourceMatchTable,
|
||||
audioPlayerStateTable,
|
||||
playlistTable,
|
||||
playlistMediaTable,
|
||||
historyTable,
|
||||
lyricsTable,
|
||||
uniqueBlacklist,
|
||||
uniqTrackMatch,
|
||||
];
|
||||
late final Shape0 authenticationTable = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'authentication_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_2,
|
||||
_column_3,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape1 blacklistTable = Shape1(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'blacklist_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_4,
|
||||
_column_5,
|
||||
_column_6,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape12 preferencesTable = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'preferences_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_7,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_22,
|
||||
_column_23,
|
||||
_column_24,
|
||||
_column_25,
|
||||
_column_26,
|
||||
_column_54,
|
||||
_column_27,
|
||||
_column_28,
|
||||
_column_29,
|
||||
_column_30,
|
||||
_column_31,
|
||||
_column_53,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape3 scrobblerTable = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'scrobbler_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_32,
|
||||
_column_33,
|
||||
_column_34,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape4 skipSegmentTable = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'skip_segment_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_35,
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_32,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape5 sourceMatchTable = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'source_match_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
_column_32,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape6 audioPlayerStateTable = Shape6(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'audio_player_state_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_40,
|
||||
_column_41,
|
||||
_column_42,
|
||||
_column_43,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape7 playlistTable = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'playlist_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_44,
|
||||
_column_45,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape8 playlistMediaTable = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'playlist_media_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_48,
|
||||
_column_49,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape9 historyTable = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'history_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_32,
|
||||
_column_50,
|
||||
_column_51,
|
||||
_column_52,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
late final Shape10 lyricsTable = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'lyrics_table',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_37,
|
||||
_column_52,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null);
|
||||
final i1.Index uniqueBlacklist = i1.Index('unique_blacklist',
|
||||
'CREATE UNIQUE INDEX unique_blacklist ON blacklist_table (element_type, element_id)');
|
||||
final i1.Index uniqTrackMatch = i1.Index('uniq_track_match',
|
||||
'CREATE UNIQUE INDEX uniq_track_match ON source_match_table (track_id, source_id, source_type)');
|
||||
}
|
||||
|
||||
class Shape12 extends i0.VersionedTable {
|
||||
Shape12({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get audioQuality =>
|
||||
columnsByName['audio_quality']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get albumColorSync =>
|
||||
columnsByName['album_color_sync']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get amoledDarkTheme =>
|
||||
columnsByName['amoled_dark_theme']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get checkUpdate =>
|
||||
columnsByName['check_update']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get normalizeAudio =>
|
||||
columnsByName['normalize_audio']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get showSystemTrayIcon =>
|
||||
columnsByName['show_system_tray_icon']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get systemTitleBar =>
|
||||
columnsByName['system_title_bar']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get skipNonMusic =>
|
||||
columnsByName['skip_non_music']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<String> get closeBehavior =>
|
||||
columnsByName['close_behavior']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get accentColorScheme =>
|
||||
columnsByName['accent_color_scheme']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get layoutMode =>
|
||||
columnsByName['layout_mode']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get locale =>
|
||||
columnsByName['locale']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get market =>
|
||||
columnsByName['market']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get searchMode =>
|
||||
columnsByName['search_mode']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get downloadLocation =>
|
||||
columnsByName['download_location']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get localLibraryLocation =>
|
||||
columnsByName['local_library_location']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get pipedInstance =>
|
||||
columnsByName['piped_instance']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get invidiousInstance =>
|
||||
columnsByName['invidious_instance']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get themeMode =>
|
||||
columnsByName['theme_mode']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get audioSource =>
|
||||
columnsByName['audio_source']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get youtubeClientEngine =>
|
||||
columnsByName['youtube_client_engine']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get streamMusicCodec =>
|
||||
columnsByName['stream_music_codec']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get downloadMusicCodec =>
|
||||
columnsByName['download_music_codec']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get discordPresence =>
|
||||
columnsByName['discord_presence']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get endlessPlayback =>
|
||||
columnsByName['endless_playback']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get enableConnect =>
|
||||
columnsByName['enable_connect']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<bool> get cacheMusic =>
|
||||
columnsByName['cache_music']! as i1.GeneratedColumn<bool>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_54(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>('youtube_client_engine', aliasedName, false,
|
||||
type: i1.DriftSqlType.string,
|
||||
defaultValue: Constant(YoutubeClientEngine.youtubeExplode.name));
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@ -923,6 +1205,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from2To3(migrator, schema);
|
||||
return 3;
|
||||
case 3:
|
||||
final schema = Schema4(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from3To4(migrator, schema);
|
||||
return 4;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@ -932,9 +1219,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
|
||||
}) =>
|
||||
i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
from2To3: from2To3,
|
||||
from3To4: from3To4,
|
||||
));
|
||||
|
@ -20,6 +20,25 @@ enum AudioSource {
|
||||
String get label => name[0].toUpperCase() + name.substring(1);
|
||||
}
|
||||
|
||||
enum YoutubeClientEngine {
|
||||
ytDlp("yt-dlp"),
|
||||
youtubeExplode("YouTubeExplode"),
|
||||
newPipe("NewPipe");
|
||||
|
||||
final String label;
|
||||
|
||||
const YoutubeClientEngine(this.label);
|
||||
|
||||
bool isAvailableForPlatform() {
|
||||
return switch (this) {
|
||||
YoutubeClientEngine.youtubeExplode =>
|
||||
YouTubeExplodeEngine.isAvailableForPlatform,
|
||||
YoutubeClientEngine.ytDlp => YtDlpEngine.isAvailableForPlatform,
|
||||
YoutubeClientEngine.newPipe => NewPipeEngine.isAvailableForPlatform,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum MusicCodec {
|
||||
m4a._("M4a (Best for downloaded music)"),
|
||||
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))();
|
||||
TextColumn get audioSource =>
|
||||
textEnum<AudioSource>().withDefault(Constant(AudioSource.youtube.name))();
|
||||
TextColumn get youtubeClientEngine => textEnum<YoutubeClientEngine>()
|
||||
.withDefault(Constant(YoutubeClientEngine.youtubeExplode.name))();
|
||||
TextColumn get streamMusicCodec =>
|
||||
textEnum<SourceCodecs>().withDefault(Constant(SourceCodecs.weba.name))();
|
||||
TextColumn get downloadMusicCodec =>
|
||||
@ -120,6 +141,7 @@ class PreferencesTable extends Table {
|
||||
invidiousInstance: "https://inv.nadeko.net",
|
||||
themeMode: ThemeMode.system,
|
||||
audioSource: AudioSource.youtube,
|
||||
youtubeClientEngine: YoutubeClientEngine.youtubeExplode,
|
||||
streamMusicCodec: SourceCodecs.m4a,
|
||||
downloadMusicCodec: SourceCodecs.m4a,
|
||||
discordPresence: true,
|
||||
|
@ -19,6 +19,7 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/audio_player/querying_track_info.dart';
|
||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/models/video_info.dart';
|
||||
import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
@ -68,6 +69,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
final playlist = ref.watch(audioPlayerProvider);
|
||||
final isFetchingActiveTrack = ref.watch(queryingTrackInfoProvider);
|
||||
final preferences = ref.watch(userPreferencesProvider);
|
||||
final youtubeEngine = ref.watch(youtubeEngineProvider);
|
||||
|
||||
final isSearching = useState(false);
|
||||
final searchMode = useState(preferences.searchMode);
|
||||
@ -115,14 +117,14 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
activeSourceInfo,
|
||||
);
|
||||
} else {
|
||||
final resultsYt = await youtubeClient.search.search(searchTerm.trim());
|
||||
final resultsYt = await youtubeEngine.searchVideos(searchTerm.trim());
|
||||
|
||||
final searchResults = await Future.wait(
|
||||
resultsYt
|
||||
.map(YoutubeVideoInfo.fromVideo)
|
||||
.mapIndexed((i, video) async {
|
||||
final siblingType =
|
||||
await YoutubeSourcedTrack.toSiblingType(i, video);
|
||||
await YoutubeSourcedTrack.toSiblingType(i, video, ref);
|
||||
return siblingType.info;
|
||||
}),
|
||||
);
|
||||
@ -139,6 +141,7 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
searchMode.value,
|
||||
activeTrack,
|
||||
preferences.audioSource,
|
||||
youtubeEngine,
|
||||
]);
|
||||
|
||||
final siblings = useMemoized(
|
||||
@ -151,12 +154,15 @@ class SiblingTracksSheet extends HookConsumerWidget {
|
||||
[activeTrack, isFetchingActiveTrack],
|
||||
);
|
||||
|
||||
final previousActiveTrack = usePrevious(activeTrack);
|
||||
useEffect(() {
|
||||
/// Populate sibling when active track changes
|
||||
if (previousActiveTrack?.id == activeTrack?.id) return;
|
||||
if (activeTrack is SourcedTrack && activeTrack.siblings.isEmpty) {
|
||||
activeTrackNotifier.populateSibling();
|
||||
}
|
||||
return null;
|
||||
}, [activeTrack]);
|
||||
}, [activeTrack, previousActiveTrack]);
|
||||
|
||||
final itemBuilder = useCallback(
|
||||
(SourceInfo sourceInfo) {
|
||||
|
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:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:spotube/hooks/configurators/use_check_yt_dlp_installed.dart';
|
||||
import 'package:spotube/modules/root/bottom_player.dart';
|
||||
import 'package:spotube/modules/root/sidebar/sidebar.dart';
|
||||
import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
||||
@ -21,9 +22,11 @@ class RootAppPage extends HookConsumerWidget {
|
||||
final brightness = Theme.of(context).brightness;
|
||||
|
||||
ref.listen(glanceProvider, (_, __) {});
|
||||
|
||||
useGlobalSubscriptions(ref);
|
||||
useDownloaderDialogs(ref);
|
||||
useEndlessPlayback(ref);
|
||||
useCheckYtDlpInstalled(ref);
|
||||
|
||||
useEffect(() {
|
||||
SystemChrome.setSystemUIOverlayStyle(
|
||||
|
@ -1,3 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.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/components/adaptive/adaptive_select_tile.dart';
|
||||
import 'package:spotube/extensions/context.dart';
|
||||
import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.dart';
|
||||
import 'package:spotube/provider/audio_player/sources/invidious_instances_provider.dart';
|
||||
import 'package:spotube/provider/audio_player/sources/piped_instances_provider.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
|
||||
class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
@ -195,28 +200,56 @@ class SettingsPlaybackSection extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
),
|
||||
AnimatedCrossFade(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
crossFadeState: preferences.audioSource == AudioSource.youtube
|
||||
? CrossFadeState.showFirst
|
||||
: CrossFadeState.showSecond,
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: AdaptiveSelectTile<SearchMode>(
|
||||
secondary: const Icon(SpotubeIcons.search),
|
||||
title: Text(context.l10n.search_mode),
|
||||
value: preferences.searchMode,
|
||||
options: SearchMode.values
|
||||
.map((e) => SelectItemButton(
|
||||
value: e,
|
||||
child: Text(e.label),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
preferencesNotifier.setSearchMode(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
switch (preferences.audioSource) {
|
||||
AudioSource.youtube => AdaptiveSelectTile<YoutubeClientEngine>(
|
||||
secondary: const Icon(SpotubeIcons.engine),
|
||||
title: Text(context.l10n.youtube_engine),
|
||||
value: preferences.youtubeClientEngine,
|
||||
options: YoutubeClientEngine.values
|
||||
.where((e) => e.isAvailableForPlatform())
|
||||
.map((e) => SelectItemButton(
|
||||
value: e,
|
||||
child: Text(e.label),
|
||||
))
|
||||
.toList(),
|
||||
onChanged: (value) async {
|
||||
if (value == null) return;
|
||||
if (value == YoutubeClientEngine.ytDlp) {
|
||||
final customPath = KVStoreService.getYoutubeEnginePath(value);
|
||||
if (!await YtDlpEngine.isInstalled() &&
|
||||
(customPath == null ||
|
||||
!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(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
crossFadeState: preferences.searchMode == SearchMode.youtube &&
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@ -110,7 +109,7 @@ final localTracksProvider =
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
).then((value) => value.whereNotNull().toList());
|
||||
).then((value) => value.nonNulls.toList());
|
||||
|
||||
final tracksFromMetadata = filesWithMetadata
|
||||
.map(
|
||||
|
@ -11,7 +11,6 @@ import 'package:shelf/shelf.dart';
|
||||
import 'package:spotube/extensions/artist_simple.dart';
|
||||
import 'package:spotube/extensions/image.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/provider/audio_player/audio_player.dart';
|
||||
import 'package:spotube/provider/audio_player/state.dart';
|
||||
@ -125,14 +124,9 @@ class ServerPlaybackRoutes {
|
||||
)
|
||||
.catchError((e, stack) async {
|
||||
AppLogger.reportError(e, stack);
|
||||
final sourcedTrack = userPreferences.audioSource == AudioSource.youtube &&
|
||||
e is DioException
|
||||
? await ref
|
||||
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
||||
.refreshStreamingUrl()
|
||||
: await ref
|
||||
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
||||
.switchToAlternativeSources();
|
||||
final sourcedTrack = await ref
|
||||
.read(sourcedTrackProvider(SpotubeMedia(track)).notifier)
|
||||
.refreshStreamingUrl();
|
||||
|
||||
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,
|
||||
|
@ -39,7 +39,7 @@ class CategoryPlaylistsNotifier extends AutoDisposeFamilyPaginatedAsyncNotifier<
|
||||
(json) => PlaylistsFeatured.fromJson(json),
|
||||
).getPage(limit, offset);
|
||||
|
||||
final items = playlists.items?.whereNotNull().toList() ?? [];
|
||||
final items = playlists.items?.nonNulls.toList() ?? [];
|
||||
|
||||
return (
|
||||
items: items,
|
||||
|
@ -207,6 +207,10 @@ class UserPreferencesNotifier extends Notifier<PreferencesTableData> {
|
||||
setData(PreferencesTableCompanion(audioSource: Value(type)));
|
||||
}
|
||||
|
||||
void setYoutubeClientEngine(YoutubeClientEngine engine) {
|
||||
setData(PreferencesTableCompanion(youtubeClientEngine: Value(engine)));
|
||||
}
|
||||
|
||||
void setSystemTitleBar(bool isSystemTitleBar) {
|
||||
setData(
|
||||
PreferencesTableCompanion(
|
||||
|
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:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
@ -87,4 +88,31 @@ abstract class KVStoreService {
|
||||
sharedPreferences.getBool('hasMigratedToDrift') ?? false;
|
||||
static Future<void> setHasMigratedToDrift(bool value) async =>
|
||||
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:spotify/spotify.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
import 'package:spotube/services/sourced_track/exceptions.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_info.dart';
|
||||
import 'package:spotube/services/sourced_track/models/source_map.dart';
|
||||
import 'package:spotube/services/sourced_track/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/youtube.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
abstract class SourcedTrack extends Track {
|
||||
final SourceMap source;
|
||||
@ -97,11 +90,8 @@ abstract class SourcedTrack extends Track {
|
||||
}
|
||||
|
||||
static String getSearchTerm(Track track) {
|
||||
final artists = (track.artists ?? [])
|
||||
.map((ar) => ar.name)
|
||||
.toList()
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
final artists =
|
||||
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
||||
|
||||
final title = ServiceUtils.getTitle(
|
||||
track.name!,
|
||||
@ -112,100 +102,21 @@ abstract class SourcedTrack extends Track {
|
||||
return "$title - ${artists.join(", ")}";
|
||||
}
|
||||
|
||||
static fetchFromTrackAltSource({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
try {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.invidious ||
|
||||
AudioSource.jiosaavn =>
|
||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.youtube =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on TrackNotFoundError catch (_) {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.youtube ||
|
||||
AudioSource.invidious =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: true,
|
||||
),
|
||||
AudioSource.jiosaavn =>
|
||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on HttpClientClosedException catch (_) {
|
||||
return await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
} on VideoUnplayableException catch (_) {
|
||||
return await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref);
|
||||
} catch (e) {
|
||||
if (e is DioException || e is ClientException || e is SocketException) {
|
||||
return await JioSaavnSourcedTrack.fetchFromTrack(
|
||||
track: track,
|
||||
ref: ref,
|
||||
weakMatch: preferences.audioSource == AudioSource.jiosaavn,
|
||||
);
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<SourcedTrack> fetchFromTrack({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
final preferences = ref.read(userPreferencesProvider);
|
||||
try {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.youtube =>
|
||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.jiosaavn =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.invidious =>
|
||||
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
} on TrackNotFoundError catch (_) {
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.piped ||
|
||||
AudioSource.youtube ||
|
||||
AudioSource.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;
|
||||
}
|
||||
return switch (preferences.audioSource) {
|
||||
AudioSource.youtube =>
|
||||
await YoutubeSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.piped =>
|
||||
await PipedSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.invidious =>
|
||||
await InvidiousSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
AudioSource.jiosaavn =>
|
||||
await JioSaavnSourcedTrack.fetchFromTrack(track: track, ref: ref),
|
||||
};
|
||||
}
|
||||
|
||||
static Future<List<SiblingType>> fetchSiblings({
|
||||
|
@ -50,6 +50,22 @@ class InvidiousSourcedTrack extends SourcedTrack {
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) 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 cachedSource = await (database.select(database.sourceMatchTable)
|
||||
..where((s) => s.trackId.equals(track.id!))
|
||||
|
@ -50,6 +50,19 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) 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 cachedSource = await (database.select(database.sourceMatchTable)
|
||||
..where((s) => s.trackId.equals(track.id!))
|
||||
@ -183,11 +196,8 @@ class PipedSourcedTrack extends SourcedTrack {
|
||||
: preference.searchMode == SearchMode.youtubeMusic;
|
||||
|
||||
if (isYouTubeMusic) {
|
||||
final artists = (track.artists ?? [])
|
||||
.map((ar) => ar.name)
|
||||
.toList()
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
final artists =
|
||||
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
||||
|
||||
return await Future.wait(
|
||||
searchResults
|
||||
|
@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/models/database/database.dart';
|
||||
import 'package:spotube/provider/database/database.dart';
|
||||
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||
import 'package:spotube/services/logger/logger.dart';
|
||||
import 'package:spotube/services/song_link/song_link.dart';
|
||||
import 'package:spotube/services/sourced_track/enums.dart';
|
||||
@ -15,7 +16,6 @@ import 'package:spotube/services/sourced_track/sourced_track.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
final youtubeClient = YoutubeExplode();
|
||||
final officialMusicRegex = RegExp(
|
||||
r"official\s(video|audio|music\svideo|lyric\svideo|visualizer)",
|
||||
caseSensitive: false,
|
||||
@ -43,24 +43,15 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
required super.ref,
|
||||
});
|
||||
|
||||
static Future<StreamManifest> _getStreamManifest(String id) async {
|
||||
return youtubeClient.videos.streamsClient.getManifest(
|
||||
id,
|
||||
requireWatchPage: false,
|
||||
ytClients: [
|
||||
YoutubeApiClient.android,
|
||||
YoutubeApiClient.mweb,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
static Future<YoutubeSourcedTrack> fetchFromTrack({
|
||||
required Track track,
|
||||
required Ref ref,
|
||||
}) async {
|
||||
// Indicates the track is requesting a stream refresh
|
||||
if (track is YoutubeSourcedTrack) {
|
||||
final manifest = await _getStreamManifest(track.sourceInfo.id);
|
||||
final manifest = await ref
|
||||
.read(youtubeEngineProvider)
|
||||
.getStreamManifest(track.sourceInfo.id);
|
||||
|
||||
final sourcedTrack = YoutubeSourcedTrack(
|
||||
ref: ref,
|
||||
@ -108,8 +99,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
track: track,
|
||||
);
|
||||
}
|
||||
final item = await youtubeClient.videos.get(cachedSource.sourceId);
|
||||
final manifest = await _getStreamManifest(cachedSource.sourceId);
|
||||
final (item, manifest) = await ref
|
||||
.read(youtubeEngineProvider)
|
||||
.getVideoWithStreamInfo(cachedSource.sourceId);
|
||||
|
||||
final sourcedTrack = YoutubeSourcedTrack(
|
||||
ref: ref,
|
||||
siblings: [],
|
||||
@ -162,10 +155,13 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
static Future<SiblingType> toSiblingType(
|
||||
int index,
|
||||
YoutubeVideoInfo item,
|
||||
dynamic ref,
|
||||
) async {
|
||||
assert(ref is WidgetRef || ref is Ref, "Invalid ref type");
|
||||
SourceMap? sourceMap;
|
||||
if (index == 0) {
|
||||
final manifest = await _getStreamManifest(item.id);
|
||||
final manifest =
|
||||
await ref.read(youtubeEngineProvider).getStreamManifest(item.id);
|
||||
sourceMap = toSourceMap(manifest);
|
||||
}
|
||||
|
||||
@ -188,11 +184,8 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
|
||||
static List<YoutubeVideoInfo> rankResults(
|
||||
List<YoutubeVideoInfo> results, Track track) {
|
||||
final artists = (track.artists ?? [])
|
||||
.map((ar) => ar.name)
|
||||
.toList()
|
||||
.whereNotNull()
|
||||
.toList();
|
||||
final artists =
|
||||
(track.artists ?? []).map((ar) => ar.name).toList().nonNulls.toList();
|
||||
|
||||
return results
|
||||
.sorted((a, b) => b.views.compareTo(a.views))
|
||||
@ -259,8 +252,11 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
await toSiblingType(
|
||||
0,
|
||||
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) {
|
||||
@ -271,15 +267,13 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
|
||||
final query = SourcedTrack.getSearchTerm(track);
|
||||
|
||||
final searchResults = await youtubeClient.search.search(
|
||||
"$query - Topic",
|
||||
filter: TypeFilters.video,
|
||||
);
|
||||
final searchResults =
|
||||
await ref.read(youtubeEngineProvider).searchVideos(query);
|
||||
|
||||
if (ServiceUtils.onlyContainsEnglish(query)) {
|
||||
return await Future.wait(searchResults
|
||||
.map(YoutubeVideoInfo.fromVideo)
|
||||
.mapIndexed(toSiblingType));
|
||||
.mapIndexed((index, info) => toSiblingType(index, info, ref)));
|
||||
}
|
||||
|
||||
final rankedSiblings = rankResults(
|
||||
@ -287,7 +281,10 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
track,
|
||||
);
|
||||
|
||||
return await Future.wait(rankedSiblings.mapIndexed(toSiblingType));
|
||||
return await Future.wait(
|
||||
rankedSiblings
|
||||
.mapIndexed((index, info) => toSiblingType(index, info, ref)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -305,7 +302,9 @@ class YoutubeSourcedTrack extends SourcedTrack {
|
||||
final newSiblings = siblings.where((s) => s.id != sibling.id).toList()
|
||||
..insert(0, sourceInfo);
|
||||
|
||||
final manifest = await _getStreamManifest(newSourceInfo.id);
|
||||
final manifest = await ref
|
||||
.read(youtubeEngineProvider)
|
||||
.getStreamManifest(newSourceInfo.id);
|
||||
|
||||
final database = ref.read(databaseProvider);
|
||||
|
||||
|
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
|
||||
- libsoup-3.0-0 | libsoup-2.4-0
|
||||
|
||||
suggested_dependencies:
|
||||
- yt-dlp
|
||||
|
||||
essential: false
|
||||
icon: assets/spotube-logo.png
|
||||
|
||||
|
26
pubspec.lock
26
pubspec.lock
@ -914,6 +914,15 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1210,13 +1219,13 @@ packages:
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http_parser
|
||||
sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
|
||||
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -2734,6 +2743,15 @@ packages:
|
||||
url: "https://github.com/Hexer10/youtube_explode_dart.git"
|
||||
source: git
|
||||
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:
|
||||
dart: ">=3.6.0 <4.0.0"
|
||||
dart: ">=3.6.1 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
@ -140,6 +140,14 @@ dependencies:
|
||||
git:
|
||||
url: https://github.com/Hexer10/youtube_explode_dart.git
|
||||
ref: e519db65ad0b0a40b12f69285932f9db509da3cf
|
||||
yt_dlp_dart:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/yt_dlp_dart.git
|
||||
ref: e2d82305fab18566408d6f8758361017d1640c3d
|
||||
flutter_new_pipe_extractor:
|
||||
git:
|
||||
url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git
|
||||
http_parser: ^4.1.2
|
||||
|
||||
dev_dependencies:
|
||||
build_runner: ^2.4.13
|
||||
|
@ -3,6 +3,7 @@
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v4.dart' as v4;
|
||||
import 'schema_v3.dart' as v3;
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v1.dart' as v1;
|
||||
@ -11,6 +12,8 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 4:
|
||||
return v4.DatabaseAtV4(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
case 2:
|
||||
@ -22,5 +25,5 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [1, 2, 3];
|
||||
static const versions = const [1, 2, 3, 4];
|
||||
}
|
||||
|
3433
test/drift/app_db/generated/schema_v4.dart
Normal file
3433
test/drift/app_db/generated/schema_v4.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -15,7 +15,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"bn": [
|
||||
@ -34,7 +41,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ca": [
|
||||
@ -53,7 +67,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"cs": [
|
||||
@ -72,7 +93,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"de": [
|
||||
@ -91,7 +119,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"es": [
|
||||
@ -110,7 +145,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"eu": [
|
||||
@ -129,7 +171,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"fa": [
|
||||
@ -148,7 +197,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"fi": [
|
||||
@ -167,7 +223,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"fr": [
|
||||
@ -186,7 +249,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"hi": [
|
||||
@ -205,7 +275,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"id": [
|
||||
@ -224,7 +301,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"it": [
|
||||
@ -243,7 +327,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
@ -262,7 +353,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ka": [
|
||||
@ -281,7 +379,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ko": [
|
||||
@ -300,7 +405,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ne": [
|
||||
@ -319,7 +431,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"nl": [
|
||||
@ -338,7 +457,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"pl": [
|
||||
@ -357,7 +483,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"pt": [
|
||||
@ -376,7 +509,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"ru": [
|
||||
@ -395,7 +535,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"th": [
|
||||
@ -414,7 +561,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"tr": [
|
||||
@ -433,7 +587,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"uk": [
|
||||
@ -452,7 +613,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"vi": [
|
||||
@ -471,7 +639,14 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
],
|
||||
|
||||
"zh": [
|
||||
@ -490,6 +665,13 @@
|
||||
"no_tracks_listened_yet",
|
||||
"not_following_artists",
|
||||
"no_favorite_albums_yet",
|
||||
"no_logs_found"
|
||||
"no_logs_found",
|
||||
"youtube_engine",
|
||||
"youtube_engine_not_installed_title",
|
||||
"youtube_engine_not_installed_message",
|
||||
"youtube_engine_set_path",
|
||||
"youtube_engine_unix_issue_message",
|
||||
"download",
|
||||
"file_not_found"
|
||||
]
|
||||
}
|
||||
|
@ -22,8 +22,8 @@ SetupIconFile={{SETUP_ICON_FILE}}
|
||||
WizardStyle=modern
|
||||
WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp"
|
||||
PrivilegesRequired={{PRIVILEGES_REQUIRED}}
|
||||
ArchitecturesAllowed=x64
|
||||
ArchitecturesInstallIn64BitMode=x64
|
||||
ArchitecturesAllowed=x64compatible
|
||||
ArchitecturesInstallIn64BitMode=x64compatible
|
||||
|
||||
[Languages]
|
||||
{% for locale in LOCALES %}
|
||||
|
Loading…
Reference in New Issue
Block a user