Compare commits

...

5 Commits

Author SHA1 Message Date
Kingkor Roy Tirtho
fda2257119 feat: add default plugin loading capability 2025-11-07 22:51:48 +06:00
Kingkor Roy Tirtho
7c632c8f06 cd: remove unnecessary stuff for android build 2025-11-07 21:56:03 +06:00
Kingkor Roy Tirtho
a012a8f3af chore: fix unique index on source_match_table causing failure on insert 2025-11-07 20:28:09 +06:00
Kingkor Roy Tirtho
64f937bd14 chore: remove useless appbundle build 2025-11-07 18:59:55 +06:00
Kingkor Roy Tirtho
d1b73dbb1c feat: add NewPipe support for desktop platforms 2025-11-07 18:48:18 +06:00
18 changed files with 139 additions and 111 deletions

View File

@ -12,10 +12,10 @@ on:
type: boolean
default: true
jobs:
description: Jobs to run (flathub,aur,winget,chocolatey,playstore)
description: Jobs to run (flathub,aur,winget,chocolatey)
required: true
type: string
default: "flathub,aur,winget,chocolatey,playstore"
default: "flathub,aur,winget,chocolatey"
jobs:
flathub:
@ -112,26 +112,26 @@ jobs:
- name: Tagname (workflow dispatch)
run: echo 'TAG_NAME=${{inputs.version}}' >> $GITHUB_ENV
- uses: robinraju/release-downloader@main
with:
repository: KRTirtho/spotube
tag: v${{ env.TAG_NAME }}
tarBall: false
zipBall: false
out-file-path: dist
fileName: "Spotube-playstore-all-arch.aab"
# - uses: robinraju/release-downloader@main
# with:
# repository: KRTirtho/spotube
# tag: v${{ env.TAG_NAME }}
# tarBall: false
# zipBall: false
# out-file-path: dist
# fileName: "Spotube-playstore-all-arch.aab"
- name: Create service-account.json
run: |
echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
# - name: Create service-account.json
# run: |
# echo "${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_BASE64 }}" | base64 -d > service-account.json
- name: Upload Android Release to Play Store
if: ${{!inputs.dry_run}}
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJson: ./service-account.json
releaseFiles: ./dist/Spotube-playstore-all-arch.aab
packageName: oss.krtirtho.spotube
track: production
status: draft
releaseName: ${{ env.TAG_NAME }}
# - name: Upload Android Release to Play Store
# if: ${{!inputs.dry_run}}
# uses: r0adkll/upload-google-play@v1
# with:
# serviceAccountJson: ./service-account.json
# releaseFiles: ./dist/Spotube-playstore-all-arch.aab
# packageName: oss.krtirtho.spotube
# track: production
# status: draft
# releaseName: ${{ env.TAG_NAME }}

View File

@ -49,7 +49,6 @@ jobs:
arch: all
files: |
build/Spotube-android-all-arch.apk
build/Spotube-playstore-all-arch.aab
- os: windows-latest
platform: windows
arch: x86
@ -77,6 +76,14 @@ jobs:
cache: true
git-source: https://github.com/flutter/flutter.git
- name: free disk space
if: ${{ matrix.platform == 'android' }}
run: |
sudo swapoff -a
sudo rm -f /swapfile
sudo apt clean
docker rmi $(docker image ls -aq)
df -h
- name: Setup Java
if: ${{matrix.platform == 'android'}}
uses: actions/setup-java@v4

View File

@ -2,9 +2,7 @@ import 'dart:async';
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:collection/collection.dart';
import 'package:path/path.dart';
import 'package:xml/xml.dart';
import '../../core/env.dart';
import 'common.dart';
@ -24,39 +22,6 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
"flutter build apk --flavor ${CliEnv.channel.name}",
);
await dotEnvFile.writeAsString(
"\nENABLE_UPDATE_CHECK=0"
"\nHIDE_DONATIONS=1",
mode: FileMode.append,
);
final androidManifestFile = File(
join(cwd.path, "android", "app", "src", "main", "AndroidManifest.xml"));
final androidManifestXml =
XmlDocument.parse(await androidManifestFile.readAsString());
final deletingElement =
androidManifestXml.findAllElements("meta-data").firstWhereOrNull(
(el) =>
el.getAttribute("android:name") ==
"com.google.android.gms.car.application",
);
deletingElement?.parent?.children.remove(deletingElement);
await androidManifestFile.writeAsString(
androidManifestXml.toXmlString(pretty: true),
);
await shell.run(
"""
dart run build_runner clean
dart run build_runner build --delete-conflicting-outputs
flutter build appbundle --flavor ${CliEnv.channel.name}
""",
);
final ogApkFile = File(
join(
"build",
@ -71,22 +36,6 @@ class AndroidBuildCommand extends Command with BuildCommandCommonSteps {
join(cwd.path, "build", "Spotube-android-all-arch.apk"),
);
final ogAppbundleFile = File(
join(
cwd.path,
"build",
"app",
"outputs",
"bundle",
"${CliEnv.channel.name}Release",
"app-${CliEnv.channel.name}-release.aab",
),
);
await ogAppbundleFile.copy(
join(cwd.path, "build", "Spotube-playstore-all-arch.aab"),
);
stdout.writeln("✅ Built Android Apk and Appbundle");
}
}

