From 79aa5514d58426dd3915ba95384f30453caca342 Mon Sep 17 00:00:00 2001 From: asimov Date: Fri, 30 May 2025 22:56:50 +1200 Subject: [PATCH] Updated version to 0.5.0, enhanced support for jable.tv, optimized the display logic of navigation and player buttons, added video source selection function, and updated related documents and styles. --- README.md | 8 +- components/NavigationButtons.ts | 88 +++++++++++++++++--- components/PlayerButtons.ts | 88 ++++++++++++++++---- entrypoints/background.ts | 48 +++++++++-- entrypoints/content/index.ts | 143 ++++++++++++++++++++++++++++---- entrypoints/popup/index.html | 8 ++ entrypoints/popup/main.ts | 20 ++++- entrypoints/popup/style.css | 24 +++++- package.json | 2 +- wxt.config.ts | 5 +- 10 files changed, 371 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 957b878..2960f98 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ # Jav Play 1. Call the local player to play the video in javdb.com/javlibrary.com directly. -2. Jump from the javdb.com/javlibrary.com page to the corresponding playback page of missav +2. Jump from the javdb.com/javlibrary.com page to the corresponding playback page of missav/jable ![JavDB Cover](/cover.png) ## Usage 1. Download extension zip file from github release page. 2. Open Chrome, navigate to chrome://extensions, then drag and drop the file onto the extensions page +3. Enable or Disable feature in popup setting. (Enable by default) +4. Choose video source from popup setting. (missav.ws by default) ## Supported Players 1. [IINA](https://iina.io/) Recommend in MacOS @@ -14,6 +16,4 @@ ## Players to be supported 1. ~~[mpv](https://mpv.io/)~~ Because the mpv player is not automatically registered as a protocol handler in the operating system, it is quite troublesome to handle, and support is not currently being considered. If someone is willing to add pmv support, please submit a request. - -## Planned Features -1. [x] Directly jump from the javdb page to the playback page of the corresponding film on missav. \ No newline at end of file +2. ~~[mpc-be](https://github.com/Aleksoid1978/MPC-BE)~~ Same reason as mpv player. diff --git a/components/NavigationButtons.ts b/components/NavigationButtons.ts index 2ebdf35..a9ee83b 100644 --- a/components/NavigationButtons.ts +++ b/components/NavigationButtons.ts @@ -4,17 +4,13 @@ import './NavigationButtons.css'; * 创建或更新一个浮动导航按钮。 * 此按钮在新标签页中打开链接。 */ -function createOrUpdateNavButton(id: string, name: string, iconClass: string, url: string, topPosition: string) { +function createOrUpdateNavButton(id: string, name: string, iconClass: string, url: string, topPosition: string, isDisabled: boolean = false) { let button = document.getElementById(id) as HTMLAnchorElement | null; if (!button) { button = document.createElement('a'); button.id = id; button.className = 'wxt-nav-button'; - button.innerHTML = ` - - ${name} - `; // 关键:设置在新标签页打开 button.target = '_blank'; @@ -24,9 +20,47 @@ function createOrUpdateNavButton(id: string, name: string, iconClass: string, ur document.body.appendChild(button); } + // 无论是新创建还是更新,都要设置innerHTML + button.innerHTML = ` + + + + + ${name} + `; + button.href = url; button.style.top = topPosition; button.style.display = 'flex'; + + // 根据是否禁用来设置样式和行为 + if (isDisabled) { + button.style.backgroundColor = '#6c757d'; // 灰色 + button.style.cursor = 'not-allowed'; + button.style.opacity = '0.6'; + + // 阻止点击事件 + button.onclick = (e) => { + e.preventDefault(); + return false; + }; + } else { + button.style.backgroundColor = '#28a745'; // 原来的绿色 + button.style.cursor = 'pointer'; + button.style.opacity = '1'; + button.onclick = null; // 移除点击阻止 + } } function hideButton(id: string) { @@ -36,20 +70,50 @@ function hideButton(id: string) { } } -const MISSAV_BUTTON_ID = 'wxt-missav-nav-button'; +const NAV_BUTTON_ID = 'wxt-nav-button'; /** - * 添加或更新 MissAV 导航按钮。 - * @param videoNumber - 视频番号 (例如, 'IPX-811')。 + * 显示检查状态的导航按钮 + * @param videoSource - 视频源 ('missav' 或 'jable') */ -export function addOrUpdateNavigationButtons(videoNumber: string) { - const missavUrl = `https://missav.ws/${videoNumber.toLowerCase()}`; - createOrUpdateNavButton(MISSAV_BUTTON_ID, 'MissAV', 'icon-play', missavUrl, '200px'); +export function showNavigationButtonChecking(videoSource: string = 'missav') { + const name = videoSource === 'jable' ? 'Jable' : 'MissAV'; + createOrUpdateNavButton(NAV_BUTTON_ID, `${name} Checking...`, 'icon-play', '#', '100px', true); +} + +/** + * 显示404状态的导航按钮 + * @param videoSource - 视频源 ('missav' 或 'jable') + */ +export function showNavigationButton404(videoSource: string = 'missav') { + const name = videoSource === 'jable' ? 'Jable' : 'MissAV'; + createOrUpdateNavButton(NAV_BUTTON_ID, `${name} 404`, 'icon-play', '#', '100px', true); +} + +/** + * 添加或更新导航按钮。 + * @param videoNumber - 视频番号 (例如, 'IPX-811')。 + * @param videoSource - 视频源 ('missav' 或 'jable')。 + */ +export function addOrUpdateNavigationButtons(videoNumber: string, videoSource: string = 'missav') { + let url: string; + let name: string; + + if (videoSource === 'jable') { + url = `https://jable.tv/videos/${videoNumber.toLowerCase()}/`; + name = 'Jable'; + } else { + // default to missav + url = `https://missav.ws/${videoNumber.toLowerCase()}`; + name = 'MissAV'; + } + + createOrUpdateNavButton(NAV_BUTTON_ID, name, 'icon-play', url, '100px', false); } /** * 隐藏所有的导航按钮。 */ export function hideNavigationButtons() { - hideButton(MISSAV_BUTTON_ID); + hideButton(NAV_BUTTON_ID); } \ No newline at end of file diff --git a/components/PlayerButtons.ts b/components/PlayerButtons.ts index afff191..58df301 100644 --- a/components/PlayerButtons.ts +++ b/components/PlayerButtons.ts @@ -3,28 +3,70 @@ import './PlayerButtons.css'; /** * 创建或更新一个浮动播放器按钮。 */ -function createOrUpdateButton(id: string, name:string, iconClass: string, url: string, topPosition: string) { +function createOrUpdateButton(id: string, name: string, iconClass: string, url: string, topPosition: string, isDisabled: boolean = false) { let button = document.getElementById(id) as HTMLAnchorElement | null; if (!button) { button = document.createElement('a'); button.id = id; button.className = 'wxt-player-button'; - button.innerHTML = ` - - ${name} - `; document.body.appendChild(button); - button.addEventListener('click', (e) => { - e.preventDefault(); - window.location.href = button!.href; - }); + // 只在按钮未禁用时添加点击事件 + if (!isDisabled) { + button.addEventListener('click', (e) => { + e.preventDefault(); + window.location.href = button!.href; + }); + } } + // 无论是新创建还是更新,都要设置innerHTML + button.innerHTML = ` + + + + + ${name} + `; + button.href = url; button.style.top = topPosition; button.style.display = 'flex'; + + // 根据是否禁用来设置样式和行为 + if (isDisabled) { + button.style.backgroundColor = '#6c757d'; // 灰色 + button.style.cursor = 'not-allowed'; + button.style.opacity = '0.6'; + + // 阻止点击事件 + button.onclick = (e) => { + e.preventDefault(); + return false; + }; + } else { + button.style.backgroundColor = '#3173dc'; // 原来的蓝色 + button.style.cursor = 'pointer'; + button.style.opacity = '1'; + + // 恢复正常点击行为 + button.onclick = (e) => { + e.preventDefault(); + window.location.href = button.href; + }; + } } /** @@ -42,19 +84,33 @@ const IINA_BUTTON_ID = 'wxt-iina-floating-button'; const POTPLAYER_BUTTON_ID = 'wxt-potplayer-floating-button'; /** - * 添加或更新播放器按钮。 - * @param missavUUID - 用于生成播放链接的 UUID。 + * 显示检查状态的播放器按钮 */ -export function addOrUpdatePlayerButtons(missavUUID: string) { - const playlistUrl = `https://surrit.com/${missavUUID}/playlist.m3u8`; - +export function showPlayerButtonsChecking() { + createOrUpdateButton(IINA_BUTTON_ID, 'IINA', 'icon-play', '#', '140px', true); + createOrUpdateButton(POTPLAYER_BUTTON_ID, 'PotPlayer', 'icon-play', '#', '180px', true); +} + +/** + * 显示404状态的播放器按钮 + */ +export function showPlayerButtons404() { + createOrUpdateButton(IINA_BUTTON_ID, 'IINA', 'icon-play', '#', '140px', true); + createOrUpdateButton(POTPLAYER_BUTTON_ID, 'PotPlayer', 'icon-play', '#', '180px', true); +} + +/** + * 添加或更新播放器按钮。 + * @param playlistUrl - 直接的播放链接。 + */ +export function addOrUpdatePlayerButtons(playlistUrl: string) { // IINA 按钮 const iinaUrl = `iina://weblink?url=${playlistUrl}`; - createOrUpdateButton(IINA_BUTTON_ID, 'IINA', 'icon-play', iinaUrl, '100px'); + createOrUpdateButton(IINA_BUTTON_ID, 'IINA', 'icon-play', iinaUrl, '140px', false); // PotPlayer 按钮 const potplayerUrl = `potplayer://${playlistUrl}`; - createOrUpdateButton(POTPLAYER_BUTTON_ID, 'PotPlayer', 'icon-play', potplayerUrl, '140px'); + createOrUpdateButton(POTPLAYER_BUTTON_ID, 'PotPlayer', 'icon-play', potplayerUrl, '180px', false); } /** diff --git a/entrypoints/background.ts b/entrypoints/background.ts index d6e51dd..eca6f9b 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -1,17 +1,24 @@ -interface FetchMissavRequest { - type: 'fetchMissav' +interface FetchVideoRequest { + type: 'fetchVideo' url: string } +interface CheckUrlRequest { + type: 'checkUrl' + url: string +} + +type BackgroundRequest = FetchVideoRequest | CheckUrlRequest; + export default defineBackground({ main() { chrome.runtime.onMessage.addListener(( - request: FetchMissavRequest, + request: BackgroundRequest, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void ) => { - if (request.type === 'fetchMissav') { - console.log('fetchMissav', request.url) + if (request.type === 'fetchVideo') { + console.log('fetchVideo', request.url) fetch(request.url, { redirect: 'follow', // Add headers to mimic a browser request @@ -20,14 +27,12 @@ export default defineBackground({ } }) .then(response => { - console.log('response', response) if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`) } return response.text() }) .then(html => { - console.log('html', html) sendResponse({ success: true, html }) }) .catch(error => { @@ -35,6 +40,35 @@ export default defineBackground({ }) return true // Keep the message channel open for the async response } + + if (request.type === 'checkUrl') { + console.log('checkUrl', request.url) + fetch(request.url, { + method: 'HEAD', // 只检查头部,不下载完整内容 + redirect: 'follow', + headers: { + 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36' + } + }) + .then(response => { + console.log('checkUrl response', response.status) + sendResponse({ + success: true, + exists: response.ok, + status: response.status + }) + }) + .catch(error => { + console.log('checkUrl error', error) + sendResponse({ + success: true, + exists: false, + status: 0, + error: error.message + }) + }) + return true // Keep the message channel open for the async response + } }) } }) \ No newline at end of file diff --git a/entrypoints/content/index.ts b/entrypoints/content/index.ts index 1f1e6a8..73873a1 100644 --- a/entrypoints/content/index.ts +++ b/entrypoints/content/index.ts @@ -1,8 +1,19 @@ // 从两个组件中导入方法 -import { addOrUpdatePlayerButtons, hidePlayerButtons } from '../../components/PlayerButtons'; -import { addOrUpdateNavigationButtons, hideNavigationButtons } from '../../components/NavigationButtons'; +import { + addOrUpdatePlayerButtons, + hidePlayerButtons, + showPlayerButtonsChecking, + showPlayerButtons404 +} from '../../components/PlayerButtons'; +import { + addOrUpdateNavigationButtons, + hideNavigationButtons, + showNavigationButtonChecking, + showNavigationButton404 +} from '../../components/NavigationButtons'; const STORAGE_KEY = 'feature_enabled'; +const VIDEO_SOURCE_KEY = 'video_source'; export default defineContentScript({ matches: ['*://*.javdb.com/v/*', '*://*.javlibrary.com/*'], @@ -21,16 +32,35 @@ export default defineContentScript({ const processPage = async () => { const videoNumber = getVideoNumber(); if (videoNumber) { - // 只要有番号,就显示导航按钮 - addOrUpdateNavigationButtons(videoNumber); - - // 异步获取 UUID 来显示播放器按钮 - const missavUUID = await getMissavUUID(videoNumber); - if (missavUUID) { - addOrUpdatePlayerButtons(missavUUID); + // 获取用户选择的视频源 + const videoSource = await storage.getItem(`sync:${VIDEO_SOURCE_KEY}`) ?? 'missav'; + + // 1. 先显示检查状态 + showNavigationButtonChecking(videoSource as string); + showPlayerButtonsChecking(); + + // 2. 检查目标页面是否存在 + const targetUrl = getTargetUrl(videoNumber, videoSource as string); + const urlExists = await checkUrlExists(targetUrl); + + if (!urlExists) { + // 3.1 目标页面不存在,显示404状态 + showNavigationButton404(videoSource as string); + showPlayerButtons404(); + console.log('目标页面不存在:', targetUrl); + return; + } + + // 3.2 目标页面存在,显示正常的导航按钮 + addOrUpdateNavigationButtons(videoNumber, videoSource as string); + + // 4. 异步获取播放链接来显示播放器按钮 + const playUrl = await getPlayUrl(videoNumber, videoSource as string); + if (playUrl) { + addOrUpdatePlayerButtons(playUrl); } else { - // 如果没有 UUID,则只隐藏播放器按钮 - hidePlayerButtons(); + // 如果没有播放链接,显示404状态 + showPlayerButtons404(); } } }; @@ -77,14 +107,54 @@ function getVideoNumber(): string | undefined { } } -// 获取 missav UUID -async function getMissavUUID(videoNumber: string): Promise { +// 获取目标URL +function getTargetUrl(videoNumber: string, videoSource: string): string { + if (videoSource === 'jable') { + return `https://jable.tv/videos/${videoNumber.toLowerCase()}/`; + } else { + // default to missav + return `https://missav.ws/${videoNumber.toLowerCase()}`; + } +} + +// 检查URL是否存在 +async function checkUrlExists(url: string): Promise { + try { + const response = await chrome.runtime.sendMessage({ + type: 'checkUrl', + url: url + }); + + if (!response.success) { + console.error('检查URL时出错:', response.error); + return false; + } + + return response.exists; + } catch (error) { + console.error('检查URL时出错:', error); + return false; + } +} + +// 获取播放链接 (支持不同视频源) +async function getPlayUrl(videoNumber: string, videoSource: string): Promise { + if (videoSource === 'jable') { + return await getJablePlayUrl(videoNumber); + } else { + // default to missav + return await getMissavPlayUrl(videoNumber); + } +} + +// 获取 MissAV 播放链接 +async function getMissavPlayUrl(videoNumber: string): Promise { const lowerTargetNumber = videoNumber.toLowerCase(); const targetUrl = `https://missav.ws/dm1/en/${lowerTargetNumber}`; try { const response = await chrome.runtime.sendMessage({ - type: 'fetchMissav', + type: 'fetchVideo', url: targetUrl }); @@ -105,16 +175,53 @@ async function getMissavUUID(videoNumber: string): Promise { const firstUrl = urlsMatch[1].split(',')[0].trim().replace(/"/g, '').replace(/\\/g, ''); const uuidMatch = firstUrl.match(/\/([0-9a-f-]+)\/seek\//i); if (uuidMatch) { - console.log('uuidMatch', uuidMatch[1]); - return uuidMatch[1]; + console.log('MissAV uuidMatch', uuidMatch[1]); + return `https://surrit.com/${uuidMatch[1]}/playlist.m3u8`; } } } } - console.warn('未找到 uuid'); + console.warn('未找到 MissAV uuid'); return ''; } catch (error) { - console.error('获取或解析文档时出错:', error); + console.error('获取或解析 MissAV 文档时出错:', error); return ''; } } + +// 获取 Jable 播放链接 +async function getJablePlayUrl(videoNumber: string): Promise { + const lowerTargetNumber = videoNumber.toLowerCase(); + const targetUrl = `https://jable.tv/videos/${lowerTargetNumber}/`; + + try { + const response = await chrome.runtime.sendMessage({ + type: 'fetchVideo', + url: targetUrl + }); + + if (!response.success) { + throw new Error(response.error); + } + + const parser = new DOMParser(); + const doc = parser.parseFromString(response.html, 'text/html'); + + const scripts = doc.getElementsByTagName('script'); + + for (const script of scripts) { + const content = script.textContent || ''; + // 查找 var hlsUrl 变量 + const hlsUrlMatch = content.match(/var\s+hlsUrl\s*=\s*['"](.*?)['"]/); + if (hlsUrlMatch) { + console.log('Jable hlsUrl', hlsUrlMatch[1]); + return hlsUrlMatch[1]; + } + } + console.warn('未找到 Jable hlsUrl'); + return ''; + } catch (error) { + console.error('获取或解析 Jable 文档时出错:', error); + return ''; + } +} \ No newline at end of file diff --git a/entrypoints/popup/index.html b/entrypoints/popup/index.html index 4faaff0..41aa4a6 100644 --- a/entrypoints/popup/index.html +++ b/entrypoints/popup/index.html @@ -23,6 +23,14 @@ + +
+ + +

