mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-02 04:52: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;
|
return this.cookie;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Not applicable for BiliBili (video platform) ---
|
// --- B站推荐内容 ---
|
||||||
|
|
||||||
async getPlaylistSongs(_playlistId: string): Promise<Song[]> {
|
/** 热门视频 (无需登录) */
|
||||||
|
async getRecommendPlaylists(): Promise<Playlist[]> {
|
||||||
|
// B站没有"歌单"概念,返回空
|
||||||
return [];
|
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 [];
|
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
|
// Get current quality
|
||||||
router.get("/quality", (_req, res) => {
|
router.get("/quality", (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const usePlayerStore = defineStore('player', {
|
|||||||
recommendPlaylists: [] as PlaylistItem[],
|
recommendPlaylists: [] as PlaylistItem[],
|
||||||
dailySongs: [] as Song[],
|
dailySongs: [] as Song[],
|
||||||
userPlaylists: [] as PlaylistItem[],
|
userPlaylists: [] as PlaylistItem[],
|
||||||
|
bilibiliPopular: [] as Song[],
|
||||||
lastFetchTime: 0,
|
lastFetchTime: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
@@ -270,10 +271,11 @@ export const usePlayerStore = defineStore('player', {
|
|||||||
return;
|
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/playlists'),
|
||||||
axios.get('/api/music/recommend/songs'),
|
axios.get('/api/music/recommend/songs'),
|
||||||
axios.get('/api/music/user/playlists'),
|
axios.get('/api/music/user/playlists'),
|
||||||
|
axios.get('/api/music/bilibili/popular?limit=12'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (playlistRes.status === 'fulfilled') {
|
if (playlistRes.status === 'fulfilled') {
|
||||||
@@ -285,6 +287,9 @@ export const usePlayerStore = defineStore('player', {
|
|||||||
if (userRes.status === 'fulfilled') {
|
if (userRes.status === 'fulfilled') {
|
||||||
this.userPlaylists = userRes.value.data.playlists;
|
this.userPlaylists = userRes.value.data.playlists;
|
||||||
}
|
}
|
||||||
|
if (biliRes.status === 'fulfilled') {
|
||||||
|
this.bilibiliPopular = biliRes.value.data.songs;
|
||||||
|
}
|
||||||
|
|
||||||
this.lastFetchTime = Date.now();
|
this.lastFetchTime = Date.now();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -95,6 +95,26 @@
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -182,6 +202,22 @@ onMounted(() => {
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 16px;
|
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
|
// Now Playing
|
||||||
|
|||||||
Reference in new issue
Block a user