feat: resolve trial-only playback via trialDuration/effectiveDuration

VIP songs for non-VIP accounts return a ~30s trial fragment. The player used the full duration for isNearEnd, so the trial end didn't trigger auto-advance (~60s stall), and currentSong.duration stayed full, leaving the UI progress stuck.

- provider.ts: SongUrlResult {url, trialDuration?}; getSongUrl signature
- netease.ts: parseNeteaseTrial (freeTrialInfo start/end in seconds) + getSongUrl
- qq.ts: parseQqTrial (isTryout/tryEnd) + getSongUrl
- bilibili/youtube: getSongUrl returns {url}
- instance.ts: resolveAndPlay uses effectiveDuration = trialDuration ?? duration -> nearEnd at trial end -> native auto-advance; BotStatus.effectiveDuration
- VIP account: freeTrialInfo absent -> full duration -> full playback (no toggle)

Backward compatible (optional fields; getSongUrl has a single caller, updated).
Tests: parseTrial assertions (seconds/alias/ms-fallback). 14 pass.
This commit is contained in:
Slldyd2077 committed 2026-06-28 21:06:52 +08:00
1 parent 7b2bd0ea6a
commit fbb127a86d
8 files changed
+90 -16

No files matched your search

+16 -3
View File
@@ -2,6 +2,7 @@ import axios, { type AxiosInstance } from "axios";
import type {
MusicProvider,
Song,
SongUrlResult,
Playlist,
PlaylistDetail,
LyricLine,
@@ -58,6 +59,18 @@ export function mapQqSongs(raw: any[] | null | undefined): Song[] {
}).filter((s) => s.id);
}
/** 解析 QQ 试听标记 → 试听秒数;非试听(VIP/免费)返回 undefined。
* 字段 isTryout===1 / tryout===true + tryBegin/tryEnd;容忍 begin/start 别名 + 毫秒兜底。 */
export function parseQqTrial(playUrl: any): number | undefined {
if (!playUrl || typeof playUrl !== "object") return undefined;
if (playUrl.isTryout !== 1 && playUrl.tryout !== true) return undefined;
const begin = Number(playUrl.tryBegin ?? playUrl.begin ?? playUrl.start ?? 0);
const end = Number(playUrl.tryEnd ?? playUrl.end);
if (!Number.isFinite(end) || end <= begin) return undefined;
const secs = end > 1000 ? (end - begin) / 1000 : end - begin;
return Math.round(secs);
}
export function mapQqAlbums(raw: any[] | null | undefined): Album[] {
if (!Array.isArray(raw)) return [];
return raw.map((a) => {
@@ -248,13 +261,13 @@ export class QQMusicProvider implements MusicProvider {
return { songs, playlists: [], albums };
}
async getSongUrl(songId: string, quality?: string): Promise<string | null> {
async getSongUrl(songId: string, quality?: string): Promise<SongUrlResult | null> {
try {
const res = await this.api.get("/getMusicPlay", {
params: { songmid: songId, quality: quality ?? this.quality, ...this.cookieParams },
});
const playUrl = res.data?.data?.playUrl?.[songId];
if (playUrl?.url) return playUrl.url;
if (playUrl?.url) return { url: playUrl.url, trialDuration: parseQqTrial(playUrl) };
} catch {
// try with songid
try {
@@ -262,7 +275,7 @@ export class QQMusicProvider implements MusicProvider {
params: { songid: songId, quality: quality ?? this.quality, ...this.cookieParams },
});
const playUrl = res.data?.data?.playUrl?.[songId];
if (playUrl?.url) return playUrl.url;
if (playUrl?.url) return { url: playUrl.url, trialDuration: parseQqTrial(playUrl) };
} catch {
// ignore
}