mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-10 17:07:30 +00:00
feat: add yt engine plugin support
This commit is contained in:
parent
4129a61d85
commit
2b9c5730c9
@ -45,8 +45,6 @@ import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
|
|||||||
import 'package:spotube/services/kv_store/kv_store.dart';
|
import 'package:spotube/services/kv_store/kv_store.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
import 'package:spotube/services/wm_tools/wm_tools.dart';
|
||||||
import 'package:spotube/src/rust/api/plugin/models/core.dart';
|
|
||||||
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
|
||||||
import 'package:spotube/src/rust/frb_generated.dart';
|
import 'package:spotube/src/rust/frb_generated.dart';
|
||||||
import 'package:spotube/utils/migrations/sandbox.dart';
|
import 'package:spotube/utils/migrations/sandbox.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -178,73 +176,6 @@ class Spotube extends HookConsumerWidget {
|
|||||||
HomeWidget.registerInteractivityCallback(glanceBackgroundCallback);
|
HomeWidget.registerInteractivityCallback(glanceBackgroundCallback);
|
||||||
}
|
}
|
||||||
|
|
||||||
start() async {
|
|
||||||
final server = await ref.read(serverProvider.future);
|
|
||||||
|
|
||||||
final plugin = SpotubePlugin();
|
|
||||||
const pluginConfiguration = PluginConfiguration(
|
|
||||||
name: "Spotube Plugin",
|
|
||||||
description: "Spotube Plugin",
|
|
||||||
version: "1.0.0",
|
|
||||||
author: "Spotube",
|
|
||||||
entryPoint: "Plugin",
|
|
||||||
pluginApiVersion: "2.0.0",
|
|
||||||
apis: [PluginApi.localstorage, PluginApi.webview],
|
|
||||||
abilities: [PluginAbility.metadata],
|
|
||||||
);
|
|
||||||
final pluginContext = plugin.createContext(
|
|
||||||
serverEndpointUrl:
|
|
||||||
"http://${server.server.address.host}:${server.port}",
|
|
||||||
serverSecret: ref.read(serverRandomSecretProvider),
|
|
||||||
pluginScript: """
|
|
||||||
console.log("Local Timezone", Timezone.getLocalTimezone());
|
|
||||||
console.log("Available Timezones", Timezone.getAvailableTimezones());
|
|
||||||
class AuthEndpoint {
|
|
||||||
}
|
|
||||||
class CoreEndpoint {
|
|
||||||
async checkUpdate() {
|
|
||||||
console.log(globalThis);
|
|
||||||
const webview = await WebView.create("https://spotube.krtirtho.dev");
|
|
||||||
webview.onUrlChange(async (url) => {
|
|
||||||
console.log("url_request: ", url);
|
|
||||||
if (url.includes("/about")) {
|
|
||||||
console.log(await webview.cookies())
|
|
||||||
webview.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
await webview.open();
|
|
||||||
// const res = await SpotubeForm.show("Hello", [
|
|
||||||
// {
|
|
||||||
// objectType: "input",
|
|
||||||
// id: "email",
|
|
||||||
// variant: "text",
|
|
||||||
// placeholder: "Enter your email",
|
|
||||||
// defaultValue: null,
|
|
||||||
// required: true,
|
|
||||||
// regex: null,
|
|
||||||
// }
|
|
||||||
// ])
|
|
||||||
// console.log("Form Result: ", res);
|
|
||||||
// console.log("LocalStorage Value: ", localStorage.getItem("test_key"));
|
|
||||||
// localStorage.setItem("test_key", "test_value");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class Plugin {
|
|
||||||
constructor() {
|
|
||||||
this.auth = new AuthEndpoint();
|
|
||||||
this.core = new CoreEndpoint();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
pluginConfig: pluginConfiguration,
|
|
||||||
);
|
|
||||||
|
|
||||||
await plugin.core.checkUpdate(
|
|
||||||
mpscTx: pluginContext, pluginConfig: pluginConfiguration);
|
|
||||||
}
|
|
||||||
|
|
||||||
start();
|
|
||||||
|
|
||||||
return () {
|
return () {
|
||||||
/// For enabling hot reload for audio player
|
/// For enabling hot reload for audio player
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import 'package:spotube/provider/server/routes/playback.dart';
|
|||||||
import 'package:spotube/provider/server/routes/plugin_apis/form.dart';
|
import 'package:spotube/provider/server/routes/plugin_apis/form.dart';
|
||||||
import 'package:spotube/provider/server/routes/plugin_apis/path_provider.dart';
|
import 'package:spotube/provider/server/routes/plugin_apis/path_provider.dart';
|
||||||
import 'package:spotube/provider/server/routes/plugin_apis/webview.dart';
|
import 'package:spotube/provider/server/routes/plugin_apis/webview.dart';
|
||||||
|
import 'package:spotube/provider/server/routes/plugin_apis/yt_engine.dart';
|
||||||
|
|
||||||
Handler pluginApiAuthMiddleware(Handler handler) {
|
Handler pluginApiAuthMiddleware(Handler handler) {
|
||||||
return (Request request) {
|
return (Request request) {
|
||||||
@ -23,6 +24,7 @@ final serverRouterProvider = Provider((ref) {
|
|||||||
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
||||||
final webviewRoutes = ref.watch(serverWebviewRoutesProvider);
|
final webviewRoutes = ref.watch(serverWebviewRoutesProvider);
|
||||||
final formRoutes = ref.watch(serverFormRoutesProvider);
|
final formRoutes = ref.watch(serverFormRoutesProvider);
|
||||||
|
final ytEngineRoutes = ref.watch(serverYTEngineRoutesProvider);
|
||||||
|
|
||||||
final router = Router();
|
final router = Router();
|
||||||
|
|
||||||
@ -64,6 +66,19 @@ final serverRouterProvider = Provider((ref) {
|
|||||||
pluginApiAuthMiddleware(ServerPathProviderRoutes.getDirectories),
|
pluginApiAuthMiddleware(ServerPathProviderRoutes.getDirectories),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
"/plugin-api/yt-engine/search",
|
||||||
|
pluginApiAuthMiddleware(ytEngineRoutes.search),
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
"/plugin-api/yt-engine/video",
|
||||||
|
pluginApiAuthMiddleware(ytEngineRoutes.getVideo),
|
||||||
|
);
|
||||||
|
router.get(
|
||||||
|
"/plugin-api/yt-engine/stream-manifest",
|
||||||
|
pluginApiAuthMiddleware(ytEngineRoutes.streamManifest),
|
||||||
|
);
|
||||||
|
|
||||||
router.all("/ws", connectRoutes.websocket);
|
router.all("/ws", connectRoutes.websocket);
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
|
|||||||
114
lib/provider/server/routes/plugin_apis/yt_engine.dart
Normal file
114
lib/provider/server/routes/plugin_apis/yt_engine.dart
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:spotube/provider/youtube_engine/youtube_engine.dart';
|
||||||
|
import 'package:spotube/services/youtube_engine/youtube_engine.dart';
|
||||||
|
|
||||||
|
class ServerYTEngineRoutes {
|
||||||
|
final Ref ref;
|
||||||
|
ServerYTEngineRoutes({required this.ref});
|
||||||
|
YouTubeEngine get youtubeEngine => ref.read(youtubeEngineProvider);
|
||||||
|
|
||||||
|
Future<Response> search(Request request) async {
|
||||||
|
final query = request.url.queryParameters["query"];
|
||||||
|
|
||||||
|
if (query == null || query.isEmpty) {
|
||||||
|
return Response.badRequest(
|
||||||
|
body: 'Query parameter "query" is required',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await youtubeEngine.searchVideos(query);
|
||||||
|
final mappedResult = result
|
||||||
|
.map((video) => {
|
||||||
|
'id': video.id.value,
|
||||||
|
'title': video.title,
|
||||||
|
'author': video.author,
|
||||||
|
'duration': video.duration?.inSeconds,
|
||||||
|
'description': video.description,
|
||||||
|
'uploadDate': video.uploadDate?.toIso8601String(),
|
||||||
|
'viewCount': video.engagement.viewCount,
|
||||||
|
'likeCount': video.engagement.likeCount,
|
||||||
|
'isLive': video.isLive,
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return Response.ok(
|
||||||
|
jsonEncode(mappedResult),
|
||||||
|
encoding: utf8,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> getVideo(Request request) async {
|
||||||
|
final videoId = request.url.queryParameters["videoId"];
|
||||||
|
|
||||||
|
if (videoId == null || videoId.isEmpty) {
|
||||||
|
return Response.badRequest(
|
||||||
|
body: 'Query parameter "videoId" is required',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final video = await youtubeEngine.getVideo(videoId);
|
||||||
|
final res = {
|
||||||
|
'id': video.id.value,
|
||||||
|
'title': video.title,
|
||||||
|
'author': video.author,
|
||||||
|
'duration': video.duration?.inSeconds,
|
||||||
|
'description': video.description,
|
||||||
|
'uploadDate': video.uploadDate?.toIso8601String(),
|
||||||
|
'viewCount': video.engagement.viewCount,
|
||||||
|
'likeCount': video.engagement.likeCount,
|
||||||
|
'isLive': video.isLive,
|
||||||
|
};
|
||||||
|
|
||||||
|
return Response.ok(
|
||||||
|
jsonEncode(res),
|
||||||
|
encoding: utf8,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Response> streamManifest(Request request) async {
|
||||||
|
final videoId = request.url.queryParameters["videoId"];
|
||||||
|
|
||||||
|
if (videoId == null || videoId.isEmpty) {
|
||||||
|
return Response.badRequest(
|
||||||
|
body: 'Query parameter "videoId" is required',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final streamManifest =
|
||||||
|
await youtubeEngine.getStreamManifest(videoId).then((manifest) {
|
||||||
|
final streams = manifest.audioOnly
|
||||||
|
.map(
|
||||||
|
(stream) => {
|
||||||
|
'url': stream.url.toString(),
|
||||||
|
'quality': stream.qualityLabel,
|
||||||
|
'bitrate': stream.bitrate.bitsPerSecond,
|
||||||
|
'container': stream.container.name,
|
||||||
|
'videoId': stream.videoId,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
return streams;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Response.ok(
|
||||||
|
jsonEncode(streamManifest),
|
||||||
|
encoding: utf8,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverYTEngineRoutesProvider = Provider<ServerYTEngineRoutes>(
|
||||||
|
(ref) => ServerYTEngineRoutes(ref: ref),
|
||||||
|
);
|
||||||
@ -2,10 +2,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:spotube/models/database/database.dart';
|
import 'package:spotube/models/database/database.dart';
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/youtube_engine/newpipe_engine.dart';
|
import 'package:spotube/services/youtube_engine/newpipe_engine.dart';
|
||||||
|
import 'package:spotube/services/youtube_engine/youtube_engine.dart';
|
||||||
import 'package:spotube/services/youtube_engine/youtube_explode_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/services/youtube_engine/yt_dlp_engine.dart';
|
||||||
|
|
||||||
final youtubeEngineProvider = Provider((ref) {
|
final youtubeEngineProvider = Provider<YouTubeEngine>((ref) {
|
||||||
final engineMode = ref.watch(
|
final engineMode = ref.watch(
|
||||||
userPreferencesProvider.select((value) => value.youtubeClientEngine),
|
userPreferencesProvider.select((value) => value.youtubeClientEngine),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -12,7 +12,7 @@ use crate::api::plugin::senders::{
|
|||||||
};
|
};
|
||||||
use crate::frb_generated::StreamSink;
|
use crate::frb_generated::StreamSink;
|
||||||
use crate::internal::apis;
|
use crate::internal::apis;
|
||||||
use crate::internal::apis::{form, get_platform_directories, timezone, webview};
|
use crate::internal::apis::{form, get_platform_directories, timezone, webview, yt_engine};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use flutter_rust_bridge::{frb, Rust2DartSendError};
|
use flutter_rust_bridge::{frb, Rust2DartSendError};
|
||||||
use llrt_modules::module_builder::ModuleBuilder;
|
use llrt_modules::module_builder::ModuleBuilder;
|
||||||
@ -56,6 +56,7 @@ async fn create_context(
|
|||||||
.with_global(util::init)
|
.with_global(util::init)
|
||||||
.with_global(form::init)
|
.with_global(form::init)
|
||||||
.with_global(webview::init)
|
.with_global(webview::init)
|
||||||
|
.with_global(yt_engine::init)
|
||||||
.with_global(timezone::init);
|
.with_global(timezone::init);
|
||||||
|
|
||||||
let (module_resolver, module_loader, global_attachment) = module_builder.build();
|
let (module_resolver, module_loader, global_attachment) = module_builder.build();
|
||||||
|
|||||||
@ -5,11 +5,12 @@ pub mod form;
|
|||||||
pub mod local_storage;
|
pub mod local_storage;
|
||||||
pub mod webview;
|
pub mod webview;
|
||||||
pub mod timezone;
|
pub mod timezone;
|
||||||
|
pub mod yt_engine;
|
||||||
|
|
||||||
pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result<()> {
|
pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result<()> {
|
||||||
ctx.globals().set("__serverUrl", endpoint_url)?;
|
ctx.globals().set("__serverUrl", endpoint_url)?;
|
||||||
ctx.globals().set("__serverSecret", secret)?;
|
ctx.globals().set("__serverSecret", secret)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
36
rust/src/internal/apis/yt_engine.rs
Normal file
36
rust/src/internal/apis/yt_engine.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use rquickjs::{Ctx, Value};
|
||||||
|
|
||||||
|
pub fn init(ctx: &Ctx) -> rquickjs::Result<()> {
|
||||||
|
ctx.eval::<Value, _>(
|
||||||
|
r#"
|
||||||
|
globalThis.YouTubeEngine = class YouTubeEngine {
|
||||||
|
async request(endpoint, qName, qValue) {
|
||||||
|
return await fetch(
|
||||||
|
`${__serverUrl}/plugin-api/yt-engine/${endpoint}?${qName}=${encodeURIComponent(qValue)}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Plugin-Secret': __serverSecret,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
).then(res=>res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
async search(query) {
|
||||||
|
return await this.request('search', 'query', query);
|
||||||
|
}
|
||||||
|
|
||||||
|
async video(videoId) {
|
||||||
|
return await this.request('video', 'videoId', videoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async streamManifest(videoId) {
|
||||||
|
return await this.request('stream-manifest', 'videoId', videoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user