mirror of
https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
synced 2026-10-01 20:42:50 +08:00
feat: add audio quality selector (standard to jymaster/Hi-Res)
- MusicProvider interface: setQuality/getQuality methods - NeteaseProvider: passes quality level to /song/url/v1 - Default: exhigh (320kbps MP3) - Settings page: 6-option grid selector - GET/POST /api/music/quality endpoints Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0439bd047f
commit
b74c8e8add
5 files changed
+142
-4
No files matched your search
+22
-2
@@ -54,10 +54,21 @@ export function parseLyrics(lrc: string, tlyric?: string): LyricLine[] {
|
|||||||
return lines.sort((a, b) => a.time - b.time);
|
return lines.sort((a, b) => a.time - b.time);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NetEase quality levels: standard(128k) higher(192k) exhigh(320k) lossless(flac) hires(hi-res) jyeffect jymaster
|
||||||
|
export const NETEASE_QUALITY_LEVELS = [
|
||||||
|
{ value: "standard", label: "标准 (128kbps)", bitrate: 128 },
|
||||||
|
{ value: "higher", label: "较高 (192kbps)", bitrate: 192 },
|
||||||
|
{ value: "exhigh", label: "极高 (320kbps)", bitrate: 320 },
|
||||||
|
{ value: "lossless", label: "无损 (FLAC)", bitrate: 900 },
|
||||||
|
{ value: "hires", label: "Hi-Res", bitrate: 1500 },
|
||||||
|
{ value: "jymaster", label: "超清母带", bitrate: 4000 },
|
||||||
|
] as const;
|
||||||
|
|
||||||
export class NeteaseProvider implements MusicProvider {
|
export class NeteaseProvider implements MusicProvider {
|
||||||
readonly platform = "netease" as const;
|
readonly platform = "netease" as const;
|
||||||
private api: AxiosInstance;
|
private api: AxiosInstance;
|
||||||
private cookie = "";
|
private cookie = "";
|
||||||
|
private quality = "exhigh";
|
||||||
|
|
||||||
constructor(baseUrl: string) {
|
constructor(baseUrl: string) {
|
||||||
this.api = axios.create({
|
this.api = axios.create({
|
||||||
@@ -66,6 +77,14 @@ export class NeteaseProvider implements MusicProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setQuality(quality: string): void {
|
||||||
|
this.quality = quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuality(): string {
|
||||||
|
return this.quality;
|
||||||
|
}
|
||||||
|
|
||||||
private get cookieParams(): Record<string, string> {
|
private get cookieParams(): Record<string, string> {
|
||||||
return this.cookie ? { cookie: this.cookie } : {};
|
return this.cookie ? { cookie: this.cookie } : {};
|
||||||
}
|
}
|
||||||
@@ -110,9 +129,10 @@ export class NeteaseProvider implements MusicProvider {
|
|||||||
return { songs, playlists, albums: [] };
|
return { songs, playlists, albums: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSongUrl(songId: string): Promise<string | null> {
|
async getSongUrl(songId: string, quality?: string): Promise<string | null> {
|
||||||
|
const level = quality ?? this.quality;
|
||||||
const res = await this.api.get("/song/url/v1", {
|
const res = await this.api.get("/song/url/v1", {
|
||||||
params: { id: songId, level: "exhigh", ...this.cookieParams },
|
params: { id: songId, level, ...this.cookieParams },
|
||||||
});
|
});
|
||||||
return res.data?.data?.[0]?.url ?? null;
|
return res.data?.data?.[0]?.url ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ export interface MusicProvider {
|
|||||||
readonly platform: "netease" | "qq";
|
readonly platform: "netease" | "qq";
|
||||||
|
|
||||||
search(query: string, limit?: number): Promise<SearchResult>;
|
search(query: string, limit?: number): Promise<SearchResult>;
|
||||||
getSongUrl(songId: string): Promise<string | null>;
|
getSongUrl(songId: string, quality?: string): Promise<string | null>;
|
||||||
|
setQuality(quality: string): void;
|
||||||
|
getQuality(): string;
|
||||||
getSongDetail(songId: string): Promise<Song | null>;
|
getSongDetail(songId: string): Promise<Song | null>;
|
||||||
getPlaylistSongs(playlistId: string): Promise<Song[]>;
|
getPlaylistSongs(playlistId: string): Promise<Song[]>;
|
||||||
getRecommendPlaylists(): Promise<Playlist[]>;
|
getRecommendPlaylists(): Promise<Playlist[]>;
|
||||||
|
|||||||
+10
-1
@@ -14,6 +14,7 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
readonly platform = "qq" as const;
|
readonly platform = "qq" as const;
|
||||||
private api: AxiosInstance;
|
private api: AxiosInstance;
|
||||||
private cookie = "";
|
private cookie = "";
|
||||||
|
private quality = "exhigh";
|
||||||
|
|
||||||
constructor(baseUrl: string) {
|
constructor(baseUrl: string) {
|
||||||
this.api = axios.create({
|
this.api = axios.create({
|
||||||
@@ -22,6 +23,14 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setQuality(quality: string): void {
|
||||||
|
this.quality = quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
getQuality(): string {
|
||||||
|
return this.quality;
|
||||||
|
}
|
||||||
|
|
||||||
private get cookieParams(): Record<string, string> {
|
private get cookieParams(): Record<string, string> {
|
||||||
return this.cookie ? { cookie: this.cookie } : {};
|
return this.cookie ? { cookie: this.cookie } : {};
|
||||||
}
|
}
|
||||||
@@ -48,7 +57,7 @@ export class QQMusicProvider implements MusicProvider {
|
|||||||
return { songs, playlists: [], albums: [] };
|
return { songs, playlists: [], albums: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSongUrl(songId: string): Promise<string | null> {
|
async getSongUrl(songId: string, _quality?: string): Promise<string | null> {
|
||||||
const res = await this.api.get("/getMusicPlay", {
|
const res = await this.api.get("/getMusicPlay", {
|
||||||
params: { songmid: songId, ...this.cookieParams },
|
params: { songmid: songId, ...this.cookieParams },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -193,5 +193,30 @@ export function createMusicRouter(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get current quality
|
||||||
|
router.get("/quality", (_req, res) => {
|
||||||
|
res.json({
|
||||||
|
netease: neteaseProvider.getQuality(),
|
||||||
|
qq: qqProvider.getQuality(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set quality
|
||||||
|
router.post("/quality", (req, res) => {
|
||||||
|
const { quality, platform } = req.body;
|
||||||
|
if (!quality) {
|
||||||
|
res.status(400).json({ error: "quality is required" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!platform || platform === "netease") {
|
||||||
|
neteaseProvider.setQuality(quality);
|
||||||
|
}
|
||||||
|
if (!platform || platform === "qq") {
|
||||||
|
qqProvider.setQuality(quality);
|
||||||
|
}
|
||||||
|
logger.info({ quality, platform }, "Audio quality changed");
|
||||||
|
res.json({ success: true, quality });
|
||||||
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
@@ -195,6 +195,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Audio Quality -->
|
||||||
|
<section class="settings-section">
|
||||||
|
<h2 class="section-title">音质设置</h2>
|
||||||
|
<div class="setting-row">
|
||||||
|
<div class="setting-label">
|
||||||
|
<Icon icon="mdi:music-note-eighth" class="setting-icon" />
|
||||||
|
音源质量
|
||||||
|
</div>
|
||||||
|
<div class="quality-options">
|
||||||
|
<button
|
||||||
|
v-for="q in qualityLevels"
|
||||||
|
:key="q.value"
|
||||||
|
class="quality-btn"
|
||||||
|
:class="{ active: currentQuality === q.value }"
|
||||||
|
@click="setQuality(q.value)"
|
||||||
|
>
|
||||||
|
<div class="quality-name">{{ q.label }}</div>
|
||||||
|
<div class="quality-desc">{{ q.desc }}</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Command Prefix -->
|
<!-- Command Prefix -->
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2 class="section-title">命令设置</h2>
|
<h2 class="section-title">命令设置</h2>
|
||||||
@@ -227,6 +250,31 @@ const neteaseCookie = ref('');
|
|||||||
const qqCookie = ref('');
|
const qqCookie = ref('');
|
||||||
const commandPrefix = ref('!');
|
const commandPrefix = ref('!');
|
||||||
|
|
||||||
|
// Audio quality
|
||||||
|
const currentQuality = ref('exhigh');
|
||||||
|
const qualityLevels = [
|
||||||
|
{ value: 'standard', label: '标准', desc: '128kbps MP3' },
|
||||||
|
{ value: 'higher', label: '较高', desc: '192kbps MP3' },
|
||||||
|
{ value: 'exhigh', label: '极高', desc: '320kbps MP3' },
|
||||||
|
{ value: 'lossless', label: '无损', desc: 'FLAC' },
|
||||||
|
{ value: 'hires', label: 'Hi-Res', desc: '高解析度' },
|
||||||
|
{ value: 'jymaster', label: '超清母带', desc: '最高质量' },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function loadQuality() {
|
||||||
|
try {
|
||||||
|
const res = await axios.get('/api/music/quality');
|
||||||
|
currentQuality.value = res.data.netease || 'exhigh';
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setQuality(q: string) {
|
||||||
|
currentQuality.value = q;
|
||||||
|
try {
|
||||||
|
await axios.post('/api/music/quality', { quality: q });
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
// Login mode: 'qr' | 'cookie' | null
|
// Login mode: 'qr' | 'cookie' | null
|
||||||
const neteaseLoginMode = ref<'qr' | 'cookie' | null>(null);
|
const neteaseLoginMode = ref<'qr' | 'cookie' | null>(null);
|
||||||
const qqLoginMode = ref<'qr' | 'cookie' | null>(null);
|
const qqLoginMode = ref<'qr' | 'cookie' | null>(null);
|
||||||
@@ -380,6 +428,7 @@ async function savePrefix() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkAuthStatus();
|
checkAuthStatus();
|
||||||
|
loadQuality();
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
@@ -653,6 +702,39 @@ onUnmounted(() => {
|
|||||||
&:focus { border-color: var(--color-primary); }
|
&:focus { border-color: var(--color-primary); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.quality-options {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-btn {
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--hover-bg);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: center;
|
||||||
|
transition: all var(--transition-fast);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover { border-color: var(--border-color); }
|
||||||
|
&.active {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: rgba(51, 94, 234, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-desc {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
.prefix-input-wrap {
|
.prefix-input-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|||||||
Reference in new issue
Block a user