初始化提交
This commit is contained in:
108
backend/src/handler/account.js
Normal file
108
backend/src/handler/account.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const AccountService = require('../service/account');
|
||||
const WYAPI = require('../service/music_platform/wycloud');
|
||||
const { storeCookie } = require('../service/music_platform/wycloud/transport.js');
|
||||
|
||||
async function get(req, res) {
|
||||
res.send({
|
||||
status: 0,
|
||||
data: {
|
||||
account: await getWyAccountInfo(req.account.uid)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function set(req, res) {
|
||||
const loginType = req.body.loginType;
|
||||
const accountName = req.body.account;
|
||||
const password = req.body.password;
|
||||
const countryCode = req.body.countryCode;
|
||||
const config = req.body.config;
|
||||
const name = req.body.name;
|
||||
|
||||
if (name) {
|
||||
// check if the name is already used by other accounts
|
||||
const allAccounts = await AccountService.getAllAccountsWithoutSensitiveInfo();
|
||||
for (const account of Object.values(allAccounts)) {
|
||||
if (account.name === name && account.uid !== req.account.uid) {
|
||||
res.status(412).send({ status: 1, message: '昵称已被占用啦,请换一个试试吧', data: {} });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ret = await AccountService.setAccount(req.account.uid, loginType, accountName, password, countryCode, config, name);
|
||||
res.send({
|
||||
status: ret ? 0 : 1,
|
||||
data: {
|
||||
account: await getWyAccountInfo(req.account.uid)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getWyAccountInfo(uid) {
|
||||
const account = AccountService.getAccount(uid)
|
||||
const wyInfo = await WYAPI.getMyAccount(uid);
|
||||
account.wyAccount = wyInfo;
|
||||
return account;
|
||||
}
|
||||
|
||||
async function qrLoginCreate(req, res) {
|
||||
const qrData = await WYAPI.qrLoginCreate(req.account.uid);
|
||||
if (qrData === false) {
|
||||
res.status(500).send({
|
||||
status: 1,
|
||||
message: 'qr login create failed',
|
||||
data: {}
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.send({
|
||||
status: 0,
|
||||
data: {
|
||||
qrKey: qrData.qrKey,
|
||||
qrCode: qrData.qrCode,
|
||||
}
|
||||
});
|
||||
}
|
||||
async function qrLoginCheck(req, res) {
|
||||
// 800 为二维码过期; 801 为等待扫码; 802 为待确认; 803 为授权登录成功
|
||||
const loginCheckRet = await WYAPI.qrLoginCheck(req.account.uid, req.query.qrKey);
|
||||
let account = false;
|
||||
if (loginCheckRet.code == 803) {
|
||||
// it's a bad design to export the transport function here. Let's refactor it at a good time.
|
||||
// should be put the cookie method to a cookie manager service
|
||||
req.account.loginType = 'qrcode';
|
||||
req.account.account = 'temp';
|
||||
storeCookie(req.account.uid, req.account, loginCheckRet.cookie);
|
||||
|
||||
account = await getWyAccountInfo(req.account.uid);
|
||||
req.account.account = account.wyAccount.userId;
|
||||
storeCookie(req.account.uid, req.account, loginCheckRet.cookie);
|
||||
|
||||
AccountService.setAccount(req.account.uid, 'qrcode', account.wyAccount.userId, '', null);
|
||||
account = await getWyAccountInfo(req.account.uid);
|
||||
}
|
||||
res.send({
|
||||
status: loginCheckRet ? 0 : 1,
|
||||
data: {
|
||||
wyQrStatus: loginCheckRet.code,
|
||||
account
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getAllAccounts(req, res) {
|
||||
const data = await AccountService.getAllAccountsWithoutSensitiveInfo();
|
||||
res.send({
|
||||
status: 0,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
get: get,
|
||||
set: set,
|
||||
qrLoginCreate,
|
||||
qrLoginCheck,
|
||||
getAllAccounts,
|
||||
}
|
||||
23
backend/src/handler/config.js
Normal file
23
backend/src/handler/config.js
Normal file
@@ -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,
|
||||
}
|
||||
41
backend/src/handler/media_fetcher_lib.js
Normal file
41
backend/src/handler/media_fetcher_lib.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const logger = require('consola');
|
||||
const { getMediaGetInfo, getLatestMediaGetVersion, downloadTheLatestMediaGet } = require('../service/media_fetcher/media_get');
|
||||
|
||||
async function checkLibVersion(req, res) {
|
||||
const query = req.query;
|
||||
|
||||
if (!['mediaGet'].includes(query.lib)) {
|
||||
res.send({
|
||||
status: 1,
|
||||
message: "lib name is invalid",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const latestVersion = await getLatestMediaGetVersion();
|
||||
const mediaGetInfo = await getMediaGetInfo();
|
||||
|
||||
res.send({
|
||||
status: 0,
|
||||
data: {
|
||||
mediaGetInfo,
|
||||
latestVersion,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadTheLatestLib(req, res) {
|
||||
const {version} = req.body;
|
||||
|
||||
const succeed = await downloadTheLatestMediaGet(version);
|
||||
|
||||
res.send({
|
||||
status: succeed ? 0 : 1,
|
||||
data: {}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkLibVersion: checkLibVersion,
|
||||
downloadTheLatestLib: downloadTheLatestLib,
|
||||
}
|
||||
92
backend/src/handler/playlists.js
Normal file
92
backend/src/handler/playlists.js
Normal file
@@ -0,0 +1,92 @@
|
||||
const logger = require('consola');
|
||||
const { getUserAllPlaylist } = require('../service/music_platform/wycloud');
|
||||
const { getPlaylistDetail } = require('../service/music_platform/tunehub');
|
||||
const Source = require('../consts/source').consts;
|
||||
|
||||
function splitArtists(artist) {
|
||||
if (!artist) {
|
||||
return [];
|
||||
}
|
||||
return artist
|
||||
.split(/[、/]/)
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeTunehubPlaylist(playlistId, detail) {
|
||||
const info = detail && detail.info ? detail.info : {};
|
||||
const list = detail && Array.isArray(detail.list) ? detail.list : [];
|
||||
|
||||
const songs = list.map(item => {
|
||||
const artists = splitArtists(item.artist);
|
||||
const primaryArtist = artists[0] || item.artist || '';
|
||||
const cover = item.pic || info.pic || '';
|
||||
const isBlocked = !item.types || item.types.length === 0;
|
||||
return {
|
||||
songId: item.id,
|
||||
songName: item.name || '',
|
||||
artists,
|
||||
artist: primaryArtist,
|
||||
duration: 0,
|
||||
album: item.album || '',
|
||||
cover,
|
||||
pageUrl: `https://music.163.com/song?id=${item.id}`,
|
||||
playUrl: item.url || '',
|
||||
isBlocked,
|
||||
isCloud: false,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: playlistId,
|
||||
name: info.name || '',
|
||||
cover: info.pic || '',
|
||||
songs,
|
||||
};
|
||||
}
|
||||
|
||||
async function listAllPlaylists(req, res) {
|
||||
const uid = req.account.uid;
|
||||
const playlists = await getUserAllPlaylist(uid);
|
||||
if (playlists === false) {
|
||||
logger.error(`get user all playlist failed, uid: ${uid}`);
|
||||
}
|
||||
|
||||
res.send({
|
||||
status: playlists ? 0 : 1,
|
||||
data: {
|
||||
playlists,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function listSongsFromPlaylist(req, res) {
|
||||
const uid = req.account.uid;
|
||||
const source = req.params.source;
|
||||
const playlistId = req.params.id;
|
||||
|
||||
if (source !== Source.Netease.code || !playlistId) {
|
||||
res.send({
|
||||
status: 1,
|
||||
message: "source or id is invalid",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const detail = await getPlaylistDetail(source, playlistId);
|
||||
if (detail === false) {
|
||||
logger.error(`get playlist detail failed, uid: ${uid}`);
|
||||
}
|
||||
const playlists = detail ? normalizeTunehubPlaylist(playlistId, detail) : false;
|
||||
|
||||
res.send({
|
||||
status: playlists ? 0 : 1,
|
||||
data: {
|
||||
playlists: playlists ? playlists : [],
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listAllPlaylists: listAllPlaylists,
|
||||
listSongsFromPlaylist: listSongsFromPlaylist,
|
||||
}
|
||||
46
backend/src/handler/proxy.js
Normal file
46
backend/src/handler/proxy.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const logger = require('consola');
|
||||
const got = require('got');
|
||||
|
||||
async function proxyAudio(req, res) {
|
||||
const url = req.query.url;
|
||||
const source = req.query.source;
|
||||
const referer = req.query.referer;
|
||||
|
||||
if (!url || !source) {
|
||||
res.status(400).send({
|
||||
status: 1,
|
||||
message: "url and source are required"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 只允许 bilibili 源
|
||||
if (source !== 'bilibili') {
|
||||
res.status(403).send({
|
||||
status: 1,
|
||||
message: "only bilibili source is allowed"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = got.stream(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko)',
|
||||
'Referer': referer || 'https://www.bilibili.com'
|
||||
}
|
||||
});
|
||||
|
||||
stream.pipe(res);
|
||||
} catch (err) {
|
||||
logger.error('proxy audio error:', err);
|
||||
res.status(500).send({
|
||||
status: 1,
|
||||
message: "proxy failed"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
proxyAudio
|
||||
};
|
||||
27
backend/src/handler/scheduler.js
Normal file
27
backend/src/handler/scheduler.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const schedulerService = require('../service/scheduler');
|
||||
const AccountService = require('../service/account');
|
||||
|
||||
async function getNextRun(req, res) {
|
||||
const localNextRun = schedulerService.getLocalSyncNextRun();
|
||||
const accounts = await AccountService.getAllAccounts();
|
||||
|
||||
const cloudNextRuns = {};
|
||||
for (const uid in accounts) {
|
||||
const nextRun = schedulerService.getCloudSyncNextRun(uid);
|
||||
if (nextRun) {
|
||||
cloudNextRuns[uid] = nextRun;
|
||||
}
|
||||
}
|
||||
|
||||
res.send({
|
||||
status: 0,
|
||||
data: {
|
||||
localNextRun,
|
||||
cloudNextRuns
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getNextRun
|
||||
};
|
||||
31
backend/src/handler/song_meta.js
Normal file
31
backend/src/handler/song_meta.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const logger = require('consola');
|
||||
const { getMetaWithUrl } = require('../service/media_fetcher');
|
||||
const { matchUrlFromStr } = require('../utils/regex');
|
||||
|
||||
async function getMeta(req, res) {
|
||||
const query = req.query;
|
||||
|
||||
const url = matchUrlFromStr(query.url);
|
||||
|
||||
if (!url) {
|
||||
res.send({
|
||||
status: 1,
|
||||
message: "url is invalid",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const songMeta = await getMetaWithUrl(url);
|
||||
songMeta && (songMeta.pageUrl = url);
|
||||
|
||||
res.send({
|
||||
status: songMeta ? 0 : 1,
|
||||
data: {
|
||||
songMeta,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMeta: getMeta
|
||||
}
|
||||
76
backend/src/handler/songs.js
Normal file
76
backend/src/handler/songs.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const logger = require('consola');
|
||||
const { searchSongsWithKeyword, searchSongsWithSongMeta } = require('../service/search_songs');
|
||||
const { getPlayUrlWithOptions } = require('../service/songs_info');
|
||||
const { getMetaWithUrl } = require('../service/media_fetcher');
|
||||
const { matchUrlFromStr } = require('../utils/regex');
|
||||
|
||||
async function search(req, res) {
|
||||
const query = req.query;
|
||||
|
||||
const keywordOrUrl = query.keyword;
|
||||
|
||||
if (!keywordOrUrl) {
|
||||
res.send({
|
||||
status: 1,
|
||||
message: "keyword is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
let songs = [];
|
||||
const url = matchUrlFromStr(keywordOrUrl);
|
||||
if (!url) {
|
||||
songs = await searchSongsWithKeyword(keywordOrUrl);
|
||||
} else {
|
||||
const songMeta = await getMetaWithUrl(url);
|
||||
if (!songMeta) {
|
||||
res.send({
|
||||
status: 2,
|
||||
message: "can not get song meta with this url",
|
||||
});
|
||||
return;
|
||||
}
|
||||
songs = await searchSongsWithSongMeta({
|
||||
songName: songMeta.songName,
|
||||
artist: songMeta.artist,
|
||||
album: songMeta.album,
|
||||
duration: songMeta.duration,
|
||||
}, {
|
||||
expectArtistAkas: [],
|
||||
allowSongsJustMatchDuration: true,
|
||||
allowSongsNotMatchMeta: true,
|
||||
});
|
||||
}
|
||||
|
||||
res.send({
|
||||
status: 0,
|
||||
data: {
|
||||
songs: songs ? songs : [],
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getPlayUrl(req, res) {
|
||||
const source = req.params.source;
|
||||
const songId = req.params.id;
|
||||
|
||||
if (!source || !songId) {
|
||||
res.send({
|
||||
status: 1,
|
||||
message: "source and songId is required",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const playUrl = await getPlayUrlWithOptions(req.account.uid, source, songId);
|
||||
|
||||
res.send({
|
||||
status: 0,
|
||||
data: {
|
||||
playUrl,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
search: search,
|
||||
getPlayUrl: getPlayUrl
|
||||
}
|
||||
183
backend/src/handler/sync_jobs.js
Normal file
183
backend/src/handler/sync_jobs.js
Normal file
@@ -0,0 +1,183 @@
|
||||
const logger = require('consola');
|
||||
const { unblockMusicInPlaylist, unblockMusicWithSongId } = require('../service/sync_music');
|
||||
const JobType = require('../consts/job_type');
|
||||
const Source = require('../consts/source').consts;
|
||||
const { matchUrlFromStr } = require('../utils/regex');
|
||||
const { syncSingleSongWithUrl, syncPlaylist } = require('../service/sync_music');
|
||||
const findTheBestMatchFromWyCloud = require('../service/search_songs/find_the_best_match_from_wycloud');
|
||||
const JobManager = require('../service/job_manager');
|
||||
const JobStatus = require('../consts/job_status');
|
||||
const BusinessCode = require('../consts/business_code');
|
||||
|
||||
|
||||
async function createJob(req, res) {
|
||||
const uid = req.account.uid;
|
||||
const request = req.body;
|
||||
|
||||
const jobType = request.jobType;
|
||||
const options = request.options;
|
||||
let jobId = 0;
|
||||
|
||||
if (jobType === JobType.UnblockedPlaylist || jobType === JobType.SyncThePlaylistToLocalService) {
|
||||
const source = request.playlist && request.playlist.source;
|
||||
const playlistId = request.playlist && request.playlist.id;
|
||||
|
||||
if (source !== Source.Netease.code || !playlistId) {
|
||||
res.status(412).send({
|
||||
status: 1,
|
||||
message: "source or id is invalid",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (jobType === JobType.UnblockedPlaylist) {
|
||||
jobId = await unblockMusicInPlaylist(uid, source, playlistId, {
|
||||
syncWySong: options.syncWySong,
|
||||
syncNotWySong: options.syncNotWySong,
|
||||
asyncExecute: true,
|
||||
});
|
||||
} else {
|
||||
jobId = await syncPlaylist(uid, source, playlistId)
|
||||
}
|
||||
} else if (jobType === JobType.UnblockedSong) {
|
||||
const source = request.source;
|
||||
const songId = request.songId;
|
||||
|
||||
if (source !== Source.Netease.code || !songId) {
|
||||
res.status(412).send({
|
||||
status: 1,
|
||||
message: "source or id is invalid",
|
||||
});
|
||||
return;
|
||||
}
|
||||
jobId = await unblockMusicWithSongId(uid, source, songId)
|
||||
} else if (jobType === JobType.SyncSongFromUrl || jobType === JobType.DownloadSongFromUrl) {
|
||||
const request = req.body;
|
||||
const url = request.urlJob && matchUrlFromStr(request.urlJob.url);
|
||||
|
||||
if (!url) {
|
||||
res.status(412).send({
|
||||
status: 1,
|
||||
message: "url is invalid",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let meta = {};
|
||||
const songId = request.urlJob && request.urlJob.meta.songId ? request.urlJob.meta.songId : "";
|
||||
|
||||
if (request.urlJob.meta && (request.urlJob.meta.songName !== "" && request.urlJob.meta.artist !== "")) {
|
||||
meta = {
|
||||
songName: request.urlJob.meta.songName,
|
||||
artist: request.urlJob.meta.artist,
|
||||
album : request.urlJob.meta.album ? request.urlJob.meta.album : "",
|
||||
};
|
||||
}
|
||||
|
||||
if (songId) {
|
||||
const songFromWyCloud = await findTheBestMatchFromWyCloud(req.account.uid, {
|
||||
songName: meta.songName,
|
||||
artist: meta.artist,
|
||||
album: meta.album,
|
||||
musicPlatformSongId: songId,
|
||||
});
|
||||
if (!songFromWyCloud) {
|
||||
logger.error(`song not found in wycloud`);
|
||||
res.status(412).send({
|
||||
status: 1,
|
||||
message: "can not find song in wycloud with your songId",
|
||||
});
|
||||
return;
|
||||
}
|
||||
meta.songFromWyCloud = songFromWyCloud;
|
||||
}
|
||||
|
||||
// create job
|
||||
const args = `${jobType}: {"url":${url}}`;
|
||||
if (await JobManager.findActiveJobByArgs(uid, args)) {
|
||||
logger.info(`${jobType} job is already running.`);
|
||||
jobId = BusinessCode.StatusJobAlreadyExisted;
|
||||
} else {
|
||||
const operation = jobType === JobType.SyncSongFromUrl ? "上传" : "下载";
|
||||
jobId = await JobManager.createJob(uid, {
|
||||
name: `${operation}歌曲:${meta.songName ? meta.songName : url}`,
|
||||
args,
|
||||
type: jobType,
|
||||
status: JobStatus.Pending,
|
||||
desc: `歌曲:${meta.songName ? meta.songName : url}`,
|
||||
progress: 0,
|
||||
tip: `等待${operation}`,
|
||||
createdAt: Date.now()
|
||||
});
|
||||
|
||||
// async job
|
||||
syncSingleSongWithUrl(req.account.uid, url, meta, jobId, jobType).then(async ret => {
|
||||
await JobManager.updateJob(uid, jobId, {
|
||||
status: ret === true ? JobStatus.Finished : JobStatus.Failed,
|
||||
progress: 1,
|
||||
tip: ret === true ? `${operation}成功` : `${operation}失败`,
|
||||
});
|
||||
})
|
||||
}
|
||||
} else {
|
||||
res.status(412).send({
|
||||
status: 1,
|
||||
message: "jobType is not supported",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (jobId === false) {
|
||||
logger.error(`create job failed, uid: ${uid}`);
|
||||
res.status(412).send({
|
||||
status: 1,
|
||||
message: "create job failed",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (jobId === BusinessCode.StatusJobAlreadyExisted) {
|
||||
res.status(412).send({
|
||||
status: BusinessCode.StatusJobAlreadyExisted,
|
||||
message: "你的任务已经在跑啦,等等吧",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (jobId === BusinessCode.StatusJobNoNeedToCreate) {
|
||||
res.status(412).send({
|
||||
status: BusinessCode.StatusJobAlreadyExisted,
|
||||
message: "你的任务无需被创建,可能是因为没有需要 sync 的歌曲",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.status(201).send({
|
||||
status: jobId ? 0 : 1,
|
||||
data: {
|
||||
jobId,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function listAllJobs(req, res) {
|
||||
res.send({
|
||||
status: 0,
|
||||
data: {
|
||||
jobs: await JobManager.listJobs(req.account.uid),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function getJob(req, res) {
|
||||
res.send({
|
||||
status: 0,
|
||||
data: {
|
||||
jobs: await JobManager.getJob(req.account.uid, req.params.id),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createJob: createJob,
|
||||
listAllJobs: listAllJobs,
|
||||
getJob: getJob,
|
||||
}
|
||||
Reference in New Issue
Block a user