diff --git a/Dockerfile b/Dockerfile index f315c8b..70b7a23 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ # stage: build code -FROM bsingh1904/alpine-node:latest AS NODE_IMAGE +FROM surnet/alpine-node-opencv:16.13.0-4.5.1 AS NODE_IMAGE +# FROM iamccc/alpine-node:16.20 AS NODE_IMAGE WORKDIR /app COPY . . RUN export MELODY_IN_DOCKER=1 && npm run init && rm -rf frontend # stage: copy -FROM iamccc/alpine-node:14.19 +FROM iamccc/alpine-node:16.20 WORKDIR /app COPY --from=pldin601/static-ffmpeg:22.04.061404-87ac0d7 /ffmpeg /ffprobe /usr/local/bin/ diff --git a/backend/src/consts/source.js b/backend/src/consts/source.js index 6feca1d..4d0c5b3 100644 --- a/backend/src/consts/source.js +++ b/backend/src/consts/source.js @@ -1,3 +1,40 @@ module.exports = { - Netease : 'netease', + consts: { + Netease : { + code: 'netease', + label: '网易云', + }, + Bilibili : { + code: 'bilibili', + label: '哔哩哔哩', + }, + Douyin : { + code: 'douyin', + label: '抖音', + }, + Kugou : { + code: 'kugou', + label: '酷狗', + }, + Kuwo : { + code: 'kuwo', + label: '酷我', + }, + Migu : { + code: 'migu', + label: '咪咕', + }, + QQ : { + code: 'qq', + label: 'QQ', + }, + Youtube : { + code: 'youtube', + label: 'Youtube', + }, + Qmkg : { + code: 'qmkg', + label: '全民K歌', + }, + }, } \ No newline at end of file diff --git a/backend/src/handler/config.js b/backend/src/handler/config.js new file mode 100644 index 0000000..dd0dfff --- /dev/null +++ b/backend/src/handler/config.js @@ -0,0 +1,23 @@ +const ConfigService = require('../service/config_manager'); + +async function getGlobalConfig(req, res) { + const config = await ConfigService.getGlobalConfig(); + res.send({ + status: 0, + data: config + }); +} + +async function setGlobalConfig(req, res) { + const config = req.body; + await ConfigService.setGlobalConfig(config); + res.send({ + status: 0, + data: config + }); +} + +module.exports = { + getGlobalConfig, + setGlobalConfig, +} \ No newline at end of file diff --git a/backend/src/handler/playlists.js b/backend/src/handler/playlists.js index 169b3ab..26556ef 100644 --- a/backend/src/handler/playlists.js +++ b/backend/src/handler/playlists.js @@ -1,6 +1,6 @@ const logger = require('consola'); const { getUserAllPlaylist, getSongsFromPlaylist } = require('../service/music_platform/wycloud'); -const Source = require('../consts/source'); +const Source = require('../consts/source').consts; async function listAllPlaylists(req, res) { const uid = req.account.uid; @@ -22,7 +22,7 @@ async function listSongsFromPlaylist(req, res) { const source = req.params.source; const playlistId = req.params.id; - if (source !== Source.Netease || !playlistId) { + if (source !== Source.Netease.code || !playlistId) { res.send({ status: 1, message: "source or id is invalid", diff --git a/backend/src/handler/sync_jobs.js b/backend/src/handler/sync_jobs.js index 7ca7972..9749572 100644 --- a/backend/src/handler/sync_jobs.js +++ b/backend/src/handler/sync_jobs.js @@ -1,7 +1,7 @@ const logger = require('consola'); const { unblockMusicInPlaylist, unblockMusicWithSongId } = require('../service/sync_music'); const JobType = require('../consts/job_type'); -const Source = require('../consts/source'); +const Source = require('../consts/source').consts; const { matchUrlFromStr } = require('../utils/regex'); const { syncSingleSongWithUrl } = require('../service/sync_music'); const findTheBestMatchFromWyCloud = require('../service/search_songs/find_the_best_match_from_wycloud'); @@ -21,7 +21,7 @@ async function createJob(req, res) { const source = request.playlist && request.playlist.source; const playlistId = request.playlist && request.playlist.id; - if (source !== Source.Netease || !playlistId) { + if (source !== Source.Netease.code || !playlistId) { res.status(429).send({ status: 1, message: "source or id is invalid", @@ -33,7 +33,7 @@ async function createJob(req, res) { const source = request.source; const songId = request.songId; - if (source !== Source.Netease || !songId) { + if (source !== Source.Netease.code || !songId) { res.status(429).send({ status: 1, message: "source or id is invalid", diff --git a/backend/src/router.js b/backend/src/router.js index af08518..9fc130e 100644 --- a/backend/src/router.js +++ b/backend/src/router.js @@ -6,6 +6,7 @@ const SongMeta = require('./handler/song_meta'); const Playlists = require('./handler/playlists'); const Account = require('./handler/account'); const MediaFetcherLib = require('./handler/media_fetcher_lib'); +const Config = require('./handler/config'); const asyncWrapper = (cb) => { return (req, res, next) => cb(req, res, next).catch(next); @@ -31,4 +32,7 @@ router.get('/api/account/qrlogin-check', asyncWrapper(Account.qrLoginCheck)); router.get('/api/media-fetcher-lib/version-check', asyncWrapper(MediaFetcherLib.checkLibVersion)); router.post('/api/media-fetcher-lib/update', asyncWrapper(MediaFetcherLib.downloadTheLatestLib)); +router.get('/api/config/global', asyncWrapper(Config.getGlobalConfig)); +router.post('/api/config/global', asyncWrapper(Config.setGlobalConfig)); + module.exports = router; \ No newline at end of file diff --git a/backend/src/service/config_manager/index.js b/backend/src/service/config_manager/index.js new file mode 100644 index 0000000..6fec71b --- /dev/null +++ b/backend/src/service/config_manager/index.js @@ -0,0 +1,42 @@ +const asyncFs = require('../../utils/fs'); + +const DataPath = `${__dirname}/../../../.profile/data`; +const ConfigPath = `${DataPath}/config`; +const GlobalConfig = `${ConfigPath}/global.json`; +const sourceConsts = require('../../consts/source').consts; + +async function init() { + if (!await asyncFs.asyncFileExisted(ConfigPath)) { + await asyncFs.asyncMkdir(ConfigPath); + } +} +init(); + +const GlobalDefaultConfig = { + localDownloadPath: '', + // don't search youtube by default + sources: Object.values(sourceConsts).map(i => i.code).filter(s => s !== sourceConsts.Youtube.code), + sourceConsts, +}; + +async function setGlobalConfig(config) { + await asyncFs.asyncWriteFile(GlobalConfig, JSON.stringify(config)); +} + +async function getGlobalConfig() { + if (!await asyncFs.asyncFileExisted(GlobalConfig)) { + return GlobalDefaultConfig; + } + const config = JSON.parse(await asyncFs.asyncReadFile(GlobalConfig)); + if (!config.sources) { + config.sources = GlobalDefaultConfig.sources; + } + config.sourceConsts = GlobalDefaultConfig.sourceConsts; + return config; +} + + +module.exports = { + setGlobalConfig, + getGlobalConfig, +} \ No newline at end of file diff --git a/backend/src/service/job_manager/index.js b/backend/src/service/job_manager/index.js index 290ebc6..3899b09 100644 --- a/backend/src/service/job_manager/index.js +++ b/backend/src/service/job_manager/index.js @@ -1,5 +1,5 @@ const logger = require('consola'); -const asyncFs = require('./fs'); +const asyncFs = require('../../utils/fs'); const genUUID = require('../../utils/uuid'); const { lock, unlock } = require('../../utils/simple_locker'); const JobStatus = require('../../consts/job_status'); diff --git a/backend/src/service/media_fetcher/index.js b/backend/src/service/media_fetcher/index.js index 8660c51..bcc0580 100644 --- a/backend/src/service/media_fetcher/index.js +++ b/backend/src/service/media_fetcher/index.js @@ -4,6 +4,7 @@ const md5 = require('md5'); const path = require('path'); const cmd = require('../../utils/cmd'); const fs = require('fs'); +const configManager = require('../config_manager') const { getBinPath } = require('./media_get'); @@ -92,10 +93,18 @@ async function searchSongFromAllPlatform({ }) { logger.info(`searchSong with ${JSON.stringify(arguments)}`); + const globalConfig = await configManager.getGlobalConfig(); + let searchParams = keyword ? ['-k', `"${keyword}"`] : ['--searchSongName', `"${songName}"`, '--searchArtist', `"${artist}"`, '--searchAlbum', `"${album}"`]; - searchParams = searchParams.concat(['--searchType="song"', '-m', '--infoFormat=json', '-l', 'silence']); + searchParams = searchParams.concat([ + '--searchType="song"', + '-m', + `--sources=${globalConfig.sources.join(',')}`, + '--infoFormat=json', + '-l', 'silence' + ]); logger.info(`cmdStr: ${getBinPath()} ${searchParams.join(' ')}`); diff --git a/backend/src/service/media_fetcher/media_get.js b/backend/src/service/media_fetcher/media_get.js index 66a2f15..24bc9d6 100644 --- a/backend/src/service/media_fetcher/media_get.js +++ b/backend/src/service/media_fetcher/media_get.js @@ -1,9 +1,11 @@ const logger = require('consola'); -const cmd = require('../../utils/cmd'); const https = require('https'); +const cmd = require('../../utils/cmd'); var isWin = require('os').platform().indexOf('win32') > -1; const isLinux = require('os').platform().indexOf('linux') > -1; const isDarwin = require('os').platform().indexOf('darwin') > -1; +const httpsGet = require('../../utils/network').asyncHttpsGet; +const RemoteConfig = require('../remote_config'); const fs = require('fs'); function getBinPath(isTemp = false) { @@ -23,26 +25,16 @@ async function getMediaGetInfo(isTempBin = false) { return { hasInstallFFmpeg, versionInfo: versionInfo ? versionInfo[1].trim() : '', + fullMessage: message, } } -function asyncHttpsGet(url) { - return new Promise((resolve) => { - https.get(url, res => { - res.on('data', data => { - resolve(data.toString()); - }) - res.on('error', err => { - l(err); - resolve(null); - }) - }); - }); -} - async function getLatestMediaGetVersion() { - const latestVerisonUrl = 'https://ghproxy.com/https://raw.githubusercontent.com/foamzou/media-get/main/LATEST_VERSION'; - const latestVersion = await asyncHttpsGet(latestVerisonUrl); + const remoteConfig = await RemoteConfig.getRemoteConfig(); + const latestVerisonUrl = `${remoteConfig.githubProxy}/https://raw.githubusercontent.com/foamzou/media-get/main/LATEST_VERSION`; + console.log('start to get latest version from: ' + latestVerisonUrl); + const latestVersion = await httpsGet(latestVerisonUrl); + console.log('latest version: ' + latestVersion); if (latestVersion === null || (latestVersion || "").split('.').length !== 3) { logger.error('获取 media-get 最新版本号失败, got: ' + latestVersion); return false; @@ -51,19 +43,23 @@ async function getLatestMediaGetVersion() { } async function downloadFile(url, filename) { - return new Promise((resolve, reject) => { + return new Promise((resolve) => { + const fileStream = fs.createWriteStream(filename); https.get(url, res => { - res.pipe(fs.createWriteStream(filename)); - res.on('end', () => { - resolve(); - } - ); - } - ); + res.pipe(fileStream); + fileStream.on('finish', () => { + fileStream.close(() => resolve(true)); // Successfully downloaded and saved, resolve with true + }); + }) + .on('error', (error) => { + console.error('Download error:', error); + fileStream.destroy(); + fs.unlink(filename, () => resolve(false)); // On error, delete the file and resolve with false + }); }); } -function getMediaGetRemoteFilename(latestVersion) { +async function getMediaGetRemoteFilename(latestVersion) { let suffix = 'win.exe'; if (isLinux) { suffix = 'linux'; @@ -74,7 +70,8 @@ function getMediaGetRemoteFilename(latestVersion) { if (process.arch === 'arm64') { suffix += '-arm64'; } - return `https://ghproxy.com/https://github.com/foamzou/media-get/releases/download/v${latestVersion}/media-get-${latestVersion}-${suffix}`; + const remoteConfig = await RemoteConfig.getRemoteConfig(); + return `${remoteConfig.githubProxy}/https://github.com/foamzou/media-get/releases/download/v${latestVersion}/media-get-${latestVersion}-${suffix}`; } const renameFile = (oldName, newName) => { @@ -91,9 +88,13 @@ const renameFile = (oldName, newName) => { }; async function downloadTheLatestMediaGet(version) { - const remoteFile = getMediaGetRemoteFilename(version); + const remoteFile = await getMediaGetRemoteFilename(version); logger.info('start to download media-get: ' + remoteFile); - await downloadFile(remoteFile, getBinPath(true)); + const ret = await downloadFile(remoteFile, getBinPath(true)); + if (ret === false) { + logger.error('download failed'); + return false; + } fs.chmodSync(getBinPath(true), '755'); logger.info('download finished'); @@ -101,7 +102,7 @@ async function downloadTheLatestMediaGet(version) { if (!temBinInfo || temBinInfo.versionInfo === "" ) { - logger.error('testing new bin failed. Don`t update') + logger.error('testing new bin failed. Don`t update', temBinInfo) return false; } diff --git a/backend/src/service/remote_config/index.js b/backend/src/service/remote_config/index.js new file mode 100644 index 0000000..2736e6c --- /dev/null +++ b/backend/src/service/remote_config/index.js @@ -0,0 +1,23 @@ +const httpsGet = require('../../utils/network').asyncHttpsGet; +const logger = require('consola'); + + +async function getRemoteConfig() { + const fallbackConfig = { + githubProxy: 'https://mirror.ghproxy.com/', + } + const remoteConfigUrl = 'https://foamzou.com/tools/melody-config.php'; + const remoteConfig = await httpsGet(remoteConfigUrl); + if (remoteConfig === null) { + logger.error('get remote config failed, use fallback config'); + return fallbackConfig; + } + const config = JSON.parse(remoteConfig); + return { + githubProxy: config.githubProxy, + } +} + +module.exports = { + getRemoteConfig: getRemoteConfig, +} \ No newline at end of file diff --git a/backend/src/service/job_manager/fs.js b/backend/src/utils/fs.js similarity index 100% rename from backend/src/service/job_manager/fs.js rename to backend/src/utils/fs.js diff --git a/backend/src/utils/network.js b/backend/src/utils/network.js new file mode 100644 index 0000000..54c1385 --- /dev/null +++ b/backend/src/utils/network.js @@ -0,0 +1,19 @@ +const https = require('https'); + +function asyncHttpsGet(url) { + return new Promise((resolve) => { + https.get(url, res => { + res.on('data', data => { + resolve(data.toString()); + }) + res.on('error', err => { + l(err); + resolve(null); + }) + }); + }); +} + +module.exports = { + asyncHttpsGet +} \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development index a6b12ff..777c83e 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -1,5 +1,5 @@ NODE_ENV = 'development' VITE_APP_MODE = 'development' -VITE_APP_API_URL = 'http://172.16.9.215:5566/api' +VITE_APP_API_URL = 'http://172.16.252.1:5566/api' VITE_APP_API_URL = 'http://10.0.0.2:5566/api' VITE_APP_API_URL = 'http://127.0.0.1:5566/api' diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 6b592c2..d9b746f 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -45,4 +45,9 @@ export const updateMediaFetcherLib = (version) => { return post("/media-fetcher-lib/update", { "version": version, }); +}; + +export const getGlobalConfig = _ => get("/config/global", {}); +export const setGlobalConfig = (config) => { + return post("/config/global", config); }; \ No newline at end of file diff --git a/frontend/src/views/pc/Setting.vue b/frontend/src/views/pc/Setting.vue index a6f1384..b21736e 100644 --- a/frontend/src/views/pc/Setting.vue +++ b/frontend/src/views/pc/Setting.vue @@ -1,30 +1,77 @@ \ No newline at end of file + diff --git a/package.json b/package.json index fe8fd86..a90ca04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "foamzou-melody", - "version": "0.1.1", + "version": "0.1.2", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "init": "node scripts/setup.js", diff --git a/scripts/setup.js b/scripts/setup.js index dd87111..8df9890 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -6,7 +6,6 @@ const isWin = require('os').platform().indexOf('win32') > -1; const isLinux = require('os').platform().indexOf('linux') > -1; const isDarwin = require('os').platform().indexOf('darwin') > -1; const ROOT_DIR = `${__dirname}/../`; -const MediaGetService = require('../backend/src/service/media_fetcher/media_get'); const l = m => console.log(m); const runCmd = (cmd, shouldOutput = true, cwd = null) => { @@ -39,80 +38,13 @@ const runCmdAndExitWhenFailed = async (cmd, msg, shouldOutput = true, cwd = null return ret; } -function asyncHttpsGet(url) { - return new Promise((resolve) => { - https.get(url, res => { - res.on('data', data => { - resolve(data.toString()); - }) - res.on('error', err => { - l(err); - resolve(null); - }) - }); - }); -} - function getMediaGetBinPath() { return path.join(ROOT_DIR, 'backend', 'bin', `media-get${isWin ? '.exe' : ''}`); } -function getMediaGetRemoteFilename(latestVersion) { - let suffix = 'win.exe'; - if (isLinux) { - suffix = 'linux'; - } - if (isDarwin) { - suffix = 'darwin'; - } - if (process.arch === 'arm64') { - suffix += '-arm64'; - } - return `https://ghproxy.com/https://github.com/foamzou/media-get/releases/download/v${latestVersion}/media-get-${latestVersion}-${suffix}`; -} - -async function downloadFile(url, filename) { - return new Promise((resolve, reject) => { - https.get(url, res => { - res.pipe(fs.createWriteStream(filename)); - res.on('end', () => { - resolve(); - } - ); - } - ); - }); -} - -const sleep = ms => new Promise(r => setTimeout(r, ms)); - -// async function getLatestMediaGetVersion() { -// const latestVerisonUrl = 'https://ghproxy.com/https://raw.githubusercontent.com/foamzou/media-get/main/LATEST_VERSION'; -// // download the file -// const latestVersion = await asyncHttpsGet(latestVerisonUrl); -// if (latestVersion === null || (latestVersion || "").split('.').length !== 3) { -// l('获取 media-get 最新版本号失败, got: ' + latestVersion); -// return false; -// } -// return latestVersion; -// } - -async function downloadTheLatestMediaGet(latestVersion = "") { - if (!latestVersion) { - latestVersion = await MediaGetService.getLatestMediaGetVersion(); - if (latestVersion === false) { - return false; - } - } - const remoteFile = getMediaGetRemoteFilename(latestVersion); - l('开始下载 media-get: ' + remoteFile); - await downloadFile(remoteFile, getMediaGetBinPath()); - fs.chmodSync(getMediaGetBinPath(), '755'); - await sleep(800); - l('download finished'); -} - async function checkAndUpdateMediaGet(currentMediaGetVersion) { + const MediaGetService = require('../backend/src/service/media_fetcher/media_get'); + const latestVersion = await MediaGetService.getLatestMediaGetVersion(); if (latestVersion === false) { return; @@ -122,7 +54,7 @@ async function checkAndUpdateMediaGet(currentMediaGetVersion) { return; } l(`当前 media-get(${currentMediaGetVersion})版本不是最新版本, 开始更新到${latestVersion}`); - await downloadTheLatestMediaGet(latestVersion); + await MediaGetService.downloadTheLatestMediaGet(latestVersion); } function copyDir(src, dest) { @@ -156,11 +88,22 @@ async function run(inDocker) { await runCmdAndExitWhenFailed('npm version', '请先安装 npm', false); !inDocker && await runCmdAndExitWhenFailed('ffmpeg -version', '请先安装 ffmpeg', false); + const pm = await getPackageManager(); + l(`安装 node_module via ${pm}`) + await runCmdAndExitWhenFailed(`${pm} install --production`, '安装后端 node_module 失败', true, path.join(ROOT_DIR, 'backend')) + l('检查 media-get'); + const MediaGetService = require('../backend/src/service/media_fetcher/media_get'); + const mediaGetRet = await runCmd(`${getMediaGetBinPath()} -h`, false); if (mediaGetRet.code !== 0) { + const latestVersion = await MediaGetService.getLatestMediaGetVersion(); + if (latestVersion === false) { + l('获取 media-get 最新版本失败,无法继续安装'); + return false; + } l('开始下载核心程序 media-get'); - if (await downloadTheLatestMediaGet() === false) { + if (await MediaGetService.downloadTheLatestMediaGet(latestVersion) === false) { l('下载核心程序 media-get 失败'); return false; } @@ -172,12 +115,9 @@ async function run(inDocker) { l('检查 media-get 是否更新成功'); await runCmdAndExitWhenFailed(`${getMediaGetBinPath()} -h`, `media-get 安装失败。请手动从 https://github.com/foamzou/media-get/releases 下载最新版本到 ${getMediaGetBinPath()}`, false); } - const pm = await getPackageManager(); - - l('安装 node_module') - await runCmdAndExitWhenFailed(`${pm} install --production`, '安装后端 node_module 失败', true, path.join(ROOT_DIR, 'backend')) await runCmdAndExitWhenFailed(`${pm} install`, '安装前端 node_module 失败', true, path.join(ROOT_DIR, 'frontend')) + l('编译前端') await runCmdAndExitWhenFailed(`${pm} run build`, '安装后端 node_module 失败', true, path.join(ROOT_DIR, 'frontend')) @@ -196,4 +136,7 @@ const inDocker = process.env.MELODY_IN_DOCKER; run(inDocker).then( isFine => { l(isFine ? `执行完毕,执行以下命令启动服务:\r\n\r\nnpm run app` : '执行出错,请检查'); + if (!isFine) { + process.exit(1); + } }); \ No newline at end of file