mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-09 00:37:31 +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/logger/logger.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/utils/migrations/sandbox.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
@ -178,73 +176,6 @@ class Spotube extends HookConsumerWidget {
|
||||
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 () {
|
||||
/// For enabling hot reload for audio player
|
||||
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/path_provider.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) {
|
||||
return (Request request) {
|
||||
@ -23,6 +24,7 @@ final serverRouterProvider = Provider((ref) {
|
||||
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
||||
final webviewRoutes = ref.watch(serverWebviewRoutesProvider);
|
||||
final formRoutes = ref.watch(serverFormRoutesProvider);
|
||||
final ytEngineRoutes = ref.watch(serverYTEngineRoutesProvider);
|
||||
|
||||
final router = Router();
|
||||
|
||||
@ -64,6 +66,19 @@ final serverRouterProvider = Provider((ref) {
|
||||
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);
|
||||
|
||||
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/provider/user_preferences/user_preferences_provider.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/yt_dlp_engine.dart';
|
||||
|
||||
final youtubeEngineProvider = Provider((ref) {
|
||||
final youtubeEngineProvider = Provider<YouTubeEngine>((ref) {
|
||||
final engineMode = ref.watch(
|
||||
userPreferencesProvider.select((value) => value.youtubeClientEngine),
|
||||
);
|
||||
|
||||
@ -12,7 +12,7 @@ use crate::api::plugin::senders::{
|
||||
};
|
||||
use crate::frb_generated::StreamSink;
|
||||
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 flutter_rust_bridge::{frb, Rust2DartSendError};
|
||||
use llrt_modules::module_builder::ModuleBuilder;
|
||||
@ -56,6 +56,7 @@ async fn create_context(
|
||||
.with_global(util::init)
|
||||
.with_global(form::init)
|
||||
.with_global(webview::init)
|
||||
.with_global(yt_engine::init)
|
||||
.with_global(timezone::init);
|
||||
|
||||
let (module_resolver, module_loader, global_attachment) = module_builder.build();
|
||||
|
||||
@ -5,11 +5,12 @@ pub mod form;
|
||||
pub mod local_storage;
|
||||
pub mod webview;
|
||||
pub mod timezone;
|
||||
pub mod yt_engine;
|
||||
|
||||
pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result<()> {
|
||||
ctx.globals().set("__serverUrl", endpoint_url)?;
|
||||
ctx.globals().set("__serverSecret", secret)?;
|
||||
|
||||
|
||||
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