View File

@ -83,6 +83,8 @@ Future<void> main(List<String> rawArgs) async {
// force High Refresh Rate on some Android devices (like One Plus)
if (kIsAndroid) {
await FlutterDisplayMode.setHighRefreshRate();
}
if (kIsAndroid || kIsDesktop) {
await NewPipeExtractor.init();
}

View File

@ -19,10 +19,10 @@ import 'package:spotube/services/kv_store/kv_store.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/logger/logger.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:spotube/utils/platform.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
@ -212,26 +212,14 @@ class AppDatabase extends _$AppDatabase {
);
},
from9To10: (m, schema) async {
try {
await m
.dropColumn(schema.preferencesTable, "piped_instance")
.catchError((e) {});
await m
.dropColumn(schema.preferencesTable, "invidious_instance")
.catchError((e) {});
await m
.addColumn(
schema.sourceMatchTable,
sourceMatchTable.sourceInfo,
)
.catchError((e) {});
await m
.dropColumn(schema.sourceMatchTable, "source_id")
.catchError((e) {});
} catch (e) {
AppLogger.log.e(e);
return;
}
await m.dropColumn(schema.preferencesTable, "piped_instance");
await m.dropColumn(schema.preferencesTable, "invidious_instance");
await m.addColumn(
schema.sourceMatchTable,
sourceMatchTable.sourceInfo,
);
await customStatement("DROP INDEX IF EXISTS uniq_track_match;");
await m.dropColumn(schema.sourceMatchTable, "source_id");
},
),
);

View File

