diff --git a/src/main/pluginSystem.ts b/src/main/pluginSystem.ts index 3c1e314..709bf20 100644 --- a/src/main/pluginSystem.ts +++ b/src/main/pluginSystem.ts @@ -91,16 +91,7 @@ export class PluginSystem { error: 'Search not implemented' } } - try { - return await this.plugin.musicSearch.search(query, page, limit) - } catch (e: any) { - return { - list: [], - total: 0, - allPage: 0, - error: e.message || 'Plugin search error' - } - } + return await this.plugin.musicSearch.search(query, page, limit) } async getLyric(id: string): Promise { diff --git a/src/main/proxyServer.ts b/src/main/proxyServer.ts index c92c183..30af482 100644 --- a/src/main/proxyServer.ts +++ b/src/main/proxyServer.ts @@ -472,6 +472,26 @@ function notifyWaiters(task: DownloadTask) { } } +/** + * Cancel all background downloads EXCEPT the one matching the given path + */ +function cancelAllDownloadsExcept(exceptPath: string) { + for (const [path, task] of downloadTasks) { + if (path !== exceptPath) { + console.log(`[Proxy] Cancelling competing download: ${path}`); + if (task.abortController) { + task.abortController.abort(); + } + downloadTasks.delete(path); + // Cleanup temp file? Handled by error handler typically, but force check + try { + const tempPath = getTempPath(path); + if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); + } catch { } + } + } +} + /** * 主代理函数 - 智能处理缓存和 Range 请求 * 修复版本:正确处理流的复制,确保下载和响应同时进行 @@ -486,6 +506,9 @@ async function proxyAndCache( const requestedRange = req.headers.range; const isSeekRequest = requestedRange && !requestedRange.startsWith('bytes=0-'); + // NEW: Cancel other downloads to enforce "Current Song Priority" + cancelAllDownloadsExcept(cacheFilePath); + // 检查是否有正在进行的后台下载 let task = downloadTasks.get(cacheFilePath); @@ -565,14 +588,25 @@ async function streamAndCache( cacheFilePath: string, cacheKey: string ): Promise { + const controller = new AbortController(); const headers: Record = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }; - const response = await fetch(targetUrl, { - method: 'GET', - headers: headers, - }); + let response; + try { + response = await fetch(targetUrl, { + method: 'GET', + headers: headers, + signal: controller.signal + }); + } catch (e: any) { + if (e.name === 'AbortError') { + console.log(`[Proxy] Request aborted for ${targetUrl}`); + return; + } + throw e; + } if (!response.ok) { throw { statusCode: response.status, statusText: response.statusText }; @@ -592,6 +626,13 @@ async function streamAndCache( // @ts-ignore const nodeStream = Readable.fromWeb(response.body); nodeStream.pipe(res); + + // Handle cleanup if aborted during pipe (though pipe handles some) + res.on('close', () => { + if (!res.writableEnded) { + nodeStream.destroy(); + } + }); } else { res.end(); } @@ -613,7 +654,7 @@ async function streamAndCache( currentSize: 0, totalSize: contentLength, contentType: contentType, - abortController: null, + abortController: controller, promise: null, waiters: [], }; @@ -645,6 +686,8 @@ async function streamAndCache( res.on('close', () => { clientConnected = false; console.log('[Proxy] Client disconnected'); + // Do NOT abort download here anymore. + // We let it finish UNLESS a new song starts (handled by cancelAllDownloadsExcept). }); return new Promise((resolve, reject) => { @@ -652,7 +695,9 @@ async function streamAndCache( task.currentSize += chunk.length; // 写入文件(始终进行) - fileStream.write(chunk); + if (!fileStream.destroyed) { + fileStream.write(chunk); + } // 写入响应(如果客户端还连接着) if (clientConnected && !res.writableEnded) { @@ -676,7 +721,11 @@ async function streamAndCache( }); nodeStream.on('error', (err: Error) => { - console.error('[Proxy] Stream error:', err); + if (err.name === 'AbortError') { + console.log('[Proxy] Stream aborted'); + } else { + console.error('[Proxy] Stream error:', err); + } fileStream.destroy(); if (clientConnected && !res.headersSent) { @@ -934,6 +983,11 @@ export function startProxyServer(persistCache: boolean = true) { console.warn(`[Proxy] Proxy error (Attempt ${currentAttempt}/${maxAttempts}):`, e); if (currentAttempt < maxAttempts) { + if (res.headersSent) { + console.warn('[Proxy] Cannot retry because headers already sent'); + break; + } + console.log('[Proxy] Error occurred, refreshing URL from plugin...'); urlCache.delete(cacheKey); diff --git a/src/renderer/src/components/Settings.vue b/src/renderer/src/components/Settings.vue index ab052d6..760364b 100644 --- a/src/renderer/src/components/Settings.vue +++ b/src/renderer/src/components/Settings.vue @@ -133,6 +133,27 @@

播放设置

+ + +
+
+
列表添加模式
+
选择点击歌曲时的播放行为
+
+
+
+ + +
+
+
+

音质、淡入淡出等设置即将推出

@@ -168,6 +189,9 @@ diff --git a/src/renderer/src/views/Search.vue b/src/renderer/src/views/Search.vue index 254e59c..bde6774 100644 --- a/src/renderer/src/views/Search.vue +++ b/src/renderer/src/views/Search.vue @@ -23,7 +23,18 @@
-
+
+
+ +
+

搜索出错了,请稍后重试

+ +
+ +
#
标题
@@ -79,8 +90,12 @@ 正在搜索...
-
+
未找到相关歌曲 +
@@ -103,8 +118,10 @@ const currentPage = ref(1); const showSettings = ref(false); const savedLimit = localStorage.getItem('qz-search-limit'); const limit = ref(savedLimit ? Number(savedLimit) : 30); + const total = ref(0); const loading = ref(false); +const error = ref(false); const songs = ref([]); const totalPages = computed(() => { @@ -140,6 +157,7 @@ const fetchData = async () => { if (!query.value) return; loading.value = true; + error.value = false; songs.value = []; try { @@ -154,6 +172,7 @@ const fetchData = async () => { } } catch (e) { console.error("Search failed", e); + error.value = true; } finally { loading.value = false; } @@ -165,7 +184,7 @@ const changePage = (page: number) => { }; const handlePlaySong = (index: number) => { - playerStore.setPlaylist(songs.value, index); + playerStore.playFromList(songs.value[index], songs.value); }; const getHighlightRegex = (q: string) => { @@ -593,18 +612,71 @@ watch(limit, (newLimit) => { } /* States */ -.loading-state, .empty-state { +.loading-state, .empty-state, .error-state { padding: 60px 0; display: flex; flex-direction: column; align-items: center; + justify-content: center; gap: 16px; color: var(--color-text-muted); + min-height: 300px; } .spin { animation: spin 1s linear infinite; font-size: 32px; + color: var(--color-accent); +} + +.error-state .icon-box { + width: 64px; + height: 64px; + background: var(--color-bg-tertiary); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 8px; +} + +.state-icon { + font-size: 32px; + color: var(--color-accent); +} + +.state-text { + font-size: 15px; + color: var(--color-text-secondary); +} + +.retry-btn { + margin-top: 8px; + padding: 8px 24px; + background: var(--color-accent); + color: white; /* Ensure text is readable on accent color */ + border-radius: var(--radius-full); + font-size: 14px; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s; + box-shadow: var(--shadow-sm); +} + +.retry-btn:hover { + transform: translateY(-1px); + box-shadow: var(--shadow-md); + filter: brightness(1.1); +} + +.retry-btn:active { + transform: translateY(0); +} + +.retry-btn .btn-icon { + font-size: 16px; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }