Skip to content

Commit

Permalink
fast forward from v1
Browse files Browse the repository at this point in the history
  • Loading branch information
PapiOphidian committed Jan 9, 2023
1 parent 57edf3e commit 14d9d0f
Show file tree
Hide file tree
Showing 14 changed files with 475 additions and 2,717 deletions.
7 changes: 7 additions & 0 deletions PLUGINS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# Plugins
Volcano has support for plugins developers can use to add functionality similar to LavaLink. The functionality plugins can offer are a little limited, but the API will expand in the future. Should your plugin require special functionality, please open an issue and I can see on expanding on the API.

## Installing plugins
Volcano comes with a cli during the runtime to install plugins. You can type `installplugin <raw link to package.json>`
do not include the <>. If installing from GitHub, make sure it points to the raw file instead of the pretty view GitHub gives you since it expects to be JSON formatted.
If the file path isn't ending with package.json, then Volcano will refuse to load it. Volcano requires the main field to have a file. Volcano will only download the main file and nothing else.

Should you update or do a clean install and need to reinstall all of the plugins, you can type `reinstallall` and it'll work with what's installed in the plugin-manifest.json

## Plugins Volcano supports currently
- Spotify (built in) by PapiOphidian (that's me!)
- [Apple Music by PapiOphidian](https://github.com/AmandaDiscord/VolcanoPlugins/tree/main/AppleMusic)
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ What LavaLink offers that Volcano doesn't:
# Plugins
Volcano supports its own plugin system like LavaLink has its own and comes with a Spotify plugin by default as support for Spotify to some degree and also for developers to look at and copy. This plugin may or may not be compatible with the Spotify plugin offered by LavaLink. Something to keep in mind is that due to how fundamentally different Volcano is from LavaLink, including being a totally different language, Volcano cannot load plugins intended to be used by LavaLink and vice versa. The scope of what Plugins can do in Volcano is also limited at the time of writing. The feature set may be expanded in the future, but this is what I was able to come up with in the limited time that I have. Plugins may have their own dependencies which you will have to install manually into your Volcano instance and re-do this for each Volcano update as the package.json may differ from update to update.

Read PLUGINS.md for more info and how to install

# Usage
Download the latest release from https://github.com/AmandaDiscord/Volcano/releases

Expand Down
2,933 changes: 272 additions & 2,661 deletions package-lock.json

Large diffs are not rendered by default.

29 changes: 15 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "volcano",
"version": "2.0.0-test-2",
"version": "2.0.0-test-3",
"description": "A light-weight LavaLink compatible replacement",
"main": "dist/index.js",
"type": "module",
Expand Down Expand Up @@ -32,35 +32,36 @@
"license": "MIT",
"dependencies": {
"@discordjs/opus": "^0.8.0",
"@discordjs/voice": "^0.11.0",
"@discordjs/voice": "^0.14.0",
"@lavalink/encoding": "^0.1.2",
"backtracker": "3.3.2",
"backtracker": "^3.3.2",
"ffmpeg-static": "^5.1.0",
"html-entities": "^2.3.3",
"m3u8stream": "^0.8.6",
"music-metadata": "^8.1.0",
"music-metadata": "^8.1.1",
"node-html-parser": "^6.1.4",
"play-dl": "^1.9.6",
"prism-media": "^1.3.4",
"sodium-native": "^3.4.1",
"twitch-m3u8": "github:AmandaDiscord/twitch-m3u8#ba927b26fb3c5cb118d75c20f990c5cce8a1ae7a",
"volcano-sdk": "github:AmandaDiscord/VolcanoSDK#c9195e6891ec641ec9a9da3e7cb87a47fa6f8033",
"ws": "^8.11.0",
"yaml": "^2.1.3",
"ytmusic-api": "^3.1.1"
"ws": "^8.12.0",
"yaml": "^2.2.1",
"ytmusic-api": "^4.1.0"
},
"devDependencies": {
"@types/node": "^18.11.9",
"@types/ws": "^8.5.3",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^5.43.0",
"eslint": "^8.27.0",
"lavalink-types": "1.0.2",
"@types/node": "^18.11.18",
"@types/ws": "^8.5.4",
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"eslint": "^8.30.0",
"lavalink-types": "1.0.3",
"tsup": "^6.5.0",
"typescript": "^4.9.3"
"typescript": "^4.9.4"
},
"files": [
"./dist",
"./plugins",
"./LICENSE",
"./README.md"
]
Expand Down
12 changes: 12 additions & 0 deletions plugin-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"ExtraWSOPs": {
"name": "ExtraWSOPs",
"version": "1.0.0",
"resolved": "builtin:ExtraWSOPs.js"
},
"SpotifyPlugin": {
"name": "SpotifyPlugin",
"version": "1.0.0",
"resolved": "builtin:SpotifyPlugin.js"
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,3 +122,4 @@ process.on("uncaughtException", (e, origin) => console.error(`${util.inspect(e,
process.title = "Volcano";

import("./loaders/plugins.js");
import("./loaders/stdin.js");
39 changes: 26 additions & 13 deletions src/loaders/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,33 @@ const isDir = await fs.promises.stat(pluginsDir).then(s => s.isDirectory()).catc
if (isDir) {
for (const file of await fs.promises.readdir(pluginsDir)) {
if (!file.endsWith(".js")) continue;
let constructed: import("volcano-sdk").Plugin;
try {
const module = await import(`file://${path.join(pluginsDir, file)}`) as { default: typeof import("volcano-sdk").Plugin };
constructed = new module.default(console, Util);
await constructed.initialize?.();
} catch (e) {
console.warn(`Plugin from ${file} had errors when initializing and has been ignored from the plugin list`);
console.error(util.inspect(e, false, Infinity, true));
continue;
}
if (lavalinkPlugins.find(p => p.source && constructed.source && p.source === constructed.source)) console.warn(`Plugin for ${constructed.source} has duplicates and could possibly be unused`);
lavalinkPlugins.push(constructed);
console.log(`Loaded plugin for ${constructed.constructor.name}`);
await loadPlugin(path.join(pluginsDir, file));
}
}

export async function loadPlugin(dir: string) {
let constructed: import("volcano-sdk").Plugin;
try {
const module = await import(`file://${dir}`) as { default: typeof import("volcano-sdk").Plugin };
constructed = new module.default(console, Util);
await constructed.initialize?.();
} catch (e) {
console.warn(`Plugin from ${dir} had errors when initializing and has been ignored from the plugin list`);
console.error(util.inspect(e, false, Infinity, true));
return;
}
if (lavalinkPlugins.find(p => p.source && constructed.source && p.source === constructed.source)) console.warn(`Plugin for ${constructed.source} has duplicates and could possibly be unused`);
lavalinkPlugins.push(constructed);
lavalinkLog(`Loaded plugin for ${constructed.constructor.name}`);

const foundIndex = lavalinkPlugins.findIndex(p => p.source === "http");
if (foundIndex !== -1) {
const found = lavalinkPlugins[foundIndex];
lavalinkPlugins.splice(foundIndex, 1);
lavalinkPlugins.push(found);
}
}

if (pushToEnd) lavalinkPlugins.push(pushToEnd);

export default { loadPlugin };
88 changes: 88 additions & 0 deletions src/loaders/stdin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import repl from "repl";
import path from "path";
import fs from "fs";
import { spawn } from "child_process";

import plugins from "./plugins.js";

const pluginManifestDir = path.join(lavalinkDirname, "../plugin-manifest.json");

type PluginManifest = {
[name: string]: {
name: string;
version: string;
resolved: string;
dependencies?: { [name: string]: string; };
}
}

const versionSpecifierRegex = /[\^@]/g;

async function install(url: string) {
if (!url.endsWith("package.json")) return console.warn("To install a plugin, you must directly link to the package.json file");
const manifestStr = await fs.promises.readFile(pluginManifestDir, { encoding: "utf-8" });
const manifest = JSON.parse(manifestStr) as PluginManifest;
let fetched: any;
try {
fetched = await fetch(url).then(res => res.json());
} catch (e) {
return console.error(e);
}
const existing = Object.values(manifest).find(p => p.name === fetched.name);
if (existing) {
existing.dependencies = fetched.dependencies;
existing.version = fetched.version;
} else manifest[fetched.name] = { name: fetched.name, version: fetched.version, resolved: url, dependencies: fetched.dependencies };

const parsed = new URL(url);
const fileDir = path.join(path.dirname(parsed.pathname), fetched.main);
parsed.pathname = fileDir;

const jsFile = await fetch(parsed.toString());
const installDir = path.join(lavalinkDirname, "../plugins", fetched.main);
await fs.promises.writeFile(installDir, Buffer.from(await jsFile.arrayBuffer()));
if (fetched.dependencies) {
const pkg = JSON.parse(await fs.promises.readFile(path.join(lavalinkDirname, "../package.json"), "utf-8"));
for (const dep of Object.keys(pkg.dependencies)) delete fetched.dependencies[dep];
if (Object.keys(fetched.dependencies).length) {
try {
await new Promise((res, rej) => {
const command = process.platform === "win32" ? "npm.cmd" : "npm";
const child = spawn(command, ["install", (Object.entries(fetched.dependencies) as unknown as [string, string]).map(([d, ver]) => ver.startsWith("github:") ? ver.replace("github:", "") : `${d}@${ver.replace(versionSpecifierRegex, "")}`).join(" ")], { cwd: path.join(lavalinkDirname, "../") });
child.once("exit", () => res);
child.once("error", (er) => {
rej(er);
child.kill();
});
});
} catch (er) {
return console.error(er);
}
}
await fs.promises.writeFile(pluginManifestDir, JSON.stringify(manifest, null, 2));
}

await plugins.loadPlugin(installDir);

console.info(`Installed ${fetched.name}@${fetched.version}`);
}

async function customEval(input: string, _context: import("vm").Context, _filename: string, callback: (err: Error | null, result: unknown) => unknown) {
const split = input.replace("\n", "").split(" ");
const command = split[0];
const afterCommand = split.slice(1).join(" ");

if (command === "exit") return callback(null, process.exit());
else if (command === "installplugin") return install(afterCommand);
else if (command === "reinstallall") {
const manifestStr = await fs.promises.readFile(pluginManifestDir, { encoding: "utf-8" });
const manifest = JSON.parse(manifestStr) as PluginManifest;
for (const plugin of Object.values(manifest)) {
if (plugin.resolved.startsWith("builtin:")) continue;
await install(plugin.resolved);
}
} else callback(null, "unknown command");
}

const cli = repl.start({ prompt: "", eval: customEval, writer: s => s });
cli.once("exit", () => process.exit());
2 changes: 1 addition & 1 deletion src/loaders/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ async function onClientClose(socket: import("ws").WebSocket, userID: string, clo
if (key.startsWith(userID)) voiceServerStates.delete(key);
}

function dataRequest(op: number, data: any) {
export function dataRequest(op: number, data: any) {
if (op === Constants.workerOPCodes.VOICE_SERVER) {
return voiceServerStates.get(`${data.clientID}.${data.guildId}`);
}
Expand Down
2 changes: 1 addition & 1 deletion src/sources/SoundcloudSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class SoundcloudSource extends Plugin {
}

private static songResultToTrack(i: import("play-dl").SoundCloudTrack) {
if (!i.formats[0]) throw new Error("NO_SOUNDCLOUD_SONG_STREAM_URL");
if (!i.formats?.[0]) throw new Error("NO_SOUNDCLOUD_SONG_STREAM_URL");
return {
identifier: `${i.formats[0].format.protocol === "hls" ? "O:" : ""}${i.formats[0].url}`,
author: i.user.name,
Expand Down
25 changes: 19 additions & 6 deletions src/sources/TwitchSource.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import twitch from "twitch-m3u8";
import m3u8 from "m3u8stream";
import htmlParse from "node-html-parser";
import entities from "html-entities";

import { Plugin } from "volcano-sdk";
import Constants from "../Constants.js";

const usableRegex = /^https:\/\/www\.twitch.\tv/;
const usableRegex = /^https:\/\/(?:www\.)?twitch\.tv/;
const vodRegex = /\/videos\/(\d+)$/;
const channelRegex = /twitch\.tv\/([^/]+)/;

Expand All @@ -22,14 +25,19 @@ class TwitchSource extends Plugin {
const audioOnly = data.find(d => d.quality === "Audio only");
const chosen = audioOnly ? audioOnly : data[0];
const streamerName = chosen.url.split("_").slice(1, audioOnly ? -3 : -2).join("_");
const res = await fetch(resource, { redirect: "follow", headers: Constants.baseHTTPRequestHeaders }).then(r => r.text());
const parser = htmlParse.default(res);
const head = parser.getElementsByTagName("head")[0];
const title = entities.decode(head.querySelector("meta[property=\"og:title\"]")?.getAttribute("content")?.split("-").slice(0, -1).join("-").trim() || `Twitch Stream of ${streamerName}`);
const duration = +(head.querySelector("meta[property=\"og:video:duration\"]")?.getAttribute("content") || 0) * 1000;
return {
entries: [
{
title: "Twitch vod",
title: title,
author: streamerName,
uri: resource,
identifier: resource,
length: 0,
length: duration,
isStream: false
}
]
Expand All @@ -40,13 +48,18 @@ class TwitchSource extends Plugin {
if (!user) throw new Error("NOT_TWITCH_VOD_OR_CHANNEL_LINK");
const data = await twitch.getStream(user[1]);
if (!data.length) throw new Error("CANNOT_EXTRACT_TWITCH_INFO_FROM_VOD");
const uri = `https://www.twitch.tv/${user[1]}`;
const res = await fetch(uri, { redirect: "follow", headers: Constants.baseHTTPRequestHeaders }).then(r => r.text());
const parser = htmlParse.default(res);
const head = parser.getElementsByTagName("head")[0];
const title = entities.decode(head.querySelector("meta[property=\"og:description\"]")?.getAttribute("content") || `Twitch Stream of ${user[1]}`);
return {
entries: [
{
title: "Twitch stream",
title: title,
author: user[1],
uri: `https://www.twitch.tv/${user[1]}`,
identifier: `https://www.twitch.tv/${user[1]}`,
uri: uri,
identifier: uri,
length: 0,
isStream: true
}
Expand Down
Loading

0 comments on commit 14d9d0f

Please sign in to comment.