feat: show requester names in play history

This commit is contained in:
Fa1nttt committed 2026-07-04 22:32:21 +08:00
1 parent f8743a904f
commit cb66d77e9c
7 files changed
+101 -39

No files matched your search

+1
View File
@@ -14,6 +14,7 @@ export interface QueuedSong {
url?: string; // resolved lazily at play time url?: string; // resolved lazily at play time
coverUrl: string; coverUrl: string;
duration: number; // seconds duration: number; // seconds
requestedBy?: string;
} }
export class PlayQueue { export class PlayQueue {
+43 -25
View File
@@ -85,6 +85,7 @@ export class BotInstance extends EventEmitter {
private profileManager: BotProfileManager; private profileManager: BotProfileManager;
private isFmMode = false; private isFmMode = false;
private fmProvider: MusicProvider | null = null; private fmProvider: MusicProvider | null = null;
private fmRequesterName: string | undefined;
/** Results of the most recent !search, for "#N" selection (issue #90). */ /** Results of the most recent !search, for "#N" selection (issue #90). */
private lastSearchResults: Song[] = []; private lastSearchResults: Song[] = [];
/** 当前曲实际播放时长(试听片段秒数或完整 duration);resolveAndPlay 赋值。 */ /** 当前曲实际播放时长(试听片段秒数或完整 duration);resolveAndPlay 赋值。 */
@@ -446,7 +447,8 @@ export class BotInstance extends EventEmitter {
async executeCommand( async executeCommand(
cmd: ParsedCommand, cmd: ParsedCommand,
msg?: TS3TextMessage msg?: TS3TextMessage,
requesterName = this.requesterNameFromMessage(msg),
): Promise<string | null> { ): Promise<string | null> {
// Reject commands that would push audio when the bot isn't connected: // Reject commands that would push audio when the bot isn't connected:
// otherwise ffmpeg spawns and voice goes to a half-initialized or // otherwise ffmpeg spawns and voice goes to a half-initialized or
@@ -474,12 +476,12 @@ export class BotInstance extends EventEmitter {
case "find": case "find":
return this.cmdSearch(cmd); return this.cmdSearch(cmd);
case "play": case "play":
return this.cmdPlay(cmd); return this.cmdPlay(cmd, requesterName);
case "add": case "add":
return this.cmdAdd(cmd); return this.cmdAdd(cmd, requesterName);
case "playnext": case "playnext":
case "pn": case "pn":
return this.cmdPlayNext(cmd); return this.cmdPlayNext(cmd, requesterName);
case "pause": case "pause":
return this.cmdPause(); return this.cmdPause();
case "resume": case "resume":
@@ -505,13 +507,13 @@ export class BotInstance extends EventEmitter {
case "mode": case "mode":
return this.cmdMode(cmd); return this.cmdMode(cmd);
case "playlist": case "playlist":
return this.cmdPlaylist(cmd); return this.cmdPlaylist(cmd, requesterName);
case "album": case "album":
return this.cmdAlbum(cmd); return this.cmdAlbum(cmd, requesterName);
case "fm": case "fm":
return this.cmdFm(cmd); return this.cmdFm(cmd, requesterName);
case "artist": case "artist":
return this.cmdArtist(cmd); return this.cmdArtist(cmd, requesterName);
case "vote": case "vote":
return this.cmdVote(msg); return this.cmdVote(msg);
case "lyrics": case "lyrics":
@@ -538,6 +540,7 @@ export class BotInstance extends EventEmitter {
private disableFmMode(): void { private disableFmMode(): void {
this.isFmMode = false; this.isFmMode = false;
this.fmProvider = null; this.fmProvider = null;
this.fmRequesterName = undefined;
} }
private getProvider(flags: Set<string>): MusicProvider { private getProvider(flags: Set<string>): MusicProvider {
@@ -548,6 +551,19 @@ export class BotInstance extends EventEmitter {
return this.neteaseProvider; return this.neteaseProvider;
} }
private requesterNameFromMessage(msg?: TS3TextMessage): string | undefined {
const name = msg?.invokerName?.trim();
return name || undefined;
}
private withRequester<T extends Song | QueuedSong>(
song: T,
requesterName?: string,
): T & { requestedBy?: string } {
const requestedBy = requesterName?.trim();
return requestedBy ? { ...song, requestedBy } : { ...song };
}
/** Resolve URL for a song and start playing it. Skips to next if URL fails. */ /** Resolve URL for a song and start playing it. Skips to next if URL fails. */
async resolveAndPlay(song: QueuedSong): Promise<boolean> { async resolveAndPlay(song: QueuedSong): Promise<boolean> {
if (!this.connected) { if (!this.connected) {
@@ -596,6 +612,7 @@ export class BotInstance extends EventEmitter {
album: song.album, album: song.album,
platform: song.platform, platform: song.platform,
coverUrl: song.coverUrl, coverUrl: song.coverUrl,
requestedBy: song.requestedBy,
}); });
// Keep TeamSpeak-side profile updates on the same path for play/next/FM. // Keep TeamSpeak-side profile updates on the same path for play/next/FM.
await this.syncProfileToSong(song); await this.syncProfileToSong(song);
@@ -668,7 +685,7 @@ export class BotInstance extends EventEmitter {
].join("\n"); ].join("\n");
} }
private async cmdPlay(cmd: ParsedCommand): Promise<string> { private async cmdPlay(cmd: ParsedCommand, requesterName?: string): Promise<string> {
if (!cmd.args) return `Usage: ${this.config.commandPrefix}play <song name | #N | id:<id> | URL>`; if (!cmd.args) return `Usage: ${this.config.commandPrefix}play <song name | #N | id:<id> | URL>`;
const { song, error } = await this.resolvePlayQuery(cmd); const { song, error } = await this.resolvePlayQuery(cmd);
if (error) return error; if (error) return error;
@@ -679,7 +696,7 @@ export class BotInstance extends EventEmitter {
} }
this.queue.clear(); this.queue.clear();
this.disableFmMode(); this.disableFmMode();
this.queue.add({ ...song0 }); this.queue.add(this.withRequester(song0, requesterName));
this.queue.play(); this.queue.play();
// Reset failure counter on user-initiated play // Reset failure counter on user-initiated play
@@ -693,14 +710,14 @@ export class BotInstance extends EventEmitter {
return `Now playing: ${song0.name} - ${song0.artist}`; return `Now playing: ${song0.name} - ${song0.artist}`;
} }
private async cmdAdd(cmd: ParsedCommand): Promise<string> { private async cmdAdd(cmd: ParsedCommand, requesterName?: string): Promise<string> {
if (!cmd.args) return `Usage: ${this.config.commandPrefix}add <song name | #N | id:<id> | URL>`; if (!cmd.args) return `Usage: ${this.config.commandPrefix}add <song name | #N | id:<id> | URL>`;
const { song, error } = await this.resolvePlayQuery(cmd); const { song, error } = await this.resolvePlayQuery(cmd);
if (error) return error; if (error) return error;
const s = song!; const s = song!;
const wasIdle = this.player.getState() === "idle"; const wasIdle = this.player.getState() === "idle";
this.queue.add({ ...s }); this.queue.add(this.withRequester(s, requesterName));
// If nothing was playing, start this newly-added song immediately. // If nothing was playing, start this newly-added song immediately.
// Matches /api/player/:id/add-by-id behavior so both add paths feel // Matches /api/player/:id/add-by-id behavior so both add paths feel
@@ -717,7 +734,7 @@ export class BotInstance extends EventEmitter {
return `Added to queue: ${s.name} - ${s.artist} (position ${this.queue.size()})`; return `Added to queue: ${s.name} - ${s.artist} (position ${this.queue.size()})`;
} }
private async cmdPlayNext(cmd: ParsedCommand): Promise<string> { private async cmdPlayNext(cmd: ParsedCommand, requesterName?: string): Promise<string> {
if (!cmd.args) return `Usage: ${this.config.commandPrefix}playnext <song name | #N | id:<id> | URL>`; if (!cmd.args) return `Usage: ${this.config.commandPrefix}playnext <song name | #N | id:<id> | URL>`;
const { song, error } = await this.resolvePlayQuery(cmd); const { song, error } = await this.resolvePlayQuery(cmd);
if (error) return error; if (error) return error;
@@ -733,7 +750,7 @@ export class BotInstance extends EventEmitter {
this.queue.getCurrentIndex() < 0 this.queue.getCurrentIndex() < 0
? this.queue.size() ? this.queue.size()
: this.queue.getCurrentIndex() + 1; : this.queue.getCurrentIndex() + 1;
this.queue.addNext({ ...s }); this.queue.addNext(this.withRequester(s, requesterName));
if (wasIdle) { if (wasIdle) {
this.queue.playAt(insertedAt); this.queue.playAt(insertedAt);
@@ -862,7 +879,7 @@ export class BotInstance extends EventEmitter {
return `Play mode set to: ${cmd.args}`; return `Play mode set to: ${cmd.args}`;
} }
private async cmdPlaylist(cmd: ParsedCommand): Promise<string> { private async cmdPlaylist(cmd: ParsedCommand, requesterName?: string): Promise<string> {
if (!cmd.args) return "Usage: !playlist <playlist name or ID>"; if (!cmd.args) return "Usage: !playlist <playlist name or ID>";
const provider = this.getProvider(cmd.flags); const provider = this.getProvider(cmd.flags);
@@ -907,7 +924,7 @@ export class BotInstance extends EventEmitter {
this.queue.clear(); this.queue.clear();
this.disableFmMode(); this.disableFmMode();
for (const song of songs) { for (const song of songs) {
this.queue.add({ ...song, platform: provider.platform }); this.queue.add(this.withRequester({ ...song, platform: provider.platform }, requesterName));
} }
const first = this.queue.play(); const first = this.queue.play();
if (first) await this.resolveAndPlay(first); if (first) await this.resolveAndPlay(first);
@@ -916,7 +933,7 @@ export class BotInstance extends EventEmitter {
return `Loaded ${songs.length} songs. Now playing: ${first?.name ?? "unknown"}`; return `Loaded ${songs.length} songs. Now playing: ${first?.name ?? "unknown"}`;
} }
private async cmdAlbum(cmd: ParsedCommand): Promise<string> { private async cmdAlbum(cmd: ParsedCommand, requesterName?: string): Promise<string> {
if (!cmd.args) return "Usage: !album <album name or ID>"; if (!cmd.args) return "Usage: !album <album name or ID>";
const provider = this.getProvider(cmd.flags); const provider = this.getProvider(cmd.flags);
@@ -944,7 +961,7 @@ export class BotInstance extends EventEmitter {
this.queue.clear(); this.queue.clear();
this.disableFmMode(); this.disableFmMode();
for (const song of songs) { for (const song of songs) {
this.queue.add({ ...song, platform: provider.platform }); this.queue.add(this.withRequester({ ...song, platform: provider.platform }, requesterName));
} }
const first = this.queue.play(); const first = this.queue.play();
if (first) await this.resolveAndPlay(first); if (first) await this.resolveAndPlay(first);
@@ -953,11 +970,11 @@ export class BotInstance extends EventEmitter {
return `Loaded ${songs.length} songs. Now playing: ${first?.name ?? "unknown"}`; return `Loaded ${songs.length} songs. Now playing: ${first?.name ?? "unknown"}`;
} }
private async cmdFm(cmd: ParsedCommand): Promise<string> { private async cmdFm(cmd: ParsedCommand, requesterName?: string): Promise<string> {
return this.startFm(this.getProvider(cmd.flags)); return this.startFm(this.getProvider(cmd.flags), requesterName);
} }
async startFm(provider: MusicProvider = this.neteaseProvider): Promise<string> { async startFm(provider: MusicProvider = this.neteaseProvider, requesterName?: string): Promise<string> {
// Match the !fm chat-command guard: refuse before mutating the queue when // Match the !fm chat-command guard: refuse before mutating the queue when
// offline, so the web /fm route can't wipe the queue + flip into FM mode // offline, so the web /fm route can't wipe the queue + flip into FM mode
// while nothing can actually play. // while nothing can actually play.
@@ -974,11 +991,12 @@ export class BotInstance extends EventEmitter {
this.player.stop(); this.player.stop();
this.queue.clear(); this.queue.clear();
for (const song of songs) { for (const song of songs) {
this.queue.add({ ...song, platform: provider.platform }); this.queue.add(this.withRequester({ ...song, platform: provider.platform }, requesterName));
} }
this.queue.setMode(PlayMode.Random); this.queue.setMode(PlayMode.Random);
this.isFmMode = true; this.isFmMode = true;
this.fmProvider = provider; this.fmProvider = provider;
this.fmRequesterName = requesterName?.trim() || undefined;
this.player.resetFailures(); this.player.resetFailures();
const first = this.queue.play(); const first = this.queue.play();
@@ -989,7 +1007,7 @@ export class BotInstance extends EventEmitter {
return `${label} started: ${first?.name ?? "unknown"} - ${first?.artist ?? ""}`; return `${label} started: ${first?.name ?? "unknown"} - ${first?.artist ?? ""}`;
} }
private async cmdArtist(cmd: ParsedCommand): Promise<string> { private async cmdArtist(cmd: ParsedCommand, requesterName?: string): Promise<string> {
if (!cmd.args) return "Usage: !artist <artist name>"; if (!cmd.args) return "Usage: !artist <artist name>";
const provider = this.getProvider(cmd.flags); const provider = this.getProvider(cmd.flags);
const result = await provider.search(cmd.args, 50); const result = await provider.search(cmd.args, 50);
@@ -1010,7 +1028,7 @@ export class BotInstance extends EventEmitter {
this.queue.clear(); this.queue.clear();
this.disableFmMode(); this.disableFmMode();
for (const song of filtered) { for (const song of filtered) {
this.queue.add({ ...song, platform: provider.platform }); this.queue.add(this.withRequester({ ...song, platform: provider.platform }, requesterName));
} }
this.queue.setMode(PlayMode.Loop); this.queue.setMode(PlayMode.Loop);
this.player.resetFailures(); this.player.resetFailures();
@@ -1029,7 +1047,7 @@ export class BotInstance extends EventEmitter {
const songs = await provider.getPersonalFm(); const songs = await provider.getPersonalFm();
if (songs.length === 0) return; if (songs.length === 0) return;
for (const song of songs) { for (const song of songs) {
this.queue.add({ ...song, platform: provider.platform }); this.queue.add(this.withRequester({ ...song, platform: provider.platform }, this.fmRequesterName));
} }
this.logger.debug({ count: songs.length, platform: provider.platform }, "FM queue refilled"); this.logger.debug({ count: songs.length, platform: provider.platform }, "FM queue refilled");
} catch (err) { } catch (err) {
+2
View File
@@ -60,6 +60,7 @@ describe("database", () => {
album: "Test Album", album: "Test Album",
platform: "netease", platform: "netease",
coverUrl: "https://example.com/cover.jpg", coverUrl: "https://example.com/cover.jpg",
requestedBy: "alice",
}); });
botDb.addPlayHistory({ botDb.addPlayHistory({
@@ -76,6 +77,7 @@ describe("database", () => {
expect(history).toHaveLength(2); expect(history).toHaveLength(2);
expect(history[0].songName).toBe("Another Song"); expect(history[0].songName).toBe("Another Song");
expect(history[1].songName).toBe("Test Song"); expect(history[1].songName).toBe("Test Song");
expect(history[1].requestedBy).toBe("alice");
}); });
it("saves and loads bot instances", () => { it("saves and loads bot instances", () => {
+11 -3
View File
@@ -10,6 +10,7 @@ export interface PlayHistoryEntry {
album: string; album: string;
platform: "netease" | "qq" | "bilibili" | "youtube" | "local" | "kugou"; platform: "netease" | "qq" | "bilibili" | "youtube" | "local" | "kugou";
coverUrl: string; coverUrl: string;
requestedBy?: string;
} }
export interface PlayHistoryRecord extends PlayHistoryEntry { export interface PlayHistoryRecord extends PlayHistoryEntry {
@@ -124,6 +125,12 @@ function migrateSchema(db: Database.Database): void {
if (!userColNames.includes("role")) { if (!userColNames.includes("role")) {
db.exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'"); db.exec("ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'");
} }
const historyColumns = db.prepare("PRAGMA table_info(play_history)").all() as Array<{ name: string }>;
const historyColNames = historyColumns.map((c) => c.name);
if (!historyColNames.includes("requestedBy")) {
db.exec("ALTER TABLE play_history ADD COLUMN requestedBy TEXT NOT NULL DEFAULT ''");
}
} }
function initTables(db: Database.Database): void { function initTables(db: Database.Database): void {
@@ -137,6 +144,7 @@ function initTables(db: Database.Database): void {
album TEXT NOT NULL, album TEXT NOT NULL,
platform TEXT NOT NULL, platform TEXT NOT NULL,
coverUrl TEXT NOT NULL, coverUrl TEXT NOT NULL,
requestedBy TEXT NOT NULL DEFAULT '',
playedAt TEXT NOT NULL DEFAULT (datetime('now')) playedAt TEXT NOT NULL DEFAULT (datetime('now'))
); );
@@ -265,8 +273,8 @@ export function createDatabase(dbPath: string): BotDatabase {
ensureGuestUser(db); ensureGuestUser(db);
const insertHistory = db.prepare(` const insertHistory = db.prepare(`
INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl) INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl, requestedBy)
VALUES (@botId, @songId, @songName, @artist, @album, @platform, @coverUrl) VALUES (@botId, @songId, @songName, @artist, @album, @platform, @coverUrl, @requestedBy)
`); `);
const selectHistory = db.prepare(` const selectHistory = db.prepare(`
@@ -338,7 +346,7 @@ export function createDatabase(dbPath: string): BotDatabase {
db, db,
addPlayHistory(record) { addPlayHistory(record) {
insertHistory.run(record); insertHistory.run({ ...record, requestedBy: record.requestedBy ?? "" });
}, },
getPlayHistory(botId, limit) { getPlayHistory(botId, limit) {
+17 -11
View File
@@ -53,6 +53,11 @@ export function createPlayerRouter(
res.status(403).json({ error: "本地音频播放已关闭" }); res.status(403).json({ error: "本地音频播放已关闭" });
} }
function requesterName(req: any): string {
const name = req.user?.username;
return typeof name === "string" && name.trim() ? name.trim() : "游客";
}
router.post("/:botId/play", authorize({ capability: "player.control" }), async (req, res) => { router.post("/:botId/play", authorize({ capability: "player.control" }), async (req, res) => {
try { try {
const bot = (req as any).bot; const bot = (req as any).bot;
@@ -66,7 +71,7 @@ export function createPlayerRouter(
res.status(400).json({ error: "Invalid command" }); res.status(400).json({ error: "Invalid command" });
return; return;
} }
const response = await bot.executeCommand(cmd); const response = await bot.executeCommand(cmd, undefined, requesterName(req));
res.json({ message: response }); res.json({ message: response });
} catch (err) { } catch (err) {
res.status(500).json({ error: (err as Error).message }); res.status(500).json({ error: (err as Error).message });
@@ -82,7 +87,7 @@ export function createPlayerRouter(
res.status(400).json({ error: "Invalid command" }); res.status(400).json({ error: "Invalid command" });
return; return;
} }
const response = await bot.executeCommand(cmd); const response = await bot.executeCommand(cmd, undefined, requesterName(req));
res.json({ message: response }); res.json({ message: response });
} catch (err) { } catch (err) {
res.status(500).json({ error: (err as Error).message }); res.status(500).json({ error: (err as Error).message });
@@ -120,7 +125,7 @@ export function createPlayerRouter(
? platform ? platform
: "netease" : "netease"
); );
const message = await bot.startFm(provider); const message = await bot.startFm(provider, requesterName(req));
res.json({ res.json({
ok: ok:
!message.startsWith("No FM songs") && !message.startsWith("No FM songs") &&
@@ -271,7 +276,7 @@ export function createPlayerRouter(
`!playlist ${platformFlag(platform)} ${playlistId}`.trim(), `!playlist ${platformFlag(platform)} ${playlistId}`.trim(),
"!" "!"
)!; )!;
const response = await bot.executeCommand(cmd); const response = await bot.executeCommand(cmd, undefined, requesterName(req));
res.json({ message: response }); res.json({ message: response });
} catch (err) { } catch (err) {
res.status(500).json({ error: (err as Error).message }); res.status(500).json({ error: (err as Error).message });
@@ -332,7 +337,7 @@ export function createPlayerRouter(
const queue = bot.getQueueManager(); const queue = bot.getQueueManager();
queue.clear(); queue.clear();
for (const song of queueable) { for (const song of queueable) {
queue.add({ ...song, platform: provider.platform }); queue.add({ ...song, platform: provider.platform, requestedBy: requesterName(req) });
} }
// Sweep AFTER the queue is rebuilt: the previous queue's local uploads are // Sweep AFTER the queue is rebuilt: the previous queue's local uploads are
// released and deleted, but an empty/failed playlist (early return above) // released and deleted, but an empty/failed playlist (early return above)
@@ -418,7 +423,7 @@ export function createPlayerRouter(
const queue = bot.getQueueManager(); const queue = bot.getQueueManager();
queue.clear(); queue.clear();
for (const song of queueable) { for (const song of queueable) {
queue.add({ ...song, platform: provider.platform }); queue.add({ ...song, platform: provider.platform, requestedBy: requesterName(req) });
} }
// Sweep AFTER the queue is rebuilt (see play-playlist). // Sweep AFTER the queue is rebuilt (see play-playlist).
bot.cleanupQueuedLocalSongs?.("queue_replaced"); bot.cleanupQueuedLocalSongs?.("queue_replaced");
@@ -468,7 +473,7 @@ export function createPlayerRouter(
const queue = bot.getQueueManager(); const queue = bot.getQueueManager();
bot.getPlayer().stop(); bot.getPlayer().stop();
queue.clear(); queue.clear();
queue.add(song); queue.add({ ...song, requestedBy: requesterName(req) });
queue.play(); queue.play();
bot.getPlayer().resetFailures(); bot.getPlayer().resetFailures();
@@ -513,7 +518,7 @@ export function createPlayerRouter(
// after natural track end without queue.clear()). // after natural track end without queue.clear()).
const insertedAt = const insertedAt =
queue.getCurrentIndex() < 0 ? queue.size() : queue.getCurrentIndex() + 1; queue.getCurrentIndex() < 0 ? queue.size() : queue.getCurrentIndex() + 1;
queue.addNext(song); queue.addNext({ ...song, requestedBy: requesterName(req) });
if (wasIdle) { if (wasIdle) {
// Promote the just-added song to current and start it. // Promote the just-added song to current and start it.
@@ -555,7 +560,7 @@ export function createPlayerRouter(
const queue = bot.getQueueManager(); const queue = bot.getQueueManager();
const insertedAt = const insertedAt =
queue.getCurrentIndex() < 0 ? queue.size() : queue.getCurrentIndex() + 1; queue.getCurrentIndex() < 0 ? queue.size() : queue.getCurrentIndex() + 1;
queue.addNext(song); queue.addNext({ ...song, requestedBy: requesterName(req) });
queue.playAt(insertedAt); queue.playAt(insertedAt);
bot.getPlayer().resetFailures(); bot.getPlayer().resetFailures();
const ok = await bot.resolveAndPlay(queue.current()!); const ok = await bot.resolveAndPlay(queue.current()!);
@@ -587,7 +592,7 @@ export function createPlayerRouter(
const body = await bot.runExclusive(async () => { const body = await bot.runExclusive(async () => {
const queue = bot.getQueueManager(); const queue = bot.getQueueManager();
const wasIdle = bot.getPlayer().getState() === "idle"; const wasIdle = bot.getPlayer().getState() === "idle";
queue.add(song); queue.add({ ...song, requestedBy: requesterName(req) });
// If nothing was playing, start this newly-added song immediately. // If nothing was playing, start this newly-added song immediately.
if (wasIdle) { if (wasIdle) {
@@ -627,7 +632,7 @@ export function createPlayerRouter(
} }
const queue = bot.getQueueManager(); const queue = bot.getQueueManager();
queue.add({ ...song, platform: provider.platform }); queue.add({ ...song, platform: provider.platform, requestedBy: requesterName(req) });
// If nothing is playing, start the first song // If nothing is playing, start the first song
if (bot.getPlayer().getState() === "idle") { if (bot.getPlayer().getState() === "idle") {
@@ -678,6 +683,7 @@ export function createPlayerRouter(
coverUrl: r.coverUrl, coverUrl: r.coverUrl,
platform: r.platform, platform: r.platform,
playedAt: r.playedAt, playedAt: r.playedAt,
requestedBy: r.requestedBy,
})); }));
res.json({ history }); res.json({ history });
}); });
+25
View File
@@ -9,6 +9,11 @@
class="platform-badge" class="platform-badge"
:class="song.platform === 'bilibili' ? 'badge-bilibili' : song.platform === 'qq' ? 'badge-qq' : song.platform === 'youtube' ? 'badge-youtube' : song.platform === 'local' ? 'badge-local' : song.platform === 'kugou' ? 'badge-kugou' : 'badge-netease'" :class="song.platform === 'bilibili' ? 'badge-bilibili' : song.platform === 'qq' ? 'badge-qq' : song.platform === 'youtube' ? 'badge-youtube' : song.platform === 'local' ? 'badge-local' : song.platform === 'kugou' ? 'badge-kugou' : 'badge-netease'"
>{{ song.platform === 'bilibili' ? 'B站' : song.platform === 'qq' ? 'QQ' : song.platform === 'youtube' ? 'YouTube' : song.platform === 'local' ? '本地' : song.platform === 'kugou' ? '酷狗' : '网易云' }}</span> >{{ song.platform === 'bilibili' ? 'B站' : song.platform === 'qq' ? 'QQ' : song.platform === 'youtube' ? 'YouTube' : song.platform === 'local' ? '本地' : song.platform === 'kugou' ? '酷狗' : '网易云' }}</span>
<span
v-if="song.requestedBy"
class="requester-badge"
:class="{ 'requester-badge-guest': song.requestedBy === '游客' }"
>{{ song.requestedBy }}</span>
</div> </div>
<div class="song-artist">{{ song.artist }}</div> <div class="song-artist">{{ song.artist }}</div>
</div> </div>
@@ -115,6 +120,26 @@ function formatDuration(seconds: number): string {
line-height: 1.4; line-height: 1.4;
} }
.requester-badge {
flex-shrink: 0;
max-width: 96px;
font-size: var(--fs-micro);
font-weight: var(--fw-semi);
padding: 1px 5px;
border-radius: var(--radius-xs);
line-height: 1.4;
background: var(--color-online-15);
color: var(--color-online);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.requester-badge-guest {
background: rgba(156, 163, 175, 0.18);
color: rgba(156, 163, 175, 0.95);
}
.badge-netease { .badge-netease {
background: var(--brand-netease-15); background: var(--brand-netease-15);
color: var(--brand-netease); color: var(--brand-netease);
+2
View File
@@ -11,6 +11,8 @@ export interface Song {
duration: number; duration: number;
coverUrl: string; coverUrl: string;
platform: 'netease' | 'qq' | 'bilibili' | 'youtube' | 'local' | 'kugou'; platform: 'netease' | 'qq' | 'bilibili' | 'youtube' | 'local' | 'kugou';
requestedBy?: string;
playedAt?: string;
} }
export type Source = 'netease' | 'qq' | 'kugou'; export type Source = 'netease' | 'qq' | 'kugou';