feat: implement custom path support for youtube engines

This commit is contained in:
Kingkor Roy Tirtho 2025-02-08 21:39:07 +06:00
parent f8663875e3
commit fc9121b488
11 changed files with 245 additions and 66 deletions

View File

@ -46,3 +46,10 @@ gensums:
migrate: migrate:
dart run drift_dev make-migrations dart run drift_dev make-migrations
dmg:
flutter build macos &&\
if [ -f dist/Spotube-macos-universal.dmg ];\
then rm dist/Spotube-macos-universal.dmg;\
fi &&\
appdmg appdmg.json dist/Spotube-macos-universal.dmg

View File

@ -1,9 +1,12 @@
import 'dart:io';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/youtube_engine_not_installed_dialog.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/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'; import 'package:spotube/services/youtube_engine/yt_dlp_engine.dart';
void useCheckYtDlpInstalled(WidgetRef ref) { void useCheckYtDlpInstalled(WidgetRef ref) {
@ -17,8 +20,12 @@ void useCheckYtDlpInstalled(WidgetRef ref) {
), ),
); );
final customPath =
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp);
if (youtubeEngine == YoutubeClientEngine.ytDlp && if (youtubeEngine == YoutubeClientEngine.ytDlp &&
!await YtDlpEngine.isInstalled() && !await YtDlpEngine.isInstalled() &&
(customPath == null || !await File(customPath).exists()) &&
context.mounted) { context.mounted) {
await showDialog( await showDialog(
context: context, context: context,

View File

@ -418,6 +418,9 @@
"no_logs_found": "No logs found", "no_logs_found": "No logs found",
"youtube_engine": "YouTube Engine", "youtube_engine": "YouTube Engine",
"youtube_engine_not_installed_title": "{engine} is not installed", "youtube_engine_not_installed_title": "{engine} is not installed",
"youtube_engine_not_installed_message": "{engine} is not installed in your system.\nPlease install it and make sure it's available in the PATH variable\n\nAfter installing, restart the app", "youtube_engine_not_installed_message": "{engine} is not installed in your system.",
"download": "Download" "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"
} }

View File

@ -84,10 +84,15 @@ Future<void> main(List<String> rawArgs) async {
MetadataGod.initialize(); MetadataGod.initialize();
} }
await KVStoreService.initialize();
if (kIsDesktop) { if (kIsDesktop) {
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);
await YtDlp.instance await YtDlp.instance
.setBinaryLocation("yt-dlp${kIsWindows ? '.exe' : ''}") .setBinaryLocation(
KVStoreService.getYoutubeEnginePath(YoutubeClientEngine.ytDlp) ??
"yt-dlp${kIsWindows ? '.exe' : ''}",
)
.catchError((e, stack) => null); .catchError((e, stack) => null);
await FlutterDiscordRPC.initialize(Env.discordAppId); await FlutterDiscordRPC.initialize(Env.discordAppId);
} }
@ -96,7 +101,6 @@ Future<void> main(List<String> rawArgs) async {
await SMTCWindows.initialize(); await SMTCWindows.initialize();
} }
await KVStoreService.initialize();
await EncryptedKvStoreService.initialize(); await EncryptedKvStoreService.initialize();
final database = AppDatabase(); final database = AppDatabase();

View File

@ -1,9 +1,18 @@
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.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/collections/spotube_icons.dart';
import 'package:spotube/components/form/text_form_field.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/database/database.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:url_launcher/url_launcher.dart';
import 'package:yt_dlp_dart/yt_dlp_dart.dart';
const engineDownloadUrls = { const engineDownloadUrls = {
YoutubeClientEngine.ytDlp: YoutubeClientEngine.ytDlp:
@ -19,6 +28,9 @@ class YouTubeEngineNotInstalledDialog extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final controller = useTextEditingController();
final formKey = useMemoized(() => GlobalKey<FormBuilderState>(), []);
return AlertDialog( return AlertDialog(
title: Row( title: Row(
spacing: 8, spacing: 8,
@ -31,33 +43,77 @@ class YouTubeEngineNotInstalledDialog extends HookConsumerWidget {
), ),
], ],
), ),
content: Column( content: ConstrainedBox(
mainAxisSize: MainAxisSize.min, constraints: const BoxConstraints(maxWidth: 400),
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
spacing: 10, mainAxisSize: MainAxisSize.min,
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( spacing: 8,
context.l10n.youtube_engine_not_installed_message(engine.label), children: [
), Text(
if (engineDownloadUrls[engine] != null) context.l10n.youtube_engine_not_installed_message(engine.label),
Row(
mainAxisSize: MainAxisSize.min,
children: [
Text("${context.l10n.download}:"),
Button.link(
child: Text(engineDownloadUrls[engine]!.split("?").first),
onPressed: () async {
launchUrl(Uri.parse(engineDownloadUrls[engine]!));
},
),
],
), ),
], 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: [ actions: [
Button.text(
onPressed: () {
if (!context.mounted) return;
Navigator.of(context).pop(false);
},
child: Text(context.l10n.cancel),
),
Button.secondary( Button.secondary(
onPressed: () => Navigator.of(context).pop(), onPressed: () async {
child: Text(context.l10n.ok), 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),
), ),
], ],
); );

View File

@ -214,12 +214,12 @@ class SettingsPlaybackSection extends HookConsumerWidget {
if (value == YoutubeClientEngine.ytDlp && if (value == YoutubeClientEngine.ytDlp &&
!await YtDlpEngine.isInstalled() && !await YtDlpEngine.isInstalled() &&
context.mounted) { context.mounted) {
await showDialog( final hasInstalled = await showDialog<bool>(
context: context, context: context,
builder: (context) => builder: (context) =>
YouTubeEngineNotInstalledDialog(engine: value), YouTubeEngineNotInstalledDialog(engine: value),
); );
return; if (hasInstalled != true) return;
} }
preferencesNotifier.setYoutubeClientEngine(value); preferencesNotifier.setYoutubeClientEngine(value);
}, },

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:encrypt/encrypt.dart'; import 'package:encrypt/encrypt.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/services/wm_tools/wm_tools.dart'; import 'package:spotube/services/wm_tools/wm_tools.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@ -87,4 +88,31 @@ abstract class KVStoreService {
sharedPreferences.getBool('hasMigratedToDrift') ?? false; sharedPreferences.getBool('hasMigratedToDrift') ?? false;
static Future<void> setHasMigratedToDrift(bool value) async => static Future<void> setHasMigratedToDrift(bool value) async =>
await sharedPreferences.setBool('hasMigratedToDrift', value); await sharedPreferences.setBool('hasMigratedToDrift', value);
static Map<String, dynamic>? get _youtubeEnginePaths {
final jsonRaw = sharedPreferences.getString('ytDlpPath');
if (jsonRaw == null) {
return null;
}
return jsonDecode(jsonRaw);
}
static String? getYoutubeEnginePath(YoutubeClientEngine engine) {
return _youtubeEnginePaths?[engine.name];
}
static Future<void> setYoutubeEnginePath(
YoutubeClientEngine engine,
String path,
) async {
await sharedPreferences.setString(
'ytDlpPath',
jsonEncode({
...?_youtubeEnginePaths,
engine.name: path,
}),
);
}
} }

