mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat: B站首页推荐 — 个性化推荐/音乐区排行/热门视频
- BiliBiliProvider.getDailyRecommendSongs: 个性化推荐(登录后更准确), fallback 到音乐区排行榜 - BiliBiliProvider.getPopularVideos: 热门视频列表 - 新增 GET /api/music/bilibili/popular 端点 - 首页新增"B站热门"板块,带B站蓝色徽标 - Store 缓存 bilibiliPopular 数据 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
05d7f7d5f7
commit
cf293299c8
4 files changed
+141
-4
No files matched your search
+82
-3
@@ -271,13 +271,92 @@ export class BiliBiliProvider implements MusicProvider {
|
||||
return this.cookie;
|
||||
}
|
||||
|
||||
// --- Not applicable for BiliBili (video platform) ---
|
||||
// --- B站推荐内容 ---
|
||||
|
||||
async getPlaylistSongs(_playlistId: string): Promise<Song[]> {
|
||||
/** 热门视频 (无需登录) */
|
||||
async getRecommendPlaylists(): Promise<Playlist[]> {
|
||||
// B站没有"歌单"概念,返回空
|
||||
return [];
|
||||
}
|
||||
|
||||
async getRecommendPlaylists(): Promise<Playlist[]> {
|
||||
/** 音乐区排行榜 + 个性化推荐(如果已登录)作为"每日推荐" */
|
||||
async getDailyRecommendSongs(): Promise<Song[]> {
|
||||
await this.ensureBuvidCookie();
|
||||
const songs: Song[] = [];
|
||||
|
||||
// 1. 个性化推荐(带 cookie 效果更好)
|
||||
try {
|
||||
const res = await this.api.get("/x/web-interface/index/top/rcmd", {
|
||||
params: { ps: 10, fresh_type: 3 },
|
||||
headers: this.cookieHeaders,
|
||||
});
|
||||
const items = res.data?.data?.item ?? [];
|
||||
for (const v of items) {
|
||||
songs.push({
|
||||
id: String(v.bvid),
|
||||
name: v.title ?? "",
|
||||
artist: v.owner?.name ?? "",
|
||||
album: "",
|
||||
duration: v.duration ?? 0,
|
||||
coverUrl: this.normalizeCover(v.pic ?? ""),
|
||||
platform: "bilibili" as const,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// fallback to popular
|
||||
}
|
||||
|
||||
// 2. 如果推荐为空,用音乐区排行榜(tid=3)
|
||||
if (songs.length === 0) {
|
||||
try {
|
||||
const res = await this.api.get("/x/web-interface/ranking/v2", {
|
||||
params: { rid: 3, type: "all" },
|
||||
headers: this.cookieHeaders,
|
||||
});
|
||||
const list = res.data?.data?.list ?? [];
|
||||
for (const v of list.slice(0, 20)) {
|
||||
songs.push({
|
||||
id: String(v.bvid),
|
||||
name: v.title ?? "",
|
||||
artist: v.owner?.name ?? "",
|
||||
album: "",
|
||||
duration: v.duration ?? 0,
|
||||
coverUrl: this.normalizeCover(v.pic ?? ""),
|
||||
platform: "bilibili" as const,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
return songs;
|
||||
}
|
||||
|
||||
/** 热门视频列表 */
|
||||
async getPopularVideos(limit = 20): Promise<Song[]> {
|
||||
try {
|
||||
const res = await this.api.get("/x/web-interface/popular", {
|
||||
params: { ps: limit, pn: 1 },
|
||||
headers: this.cookieHeaders,
|
||||
});
|
||||
return (res.data?.data?.list ?? []).map((v: any) => ({
|
||||
id: String(v.bvid),
|
||||
name: v.title ?? "",
|
||||
artist: v.owner?.name ?? "",
|
||||
album: "",
|
||||
duration: v.duration ?? 0,
|
||||
coverUrl: this.normalizeCover(v.pic ?? ""),
|
||||
platform: "bilibili" as const,
|
||||
}));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// --- 不适用于B站 ---
|
||||
|
||||
async getPlaylistSongs(_playlistId: string): Promise<Song[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,23 @@ export function createMusicRouter(
|
||||
}
|
||||
});
|
||||
|
||||
// B站热门视频
|
||||
router.get("/bilibili/popular", async (req, res) => {
|
||||
try {
|
||||
const provider = bilibiliProvider as any;
|
||||
if (provider.getPopularVideos) {
|
||||
const limit = parseInt(req.query.limit as string) || 20;
|
||||
const songs = await provider.getPopularVideos(limit);
|
||||
res.json({ songs });
|
||||
} else {
|
||||
res.json({ songs: [] });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Get bilibili popular failed");
|
||||
res.status(500).json({ error: (err as Error).message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get current quality
|
||||
router.get("/quality", (_req, res) => {
|
||||
res.json({
|
||||
|
||||
@@ -50,6 +50,7 @@ export const usePlayerStore = defineStore('player', {
|
||||
recommendPlaylists: [] as PlaylistItem[],
|
||||
dailySongs: [] as Song[],
|
||||
userPlaylists: [] as PlaylistItem[],
|
||||
bilibiliPopular: [] as Song[],
|
||||
lastFetchTime: 0,
|
||||
}),
|
||||
|
||||
@@ -270,10 +271,11 @@ export const usePlayerStore = defineStore('player', {
|
||||
return;
|
||||
}
|
||||
|
||||
const [playlistRes, dailyRes, userRes] = await Promise.allSettled([
|
||||
const [playlistRes, dailyRes, userRes, biliRes] = await Promise.allSettled([
|
||||
axios.get('/api/music/recommend/playlists'),
|
||||
axios.get('/api/music/recommend/songs'),
|
||||
axios.get('/api/music/user/playlists'),
|
||||
axios.get('/api/music/bilibili/popular?limit=12'),
|
||||
]);
|
||||
|
||||
if (playlistRes.status === 'fulfilled') {
|
||||
@@ -285,6 +287,9 @@ export const usePlayerStore = defineStore('player', {
|
||||
if (userRes.status === 'fulfilled') {
|
||||
this.userPlaylists = userRes.value.data.playlists;
|
||||
}
|
||||
if (biliRes.status === 'fulfilled') {
|
||||
this.bilibiliPopular = biliRes.value.data.songs;
|
||||
}
|
||||
|
||||
this.lastFetchTime = Date.now();
|
||||
},
|
||||
|
||||
@@ -95,6 +95,26 @@
|
||||
</RouterLink>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- B站热门 -->
|
||||
<section class="section" v-if="store.bilibiliPopular.length > 0">
|
||||
<h2 class="section-title">
|
||||
<span class="bili-badge">B</span>
|
||||
B站热门
|
||||
</h2>
|
||||
<div class="daily-grid">
|
||||
<div
|
||||
v-for="song in store.bilibiliPopular.slice(0, 12)"
|
||||
:key="song.id"
|
||||
class="daily-card hover-scale"
|
||||
@click="store.playById(song.id, song.platform)"
|
||||
>
|
||||
<CoverArt :url="song.coverUrl" :size="120" :radius="10" :show-shadow="true" />
|
||||
<div class="daily-name">{{ song.name }}</div>
|
||||
<div class="daily-artist">{{ song.artist }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -182,6 +202,22 @@ onMounted(() => {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bili-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: #00a1d6;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
// Now Playing
|
||||
|
||||
Reference in new issue
Block a user