initial commit
This commit is contained in:
commit
a4e457eb52
|
|
@ -0,0 +1,136 @@
|
||||||
|
# ---> Node
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
data/
|
||||||
|
cache/
|
||||||
|
cacheStore/
|
||||||
|
tmpStore/
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Extera Music
|
||||||
|
This is repository of Extera Music backend server.
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
export type Config = {
|
||||||
|
api_port: number;
|
||||||
|
web_port: number;
|
||||||
|
api_base_url: string;
|
||||||
|
web_base_url: string;
|
||||||
|
jwt_privkey: string;
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
api_port: 33223
|
||||||
|
api_base_url: http://localhost:33223
|
||||||
|
web_port: 8888
|
||||||
|
web_base_url: http://localhost:8888
|
||||||
|
jwt_privkey: AfhrynxvaH7XevethWCLzzp3Xq49NpqeENdgVVPJnqKNfdAJFuRNdPosAJXYznez
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
function createTableIfNotExists(table: string) {
|
||||||
|
if (!fs.existsSync(`./data/${table}/`)) fs.mkdirSync(`./data/${table}/`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbRead(table: string, key: string) {
|
||||||
|
createTableIfNotExists(table);
|
||||||
|
if (fs.existsSync(`./data/${table}/${key}`)) return JSON.parse(fs.readFileSync(`./data/${table}/${key}`, 'utf-8'));
|
||||||
|
if (!fs.existsSync(`./data/${table}/${encodeURIComponent(key)}`)) return null;
|
||||||
|
return JSON.parse(fs.readFileSync(`./data/${table}/${encodeURIComponent(key)}`, 'utf-8'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbExists(table: string, key: string) {
|
||||||
|
createTableIfNotExists(table);
|
||||||
|
if (fs.existsSync(`./data/${table}/${key}`)) return true;
|
||||||
|
if (!fs.existsSync(`./data/${table}/${encodeURIComponent(key)}`)) return null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbStore(table: string, key: string, value: any) {
|
||||||
|
createTableIfNotExists(table);
|
||||||
|
fs.writeFileSync(`./data/${table}/${encodeURIComponent(key)}`, JSON.stringify(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbDelete(table: string, key: string) {
|
||||||
|
createTableIfNotExists(table);
|
||||||
|
fs.rmSync(`./data/${table}/${encodeURIComponent(key)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbSearch(table: string, condition: (v: any) => boolean) {
|
||||||
|
createTableIfNotExists(table);
|
||||||
|
const values: Record<string, any> = {};
|
||||||
|
for (const key of fs.readdirSync('./data/' + table + '/')) {
|
||||||
|
const value = dbRead(table, decodeURIComponent(key));
|
||||||
|
if (condition(value)) values[decodeURIComponent(key)] = value;
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbAll(table: string) {
|
||||||
|
createTableIfNotExists(table);
|
||||||
|
const values: Record<string, any> = {};
|
||||||
|
for (const key of fs.readdirSync('./data/' + table + '/')) {
|
||||||
|
const value = dbRead(table, decodeURIComponent(key));
|
||||||
|
values[decodeURIComponent(key)] = value;
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dbFind(table: string, condition: (v: any) => boolean) {
|
||||||
|
createTableIfNotExists(table);
|
||||||
|
for (const key of fs.readdirSync('./data/' + table + '/')) {
|
||||||
|
const value = dbRead(table, decodeURIComponent(key));
|
||||||
|
if (condition(value)) return { key: decodeURIComponent(key), value };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
dbAll,
|
||||||
|
dbDelete,
|
||||||
|
dbExists,
|
||||||
|
dbFind,
|
||||||
|
dbRead,
|
||||||
|
dbSearch,
|
||||||
|
dbStore
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {randomInt} from 'crypto';
|
||||||
|
|
||||||
|
export default function generateID() {
|
||||||
|
return `${Date.now()}${randomInt(100000, 999999)}`;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
import express from 'express';
|
||||||
|
import YAML from 'yaml';
|
||||||
|
import {readFileSync} from 'fs';
|
||||||
|
import { Config } from './config.js';
|
||||||
|
import init from './routes/index.js';
|
||||||
|
import ytmApi from './utils/ytmApi.js';
|
||||||
|
import cors from 'cors';
|
||||||
|
|
||||||
|
const config: Config = YAML.parse(
|
||||||
|
readFileSync('config.yml', 'utf-8')
|
||||||
|
);
|
||||||
|
|
||||||
|
const api = express();
|
||||||
|
|
||||||
|
api.use(cors());
|
||||||
|
api.use(express.json());
|
||||||
|
api.use(express.query({
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
init(api);
|
||||||
|
|
||||||
|
api.listen(config.api_port);
|
||||||
|
|
||||||
|
console.log(`API listening on ${config.api_port}`);
|
||||||
|
|
||||||
|
export { config };
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"name": "exteramusic",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Mom can we have Spotify? Mom: we already have spotify at home. Spotify at home:",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"build": "tsc -p .",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
},
|
||||||
|
"author": "extera.xyz",
|
||||||
|
"type": "module",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"express-async-handler": "^1.2.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"matrix-bot-sdk": "^0.7.1",
|
||||||
|
"md5": "^2.3.0",
|
||||||
|
"node-youtube-music": "^0.10.3",
|
||||||
|
"shelljs": "^0.8.5",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"yaml": "^2.5.1",
|
||||||
|
"yt-dlp-wrapper": "^1.2.2",
|
||||||
|
"ytmusic-api": "file:../ts-npm-ytmusic-api"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jsonwebtoken": "^9.0.7",
|
||||||
|
"@types/md5": "^2.3.5",
|
||||||
|
"@types/shelljs": "^0.8.15",
|
||||||
|
"@types/uuid": "^10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { Application } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { dbExists, dbRead, dbStore } from "../database.js";
|
||||||
|
import md5 from "md5";
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { config } from "../index.js";
|
||||||
|
|
||||||
|
export default function authRoutes(app: Application) {
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path.startsWith('/auth/')) return next();
|
||||||
|
var authHeader = req.headers['authorization'];
|
||||||
|
if (!authHeader) throw createHttpError[401]();
|
||||||
|
if (req.headers['x-username']) throw createHttpError[403]('bruh');
|
||||||
|
if (authHeader.startsWith('Bearer ')) authHeader = authHeader.slice(7);
|
||||||
|
const username = jwt.verify(authHeader, config.jwt_privkey, {
|
||||||
|
complete: false
|
||||||
|
}) as string;
|
||||||
|
req.headers['x-username'] = username;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/auth/register', (req, res) => {
|
||||||
|
if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') {
|
||||||
|
throw createHttpError[400]();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dbExists('users', req.body.username)) throw createHttpError[409]('Username is already taken');
|
||||||
|
const user = {
|
||||||
|
username: req.body.username,
|
||||||
|
password: md5(req.body.password),
|
||||||
|
playlists: []
|
||||||
|
};
|
||||||
|
|
||||||
|
dbStore('users', req.body.username, user);
|
||||||
|
|
||||||
|
const token = jwt.sign(req.body.username, config.jwt_privkey);
|
||||||
|
res.send({
|
||||||
|
token
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/auth/login', (req, res) => {
|
||||||
|
if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') {
|
||||||
|
throw createHttpError[400]();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dbExists('users', req.body.username)) throw createHttpError[404]('User not found');
|
||||||
|
const user = dbRead('users', req.body.username);
|
||||||
|
|
||||||
|
if (user.password !== md5(req.body.password)) throw createHttpError[403]('Invalid password');
|
||||||
|
|
||||||
|
const token = jwt.sign(req.body.username, config.jwt_privkey);
|
||||||
|
res.send({
|
||||||
|
token
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import express from 'express';
|
||||||
|
import { config } from '../index.js';
|
||||||
|
|
||||||
|
export default function initConfigRoute(app: express.Application) {
|
||||||
|
app.get('/config', (req, res) => {
|
||||||
|
res.send({
|
||||||
|
api_base_url: config.api_base_url,
|
||||||
|
web_base_url: config.web_base_url
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { Application, Request, Response } from "express";
|
||||||
|
import initConfigRoute from "./config.js";
|
||||||
|
import routeYTM from "./ytm.js";
|
||||||
|
import authRoutes from "./auth.js";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
|
||||||
|
export default function init(app: Application) {
|
||||||
|
|
||||||
|
initConfigRoute(app);
|
||||||
|
routeYTM(app);
|
||||||
|
|
||||||
|
authRoutes(app);
|
||||||
|
|
||||||
|
app.use((err: Error, req: Request, res: Response, next: any) => {
|
||||||
|
if (err && typeof (err as createHttpError.HttpError)['statusCode'] === 'number') {
|
||||||
|
const he = (err as createHttpError.HttpError);
|
||||||
|
res.status(he.statusCode).send({
|
||||||
|
code: he.statusCode,
|
||||||
|
message: he.message
|
||||||
|
});
|
||||||
|
} else if (err) {
|
||||||
|
console.error(err);
|
||||||
|
res.status(500).send({
|
||||||
|
code: 500,
|
||||||
|
message: 'Internal Server Error'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
import { Application } from "express";
|
||||||
|
import { dbDelete, dbRead, dbStore } from "../database.js";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import generateID from "../idgen.js";
|
||||||
|
|
||||||
|
export default function playlists(app: Application) {
|
||||||
|
|
||||||
|
app.get('/playlists/list', (req, res) => {
|
||||||
|
const username = req.headers['x-username'] as string;
|
||||||
|
const user = dbRead('users', username);
|
||||||
|
|
||||||
|
res.send(
|
||||||
|
user.playlists
|
||||||
|
.map((pId: string) => dbRead('playlists', pId))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/playlists/:id', (req, res) => {
|
||||||
|
const username = req.headers['x-username'] as string;
|
||||||
|
const user = dbRead('users', username);
|
||||||
|
|
||||||
|
if (!user.playlists.includes(req.params.id)) throw createHttpError[403]();
|
||||||
|
|
||||||
|
res.send(
|
||||||
|
dbRead('playlists', req.params.id)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/playlists/new', (req, res) => {
|
||||||
|
if (typeof req.body.name !== 'string') throw createHttpError[400]();
|
||||||
|
const username = req.headers['x-username'] as string;
|
||||||
|
const user = dbRead('users', username);
|
||||||
|
const id = generateID();
|
||||||
|
|
||||||
|
user.playlists.push(id);
|
||||||
|
|
||||||
|
dbStore('playlists', id, {
|
||||||
|
id,
|
||||||
|
owner: username,
|
||||||
|
songs: []
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
id
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/playlists/:id', (req, res) => {
|
||||||
|
const username = req.headers['x-username'] as string;
|
||||||
|
const user = dbRead('users', username);
|
||||||
|
|
||||||
|
if (!user.playlists.includes(req.params.id)) throw createHttpError[403]();
|
||||||
|
user.playlists = user.playlists.filter((x: string) => x !== req.params.id);
|
||||||
|
|
||||||
|
dbDelete('playlists', req.params.id);
|
||||||
|
dbStore('users', username, user);
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
ok: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/playlists/:id', (req, res) => {
|
||||||
|
if (typeof req.params.id !== 'string' || typeof req.body.songId !== 'string') throw createHttpError[400]();
|
||||||
|
const username = req.headers['x-username'] as string;
|
||||||
|
const user = dbRead('users', username);
|
||||||
|
|
||||||
|
if (!user.playlists.includes(req.params.id)) throw createHttpError[404]();
|
||||||
|
const playlist = dbRead('playlists', req.params.id);
|
||||||
|
playlist.songs.push(req.body.songId);
|
||||||
|
|
||||||
|
dbStore('playlists', req.params.id, playlist);
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
ok: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete('/playlists/:id/:songId', (req, res) => {
|
||||||
|
if (typeof req.params.id !== 'string' || typeof req.params.songId !== 'string') throw createHttpError[400]();
|
||||||
|
const username = req.headers['x-username'] as string;
|
||||||
|
const user = dbRead('users', username);
|
||||||
|
|
||||||
|
if (!user.playlists.includes(req.params.id)) throw createHttpError[404]();
|
||||||
|
const playlist = dbRead('playlists', req.params.id);
|
||||||
|
playlist.songs = playlist.songs.filter((x: string) => x !== req.params.songId);
|
||||||
|
|
||||||
|
dbStore('playlists', req.params.id, playlist);
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
ok: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Application } from "express";
|
||||||
|
import expressAsyncHandler from "express-async-handler";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import ytmApi from "../utils/ytmApi.js";
|
||||||
|
import downloadSong from "../utils/download.js";
|
||||||
|
import { existsSync, realpathSync } from 'fs';
|
||||||
|
import { dbRead, dbStore } from "../database.js";
|
||||||
|
import md5 from "md5";
|
||||||
|
import { SongDetailed, SongFull } from "ytmusic-api";
|
||||||
|
|
||||||
|
export default function routeYTM(app: Application) {
|
||||||
|
|
||||||
|
app.get('/music/search', expressAsyncHandler(
|
||||||
|
async (req, res) => {
|
||||||
|
if (typeof req.query.q !== 'string') throw createHttpError[400]();
|
||||||
|
var songs: SongDetailed[] | null = dbRead('cache', md5(req.query.q));
|
||||||
|
if (!songs) {
|
||||||
|
songs = await ytmApi.searchSongs(req.query.q);
|
||||||
|
dbStore('cache', md5(req.query.q), songs);
|
||||||
|
}
|
||||||
|
res.send(songs);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
app.get('/music/get', expressAsyncHandler(
|
||||||
|
async (req, res) => {
|
||||||
|
if (typeof req.query.id !== 'string') throw createHttpError[400]();
|
||||||
|
var song: SongDetailed | SongFull | null = dbRead('cache', req.query.id);
|
||||||
|
if (!song) {
|
||||||
|
song = await ytmApi.getSong(req.query.id);
|
||||||
|
dbStore('cache', req.query.id, song);
|
||||||
|
}
|
||||||
|
res.send(song);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
app.get('/music/lyrics', expressAsyncHandler(
|
||||||
|
async (req, res) => {
|
||||||
|
if (typeof req.query.id !== 'string') throw createHttpError[400]();
|
||||||
|
var lyrics = dbRead('cache', md5(`lyrics_${req.query.id}`));
|
||||||
|
if (!lyrics) {
|
||||||
|
lyrics = await ytmApi.getLyrics(req.query.id);
|
||||||
|
dbStore('cache', md5(`lyrics_${req.query.id}`), lyrics ?? 1);
|
||||||
|
}
|
||||||
|
if (lyrics === 1) throw createHttpError[404]();
|
||||||
|
res.send(lyrics);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
app.get('/music/file', expressAsyncHandler(
|
||||||
|
async (req, res) => {
|
||||||
|
if (typeof req.query.id !== 'string') throw createHttpError[400]();
|
||||||
|
const filePath = `./tmpStore/${md5(req.query.id)}.mp3`;
|
||||||
|
if (!existsSync(filePath)) await downloadSong(req.query.id);
|
||||||
|
if (existsSync(filePath)) {
|
||||||
|
res.sendFile(realpathSync(filePath));
|
||||||
|
} else {
|
||||||
|
throw createHttpError[404]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"exclude": [
|
||||||
|
"node_modules/",
|
||||||
|
"dist/",
|
||||||
|
"ts-npm-ytmusic-api/"
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"inlineSources": false,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"preserveWatchOutput": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"allowJs": true,
|
||||||
|
"target": "ES6",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "nodenext"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import shjs from 'shelljs';
|
||||||
|
import fs from 'fs';
|
||||||
|
import md5 from 'md5';
|
||||||
|
|
||||||
|
const process: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
export default function downloadSong(id: string): Promise<string> {
|
||||||
|
return new Promise<string>((resolve, reject) => {
|
||||||
|
const url: string = `https://music.youtube.com/watch?v=${id}`;
|
||||||
|
const filename: string = md5(id);
|
||||||
|
if (process[id]) {
|
||||||
|
reject(new Error(`Already downloading ${id}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
process[id] = true;
|
||||||
|
|
||||||
|
shjs.exec(`../yt-dlp.sh --sponsorblock-remove all -o ${filename} ${url}`, {
|
||||||
|
cwd: './tmpStore',
|
||||||
|
async: true
|
||||||
|
}).on('exit', () => {
|
||||||
|
const fileName = fs.readdirSync('./tmpStore').find(x => x.includes(filename));
|
||||||
|
const newFileName = `${filename}.mp3`;
|
||||||
|
shjs.exec(`ffmpeg -i '${fileName}' '${newFileName}'`, {
|
||||||
|
async: true,
|
||||||
|
cwd: './tmpStore'
|
||||||
|
}).on('exit', () => {
|
||||||
|
resolve(newFileName);
|
||||||
|
delete process[id];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import YTMusic from "ytmusic-api";
|
||||||
|
|
||||||
|
const ytmApi = new YTMusic({
|
||||||
|
proxy: {
|
||||||
|
protocol: 'http',
|
||||||
|
host: 'localhost',
|
||||||
|
port: 12334
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ytmApi.initialize().then(() => {
|
||||||
|
console.log(`YT Music API initialized`);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ytmApi;
|
||||||
Loading…
Reference in New Issue