Skip to content

Commit f1d49de

Browse files
committed
feat: 音乐播放器
1 parent dea59b9 commit f1d49de

34 files changed

+1346
-0
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"astro": "astro",
1212
"type-check": "tsc --noEmit --isolatedDeclarations",
1313
"new-post": "node scripts/new-post.js",
14+
"extract-covers": "node scripts/extract-music-covers.mjs",
1415
"format": "biome format --write ./src",
1516
"lint": "biome check --write ./src",
1617
"preinstall": "npx only-allow pnpm"
@@ -69,6 +70,7 @@
6970
"@types/markdown-it": "^14.1.2",
7071
"@types/mdast": "^4.0.4",
7172
"@types/sanitize-html": "^2.16.0",
73+
"music-metadata": "^11.12.3",
7274
"postcss-import": "^16.1.1",
7375
"postcss-nesting": "^13.0.2"
7476
},

pnpm-lock.yaml

Lines changed: 102 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
514 KB
Loading
156 KB
Loading
11.7 MB
Binary file not shown.
10 MB
Binary file not shown.

scripts/extract-music-covers.mjs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* 从 public/assets/music/mp3/ 下的 mp3 文件提取 ID3 封面
3+
* 输出封面到 public/assets/music/cover/
4+
* 并自动更新 src/_data/music.json
5+
*
6+
* 用法: pnpm extract-covers
7+
*/
8+
9+
import * as mm from "music-metadata";
10+
import fs from "node:fs";
11+
import path from "node:path";
12+
import { fileURLToPath } from "node:url";
13+
14+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
15+
const root = path.resolve(__dirname, "..");
16+
17+
const mp3Dir = path.join(root, "public/assets/music/mp3");
18+
const coverDir = path.join(root, "public/assets/music/cover");
19+
const jsonPath = path.join(root, "src/_data/music.json");
20+
21+
fs.mkdirSync(coverDir, { recursive: true });
22+
23+
const mp3Files = fs
24+
.readdirSync(mp3Dir)
25+
.filter((f) => f.toLowerCase().endsWith(".mp3"))
26+
.sort();
27+
28+
if (mp3Files.length === 0) {
29+
console.log("没有找到 mp3 文件");
30+
process.exit(0);
31+
}
32+
33+
const playlist = [];
34+
35+
for (let i = 0; i < mp3Files.length; i++) {
36+
const file = mp3Files[i];
37+
const filePath = path.join(mp3Dir, file);
38+
const baseName = path.basename(file, ".mp3");
39+
40+
console.log(`处理: ${file}`);
41+
42+
let title = baseName;
43+
let artist = "未知艺术家";
44+
let coverPath = "/assets/music/cover/default.webp";
45+
46+
try {
47+
const meta = await mm.parseFile(filePath);
48+
const tags = meta.common;
49+
50+
if (tags.title) title = tags.title;
51+
if (tags.artist) artist = tags.artist;
52+
53+
// 提取内嵌封面
54+
const pic = tags.picture?.[0];
55+
if (pic) {
56+
const ext = pic.format.includes("png") ? "png" : "jpg";
57+
const coverFile = `${baseName}.${ext}`;
58+
const coverFullPath = path.join(coverDir, coverFile);
59+
fs.writeFileSync(coverFullPath, pic.data);
60+
coverPath = `/assets/music/cover/${coverFile}`;
61+
console.log(` ✓ 提取封面 → ${coverFile}`);
62+
} else {
63+
console.log(` ! 无内嵌封面,使用默认封面`);
64+
}
65+
} catch (e) {
66+
console.error(` ✗ 读取失败: ${e.message}`);
67+
}
68+
69+
playlist.push({
70+
id: i + 1,
71+
title,
72+
artist,
73+
cover: coverPath,
74+
url: `/assets/music/mp3/${file}`,
75+
duration: 0,
76+
});
77+
}
78+
79+
fs.writeFileSync(jsonPath, JSON.stringify(playlist, null, "\t"), "utf-8");
80+
console.log(`\n✓ 已更新 src/_data/music.json(共 ${playlist.length} 首)`);

src/_data/music.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[
2+
{
3+
"id": 1,
4+
"title": "Memories of Kindness",
5+
"artist": "鹿乃",
6+
"cover": "/assets/music/cover/Memories of Kindness.jpg",
7+
"url": "/assets/music/mp3/Memories of Kindness.mp3",
8+
"duration": 0
9+
},
10+
{
11+
"id": 2,
12+
"title": "星座になれたら",
13+
"artist": "結束バンド",
14+
"cover": "/assets/music/cover/若能化为星座.jpg",
15+
"url": "/assets/music/mp3/若能化为星座.mp3",
16+
"duration": 0
17+
}
18+
]

src/components/widget/SideBar.astro

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
---
22
import type { MarkdownHeading } from "astro";
3+
import { musicPlayerConfig } from "../../config";
34
import Categories from "./Categories.astro";
5+
import MusicPlayerWidget from "./music/MusicPlayerWidget.astro";
46
import Profile from "./Profile.astro";
57
import Tag from "./Tags.astro";
68
@@ -16,6 +18,7 @@ const className = Astro.props.class;
1618
<Profile></Profile>
1719
</div>
1820
<div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4">
21+
{musicPlayerConfig.enable && <MusicPlayerWidget class="onload-animation" style="animation-delay: 100ms" />}
1922
<Categories class="onload-animation" style="animation-delay: 150ms"></Categories>
2023
<Tag class="onload-animation" style="animation-delay: 200ms"></Tag>
2124
</div>

0 commit comments

Comments
 (0)