backend: music metadata submit, retrieve & search
This commit is contained in:
parent
ce51da59c9
commit
a3064385c2
5
data/song/BV1Xp421o7hr.json
Normal file
5
data/song/BV1Xp421o7hr.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "大哉乾元",
|
||||
"singer": "洛天依",
|
||||
"lyric": "[ti: 大哉乾元]\n[ar: 洛天依]\n[al: 2024哔哩哔哩拜年纪]\n[tool: 歌词滚动姬 https://lrc-maker.github.io]\n[length: 04:19.536]\n[00:05.630] 经起幽明 悟处通玄\n[00:09.680] 首窥龙堑 见岳见渊\n[00:13.640] 道不善宣 义不善绻\n[00:17.270] 源流万世 大哉乾元!\n[00:21.837]\n[00:36.390] 不曾闻日月争辉\n[00:38.170] 坎离复往 立下恒规\n[00:40.330] 照东南 有坤徇乾 承西北\n[00:43.680] 天道自昆仑巍巍\n[00:45.590] 翻起华夏巽震艮兑\n[00:47.810] 万象予万灵得见 两相盈岁\n[00:51.210] 潜龙长生应紫微\n[00:53.000] 惟向四方五气寻遂\n[00:55.200] 燧火旁八卦百草揆经纬\n[00:58.220] 正位 纪天下一归\n[01:00.370] 不消祈天退水\n[01:02.590] 初难知一念一决生龙髓\n[01:05.930] 百家注龙慧 千军起龙威 砥淬\n[01:10.260] 妙笔生文穗 罡风抚长麾\n[01:13.030] 始见龙形汇 以天田冲腾直向九陲\n[01:20.490] 龙震于疆 万里宁壤 天地皆可往\n[01:24.140] 龙秀于象 引仙来访 诗蜀道河江\n[01:27.940] 龙明于章 执笔成鉴 映五千煌煌\n[01:31.820] 不独九州五岳 帝王将相见苍茫\n[01:35.340] 龙泽于汤 唤水筑乡 单舟见京杭\n[01:39.040] 龙健于常 百音同讲 道一种炎黄\n[01:42.740] 龙景于康 见之庙堂 亦显于曲坊\n[01:46.580] 不劳此间祥云瑞兽频频诰春长!\n[01:57.280] 干支移晷又几回\n[01:59.500] 揽尽天骄襄助一醉\n[02:01.790] 虽万言竟道不尽无字碑\n[02:04.680] 临渊乾乾 君子催\n[02:06.600] 或跃 无咎相随\n[02:09.130] 同为龙 却与往昔不连讳\n[02:12.390] 且待飞龙归 簸却沧溟水 如沸\n[02:16.700] 有龙掸风雷 见首不见尾\n[02:19.540] 苏苏万物蜕 证元亨利贞变易轮回\n[02:27.080] 龙华于旸 红旗漫卷 新水濯旧隍\n[02:30.740] 龙泰于霜 烽烟消长 更赳赳昂昂\n[02:34.430] 龙温于壮 留待潺潺 驰涌成泱泱\n[02:38.260] 好教流光紫极 鹊渡银潢伴流觞\n[02:41.660] 龙韧于刚 龙吟激荡 云止聆佳响\n[02:45.490] 龙德于昌 喜见船马 纵横间丰仓\n[02:49.150] 龙眷于邦 情习众广 仍化为一方\n[02:52.930] 其妙错综复杂 不孤兵车付一匡!\n[02:56.602]\n[03:40.980] 此去向东 瀚海游龙 滔滔几万重\n[03:44.620] 一跃破空 乘风逐虹 猎猎青云中\n[03:48.250] 天音入梦 扶摇上穹 矫矫游星宫\n[03:52.080] 犹念神州谷稻耕耘收藏守时无?\n[03:55.670] 一息一动 似异似同 无之以为用\n[03:59.240] 天地辰龙 龙生九种 但两爻合共\n[04:03.130] 假逢童蒙 欲解懵懂 何处有真龙\n[04:06.830] 只道「大哉乾元」秩秩幽幽必然中\n[04:10.350] 也道「大哉乾元」切切实实一言中!\n[04:14.693]"
|
||||
}
|
@ -8,6 +8,7 @@ AquaVox 由于支持多种歌曲源,因此在代码层面标识时需要以不
|
||||
- 对于本地添加,开启跨设备同步后在云端音乐库匹配的歌曲,我们会用云端的歌曲唯一 ID 覆盖本地 ID。
|
||||
- 对于通过哔哩哔哩收藏夹导入,在云端音乐库中不存在的音乐,我们会以 BV 号作为唯一 ID。
|
||||
- 对于云端音乐库中的歌曲,我们以 BV 号(首选)或 `md5(歌曲名+作者[主发布人])` 作为唯一 ID。
|
||||
- 仅发布在网易云平台的,以 \`NE{id}\` 作为唯一 ID。(例如,`NE2141645940`)
|
||||
|
||||
但是,AquaVox 的云端音乐库由于其特殊性质,不显式公开其存在。
|
||||
我们未来可能允许基于社区的歌曲分享及交流,但不会像传统音乐平台一样直接公开音乐库。
|
||||
|
99
package.json
99
package.json
@ -1,50 +1,53 @@
|
||||
{
|
||||
"name": "aquavox",
|
||||
"version": "1.10.1",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"test": "vitest",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"go": "PORT=4173 node build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/svelte": "^4.0.2",
|
||||
"@sveltejs/adapter-auto": "^3.2.0",
|
||||
"@sveltejs/adapter-node": "^5.0.1",
|
||||
"@sveltejs/kit": "^2.5.9",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"@types/eslint": "^8.56.10",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.39.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.3",
|
||||
"svelte": "^4.2.17",
|
||||
"svelte-check": "^3.7.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
"bezier-easing": "^2.1.0",
|
||||
"jotai": "^2.8.0",
|
||||
"jotai-svelte": "^0.0.2",
|
||||
"localforage": "^1.10.0",
|
||||
"lrc-parser-ts": "^1.0.3",
|
||||
"music-metadata-browser": "^2.5.10",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
"name": "aquavox",
|
||||
"version": "1.10.1",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch",
|
||||
"test": "vitest",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"go": "PORT=4173 node build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify/svelte": "^4.0.2",
|
||||
"@sveltejs/adapter-auto": "^3.2.0",
|
||||
"@sveltejs/adapter-node": "^5.0.1",
|
||||
"@sveltejs/kit": "^2.5.9",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.1.0",
|
||||
"@types/eslint": "^8.56.10",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.39.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-svelte": "^3.2.3",
|
||||
"svelte": "^4.2.17",
|
||||
"svelte-check": "^3.7.1",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.6.0"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||
"@types/bun": "^1.1.6",
|
||||
"bezier-easing": "^2.1.0",
|
||||
"jotai": "^2.8.0",
|
||||
"jotai-svelte": "^0.0.2",
|
||||
"localforage": "^1.10.0",
|
||||
"lrc-parser-ts": "^1.0.3",
|
||||
"music-metadata-browser": "^2.5.10",
|
||||
"node-cache": "^5.1.2",
|
||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||
"uuid": "^9.0.1"
|
||||
}
|
||||
}
|
||||
|
3374
pnpm-lock.yaml
3374
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
4
src/lib/server/cache.ts
Normal file
4
src/lib/server/cache.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import NodeCache from "node-cache";
|
||||
export const songData = new NodeCache( { checkperiod: 0 } );
|
||||
export const songNameCache = new NodeCache( { checkperiod: 0} );
|
||||
export const globalMemoryStorage = new NodeCache( { checkperiod: 0} );
|
33
src/lib/server/database/loadData.ts
Normal file
33
src/lib/server/database/loadData.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import fs from 'fs';
|
||||
import { globalMemoryStorage, songData, songNameCache } from '$lib/server/cache.js';
|
||||
|
||||
export async function loadData() {
|
||||
const LastLoaded: number | undefined = globalMemoryStorage.get("lastLoadData");
|
||||
const currentTime = new Date().getTime();
|
||||
// Already loaded.
|
||||
if (LastLoaded && currentTime - LastLoaded < 3600) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dataPath = "./data/song/";
|
||||
const songList = fs.readdirSync(dataPath)
|
||||
.map(fileName => {
|
||||
if (fileName.endsWith(".json"))
|
||||
return fileName.slice(0, fileName.length - 5);
|
||||
else return null;
|
||||
})
|
||||
.filter(fileName => fileName !== null);
|
||||
for (const songID of songList) {
|
||||
try {
|
||||
const fileContentString = fs.readFileSync(dataPath + songID + ".json").toString();
|
||||
const data = JSON.parse(fileContentString);
|
||||
songData.set(songID, data);
|
||||
const metadata: MusicMetadata = data;
|
||||
songNameCache.set(metadata.name, metadata);
|
||||
}
|
||||
catch {
|
||||
console.error(`[load-song-data] Could not load song ID ${songID}`);
|
||||
}
|
||||
}
|
||||
globalMemoryStorage.set("lastLoadData", new Date().getTime());
|
||||
}
|
18
src/lib/server/database/musicInfo.d.ts
vendored
Normal file
18
src/lib/server/database/musicInfo.d.ts
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
interface MusicMetadata {
|
||||
name: string;
|
||||
signer?: string | string[];
|
||||
producer?: string;
|
||||
lyric?: string;
|
||||
tuning?: string | string[];
|
||||
lyricist?: string | string[];
|
||||
composer?: string | string[];
|
||||
arranger?: string | string[];
|
||||
mixing?: string | string[];
|
||||
video?: string | string[];
|
||||
illustrator?: string | string[];
|
||||
songURL?: string;
|
||||
duration?: number;
|
||||
publishTime?: string;
|
||||
views?: number;
|
||||
updateTime?: string;
|
||||
}
|
27
src/routes/api/database/search/+server.ts
Normal file
27
src/routes/api/database/search/+server.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { songNameCache } from '$lib/server/cache.js';
|
||||
import { loadData } from '$lib/server/database/loadData';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url }) {
|
||||
const keyword = url.searchParams.get("keyword");
|
||||
|
||||
loadData();
|
||||
|
||||
if (keyword === null) {
|
||||
return error(400, {
|
||||
"message": "Miss parameter: keyword"
|
||||
})
|
||||
}
|
||||
|
||||
const resultList: MusicMetadata[] = [];
|
||||
|
||||
for (const songName of songNameCache.keys()){
|
||||
if (songName.toLocaleLowerCase().includes(keyword.toLocaleLowerCase())) {
|
||||
resultList.push(songNameCache.get(songName)!);
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
"result": resultList
|
||||
});
|
||||
}
|
25
src/routes/api/database/song/[id]/+server.ts
Normal file
25
src/routes/api/database/song/[id]/+server.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import fs from 'fs';
|
||||
|
||||
export async function GET({ params }) {
|
||||
const filePath = `./data/song/${params.id}.json`;
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return error(404, {
|
||||
message: "No correspoding song."
|
||||
})
|
||||
}
|
||||
const data = fs.readFileSync(filePath);
|
||||
return json(JSON.parse(data.toString()));
|
||||
}
|
||||
|
||||
export async function POST({ params, request }) {
|
||||
const timeStamp = new Date().getTime();
|
||||
if (!fs.existsSync("./data/pending/")) {
|
||||
fs.mkdirSync("./data/pending");
|
||||
}
|
||||
const filePath = `./data/pending/${params.id}-${timeStamp}.json`;
|
||||
const data: MusicMetadata = await request.json();
|
||||
data.updateTime = new Date().getTime().toString();
|
||||
fs.writeFileSync(filePath, JSON.stringify(data));
|
||||
return json({});
|
||||
}
|
15
src/routes/api/database/songs/+server.ts
Normal file
15
src/routes/api/database/songs/+server.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { songData } from '$lib/server/cache.js';
|
||||
import { loadData } from '$lib/server/database/loadData.js';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export async function GET({ url }) {
|
||||
const limit = parseInt(url.searchParams.get("limit") ?? "20");
|
||||
const offset = parseInt(url.searchParams.get("offset") ?? "0");
|
||||
loadData();
|
||||
const songIDList = songData.keys().slice(offset, offset + limit);
|
||||
const songDataList = [];
|
||||
for (const songID of songIDList) {
|
||||
songDataList.push(songData.get(songID)!);
|
||||
}
|
||||
return json(songDataList);
|
||||
}
|
3
src/routes/database/+layout.svelte
Normal file
3
src/routes/database/+layout.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="absolute w-screen md:w-2/3 lg:w-1/2 left-0 md:left-[16.6667%] lg:left-1/4 px-[3%] md:px-0 top-16">
|
||||
<slot />
|
||||
</div>
|
3
src/routes/database/+page.svelte
Normal file
3
src/routes/database/+page.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<div>
|
||||
<h1>AquaVox 音乐数据库</h1>
|
||||
</div>
|
@ -9,7 +9,8 @@
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"moduleResolution": "bundler"
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["bun"]
|
||||
}
|
||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
||||
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
|
Loading…
Reference in New Issue
Block a user