From 3508814d14e0f0c3c018fc229dc888e692d8e007 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 11:00:32 +0000 Subject: [PATCH 1/4] Add MPRIS media session plugin for Bilibili video integration Agent-Logs-Url: https://github.com/BlockG-ws/b23-evolved/sessions/a2cea8b2-49fe-462f-8e10-d41d3e953f47 Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com> --- registry/dist/plugins/video/mpris.js | 1 + registry/lib/plugins/video/mpris/index.ts | 237 ++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 registry/dist/plugins/video/mpris.js create mode 100644 registry/lib/plugins/video/mpris/index.ts diff --git a/registry/dist/plugins/video/mpris.js b/registry/dist/plugins/video/mpris.js new file mode 100644 index 0000000000..849b30131e --- /dev/null +++ b/registry/dist/plugins/video/mpris.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["video/mpris"]=t():e["video/mpris"]=t()}(globalThis,()=>(()=>{"use strict";var e={d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{plugin:()=>l});const a=coreApis.observer,n=coreApis.componentApis.video.playerAgent,i=coreApis.ajax,r=(e,t)=>{try{navigator.mediaSession.setActionHandler(e,t)}catch{}},o=e=>{const t=new URL(window.location.href);t.searchParams.set("p",String(e)),window.location.href=t.toString()},s=e=>{window.location.href=`https://www.bilibili.com/video/${e}`},l={name:"video.mpris",displayName:"MPRIS 媒体控制",description:"通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS):\n- 标题为视频标题(多 P 时附带分 P 标题)\n- 艺术家为 UP 主名称\n- 专辑封面为视频封面\n- 播放进度为视频进度\n- 支持快进、快退、倍速\n- 分 P 视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息",setup:()=>{if(!("mediaSession"in navigator))return;const e=navigator.mediaSession;let t=null,l=null,c=[];const d=()=>{const a=t;if(a&&!isNaN(a.duration)&&0!==a.duration)try{e.setPositionState({duration:a.duration,position:Math.min(a.currentTime,a.duration),playbackRate:a.playbackRate})}catch{}};(0,a.videoChange)(async a=>{let{aid:p,cid:u}=a;null!==l&&(clearInterval(l),l=null),c.forEach(e=>e()),c=[];const m=await n.playerAgent.query.video.element();if(t=m,!m)return;const g=await(0,i.getJsonWithCredentials)(`https://api.bilibili.com/x/web-interface/view?aid=${p}`);if(0!==g.code)return;const{data:b}=g,f=b.title??"",v=b.owner?.name??"",y=(b.pic??"").replace("http:","https:"),h=(b.pages??[]).map(e=>({cid:Number(e.cid),title:String(e.part??""),pageNumber:Number(e.page)})),k=Number(u),w=h.length>1?h.findIndex(e=>e.cid===k):-1,x=h.length>1&&w>=0?`${f} P${h[w].pageNumber} ${h[w].title}`:f,A=lodash.get(b,"ugc_season.sections",[]).flatMap(e=>e.episodes??[]),S=A.length>0?A.findIndex(e=>Number(e.cid)===k):-1;if(e.metadata=new MediaMetadata({title:x,artist:v,album:f,artwork:y?[{src:y,sizes:"480x270",type:"image/jpeg"}]:[]}),e.playbackState=m.paused?"paused":"playing",r("play",()=>{m.play()}),r("pause",()=>{m.pause()}),r("seekbackward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(-t),d()}),r("seekforward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(t),d()}),r("seekto",e=>{void 0!==e.seekTime&&(n.playerAgent.seek(e.seekTime),d())}),h.length>1&&w>=0){const e=w>0,t=wo(h[w-1].pageNumber):null),r("nexttrack",t?()=>o(h[w+1].pageNumber):null)}else if(A.length>0&&S>=0){const e=S>0,t=Ss(A[S-1].bvid):null),r("nexttrack",t?()=>s(A[S+1].bvid):null)}else{r("previoustrack",null);const e=document.querySelector(".bpx-player-ctrl-next"),t=null!==e&&!e.classList.contains("bpx-state-disabled");r("nexttrack",t?()=>e.click():null)}const E=()=>d();m.readyState>=HTMLMediaElement.HAVE_METADATA?E():(m.addEventListener("loadedmetadata",E,{once:!0}),c.push(()=>m.removeEventListener("loadedmetadata",E)));const P=()=>{e.playbackState="playing",d()},L=()=>{e.playbackState="paused",d()},N=()=>d();m.addEventListener("play",P),m.addEventListener("pause",L),m.addEventListener("ratechange",N),c.push(()=>m.removeEventListener("play",P),()=>m.removeEventListener("pause",L),()=>m.removeEventListener("ratechange",N)),l=setInterval(d,5e3)})},commitHash:"bbb8c51ee3ce4f0929cf1f41656d25356afcde34",coreVersion:"2.10.7"};return t=t.plugin})()); \ No newline at end of file diff --git a/registry/lib/plugins/video/mpris/index.ts b/registry/lib/plugins/video/mpris/index.ts new file mode 100644 index 0000000000..60d50886a1 --- /dev/null +++ b/registry/lib/plugins/video/mpris/index.ts @@ -0,0 +1,237 @@ +import type { PluginMetadata } from '@/plugins/plugin' +import { videoChange } from '@/core/observer' +import { playerAgent } from '@/components/video/player-agent' +import { getJsonWithCredentials } from '@/core/ajax' + +interface PageInfo { + cid: number + title: string + pageNumber: number +} + +interface SeasonEpisode { + aid: string + bvid: string + cid: number + title: string +} + +const setOrClearAction = ( + action: MediaSessionAction, + handler: MediaSessionActionHandler | null, +) => { + try { + navigator.mediaSession.setActionHandler(action, handler) + } catch { + // action may not be supported in this browser + } +} + +const navigateToPage = (pageNumber: number) => { + const url = new URL(window.location.href) + url.searchParams.set('p', String(pageNumber)) + window.location.href = url.toString() +} + +const navigateToBvid = (bvid: string) => { + window.location.href = `https://www.bilibili.com/video/${bvid}` +} + +export const plugin: PluginMetadata = { + name: 'video.mpris', + displayName: 'MPRIS 媒体控制', + description: `通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS): +- 标题为视频标题(多 P 时附带分 P 标题) +- 艺术家为 UP 主名称 +- 专辑封面为视频封面 +- 播放进度为视频进度 +- 支持快进、快退、倍速 +- 分 P 视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息`, + setup: () => { + if (!('mediaSession' in navigator)) { + return + } + + const ms = navigator.mediaSession + let currentVideoElement: HTMLVideoElement | null = null + let positionUpdateTimer: ReturnType | null = null + let videoEventCleanups: (() => void)[] = [] + + const clearTimer = () => { + if (positionUpdateTimer !== null) { + clearInterval(positionUpdateTimer) + positionUpdateTimer = null + } + } + + const clearVideoEvents = () => { + videoEventCleanups.forEach(fn => fn()) + videoEventCleanups = [] + } + + const updatePositionState = () => { + const v = currentVideoElement + if (!v || isNaN(v.duration) || v.duration === 0) { + return + } + try { + ms.setPositionState({ + duration: v.duration, + position: Math.min(v.currentTime, v.duration), + playbackRate: v.playbackRate, + }) + } catch { + // setPositionState may not be supported + } + } + + videoChange(async ({ aid, cid }) => { + clearTimer() + clearVideoEvents() + + const videoEl = (await playerAgent.query.video.element()) as HTMLVideoElement | null + currentVideoElement = videoEl + if (!videoEl) { + return + } + + // Fetch video info from API + const json = await getJsonWithCredentials( + `https://api.bilibili.com/x/web-interface/view?aid=${aid}`, + ) + if (json.code !== 0) { + return + } + const { data } = json + + const videoTitle: string = data.title ?? '' + const upName: string = data.owner?.name ?? '' + const coverUrl: string = (data.pic ?? '').replace('http:', 'https:') + + // Multi-page info (分P) + const pages: PageInfo[] = (data.pages ?? []).map((p: Record) => ({ + cid: Number(p.cid), + title: String(p.part ?? ''), + pageNumber: Number(p.page), + })) + const cidNum = Number(cid) + const currentPageIdx = pages.length > 1 ? pages.findIndex(p => p.cid === cidNum) : -1 + + // Display title: append part title when multi-page + const displayTitle = + pages.length > 1 && currentPageIdx >= 0 + ? `${videoTitle} P${pages[currentPageIdx].pageNumber} ${pages[currentPageIdx].title}` + : videoTitle + + // Collection (ugc_season) episode list + const seasonSections: { episodes: SeasonEpisode[] }[] = lodash.get( + data, + 'ugc_season.sections', + [], + ) + const allSeasonEpisodes: SeasonEpisode[] = seasonSections.flatMap(s => s.episodes ?? []) + const currentSeasonIdx = + allSeasonEpisodes.length > 0 + ? allSeasonEpisodes.findIndex(e => Number(e.cid) === cidNum) + : -1 + + // Set media metadata + ms.metadata = new MediaMetadata({ + title: displayTitle, + artist: upName, + album: videoTitle, + artwork: coverUrl ? [{ src: coverUrl, sizes: '480x270', type: 'image/jpeg' }] : [], + }) + ms.playbackState = videoEl.paused ? 'paused' : 'playing' + + // Playback control handlers + setOrClearAction('play', () => { + videoEl.play() + }) + setOrClearAction('pause', () => { + videoEl.pause() + }) + setOrClearAction('seekbackward', details => { + const offset = details.seekOffset ?? 10 + playerAgent.changeTime(-offset) + updatePositionState() + }) + setOrClearAction('seekforward', details => { + const offset = details.seekOffset ?? 10 + playerAgent.changeTime(offset) + updatePositionState() + }) + setOrClearAction('seekto', details => { + if (details.seekTime !== undefined) { + playerAgent.seek(details.seekTime) + updatePositionState() + } + }) + + // Previous / next track navigation + if (pages.length > 1 && currentPageIdx >= 0) { + // Case 1: multi-page video (分P) + const hasPrev = currentPageIdx > 0 + const hasNext = currentPageIdx < pages.length - 1 + setOrClearAction( + 'previoustrack', + hasPrev ? () => navigateToPage(pages[currentPageIdx - 1].pageNumber) : null, + ) + setOrClearAction( + 'nexttrack', + hasNext ? () => navigateToPage(pages[currentPageIdx + 1].pageNumber) : null, + ) + } else if (allSeasonEpisodes.length > 0 && currentSeasonIdx >= 0) { + // Case 2: collection (ugc_season 合集) + const hasPrev = currentSeasonIdx > 0 + const hasNext = currentSeasonIdx < allSeasonEpisodes.length - 1 + setOrClearAction( + 'previoustrack', + hasPrev ? () => navigateToBvid(allSeasonEpisodes[currentSeasonIdx - 1].bvid) : null, + ) + setOrClearAction( + 'nexttrack', + hasNext ? () => navigateToBvid(allSeasonEpisodes[currentSeasonIdx + 1].bvid) : null, + ) + } else { + // Case 3: single video or bangumi — delegate to player's built-in next button + setOrClearAction('previoustrack', null) + const nextBtn = document.querySelector('.bpx-player-ctrl-next') as HTMLElement | null + const hasPlayerNext = nextBtn !== null && !nextBtn.classList.contains('bpx-state-disabled') + setOrClearAction('nexttrack', hasPlayerNext ? () => nextBtn.click() : null) + } + + // Position state — wait for metadata if needed + const initPosition = () => updatePositionState() + if (videoEl.readyState >= HTMLMediaElement.HAVE_METADATA) { + initPosition() + } else { + videoEl.addEventListener('loadedmetadata', initPosition, { once: true }) + videoEventCleanups.push(() => videoEl.removeEventListener('loadedmetadata', initPosition)) + } + + // Sync playback state and position on events + const onPlay = () => { + ms.playbackState = 'playing' + updatePositionState() + } + const onPause = () => { + ms.playbackState = 'paused' + updatePositionState() + } + const onRateChange = () => updatePositionState() + + videoEl.addEventListener('play', onPlay) + videoEl.addEventListener('pause', onPause) + videoEl.addEventListener('ratechange', onRateChange) + videoEventCleanups.push( + () => videoEl.removeEventListener('play', onPlay), + () => videoEl.removeEventListener('pause', onPause), + () => videoEl.removeEventListener('ratechange', onRateChange), + ) + + // Periodic position update every 5 s + positionUpdateTimer = setInterval(updatePositionState, 5000) + }) + }, +} From feaafc336e0751aa4eaae70eacfd36c0932d31ce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 11:02:50 +0000 Subject: [PATCH 2/4] Address code review: remove lodash.get, extract interval constant Agent-Logs-Url: https://github.com/BlockG-ws/b23-evolved/sessions/a2cea8b2-49fe-462f-8e10-d41d3e953f47 Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com> --- registry/dist/plugins/video/mpris.js | 4 +++- registry/lib/plugins/video/mpris/index.ts | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/registry/dist/plugins/video/mpris.js b/registry/dist/plugins/video/mpris.js index 849b30131e..df91a8776d 100644 --- a/registry/dist/plugins/video/mpris.js +++ b/registry/dist/plugins/video/mpris.js @@ -1 +1,3 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["video/mpris"]=t():e["video/mpris"]=t()}(globalThis,()=>(()=>{"use strict";var e={d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{plugin:()=>l});const a=coreApis.observer,n=coreApis.componentApis.video.playerAgent,i=coreApis.ajax,r=(e,t)=>{try{navigator.mediaSession.setActionHandler(e,t)}catch{}},o=e=>{const t=new URL(window.location.href);t.searchParams.set("p",String(e)),window.location.href=t.toString()},s=e=>{window.location.href=`https://www.bilibili.com/video/${e}`},l={name:"video.mpris",displayName:"MPRIS 媒体控制",description:"通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS):\n- 标题为视频标题(多 P 时附带分 P 标题)\n- 艺术家为 UP 主名称\n- 专辑封面为视频封面\n- 播放进度为视频进度\n- 支持快进、快退、倍速\n- 分 P 视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息",setup:()=>{if(!("mediaSession"in navigator))return;const e=navigator.mediaSession;let t=null,l=null,c=[];const d=()=>{const a=t;if(a&&!isNaN(a.duration)&&0!==a.duration)try{e.setPositionState({duration:a.duration,position:Math.min(a.currentTime,a.duration),playbackRate:a.playbackRate})}catch{}};(0,a.videoChange)(async a=>{let{aid:p,cid:u}=a;null!==l&&(clearInterval(l),l=null),c.forEach(e=>e()),c=[];const m=await n.playerAgent.query.video.element();if(t=m,!m)return;const g=await(0,i.getJsonWithCredentials)(`https://api.bilibili.com/x/web-interface/view?aid=${p}`);if(0!==g.code)return;const{data:b}=g,f=b.title??"",v=b.owner?.name??"",y=(b.pic??"").replace("http:","https:"),h=(b.pages??[]).map(e=>({cid:Number(e.cid),title:String(e.part??""),pageNumber:Number(e.page)})),k=Number(u),w=h.length>1?h.findIndex(e=>e.cid===k):-1,x=h.length>1&&w>=0?`${f} P${h[w].pageNumber} ${h[w].title}`:f,A=lodash.get(b,"ugc_season.sections",[]).flatMap(e=>e.episodes??[]),S=A.length>0?A.findIndex(e=>Number(e.cid)===k):-1;if(e.metadata=new MediaMetadata({title:x,artist:v,album:f,artwork:y?[{src:y,sizes:"480x270",type:"image/jpeg"}]:[]}),e.playbackState=m.paused?"paused":"playing",r("play",()=>{m.play()}),r("pause",()=>{m.pause()}),r("seekbackward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(-t),d()}),r("seekforward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(t),d()}),r("seekto",e=>{void 0!==e.seekTime&&(n.playerAgent.seek(e.seekTime),d())}),h.length>1&&w>=0){const e=w>0,t=wo(h[w-1].pageNumber):null),r("nexttrack",t?()=>o(h[w+1].pageNumber):null)}else if(A.length>0&&S>=0){const e=S>0,t=Ss(A[S-1].bvid):null),r("nexttrack",t?()=>s(A[S+1].bvid):null)}else{r("previoustrack",null);const e=document.querySelector(".bpx-player-ctrl-next"),t=null!==e&&!e.classList.contains("bpx-state-disabled");r("nexttrack",t?()=>e.click():null)}const E=()=>d();m.readyState>=HTMLMediaElement.HAVE_METADATA?E():(m.addEventListener("loadedmetadata",E,{once:!0}),c.push(()=>m.removeEventListener("loadedmetadata",E)));const P=()=>{e.playbackState="playing",d()},L=()=>{e.playbackState="paused",d()},N=()=>d();m.addEventListener("play",P),m.addEventListener("pause",L),m.addEventListener("ratechange",N),c.push(()=>m.removeEventListener("play",P),()=>m.removeEventListener("pause",L),()=>m.removeEventListener("ratechange",N)),l=setInterval(d,5e3)})},commitHash:"bbb8c51ee3ce4f0929cf1f41656d25356afcde34",coreVersion:"2.10.7"};return t=t.plugin})()); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["video/mpris"]=t():e["video/mpris"]=t()}(globalThis,()=>(()=>{"use strict";var e={d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{plugin:()=>l});const a=coreApis.observer,n=coreApis.componentApis.video.playerAgent,i=coreApis.ajax,r=(e,t)=>{try{navigator.mediaSession.setActionHandler(e,t)}catch{}},o=e=>{const t=new URL(window.location.href);t.searchParams.set("p",String(e)),window.location.href=t.toString()},s=e=>{window.location.href=`https://www.bilibili.com/video/${e}`},l={name:"video.mpris",displayName:"MPRIS 媒体控制",description:"通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS):\n- 标题为视频标题(多 P 时附带分 P 标题)\n- 艺术家为 UP 主名称\n- 专辑封面为视频封面\n- 播放进度为视频进度\n- 支持快进、快退、倍速\n- 分 P 视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息",setup:()=>{if(!("mediaSession"in navigator))return;const e=navigator.mediaSession;let t=null,l=null,c=[];const d=()=>{const a=t;if(a&&!isNaN(a.duration)&&0!==a.duration)try{e.setPositionState({duration:a.duration,position:Math.min(a.currentTime,a.duration),playbackRate:a.playbackRate})}catch{}};(0,a.videoChange)(async a=>{let{aid:p,cid:u}=a;null!==l&&(clearInterval(l),l=null),c.forEach(e=>e()),c=[];const m=await n.playerAgent.query.video.element();if(t=m,!m)return;const g=await(0,i.getJsonWithCredentials)(`https://api.bilibili.com/x/web-interface/view?aid=${p}`);if(0!==g.code)return;const{data:b}=g,v=b.title??"",f=b.owner?.name??"",y=(b.pic??"").replace("http:","https:"),h=(b.pages??[]).map(e=>({cid:Number(e.cid),title:String(e.part??""),pageNumber:Number(e.page)})),k=Number(u),w=h.length>1?h.findIndex(e=>e.cid===k):-1,x=h.length>1&&w>=0?`${v} P${h[w].pageNumber} ${h[w].title}`:v,A=( +// eslint-disable-next-line @typescript-eslint/no-explicit-any +b.ugc_season?.sections??[]).flatMap(e=>e.episodes??[]),S=A.length>0?A.findIndex(e=>Number(e.cid)===k):-1;if(e.metadata=new MediaMetadata({title:x,artist:f,album:v,artwork:y?[{src:y,sizes:"480x270",type:"image/jpeg"}]:[]}),e.playbackState=m.paused?"paused":"playing",r("play",()=>{m.play()}),r("pause",()=>{m.pause()}),r("seekbackward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(-t),d()}),r("seekforward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(t),d()}),r("seekto",e=>{void 0!==e.seekTime&&(n.playerAgent.seek(e.seekTime),d())}),h.length>1&&w>=0){const e=w>0,t=wo(h[w-1].pageNumber):null),r("nexttrack",t?()=>o(h[w+1].pageNumber):null)}else if(A.length>0&&S>=0){const e=S>0,t=Ss(A[S-1].bvid):null),r("nexttrack",t?()=>s(A[S+1].bvid):null)}else{r("previoustrack",null);const e=document.querySelector(".bpx-player-ctrl-next"),t=null!==e&&!e.classList.contains("bpx-state-disabled");r("nexttrack",t?()=>e.click():null)}const E=()=>d();m.readyState>=HTMLMediaElement.HAVE_METADATA?E():(m.addEventListener("loadedmetadata",E,{once:!0}),c.push(()=>m.removeEventListener("loadedmetadata",E)));const P=()=>{e.playbackState="playing",d()},L=()=>{e.playbackState="paused",d()},N=()=>d();m.addEventListener("play",P),m.addEventListener("pause",L),m.addEventListener("ratechange",N),c.push(()=>m.removeEventListener("play",P),()=>m.removeEventListener("pause",L),()=>m.removeEventListener("ratechange",N)),l=setInterval(d,5e3)})},commitHash:"3508814d14e0f0c3c018fc229dc888e692d8e007",coreVersion:"2.10.7"};return t=t.plugin})()); \ No newline at end of file diff --git a/registry/lib/plugins/video/mpris/index.ts b/registry/lib/plugins/video/mpris/index.ts index 60d50886a1..89acce4137 100644 --- a/registry/lib/plugins/video/mpris/index.ts +++ b/registry/lib/plugins/video/mpris/index.ts @@ -3,6 +3,8 @@ import { videoChange } from '@/core/observer' import { playerAgent } from '@/components/video/player-agent' import { getJsonWithCredentials } from '@/core/ajax' +const POSITION_UPDATE_INTERVAL_MS = 5000 + interface PageInfo { cid: number title: string @@ -124,11 +126,9 @@ export const plugin: PluginMetadata = { : videoTitle // Collection (ugc_season) episode list - const seasonSections: { episodes: SeasonEpisode[] }[] = lodash.get( - data, - 'ugc_season.sections', - [], - ) + const seasonSections: { episodes: SeasonEpisode[] }[] = + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (data as any).ugc_season?.sections ?? [] const allSeasonEpisodes: SeasonEpisode[] = seasonSections.flatMap(s => s.episodes ?? []) const currentSeasonIdx = allSeasonEpisodes.length > 0 @@ -231,7 +231,7 @@ export const plugin: PluginMetadata = { ) // Periodic position update every 5 s - positionUpdateTimer = setInterval(updatePositionState, 5000) + positionUpdateTimer = setInterval(updatePositionState, POSITION_UPDATE_INTERVAL_MS) }) }, } From 84a082518e541aa44fa0999a9eaab0af7e8f8eee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 11:09:23 +0000 Subject: [PATCH 3/4] =?UTF-8?q?Use=20VideoInfo=20API=20in=20MPRIS=20plugin?= =?UTF-8?q?;=20fix=20mutually-exclusive=20=E5=88=86P/=E5=90=88=E9=9B=86=20?= =?UTF-8?q?nav=20logic;=20add=20ugcSeason=20to=20VideoInfo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Agent-Logs-Url: https://github.com/BlockG-ws/b23-evolved/sessions/062bb310-df3b-4b33-8663-34ca6e9695bc Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com> --- registry/dist/plugins/video/mpris.js | 4 +- registry/lib/plugins/video/mpris/index.ts | 135 +++++++++------------- src/components/video/video-info.ts | 41 +++++++ 3 files changed, 99 insertions(+), 81 deletions(-) diff --git a/registry/dist/plugins/video/mpris.js b/registry/dist/plugins/video/mpris.js index df91a8776d..f678fbcb74 100644 --- a/registry/dist/plugins/video/mpris.js +++ b/registry/dist/plugins/video/mpris.js @@ -1,3 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["video/mpris"]=t():e["video/mpris"]=t()}(globalThis,()=>(()=>{"use strict";var e={d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{plugin:()=>l});const a=coreApis.observer,n=coreApis.componentApis.video.playerAgent,i=coreApis.ajax,r=(e,t)=>{try{navigator.mediaSession.setActionHandler(e,t)}catch{}},o=e=>{const t=new URL(window.location.href);t.searchParams.set("p",String(e)),window.location.href=t.toString()},s=e=>{window.location.href=`https://www.bilibili.com/video/${e}`},l={name:"video.mpris",displayName:"MPRIS 媒体控制",description:"通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS):\n- 标题为视频标题(多 P 时附带分 P 标题)\n- 艺术家为 UP 主名称\n- 专辑封面为视频封面\n- 播放进度为视频进度\n- 支持快进、快退、倍速\n- 分 P 视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息",setup:()=>{if(!("mediaSession"in navigator))return;const e=navigator.mediaSession;let t=null,l=null,c=[];const d=()=>{const a=t;if(a&&!isNaN(a.duration)&&0!==a.duration)try{e.setPositionState({duration:a.duration,position:Math.min(a.currentTime,a.duration),playbackRate:a.playbackRate})}catch{}};(0,a.videoChange)(async a=>{let{aid:p,cid:u}=a;null!==l&&(clearInterval(l),l=null),c.forEach(e=>e()),c=[];const m=await n.playerAgent.query.video.element();if(t=m,!m)return;const g=await(0,i.getJsonWithCredentials)(`https://api.bilibili.com/x/web-interface/view?aid=${p}`);if(0!==g.code)return;const{data:b}=g,v=b.title??"",f=b.owner?.name??"",y=(b.pic??"").replace("http:","https:"),h=(b.pages??[]).map(e=>({cid:Number(e.cid),title:String(e.part??""),pageNumber:Number(e.page)})),k=Number(u),w=h.length>1?h.findIndex(e=>e.cid===k):-1,x=h.length>1&&w>=0?`${v} P${h[w].pageNumber} ${h[w].title}`:v,A=( -// eslint-disable-next-line @typescript-eslint/no-explicit-any -b.ugc_season?.sections??[]).flatMap(e=>e.episodes??[]),S=A.length>0?A.findIndex(e=>Number(e.cid)===k):-1;if(e.metadata=new MediaMetadata({title:x,artist:f,album:v,artwork:y?[{src:y,sizes:"480x270",type:"image/jpeg"}]:[]}),e.playbackState=m.paused?"paused":"playing",r("play",()=>{m.play()}),r("pause",()=>{m.pause()}),r("seekbackward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(-t),d()}),r("seekforward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(t),d()}),r("seekto",e=>{void 0!==e.seekTime&&(n.playerAgent.seek(e.seekTime),d())}),h.length>1&&w>=0){const e=w>0,t=wo(h[w-1].pageNumber):null),r("nexttrack",t?()=>o(h[w+1].pageNumber):null)}else if(A.length>0&&S>=0){const e=S>0,t=Ss(A[S-1].bvid):null),r("nexttrack",t?()=>s(A[S+1].bvid):null)}else{r("previoustrack",null);const e=document.querySelector(".bpx-player-ctrl-next"),t=null!==e&&!e.classList.contains("bpx-state-disabled");r("nexttrack",t?()=>e.click():null)}const E=()=>d();m.readyState>=HTMLMediaElement.HAVE_METADATA?E():(m.addEventListener("loadedmetadata",E,{once:!0}),c.push(()=>m.removeEventListener("loadedmetadata",E)));const P=()=>{e.playbackState="playing",d()},L=()=>{e.playbackState="paused",d()},N=()=>d();m.addEventListener("play",P),m.addEventListener("pause",L),m.addEventListener("ratechange",N),c.push(()=>m.removeEventListener("play",P),()=>m.removeEventListener("pause",L),()=>m.removeEventListener("ratechange",N)),l=setInterval(d,5e3)})},commitHash:"3508814d14e0f0c3c018fc229dc888e692d8e007",coreVersion:"2.10.7"};return t=t.plugin})()); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["video/mpris"]=t():e["video/mpris"]=t()}(globalThis,()=>(()=>{"use strict";var e={d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{plugin:()=>l});const a=coreApis.observer,n=coreApis.componentApis.video.playerAgent,i=coreApis.componentApis.video.videoInfo,o=(e,t)=>{try{navigator.mediaSession.setActionHandler(e,t)}catch{}},r=e=>{const t=new URL(window.location.href);t.searchParams.set("p",String(e)),window.location.href=t.toString()},s=e=>{window.location.href=`https://www.bilibili.com/video/${e}`},l={name:"video.mpris",displayName:"MPRIS 媒体控制",description:"通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS):\n- 标题为视频标题(多 P 时附带分 P 标题)\n- 艺术家为 UP 主名称\n- 专辑封面为视频封面\n- 播放进度为视频进度\n- 支持快进、快退、倍速\n- 分 P 视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息",setup:()=>{if(!("mediaSession"in navigator))return;const e=navigator.mediaSession;let t=null,l=null,c=[];const d=()=>{const a=t;if(a&&!isNaN(a.duration)&&0!==a.duration)try{e.setPositionState({duration:a.duration,position:Math.min(a.currentTime,a.duration),playbackRate:a.playbackRate})}catch{}};(0,a.videoChange)(async a=>{let{aid:p,cid:u}=a;null!==l&&(clearInterval(l),l=null),c.forEach(e=>e()),c=[];const m=await n.playerAgent.query.video.element();if(t=m,!m)return;let f;try{f=await new i.VideoInfo(String(p)).fetchInfo()}catch{return}const v=Number(u),{pages:g}=f,y=g.length>1,b=y?g.findIndex(e=>e.cid===v):-1,h=y&&b>=0?`${f.title} P${g[b].pageNumber} ${g[b].title}`:f.title,k=f.ugcSeason?.episodes??[],w=k.length>0?k.findIndex(e=>e.cid===v):-1;e.metadata=new MediaMetadata({title:h,artist:f.up.name,album:f.title,artwork:f.coverUrl?[{src:f.coverUrl,sizes:"480x270",type:"image/jpeg"}]:[]}),e.playbackState=m.paused?"paused":"playing",o("play",()=>{m.play()}),o("pause",()=>{m.pause()}),o("seekbackward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(-t),d()}),o("seekforward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(t),d()}),o("seekto",e=>{void 0!==e.seekTime&&(n.playerAgent.seek(e.seekTime),d())});let A=null,S=null;if(y&&b>0?A=()=>r(g[b-1].pageNumber):w>0&&(A=()=>s(k[w-1].bvid)),y&&br(g[b+1].pageNumber);else if(w>=0&&ws(k[w+1].bvid);else if(!y&&0===k.length){const e=document.querySelector(".bpx-player-ctrl-next");null===e||e.classList.contains("bpx-state-disabled")||(S=()=>e.click())}o("previoustrack",A),o("nexttrack",S);const E=()=>d();m.readyState>=HTMLMediaElement.HAVE_METADATA?E():(m.addEventListener("loadedmetadata",E,{once:!0}),c.push(()=>m.removeEventListener("loadedmetadata",E)));const P=()=>{e.playbackState="playing",d()},x=()=>{e.playbackState="paused",d()},L=()=>d();m.addEventListener("play",P),m.addEventListener("pause",x),m.addEventListener("ratechange",L),c.push(()=>m.removeEventListener("play",P),()=>m.removeEventListener("pause",x),()=>m.removeEventListener("ratechange",L)),l=setInterval(d,5e3)})},commitHash:"feaafc336e0751aa4eaae70eacfd36c0932d31ce",coreVersion:"2.10.7"};return t=t.plugin})()); \ No newline at end of file diff --git a/registry/lib/plugins/video/mpris/index.ts b/registry/lib/plugins/video/mpris/index.ts index 89acce4137..fcf2d06a13 100644 --- a/registry/lib/plugins/video/mpris/index.ts +++ b/registry/lib/plugins/video/mpris/index.ts @@ -1,23 +1,10 @@ import type { PluginMetadata } from '@/plugins/plugin' import { videoChange } from '@/core/observer' import { playerAgent } from '@/components/video/player-agent' -import { getJsonWithCredentials } from '@/core/ajax' +import { VideoInfo } from '@/components/video/video-info' const POSITION_UPDATE_INTERVAL_MS = 5000 -interface PageInfo { - cid: number - title: string - pageNumber: number -} - -interface SeasonEpisode { - aid: string - bvid: string - cid: number - title: string -} - const setOrClearAction = ( action: MediaSessionAction, handler: MediaSessionActionHandler | null, @@ -97,50 +84,40 @@ export const plugin: PluginMetadata = { return } - // Fetch video info from API - const json = await getJsonWithCredentials( - `https://api.bilibili.com/x/web-interface/view?aid=${aid}`, - ) - if (json.code !== 0) { + // Use VideoInfo API to retrieve all video metadata in one call + let info: VideoInfo + try { + info = await new VideoInfo(String(aid)).fetchInfo() + } catch { return } - const { data } = json - - const videoTitle: string = data.title ?? '' - const upName: string = data.owner?.name ?? '' - const coverUrl: string = (data.pic ?? '').replace('http:', 'https:') - - // Multi-page info (分P) - const pages: PageInfo[] = (data.pages ?? []).map((p: Record) => ({ - cid: Number(p.cid), - title: String(p.part ?? ''), - pageNumber: Number(p.page), - })) + const cidNum = Number(cid) - const currentPageIdx = pages.length > 1 ? pages.findIndex(p => p.cid === cidNum) : -1 + + // --- 分P (multi-page) --- + const { pages } = info + const isMultiPage = pages.length > 1 + const currentPageIdx = isMultiPage ? pages.findIndex(p => p.cid === cidNum) : -1 // Display title: append part title when multi-page const displayTitle = - pages.length > 1 && currentPageIdx >= 0 - ? `${videoTitle} P${pages[currentPageIdx].pageNumber} ${pages[currentPageIdx].title}` - : videoTitle - - // Collection (ugc_season) episode list - const seasonSections: { episodes: SeasonEpisode[] }[] = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (data as any).ugc_season?.sections ?? [] - const allSeasonEpisodes: SeasonEpisode[] = seasonSections.flatMap(s => s.episodes ?? []) + isMultiPage && currentPageIdx >= 0 + ? `${info.title} P${pages[currentPageIdx].pageNumber} ${pages[currentPageIdx].title}` + : info.title + + // --- 合集 (ugc_season) --- + const seasonEpisodes = info.ugcSeason?.episodes ?? [] const currentSeasonIdx = - allSeasonEpisodes.length > 0 - ? allSeasonEpisodes.findIndex(e => Number(e.cid) === cidNum) - : -1 + seasonEpisodes.length > 0 ? seasonEpisodes.findIndex(e => e.cid === cidNum) : -1 - // Set media metadata + // Set media metadata via VideoInfo fields ms.metadata = new MediaMetadata({ title: displayTitle, - artist: upName, - album: videoTitle, - artwork: coverUrl ? [{ src: coverUrl, sizes: '480x270', type: 'image/jpeg' }] : [], + artist: info.up.name, + album: info.title, + artwork: info.coverUrl + ? [{ src: info.coverUrl, sizes: '480x270', type: 'image/jpeg' }] + : [], }) ms.playbackState = videoEl.paused ? 'paused' : 'playing' @@ -168,39 +145,41 @@ export const plugin: PluginMetadata = { } }) - // Previous / next track navigation - if (pages.length > 1 && currentPageIdx >= 0) { - // Case 1: multi-page video (分P) - const hasPrev = currentPageIdx > 0 - const hasNext = currentPageIdx < pages.length - 1 - setOrClearAction( - 'previoustrack', - hasPrev ? () => navigateToPage(pages[currentPageIdx - 1].pageNumber) : null, - ) - setOrClearAction( - 'nexttrack', - hasNext ? () => navigateToPage(pages[currentPageIdx + 1].pageNumber) : null, - ) - } else if (allSeasonEpisodes.length > 0 && currentSeasonIdx >= 0) { - // Case 2: collection (ugc_season 合集) - const hasPrev = currentSeasonIdx > 0 - const hasNext = currentSeasonIdx < allSeasonEpisodes.length - 1 - setOrClearAction( - 'previoustrack', - hasPrev ? () => navigateToBvid(allSeasonEpisodes[currentSeasonIdx - 1].bvid) : null, - ) - setOrClearAction( - 'nexttrack', - hasNext ? () => navigateToBvid(allSeasonEpisodes[currentSeasonIdx + 1].bvid) : null, - ) - } else { - // Case 3: single video or bangumi — delegate to player's built-in next button - setOrClearAction('previoustrack', null) + // --- Previous / next track --- + // A video can be BOTH in a collection (合集) AND have multiple parts (分P). + // Navigation priority: move between parts first; when at the boundary of + // the current video's pages, move to the adjacent episode in the collection. + // If neither applies, fall back to the player's built-in next button. + + // Compute prev/next handlers independently so both dimensions can contribute. + let prevHandler: MediaSessionActionHandler | null = null + let nextHandler: MediaSessionActionHandler | null = null + + if (isMultiPage && currentPageIdx > 0) { + // There is a previous part within the current video + prevHandler = () => navigateToPage(pages[currentPageIdx - 1].pageNumber) + } else if (currentSeasonIdx > 0) { + // At first part (or single-page) of a collection episode — go to previous episode + prevHandler = () => navigateToBvid(seasonEpisodes[currentSeasonIdx - 1].bvid) + } + + if (isMultiPage && currentPageIdx < pages.length - 1) { + // There is a next part within the current video + nextHandler = () => navigateToPage(pages[currentPageIdx + 1].pageNumber) + } else if (currentSeasonIdx >= 0 && currentSeasonIdx < seasonEpisodes.length - 1) { + // At last part (or single-page) of a collection episode — go to next episode + nextHandler = () => navigateToBvid(seasonEpisodes[currentSeasonIdx + 1].bvid) + } else if (!isMultiPage && seasonEpisodes.length === 0) { + // Single video or bangumi — delegate to player's built-in next button const nextBtn = document.querySelector('.bpx-player-ctrl-next') as HTMLElement | null - const hasPlayerNext = nextBtn !== null && !nextBtn.classList.contains('bpx-state-disabled') - setOrClearAction('nexttrack', hasPlayerNext ? () => nextBtn.click() : null) + if (nextBtn !== null && !nextBtn.classList.contains('bpx-state-disabled')) { + nextHandler = () => nextBtn.click() + } } + setOrClearAction('previoustrack', prevHandler) + setOrClearAction('nexttrack', nextHandler) + // Position state — wait for metadata if needed const initPosition = () => updatePositionState() if (videoEl.readyState >= HTMLMediaElement.HAVE_METADATA) { diff --git a/src/components/video/video-info.ts b/src/components/video/video-info.ts index 1357744eb8..51b1299eca 100644 --- a/src/components/video/video-info.ts +++ b/src/components/video/video-info.ts @@ -12,6 +12,26 @@ export interface VideoPageInfo { pageNumber: number } +export interface UgcSeasonEpisodeInfo { + aid: string + bvid: string + cid: number + title: string +} + +export interface UgcSeasonSectionInfo { + title: string + episodes: UgcSeasonEpisodeInfo[] +} + +export interface UgcSeasonInfo { + seasonId: number + title: string + sections: UgcSeasonSectionInfo[] + /** All episodes flattened across sections, in order */ + episodes: UgcSeasonEpisodeInfo[] +} + export interface VideoStat { view: number like: number @@ -39,6 +59,7 @@ export class VideoInfo { description: string up: UpInfo pages: VideoPageInfo[] + ugcSeason?: UgcSeasonInfo redirectUrl?: string stat: VideoStat @@ -85,6 +106,26 @@ export class VideoInfo { title: it.part, pageNumber: it.page, })) + const ugcSeasonData = data.ugc_season + if (ugcSeasonData) { + const sections: UgcSeasonSectionInfo[] = (ugcSeasonData.sections ?? []).map((s: any) => ({ + title: s.title ?? '', + episodes: (s.episodes ?? []).map((e: any) => ({ + aid: String(e.aid), + bvid: String(e.bvid), + cid: Number(e.cid), + title: String(e.title ?? ''), + })), + })) + this.ugcSeason = { + seasonId: ugcSeasonData.id, + title: ugcSeasonData.title ?? '', + sections, + episodes: sections.flatMap(s => s.episodes), + } + } else { + this.ugcSeason = undefined + } this.redirectUrl = data.redirect_url this.stat = data.stat return this From 9ab7b0e6812d409a5bcf230c4bea03bac3b5466b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 2 May 2026 11:10:59 +0000 Subject: [PATCH 4/4] Fix description spacing and add comment on next-button fallback Agent-Logs-Url: https://github.com/BlockG-ws/b23-evolved/sessions/062bb310-df3b-4b33-8663-34ca6e9695bc Co-authored-by: GrassBlock1 <46253950+GrassBlock1@users.noreply.github.com> --- registry/dist/plugins/video/mpris.js | 2 +- registry/lib/plugins/video/mpris/index.ts | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/registry/dist/plugins/video/mpris.js b/registry/dist/plugins/video/mpris.js index f678fbcb74..62a39243ca 100644 --- a/registry/dist/plugins/video/mpris.js +++ b/registry/dist/plugins/video/mpris.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["video/mpris"]=t():e["video/mpris"]=t()}(globalThis,()=>(()=>{"use strict";var e={d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{plugin:()=>l});const a=coreApis.observer,n=coreApis.componentApis.video.playerAgent,i=coreApis.componentApis.video.videoInfo,o=(e,t)=>{try{navigator.mediaSession.setActionHandler(e,t)}catch{}},r=e=>{const t=new URL(window.location.href);t.searchParams.set("p",String(e)),window.location.href=t.toString()},s=e=>{window.location.href=`https://www.bilibili.com/video/${e}`},l={name:"video.mpris",displayName:"MPRIS 媒体控制",description:"通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS):\n- 标题为视频标题(多 P 时附带分 P 标题)\n- 艺术家为 UP 主名称\n- 专辑封面为视频封面\n- 播放进度为视频进度\n- 支持快进、快退、倍速\n- 分 P 视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息",setup:()=>{if(!("mediaSession"in navigator))return;const e=navigator.mediaSession;let t=null,l=null,c=[];const d=()=>{const a=t;if(a&&!isNaN(a.duration)&&0!==a.duration)try{e.setPositionState({duration:a.duration,position:Math.min(a.currentTime,a.duration),playbackRate:a.playbackRate})}catch{}};(0,a.videoChange)(async a=>{let{aid:p,cid:u}=a;null!==l&&(clearInterval(l),l=null),c.forEach(e=>e()),c=[];const m=await n.playerAgent.query.video.element();if(t=m,!m)return;let f;try{f=await new i.VideoInfo(String(p)).fetchInfo()}catch{return}const v=Number(u),{pages:g}=f,y=g.length>1,b=y?g.findIndex(e=>e.cid===v):-1,h=y&&b>=0?`${f.title} P${g[b].pageNumber} ${g[b].title}`:f.title,k=f.ugcSeason?.episodes??[],w=k.length>0?k.findIndex(e=>e.cid===v):-1;e.metadata=new MediaMetadata({title:h,artist:f.up.name,album:f.title,artwork:f.coverUrl?[{src:f.coverUrl,sizes:"480x270",type:"image/jpeg"}]:[]}),e.playbackState=m.paused?"paused":"playing",o("play",()=>{m.play()}),o("pause",()=>{m.pause()}),o("seekbackward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(-t),d()}),o("seekforward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(t),d()}),o("seekto",e=>{void 0!==e.seekTime&&(n.playerAgent.seek(e.seekTime),d())});let A=null,S=null;if(y&&b>0?A=()=>r(g[b-1].pageNumber):w>0&&(A=()=>s(k[w-1].bvid)),y&&br(g[b+1].pageNumber);else if(w>=0&&ws(k[w+1].bvid);else if(!y&&0===k.length){const e=document.querySelector(".bpx-player-ctrl-next");null===e||e.classList.contains("bpx-state-disabled")||(S=()=>e.click())}o("previoustrack",A),o("nexttrack",S);const E=()=>d();m.readyState>=HTMLMediaElement.HAVE_METADATA?E():(m.addEventListener("loadedmetadata",E,{once:!0}),c.push(()=>m.removeEventListener("loadedmetadata",E)));const P=()=>{e.playbackState="playing",d()},x=()=>{e.playbackState="paused",d()},L=()=>d();m.addEventListener("play",P),m.addEventListener("pause",x),m.addEventListener("ratechange",L),c.push(()=>m.removeEventListener("play",P),()=>m.removeEventListener("pause",x),()=>m.removeEventListener("ratechange",L)),l=setInterval(d,5e3)})},commitHash:"feaafc336e0751aa4eaae70eacfd36c0932d31ce",coreVersion:"2.10.7"};return t=t.plugin})()); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports["video/mpris"]=t():e["video/mpris"]=t()}(globalThis,()=>(()=>{"use strict";var e={d:(t,a)=>{for(var n in a)e.o(a,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:a[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)},t={};e.d(t,{plugin:()=>l});const a=coreApis.observer,n=coreApis.componentApis.video.playerAgent,i=coreApis.componentApis.video.videoInfo,o=(e,t)=>{try{navigator.mediaSession.setActionHandler(e,t)}catch{}},r=e=>{const t=new URL(window.location.href);t.searchParams.set("p",String(e)),window.location.href=t.toString()},s=e=>{window.location.href=`https://www.bilibili.com/video/${e}`},l={name:"video.mpris",displayName:"MPRIS 媒体控制",description:"通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS):\n- 标题为视频标题(多P时附带分P标题)\n- 艺术家为 UP 主名称\n- 专辑封面为视频封面\n- 播放进度为视频进度\n- 支持快进、快退、倍速\n- 分P视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息",setup:()=>{if(!("mediaSession"in navigator))return;const e=navigator.mediaSession;let t=null,l=null,c=[];const d=()=>{const a=t;if(a&&!isNaN(a.duration)&&0!==a.duration)try{e.setPositionState({duration:a.duration,position:Math.min(a.currentTime,a.duration),playbackRate:a.playbackRate})}catch{}};(0,a.videoChange)(async a=>{let{aid:p,cid:u}=a;null!==l&&(clearInterval(l),l=null),c.forEach(e=>e()),c=[];const m=await n.playerAgent.query.video.element();if(t=m,!m)return;let f;try{f=await new i.VideoInfo(String(p)).fetchInfo()}catch{return}const v=Number(u),{pages:g}=f,y=g.length>1,b=y?g.findIndex(e=>e.cid===v):-1,h=y&&b>=0?`${f.title} P${g[b].pageNumber} ${g[b].title}`:f.title,k=f.ugcSeason?.episodes??[],w=k.length>0?k.findIndex(e=>e.cid===v):-1;e.metadata=new MediaMetadata({title:h,artist:f.up.name,album:f.title,artwork:f.coverUrl?[{src:f.coverUrl,sizes:"480x270",type:"image/jpeg"}]:[]}),e.playbackState=m.paused?"paused":"playing",o("play",()=>{m.play()}),o("pause",()=>{m.pause()}),o("seekbackward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(-t),d()}),o("seekforward",e=>{const t=e.seekOffset??10;n.playerAgent.changeTime(t),d()}),o("seekto",e=>{void 0!==e.seekTime&&(n.playerAgent.seek(e.seekTime),d())});let A=null,S=null;if(y&&b>0?A=()=>r(g[b-1].pageNumber):w>0&&(A=()=>s(k[w-1].bvid)),y&&br(g[b+1].pageNumber);else if(w>=0&&ws(k[w+1].bvid);else if(!y&&0===k.length){const e=document.querySelector(".bpx-player-ctrl-next");null===e||e.classList.contains("bpx-state-disabled")||(S=()=>e.click())}o("previoustrack",A),o("nexttrack",S);const E=()=>d();m.readyState>=HTMLMediaElement.HAVE_METADATA?E():(m.addEventListener("loadedmetadata",E,{once:!0}),c.push(()=>m.removeEventListener("loadedmetadata",E)));const P=()=>{e.playbackState="playing",d()},x=()=>{e.playbackState="paused",d()},L=()=>d();m.addEventListener("play",P),m.addEventListener("pause",x),m.addEventListener("ratechange",L),c.push(()=>m.removeEventListener("play",P),()=>m.removeEventListener("pause",x),()=>m.removeEventListener("ratechange",L)),l=setInterval(d,5e3)})},commitHash:"84a082518e541aa44fa0999a9eaab0af7e8f8eee",coreVersion:"2.10.7"};return t=t.plugin})()); \ No newline at end of file diff --git a/registry/lib/plugins/video/mpris/index.ts b/registry/lib/plugins/video/mpris/index.ts index fcf2d06a13..32baee3298 100644 --- a/registry/lib/plugins/video/mpris/index.ts +++ b/registry/lib/plugins/video/mpris/index.ts @@ -30,12 +30,12 @@ export const plugin: PluginMetadata = { name: 'video.mpris', displayName: 'MPRIS 媒体控制', description: `通过 Media Session API 将 Bilibili 视频集成到系统媒体控制(MPRIS): -- 标题为视频标题(多 P 时附带分 P 标题) +- 标题为视频标题(多P时附带分P标题) - 艺术家为 UP 主名称 - 专辑封面为视频封面 - 播放进度为视频进度 - 支持快进、快退、倍速 -- 分 P 视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息`, +- 分P视频/合集支持上一曲和下一曲切换,并在切换视频时自动更新以上信息`, setup: () => { if (!('mediaSession' in navigator)) { return @@ -170,7 +170,9 @@ export const plugin: PluginMetadata = { // At last part (or single-page) of a collection episode — go to next episode nextHandler = () => navigateToBvid(seasonEpisodes[currentSeasonIdx + 1].bvid) } else if (!isMultiPage && seasonEpisodes.length === 0) { - // Single video or bangumi — delegate to player's built-in next button + // Single video or bangumi — delegate to player's built-in next button if available. + // The selector targets the bpx player's next-video button; when absent or disabled + // the handler is left as null (no nexttrack action registered), which is intentional. const nextBtn = document.querySelector('.bpx-player-ctrl-next') as HTMLElement | null if (nextBtn !== null && !nextBtn.classList.contains('bpx-state-disabled')) { nextHandler = () => nextBtn.click()