初始化提交

This commit is contained in:
史悦
2026-01-07 16:46:09 +08:00
commit 0dbb36be9d
113 changed files with 16197 additions and 0 deletions

View 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,
}

View 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,
}

View 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,
}

View 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,
}

View 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
};

View 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
};

View 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
}

View 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
}

View 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,
}