View File

@ -2737,11 +2737,9 @@ packages:
yt_dlp_dart: yt_dlp_dart:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "../yt_dlp_dart"
ref: "4199bb019542bae361fbb38b3448b3583fbca022" relative: true
resolved-ref: "4199bb019542bae361fbb38b3448b3583fbca022" source: path
url: "https://github.com/KRTirtho/yt_dlp_dart.git"
source: git
version: "1.0.0" version: "1.0.0"
sdks: sdks:
dart: ">=3.6.1 <4.0.0" dart: ">=3.6.1 <4.0.0"

View File

@ -141,9 +141,7 @@ dependencies:
url: https://github.com/Hexer10/youtube_explode_dart.git url: https://github.com/Hexer10/youtube_explode_dart.git
ref: e519db65ad0b0a40b12f69285932f9db509da3cf ref: e519db65ad0b0a40b12f69285932f9db509da3cf
yt_dlp_dart: yt_dlp_dart:
git: path: ../yt_dlp_dart
url: https://github.com/KRTirtho/yt_dlp_dart.git
ref: 4199bb019542bae361fbb38b3448b3583fbca022
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13

View File

@ -19,7 +19,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"bn": [ "bn": [
@ -42,7 +45,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"ca": [ "ca": [
@ -65,7 +71,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"cs": [ "cs": [
@ -88,7 +97,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"de": [ "de": [
@ -111,7 +123,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"es": [ "es": [
@ -134,7 +149,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"eu": [ "eu": [
@ -157,7 +175,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"fa": [ "fa": [
@ -180,7 +201,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"fi": [ "fi": [
@ -203,7 +227,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"fr": [ "fr": [
@ -226,7 +253,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"hi": [ "hi": [
@ -249,7 +279,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"id": [ "id": [
@ -272,7 +305,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"it": [ "it": [
@ -295,7 +331,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"ja": [ "ja": [
@ -318,7 +357,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"ka": [ "ka": [
@ -341,7 +383,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"ko": [ "ko": [
@ -364,7 +409,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"ne": [ "ne": [
@ -387,7 +435,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"nl": [ "nl": [
@ -410,7 +461,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"pl": [ "pl": [
@ -433,7 +487,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"pt": [ "pt": [
@ -456,7 +513,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"ru": [ "ru": [
@ -479,7 +539,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"th": [ "th": [
@ -502,7 +565,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"tr": [ "tr": [
@ -525,7 +591,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"uk": [ "uk": [
@ -548,7 +617,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"vi": [ "vi": [
@ -571,7 +643,10 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
], ],
"zh": [ "zh": [
@ -594,6 +669,9 @@
"youtube_engine", "youtube_engine",
"youtube_engine_not_installed_title", "youtube_engine_not_installed_title",
"youtube_engine_not_installed_message", "youtube_engine_not_installed_message",
"download" "youtube_engine_set_path",
"youtube_engine_unix_issue_message",
"download",
"file_not_found"
] ]
} }

View File

@ -22,8 +22,8 @@ SetupIconFile={{SETUP_ICON_FILE}}
WizardStyle=modern WizardStyle=modern
WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp" WizardSmallImageFile="..\\..\\assets\\spotube-logo.bmp"
PrivilegesRequired={{PRIVILEGES_REQUIRED}} PrivilegesRequired={{PRIVILEGES_REQUIRED}}
ArchitecturesAllowed=x64 ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64 ArchitecturesInstallIn64BitMode=x64compatible
[Languages] [Languages]
{% for locale in LOCALES %} {% for locale in LOCALES %}