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

+8 -1
View File
@@ -15,6 +15,13 @@ export interface SongWithUrl extends Song {
url: string;
}
/** getSongUrl 解析结果。trialDuration 缺省 = 完整可播放(VIP 账号 / 免费曲)。 */
export interface SongUrlResult {
url: string;
/** 试听片段时长(秒)。VIP/免费曲为 undefined → 调用方回退完整 duration。 */
trialDuration?: number;
}
export interface Playlist {
id: string;
name: string;
@@ -68,7 +75,7 @@ export interface MusicProvider {
readonly platform: "netease" | "qq" | "bilibili" | "youtube";
search(query: string, limit?: number): Promise<SearchResult>;
getSongUrl(songId: string, quality?: string): Promise<string | null>;
getSongUrl(songId: string, quality?: string): Promise<SongUrlResult | null>;
setQuality(quality: string): void;
getQuality(): string;
getSongDetail(songId: string): Promise<Song | null>;