diff --git a/CHANGELOG.md b/CHANGELOG.md index 725553a7cf..feb8443e0a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this maintained LibreTV fork are documented here. +## 1.2.13 - 2026-05-12 + +### Fixed + +- Fixed player resource switching after search relevance helpers moved out of the player page load path. +- Player resource switching now falls back to the default selected sources when no local source selection exists. +- Added empty-state handling when no switchable resources are found. + ## 1.2.12 - 2026-05-12 ### Changed diff --git a/VERSION.txt b/VERSION.txt index f3bcbbfe32..a3fddd180b 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -202605120223 +202605120255 diff --git a/js/config.js b/js/config.js index 9d0dba9027..d9c075d0a9 100644 --- a/js/config.js +++ b/js/config.js @@ -17,7 +17,7 @@ const SITE_CONFIG = { url: '', description: '免费在线视频搜索与观看平台', logo: 'image/logo.png', - version: '1.2.12' + version: '1.2.13' }; const DEFAULT_SELECTED_APIS = ['ysgc', 'jszy', 'wujin', 'maoyan']; diff --git a/js/player.js b/js/player.js index 59e92f4e3c..b6fb1b654b 100644 --- a/js/player.js +++ b/js/player.js @@ -1,4 +1,4 @@ -const selectedAPIs = JSON.parse(localStorage.getItem('selectedAPIs') || '[]'); +const selectedAPIs = JSON.parse(localStorage.getItem('selectedAPIs') || JSON.stringify(DEFAULT_SELECTED_APIS)); const customAPIs = JSON.parse(localStorage.getItem('customAPIs') || '[]'); // 存储自定义API列表 // 改进返回功能 @@ -2010,10 +2010,16 @@ async function showSwitchResourceModal() { } return { key: curr, name: '未知资源' }; }); + + if (resourceOptions.length === 0) { + modalContent.innerHTML = '
暂无可切换的数据源,请先到设置中选择数据源。
'; + return; + } + let allResults = {}; - await Promise.all(resourceOptions.map(async (opt) => { - let queryResult = await searchByAPIAndKeyWord(opt.key, currentVideoTitle); - if (queryResult.length == 0) { + await Promise.allSettled(resourceOptions.map(async (opt) => { + const queryResult = await searchByAPIAndKeyWord(opt.key, currentVideoTitle); + if (!Array.isArray(queryResult) || queryResult.length === 0) { return } // 优先取完全同名资源,否则默认取第一个 @@ -2026,6 +2032,11 @@ async function showSwitchResourceModal() { allResults[opt.key] = result; })); + if (Object.keys(allResults).length === 0) { + modalContent.innerHTML = '
未找到可切换资源,请稍后重试或更换片名搜索。
'; + return; + } + // 更新状态显示:开始速率测试 modalContent.innerHTML = '
正在测试各资源速率...
'; diff --git a/js/search.js b/js/search.js index a3c639b47e..34d6995292 100644 --- a/js/search.js +++ b/js/search.js @@ -1,3 +1,49 @@ +const SEARCH_RESULT_TITLE_FIELDS = [ + 'vod_name', + 'vod_sub', + 'vod_en', + 'title', + 'name' +]; + +function normalizeSearchResultText(value) { + return String(value || '').toLowerCase().replace(/\s+/g, ''); +} + +function getSearchResultTitleTexts(item) { + return SEARCH_RESULT_TITLE_FIELDS + .map(field => normalizeSearchResultText(item?.[field])) + .filter(Boolean); +} + +function doesSearchResultMatchQuery(item, query) { + const compactQuery = normalizeSearchResultText(query); + if (!compactQuery) return true; + + const titleTexts = getSearchResultTitleTexts(item); + if (titleTexts.some(text => text.includes(compactQuery))) { + return true; + } + + const queryTerms = String(query || '') + .toLowerCase() + .split(/\s+/) + .map(normalizeSearchResultText) + .filter(Boolean); + + return queryTerms.length > 1 && queryTerms.every(term => + titleTexts.some(text => text.includes(term)) + ); +} + +function filterSearchResultsByQuery(results, query) { + const list = Array.isArray(results) ? results : []; + return list.filter(item => doesSearchResultMatchQuery(item, query)); +} + +window.doesResultMatchQuery = window.doesResultMatchQuery || doesSearchResultMatchQuery; +window.filterResultsByQuery = window.filterResultsByQuery || filterSearchResultsByQuery; + function getApiSearchContext(apiId) { if (apiId.startsWith('custom_')) { const customIndex = apiId.replace('custom_', ''); @@ -75,7 +121,7 @@ async function searchByAPIAndKeyWord(apiId, query) { // 处理第一页结果 const firstPageResults = normalizeApiList(data, apiId, context); - const results = filterResultsByQuery(firstPageResults, query); + const results = filterSearchResultsByQuery(firstPageResults, query); if (firstPageResults.length > 0 && results.length === 0) { return []; } @@ -103,7 +149,7 @@ async function searchByAPIAndKeyWord(apiId, query) { if (!pageData || !pageData.list || !Array.isArray(pageData.list)) return []; // 处理当前页结果 - return filterResultsByQuery(normalizeApiList(pageData, apiId, context), query); + return filterSearchResultsByQuery(normalizeApiList(pageData, apiId, context), query); } catch (error) { console.warn(`API ${apiId} 第${page}页搜索失败:`, error); return []; diff --git a/package-lock.json b/package-lock.json index 0c5f8ea792..d74c5c1dd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "libretv", - "version": "1.2.12", + "version": "1.2.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "libretv", - "version": "1.2.12", + "version": "1.2.13", "license": "Apache-2.0", "dependencies": { "axios": "^1.9.0", diff --git a/package.json b/package.json index a8c3084e44..5ba09546b8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "libretv", "type": "module", - "version": "1.2.12", + "version": "1.2.13", "private": true, "description": "免费在线视频搜索与观看平台", "author": "bestZwei", diff --git a/test/libretv-defaults.test.mjs b/test/libretv-defaults.test.mjs index b095645d56..73cb783247 100644 --- a/test/libretv-defaults.test.mjs +++ b/test/libretv-defaults.test.mjs @@ -667,6 +667,56 @@ test('search results require title relevance and ignored sources do not paginate assert.equal(requestedUrls.some(url => url.includes('pg=2')), false); }); +test('player resource switch can search without loading homepage app script', async () => { + const requestedUrls = []; + const warnings = []; + const sandbox = { + console: { + ...console, + warn(...args) { + warnings.push(args.map(String).join(' ')); + } + }, + URL, + window: {}, + fetch: async url => { + requestedUrls.push(decodeURIComponent(String(url))); + return { + ok: true, + async json() { + return { + pagecount: 1, + list: [ + { vod_id: 'match-1', vod_name: '世界的主人', vod_pic: '/poster.jpg' }, + { vod_id: 'noise-1', vod_name: '美女主播私拍' } + ] + }; + } + }; + }, + setTimeout, + clearTimeout, + AbortController + }; + vm.createContext(sandbox); + vm.runInContext(await readProjectFile('js/config.js'), sandbox); + vm.runInContext(await readProjectFile('js/customer_site.js'), sandbox); + vm.runInContext(await readProjectFile('js/search.js'), sandbox); + const player = await readProjectFile('js/player.js'); + + assert.equal(typeof sandbox.searchByAPIAndKeyWord, 'function'); + assert.equal(typeof sandbox.window.filterResultsByQuery, 'function'); + assert.match(player, /JSON\.stringify\(DEFAULT_SELECTED_APIS\)/); + assert.match(player, /Promise\.allSettled\(resourceOptions\.map/); + assert.match(player, /未找到可切换资源/); + + const results = await sandbox.searchByAPIAndKeyWord('ysgc', '世界的主人'); + + assert.deepEqual(Array.from(results).map(item => item.vod_id), ['match-1']); + assert.equal(requestedUrls.some(url => url.includes('cj.lziapi.com')), true); + assert.equal(warnings.some(message => message.includes('filterResultsByQuery')), false); +}); + test('adult recommendation tag uses selected adult sources instead of Douban', async () => { const storage = new Map([ ['yellowFilterEnabled', 'false'], @@ -966,12 +1016,12 @@ test('release metadata is bumped for this update', async () => { const changelog = await readProjectFile('CHANGELOG.md'); - assert.equal(packageJson.version, '1.2.12'); - assert.equal(lockJson.version, '1.2.12'); - assert.equal(lockJson.packages[''].version, '1.2.12'); - assert.match(config, /version:\s*'1\.2\.12'/); - assert.match(changelog, /1\.2\.12/); - assert.match(changelog, /播放器|ArtPlayer|hls\.js/); + assert.equal(packageJson.version, '1.2.13'); + assert.equal(lockJson.version, '1.2.13'); + assert.equal(lockJson.packages[''].version, '1.2.13'); + assert.match(config, /version:\s*'1\.2\.13'/); + assert.match(changelog, /1\.2\.13/); + assert.match(changelog, /resource switching|资源切换|switchable resources/); assert.match(versionTxt, /^\d{12}$/); assert.ok(Number(versionTxt) > 202508060117); });