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);
});