@ -111,7 +111,9 @@ class PreferencesTable extends Table {
localLibraryLocation: [],
themeMode: ThemeMode.system,
audioSourceId: null,
youtubeClientEngine: YoutubeClientEngine.youtubeExplode,
youtubeClientEngine: kIsIOS
? YoutubeClientEngine.youtubeExplode
: YoutubeClientEngine.newPipe,
discordPresence: true,
endlessPlayback: true,
enableConnect: false,

View File

@ -1,10 +1,5 @@
part of '../database.dart';
@TableIndex(
name: "uniq_track_match",
columns: {#trackId, #sourceInfo, #sourceType},
unique: true,
)
class SourceMatchTable extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get trackId => text()();

View File

@ -4,6 +4,7 @@ import 'dart:io';
import 'package:collection/collection.dart';
import 'package:dio/dio.dart';
import 'package:drift/drift.dart';
import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
@ -101,7 +102,11 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
final plugins = await database.pluginsTable.select().get();
return await toStatePlugins(plugins);
final pluginState = await toStatePlugins(plugins);
await _loadDefaultPlugins(pluginState);
return pluginState;
}
Future<MetadataPluginState> toStatePlugins(
@ -171,6 +176,45 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
);
}
Future<void> _loadDefaultPlugins(MetadataPluginState pluginState) async {
const plugins = [
"spotube-plugin-musicbrainz-listenbrainz",
"spotube-plugin-youtube-audio",
];
for (final plugin in plugins) {
final byteData = await rootBundle.load(
"assets/plugins/$plugin/plugin.smplug",
);
final pluginConfig =
await extractPluginArchive(byteData.buffer.asUint8List());
try {
await addPlugin(pluginConfig);
} on MetadataPluginException catch (e) {
if (e.errorCode == MetadataPluginErrorCode.duplicatePlugin &&
await isPluginUpdate(pluginConfig)) {
final oldConfig = pluginState.plugins
.firstWhereOrNull((p) => p.slug == pluginConfig.slug);
if (oldConfig == null) continue;
final isDefaultMetadata =
oldConfig == pluginState.defaultMetadataPluginConfig;
final isDefaultAudioSource =
oldConfig == pluginState.defaultAudioSourcePluginConfig;
await removePlugin(pluginConfig);
await addPlugin(pluginConfig);
if (isDefaultMetadata) {
await setDefaultMetadataPlugin(pluginConfig);
}
if (isDefaultAudioSource) {
await setDefaultAudioSourcePlugin(pluginConfig);
}
}
}
}
}
Uri _getGithubReleasesUrl(String repoUrl) {
final parsedUri = Uri.parse(repoUrl);
final uri = parsedUri.replace(
@ -373,11 +417,19 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
repository: Value(plugin.repository),
// Setting the very first plugin as the default plugin
selectedForMetadata: Value(
(state.valueOrNull?.plugins.isEmpty ?? true) &&
(state.valueOrNull?.plugins
.where(
(d) => d.abilities.contains(PluginAbilities.metadata))
.isEmpty ??
true) &&
plugin.abilities.contains(PluginAbilities.metadata),
),
selectedForAudioSource: Value(
(state.valueOrNull?.plugins.isEmpty ?? true) &&
(state.valueOrNull?.plugins
.where((d) =>
d.abilities.contains(PluginAbilities.audioSource))
.isEmpty ??
true) &&
plugin.abilities.contains(PluginAbilities.audioSource),
),
),
@ -420,6 +472,27 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
}
}
Future<bool> isPluginUpdate(PluginConfiguration newPlugin) async {
final pluginRes = await (database.pluginsTable.select()
..where(
(tbl) =>
tbl.name.equals(newPlugin.name) &
tbl.author.equals(newPlugin.author),
)
..limit(1))
.get();
if (pluginRes.isEmpty) {
return false;
}
final oldPlugin = pluginRes.first;
final oldPluginApiVersion = Version.parse(oldPlugin.pluginApiVersion);
final newPluginApiVersion = Version.parse(newPlugin.pluginApiVersion);
return newPluginApiVersion > oldPluginApiVersion;
}
Future<void> updatePlugin(
PluginConfiguration plugin,
PluginUpdateAvailable update,

View File

@ -6,7 +6,7 @@ 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;
static bool get isAvailableForPlatform => kIsAndroid || kIsDesktop;
AudioOnlyStreamInfo _parseAudioStream(AudioStream stream, String videoId) {
return AudioOnlyStreamInfo(

View File

@ -8,6 +8,7 @@
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_new_pipe_extractor/flutter_new_pipe_extractor_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <flutter_timezone/flutter_timezone_plugin.h>
#include <gtk/gtk_plugin.h>
@ -30,6 +31,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
g_autoptr(FlPluginRegistrar) flutter_new_pipe_extractor_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterNewPipeExtractorPlugin");
flutter_new_pipe_extractor_plugin_register_with_registrar(flutter_new_pipe_extractor_registrar);
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_webview_window
file_selector_linux
flutter_new_pipe_extractor
flutter_secure_storage_linux
flutter_timezone
gtk

View File

@ -15,6 +15,7 @@ import device_info_plus
import file_picker
import file_selector_macos
import flutter_inappwebview_macos
import flutter_new_pipe_extractor
import flutter_secure_storage_macos
import flutter_timezone
import irondash_engine_context
@ -44,6 +45,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
FlutterNewPipeExtractorPlugin.register(with: registry.registrar(forPlugin: "FlutterNewPipeExtractorPlugin"))
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin"))

View File

@ -955,7 +955,7 @@ packages:
description:
path: "."
ref: HEAD
resolved-ref: d4d71545111c8ca6c91f0040091c42d74cce1762
resolved-ref: "898fd4ebcef77f5177b08aa6f9b9047bd02c6b9b"
url: "https://github.com/KRTirtho/flutter_new_pipe_extractor.git"
source: git
version: "0.1.0"
@ -2002,10 +2002,10 @@ packages:
dependency: transitive
description:
name: random_user_agents
sha256: "19facde509a2482dababb454faf2aceff797a6ae08e80f91268c0c8a7420f03b"
sha256: "95647149687167e82a7b39e1b4616fdebb574981b71b6f0cfca21b69f36293a8"
url: "https://pub.dev"
source: hosted
version: "1.0.15"
version: "1.0.17"
recase:
dependency: transitive
description:
@ -2821,7 +2821,7 @@ packages:
source: hosted
version: "1.1.0"
xml:
dependency: "direct dev"
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226

View File

@ -177,7 +177,6 @@ dev_dependencies:
process_run: ^0.14.2
pubspec_parse: ^1.3.0
pub_api_client: ^3.0.0
xml: ^6.5.0
io: ^1.0.4
drift_dev: ^2.21.0
auto_route_generator: ^9.0.0
@ -228,6 +227,8 @@ flutter:
- assets/branding/spotube-logo.png
- assets/branding/spotube-logo-light.png
- assets/branding/spotube-logo.ico
- assets/plugins/spotube-plugin-musicbrainz-listenbrainz/plugin.smplug
- assets/plugins/spotube-plugin-youtube-audio/plugin.smplug
- LICENSE
- packages/flutter_undraw/assets/undraw/access_denied.svg
- packages/flutter_undraw/assets/undraw/fixing_bugs.svg

View File

@ -12,6 +12,7 @@
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_new_pipe_extractor/flutter_new_pipe_extractor_plugin_c_api.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
#include <irondash_engine_context/irondash_engine_context_plugin_c_api.h>
@ -39,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
FlutterNewPipeExtractorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterNewPipeExtractorPluginCApi"));
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
FlutterTimezonePluginCApiRegisterWithRegistrar(

View File

@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
desktop_webview_window
file_selector_windows
flutter_inappwebview_windows
flutter_new_pipe_extractor
flutter_secure_storage_windows
flutter_timezone
irondash_engine_context