diff --git a/entrypoints/popup/main.ts b/entrypoints/popup/main.ts index 0561cfd..836d7a8 100644 --- a/entrypoints/popup/main.ts +++ b/entrypoints/popup/main.ts @@ -1,11 +1,13 @@ import './style.css'; const featureToggle = document.querySelector('#feature-toggle'); +const videoSourceSelect = document.querySelector('#video-source'); -// storage key, define as constant, for easy use in multiple places +// storage keys, define as constants, for easy use in multiple places const STORAGE_KEY = 'feature_enabled'; +const VIDEO_SOURCE_KEY = 'video_source'; -// 1. when popup is opened, load and set the initial state of the switch +// 1. when popup is opened, load and set the initial state of the switch and select // WXT provided storage API is a wrapper of chrome.storage, usage is basically the same storage.getItem(`sync:${STORAGE_KEY}`).then((result) => { // if there is no value in the storage, we default to true @@ -16,8 +18,22 @@ storage.getItem(`sync:${STORAGE_KEY}`).then((result) => { } }); +storage.getItem(`sync:${VIDEO_SOURCE_KEY}`).then((result) => { + // default to missav if no value in storage + const videoSource = (result as string | null) ?? 'missav'; + if (videoSourceSelect) { + videoSourceSelect.value = videoSource; + } +}); + // 2. listen for the change of the switch state, and save the new setting featureToggle?.addEventListener('change', () => { const isEnabled = featureToggle.checked; storage.setItem(`sync:${STORAGE_KEY}`, isEnabled); +}); + +// 3. listen for the change of the video source selection, and save the new setting +videoSourceSelect?.addEventListener('change', () => { + const videoSource = videoSourceSelect.value; + storage.setItem(`sync:${VIDEO_SOURCE_KEY}`, videoSource); }); \ No newline at end of file diff --git a/entrypoints/popup/style.css b/entrypoints/popup/style.css index f3cfcf8..ecacabe 100644 --- a/entrypoints/popup/style.css +++ b/entrypoints/popup/style.css @@ -53,6 +53,11 @@ h1 { justify-content: space-between; align-items: center; font-size: 14px; + margin-bottom: 15px; +} + +.setting-row:last-child { + margin-bottom: 0; } footer { @@ -75,7 +80,24 @@ footer a:hover { text-decoration: underline; } -/* --- 开关样式 (保持不变) --- */ +/* --- 下拉选择框样式 --- */ +.source-select { + padding: 4px 8px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; + background-color: white; + cursor: pointer; + min-width: 100px; +} + +.source-select:focus { + outline: none; + border-color: #007bff; + box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25); +} + +/* --- 开关样式 --- */ .switch { position: relative; display: inline-block; diff --git a/package.json b/package.json index 2e3447d..6e2ca91 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "jav-play", "description": "manifest.json description", "private": true, - "version": "0.4.0", + "version": "0.5.0", "type": "module", "scripts": { "dev": "wxt", diff --git a/wxt.config.ts b/wxt.config.ts index 635c311..11d552f 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -12,8 +12,9 @@ export default defineConfig({ // Explicitly grant permission for the background script to access this host. host_permissions: [ '*://*.javdb.com/*', + '*://*.javlibrary.com/*', '*://*.missav.ws/*', - '*://*.javlibrary.com/*' + '*://*.jable.tv/*' ], }, -}); +}); \ No newline at end of file