This commit is contained in:
阿梓喵_あずにゃん committed 2026-04-20 23:48:46 +08:00
1 parent 1bd6ab9975
commit 47514f57aa
9 files changed
+91 -104

No files matched your search

+25 -9
View File
@@ -42,7 +42,7 @@ export class QQMusicProvider implements MusicProvider {
const songs: Song[] = (res.data?.response?.data?.song?.list ?? []).map( const songs: Song[] = (res.data?.response?.data?.song?.list ?? []).map(
(s: any) => ({ (s: any) => ({
id: String(s.songmid ?? s.songid), id: String(s.mid ?? s.songmid ?? s.songid),
name: s.songname ?? "", name: s.songname ?? "",
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "), artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
album: s.albumname ?? "", album: s.albumname ?? "",
@@ -57,12 +57,26 @@ export class QQMusicProvider implements MusicProvider {
return { songs, playlists: [], albums: [] }; return { songs, playlists: [], albums: [] };
} }
async getSongUrl(songId: string, _quality?: string): Promise<string | null> { async getSongUrl(songId: string, quality?: string): Promise<string | null> {
const res = await this.api.get("/getMusicPlay", { try {
params: { songmid: songId, ...this.cookieParams }, const res = await this.api.get("/getMusicPlay", {
}); params: { songmid: songId, quality: quality ?? this.quality, ...this.cookieParams },
const playUrl = res.data?.data?.playUrl?.[songId]; });
return playUrl?.url || null; const playUrl = res.data?.data?.playUrl?.[songId];
if (playUrl?.url) return playUrl.url;
} catch {
// try with songid
try {
const res = await this.api.get("/getMusicPlay", {
params: { songid: songId, quality: quality ?? this.quality, ...this.cookieParams },
});
const playUrl = res.data?.data?.playUrl?.[songId];
if (playUrl?.url) return playUrl.url;
} catch {
// ignore
}
}
return null;
} }
async getSongDetail(songId: string): Promise<Song | null> { async getSongDetail(songId: string): Promise<Song | null> {
@@ -115,12 +129,14 @@ export class QQMusicProvider implements MusicProvider {
const cdlist = res.data?.response?.cdlist ?? []; const cdlist = res.data?.response?.cdlist ?? [];
if (cdlist.length === 0) return []; if (cdlist.length === 0) return [];
return (cdlist[0].songlist ?? []).map((s: any) => ({ return (cdlist[0].songlist ?? []).map((s: any) => ({
id: String(s.songmid ?? s.songid), id: String(s.mid ?? s.songmid ?? s.songid),
name: s.songname ?? s.name ?? "", name: s.songname ?? s.name ?? "",
artist: (s.singer ?? []).map((a: any) => a.name).join(" / "), artist: (s.singer ?? []).map((a: any) => a.name).join(" / "),
album: s.albumname ?? "", album: s.albumname ?? "",
duration: s.interval ?? 0, duration: s.interval ?? 0,
coverUrl: s.albummid coverUrl: s.album?.mid
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.album.mid}.jpg`
: s.albummid
? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg` ? `https://y.gtimg.cn/music/photo_new/T002R300x300M000${s.albummid}.jpg`
: "", : "",
platform: "qq", platform: "qq",
+34 -15
View File
@@ -277,35 +277,54 @@ export function createPlayerRouter(
}); });
// Play a single song by ID — resolves URL on demand // Play a single song by ID — resolves URL on demand
router.post("/:botId/play-by-id", async (req, res) => { router.post("/:botId/play-song", async (req, res) => {
try { try {
const bot = (req as any).bot; const bot = (req as any).bot;
const { songId, platform } = req.body; const { song } = req.body;
const provider = bot.getProviderFor( if (!song || !song.id || !song.platform) {
platform === "bilibili" || platform === "qq" || platform === "youtube" res.status(400).json({ error: "song object with id and platform is required" });
? platform
: "netease"
);
const song = await provider.getSongDetail(songId);
if (!song) {
res.json({ message: "Song not found" });
return; return;
} }
const queue = bot.getQueueManager(); const queue = bot.getQueueManager();
queue.clear(); queue.clear();
queue.add({ ...song, platform: provider.platform }); queue.add(song);
queue.play(); queue.play();
bot.getPlayer().resetFailures(); bot.getPlayer().resetFailures();
const ok = await bot.resolveAndPlay(queue.current()!); const ok = await bot.resolveAndPlay(queue.current()!);
if (!ok) { if (!ok) {
res.json({ message: `Cannot play: ${song.name}` }); res.json({ message: `Cannot play: ${song.name || song.id}` });
return; return;
} }
res.json({ message: `Now playing: ${song.name} - ${song.artist}` }); res.json({ message: `Now playing: ${song.name || 'Unknown'} - ${song.artist || 'Unknown'}` });
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
router.post("/:botId/add-song", async (req, res) => {
try {
const bot = (req as any).bot;
const { song } = req.body;
if (!song || !song.id || !song.platform) {
res.status(400).json({ error: "song object with id and platform is required" });
return;
}
const queue = bot.getQueueManager();
const wasIdle = bot.getPlayer().getState() === "idle";
queue.add(song);
// If nothing was playing, start this newly-added song immediately.
if (wasIdle) {
queue.playAt(queue.size() - 1);
bot.getPlayer().resetFailures();
await bot.resolveAndPlay(queue.current()!);
res.json({ message: `Now playing: ${song.name || 'Unknown'} - ${song.artist || 'Unknown'}` });
return;
}
res.json({ message: `Added to queue: ${song.name || 'Unknown'} - ${song.artist || 'Unknown'} (position ${queue.size()})` });
} catch (err) { } catch (err) {
res.status(500).json({ error: (err as Error).message }); res.status(500).json({ error: (err as Error).message });
} }
-57
View File
@@ -797,9 +797,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -821,9 +818,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -845,9 +839,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -869,9 +860,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -893,9 +881,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -917,9 +902,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1088,9 +1070,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1105,9 +1084,6 @@
"arm" "arm"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1122,9 +1098,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1139,9 +1112,6 @@
"arm64" "arm64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1156,9 +1126,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1173,9 +1140,6 @@
"loong64" "loong64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1190,9 +1154,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1207,9 +1168,6 @@
"ppc64" "ppc64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1224,9 +1182,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1241,9 +1196,6 @@
"riscv64" "riscv64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1258,9 +1210,6 @@
"s390x" "s390x"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1275,9 +1224,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1292,9 +1238,6 @@
"x64" "x64"
], ],
"dev": true, "dev": true,
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
+2 -1
View File
@@ -28,9 +28,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { Icon } from '@iconify/vue'; import { Icon } from '@iconify/vue';
import CoverArt from './CoverArt.vue'; import CoverArt from './CoverArt.vue';
import { Song } from '../stores/player.js';
defineProps<{ defineProps<{
song: { id: string; name: string; artist: string; album: string; duration: number; coverUrl: string; platform: string }; song: Song;
index: number; index: number;
active?: boolean; active?: boolean;
}>(); }>();
+13 -1
View File
@@ -8,7 +8,7 @@ export interface Song {
album: string; album: string;
duration: number; duration: number;
coverUrl: string; coverUrl: string;
platform: 'netease' | 'qq'; platform: 'netease' | 'qq' | 'bilibili' | 'youtube';
} }
export interface BotStatus { export interface BotStatus {
@@ -259,6 +259,13 @@ export const usePlayerStore = defineStore('player', {
this._syncAfterAction(); this._syncAfterAction();
}, },
async playSong(song: Song) {
if (!this.activeBotId) return;
await axios.post(`/api/player/${this.activeBotId}/play-song`, { song });
this._setTiming(this.activeBotId, { serverElapsed: 0 });
this._syncAfterAction();
},
async addToQueue(query: string, platform = 'netease') { async addToQueue(query: string, platform = 'netease') {
if (!this.activeBotId) return; if (!this.activeBotId) return;
await axios.post(`/api/player/${this.activeBotId}/add`, { query, platform }); await axios.post(`/api/player/${this.activeBotId}/add`, { query, platform });
@@ -269,6 +276,11 @@ export const usePlayerStore = defineStore('player', {
await axios.post(`/api/player/${this.activeBotId}/add-by-id`, { songId, platform }); await axios.post(`/api/player/${this.activeBotId}/add-by-id`, { songId, platform });
}, },
async addSong(song: Song) {
if (!this.activeBotId) return;
await axios.post(`/api/player/${this.activeBotId}/add-song`, { song });
},
async playPlaylist(playlistId: string, platform = 'netease') { async playPlaylist(playlistId: string, platform = 'netease') {
if (!this.activeBotId) return; if (!this.activeBotId) return;
await axios.post(`/api/player/${this.activeBotId}/play-playlist`, { playlistId, platform }); await axios.post(`/api/player/${this.activeBotId}/play-playlist`, { playlistId, platform });
+5 -3
View File
@@ -19,8 +19,8 @@
:song="song" :song="song"
:index="i + 1" :index="i + 1"
:active="store.currentSong?.id === song.id" :active="store.currentSong?.id === song.id"
@play="store.play(song.name, song.platform)" @play="store.playSong(song)"
@add="store.addToQueue(song.name, song.platform)" @add="store.addSong(song)"
/> />
</div> </div>
</div> </div>
@@ -35,7 +35,9 @@ import SongCard from '../components/SongCard.vue';
const store = usePlayerStore(); const store = usePlayerStore();
const history = ref<Array<{ id: string; name: string; artist: string; album: string; duration: number; coverUrl: string; platform: string }>>([]); import { Song } from '../stores/player.js';
const history = ref<Song[]>([]);
const loading = ref(true); const loading = ref(true);
onMounted(async () => { onMounted(async () => {
+2 -2
View File
@@ -41,7 +41,7 @@
v-for="song in store.dailySongs.slice(0, 12)" v-for="song in store.dailySongs.slice(0, 12)"
:key="song.id" :key="song.id"
class="daily-card hover-scale" class="daily-card hover-scale"
@click="store.playById(song.id, song.platform)" @click="store.playSong(song)"
> >
<CoverArt :url="song.coverUrl" :size="120" :radius="10" :show-shadow="true" /> <CoverArt :url="song.coverUrl" :size="120" :radius="10" :show-shadow="true" />
<div class="daily-name">{{ song.name }}</div> <div class="daily-name">{{ song.name }}</div>
@@ -105,7 +105,7 @@
v-for="song in store.bilibiliPopular.slice(0, 12)" v-for="song in store.bilibiliPopular.slice(0, 12)"
:key="song.id" :key="song.id"
class="daily-card hover-scale" class="daily-card hover-scale"
@click="store.playById(song.id, song.platform)" @click="store.playSong(song)"
> >
<CoverArt :url="song.coverUrl" :size="120" :radius="10" :show-shadow="true" /> <CoverArt :url="song.coverUrl" :size="120" :radius="10" :show-shadow="true" />
<div class="daily-name">{{ song.name }}</div> <div class="daily-name">{{ song.name }}</div>
+5 -13
View File
@@ -31,8 +31,8 @@
:song="song" :song="song"
:index="i + 1" :index="i + 1"
:active="store.currentSong?.id === song.id" :active="store.currentSong?.id === song.id"
@play="store.playById(song.id, song.platform)" @play="store.playSong(song)"
@add="store.addToQueueById(song.id, song.platform)" @add="store.addSong(song)"
/> />
</div> </div>
</template> </template>
@@ -53,6 +53,8 @@ import SongCard from '../components/SongCard.vue';
const store = usePlayerStore(); const store = usePlayerStore();
const route = useRoute(); const route = useRoute();
import { Song } from '../stores/player.js';
interface PlaylistDetail { interface PlaylistDetail {
id: string; id: string;
name: string; name: string;
@@ -61,18 +63,8 @@ interface PlaylistDetail {
songCount: number; songCount: number;
} }
interface SongItem {
id: string;
name: string;
artist: string;
album: string;
duration: number;
coverUrl: string;
platform: string;
}
const playlist = ref<PlaylistDetail | null>(null); const playlist = ref<PlaylistDetail | null>(null);
const songs = ref<SongItem[]>([]); const songs = ref<Song[]>([]);
const loading = ref(true); const loading = ref(true);
async function playAll() { async function playAll() {
+5 -3
View File
@@ -27,8 +27,8 @@
:song="song" :song="song"
:index="i + 1" :index="i + 1"
:active="store.currentSong?.id === song.id" :active="store.currentSong?.id === song.id"
@play="store.playById(song.id, song.platform)" @play="store.playSong(song)"
@add="store.addToQueueById(song.id, song.platform)" @add="store.addSong(song)"
/> />
</div> </div>
@@ -50,7 +50,9 @@ const store = usePlayerStore();
const route = useRoute(); const route = useRoute();
const query = ref((route.query.q as string) || ''); const query = ref((route.query.q as string) || '');
const results = ref<Array<{ id: string; name: string; artist: string; album: string; duration: number; coverUrl: string; platform: string }>>([]); import { Song } from '../stores/player.js';
const results = ref<Song[]>([]);
const loading = ref(false); const loading = ref(false);
const searched = ref(false); const searched = ref(false);