From f3d33609d54b86ee7b3fdca7c5e1cce5c8377d6a Mon Sep 17 00:00:00 2001 From: magicdawn Date: Mon, 30 Dec 2024 22:17:11 +0800 Subject: [PATCH] feat: impl settings.videoCard.__internal.useLargePreview --- src/components/VideoCard/card.service.ts | 36 +-- .../child-components/LargePreviewImage.tsx | 301 +++++++++--------- src/components/VideoCard/index.tsx | 35 +- .../VideoCard/process/normalize.tsx | 12 + .../VideoCard/use/usePreviewAnimation.tsx | 22 +- src/modules/bilibili/video/play-url-types.ts | 117 +++++++ src/modules/bilibili/video/play-url.ts | 68 ++++ src/modules/bilibili/video/video-detail.ts | 5 + src/modules/settings/index.ts | 5 + 9 files changed, 429 insertions(+), 172 deletions(-) create mode 100644 src/modules/bilibili/video/play-url-types.ts create mode 100644 src/modules/bilibili/video/play-url.ts diff --git a/src/components/VideoCard/card.service.ts b/src/components/VideoCard/card.service.ts index 1aee31b8..667538b4 100644 --- a/src/components/VideoCard/card.service.ts +++ b/src/components/VideoCard/card.service.ts @@ -1,5 +1,7 @@ -import { HOST_APP, OPERATION_FAIL_MSG, appWarn } from '$common' -import type { AppRecItem, DmJson, PvideoJson } from '$define' +import { HOST_APP, OPERATION_FAIL_MSG, appLog, appWarn } from '$common' +import type { AppRecItem, PvideoJson } from '$define' +import { getVideoPlayUrl } from '$modules/bilibili/video/play-url' +import { getVideoCid } from '$modules/bilibili/video/video-detail' import { settings } from '$modules/settings' import { gmrequest, isWebApiSuccess, request } from '$request' import { getCsrfToken } from '$utility/cookie' @@ -45,27 +47,14 @@ export function isVideoshotJsonCacheable(json: PvideoJson) { } } -// dm -export async function dm(aid: string) { - // 暂时没有支持弹幕预览, 不调用该接口 - return [] - - const res = await request.get('/x/v2/dm/ajax', { params: { aid } }) - const json = res.data as DmJson - - // TODO: process errors - - return json.data -} - const cache = new QuickLRU({ maxSize: 1_0000 }) export type VideoData = { - videoshotJson: PvideoJson - // dmJson?: DmJson + videoshotJson?: PvideoJson + playUrl?: string } -export async function fetchVideoData(bvid: string): Promise { +export async function fetchVideoData(bvid: string, cid?: number): Promise { // cache:lookup if (cache.has(bvid)) { const cached = cache.get(bvid) @@ -84,10 +73,17 @@ export async function fetchVideoData(bvid: string): Promise { } } while (retryTimes < 3) + let playUrl: string | undefined + if (settings.videoCard.__internal.useLargePreview) { + cid ??= await getVideoCid(bvid) + playUrl = await getVideoPlayUrl(bvid, cid) + appLog('playUrl: bvid=%s cid=%s %s', bvid, cid, playUrl) + } + // cache:save const cacheable = isVideoshotJsonCacheable(videoshotJson) if (cacheable) { - cache.set(bvid, { videoshotJson }) + cache.set(bvid, { videoshotJson, playUrl }) } const videoshotData = videoshotJson.data @@ -102,7 +98,7 @@ export async function fetchVideoData(bvid: string): Promise { })() } - return { videoshotJson } + return { videoshotJson, playUrl } } /** diff --git a/src/components/VideoCard/child-components/LargePreviewImage.tsx b/src/components/VideoCard/child-components/LargePreviewImage.tsx index 9e996475..bd9637fc 100644 --- a/src/components/VideoCard/child-components/LargePreviewImage.tsx +++ b/src/components/VideoCard/child-components/LargePreviewImage.tsx @@ -1,4 +1,5 @@ import { APP_CLS_CARD, appLog } from '$common' +import { colorPrimaryValue } from '$components/css-vars' import { css } from '@emotion/react' import { useEventListener } from 'ahooks' import { orderBy, throttle } from 'es-toolkit' @@ -20,155 +21,165 @@ const reverseDirection = (direction: Direction) => { } } -export function LargePreviewImage({ children }: { children?: ReactNode }) { - const ref = useRef>(null) - - const [visible, setVisible] = useState(false) - const [position, setPosition] = useState< - | { - direction: Direction - imgWidth: number - imgHeight: number - imgPosX: number - imgPosY: number +export const LargePreview = forwardRef, { children?: ReactNode }>( + ({ children }, forwardedRef) => { + const ref = useRef>(null) + + const [visible, setVisible] = useState(false) + const [position, setPosition] = useState< + | { + direction: Direction + elWidth: number + elHeight: number + elPosX: number + elPosY: number + } + | undefined + >(undefined) + + const calculatePostion = useMemoizedFn(() => { + const card = ref.current?.closest('.' + APP_CLS_CARD) + if (!card) return + + const viewportWidth = document.documentElement.clientWidth + const viewportHeight = document.documentElement.clientHeight + const cardRect = card.getBoundingClientRect() + + const possibleBoundingBox: Record = { + top: { x: 0, y: 0, width: viewportWidth, height: cardRect.top }, + bottom: { + x: 0, + y: cardRect.bottom, + width: viewportWidth, + height: viewportHeight - cardRect.bottom, + }, + left: { x: 0, y: 0, width: cardRect.left, height: viewportHeight }, + right: { + x: cardRect.right, + y: 0, + width: viewportWidth - cardRect.right, + height: viewportHeight, + }, } - | undefined - >(undefined) - - const calculatePostion = useMemoizedFn(() => { - const card = ref.current?.closest('.' + APP_CLS_CARD) - if (!card) return - - const viewportWidth = document.documentElement.clientWidth - const viewportHeight = document.documentElement.clientHeight - const cardRect = card.getBoundingClientRect() - - const possibleBoundingBox: Record = { - top: { x: 0, y: 0, width: viewportWidth, height: cardRect.top }, - bottom: { - x: 0, - y: cardRect.bottom, - width: viewportWidth, - height: viewportHeight - cardRect.bottom, - }, - left: { x: 0, y: 0, width: cardRect.left, height: viewportHeight }, - right: { - x: cardRect.right, - y: 0, - width: viewportWidth - cardRect.right, - height: viewportHeight, - }, - } - - const aspectRatio = 16 / 9 - const getScaleInBox = (bbox: Bbox) => { - const w = aspectRatio - const h = 1 - const scale = Math.min(bbox.width / w, bbox.height / h) - return scale - } - - const picked = orderBy( - Object.entries(possibleBoundingBox).map(([direction, bbox]) => ({ - direction: direction as Direction, - bbox, - scale: getScaleInBox(bbox), - })), - ['scale'], - ['desc'], - )[0] - - const { direction, bbox, scale } = picked - appLog('picked', picked) - - const imgHeight = Math.floor(scale) - 40 /* padding */ - const imgWidth = Math.floor(imgHeight * aspectRatio) - - let imgPosX = 0 - let imgPosY = 0 - const visualPadding = 20 - - const fixX = () => { - if (imgPosX < visualPadding) { - imgPosX = visualPadding - return + + const aspectRatio = 16 / 9 + const getScaleInBox = (bbox: Bbox) => { + const w = aspectRatio + const h = 1 + const scale = Math.min(bbox.width / w, bbox.height / h) + return scale } - if (imgPosX + imgWidth > viewportWidth - visualPadding) { - imgPosX = viewportWidth - visualPadding - imgWidth - return + + const picked = orderBy( + Object.entries(possibleBoundingBox).map(([direction, bbox]) => ({ + direction: direction as Direction, + bbox, + scale: getScaleInBox(bbox), + })), + ['scale'], + ['desc'], + )[0] + + const { direction, bbox, scale } = picked + appLog('picked', picked) + + const elHeight = Math.floor(scale) - 40 /* padding */ + const elWidth = Math.floor(elHeight * aspectRatio) + + let elPosX = 0 + let elPosY = 0 + const visualPadding = 20 + + const fixX = () => { + if (elPosX < visualPadding) { + elPosX = visualPadding + return + } + if (elPosX + elWidth > viewportWidth - visualPadding) { + elPosX = viewportWidth - visualPadding - elWidth + return + } } - } - const fixY = () => { - if (imgPosY < visualPadding) { - imgPosY = visualPadding - return + const fixY = () => { + if (elPosY < visualPadding) { + elPosY = visualPadding + return + } + if (elPosY + elHeight > viewportHeight - visualPadding) { + elPosY = viewportHeight - visualPadding - elHeight + return + } } - if (imgPosY + imgHeight > viewportHeight - visualPadding) { - imgPosY = viewportHeight - visualPadding - imgHeight - return + + switch (direction) { + case 'top': + elPosX = cardRect.x + cardRect.width / 2 - elWidth / 2 + elPosY = cardRect.top - visualPadding - elHeight + fixX() + break + case 'bottom': + elPosX = cardRect.x + cardRect.width / 2 - elWidth / 2 + elPosY = cardRect.bottom + visualPadding + fixX() + break + case 'right': + elPosX = cardRect.right + visualPadding + elPosY = cardRect.y + cardRect.height / 2 - elHeight / 2 + fixY() + break + case 'left': + elPosX = cardRect.left - visualPadding - elWidth + elPosY = cardRect.y + cardRect.height / 2 - elHeight / 2 + fixY() + break + } + + setVisible(true) + setPosition({ direction, elWidth, elHeight, elPosX, elPosY }) + }) + + const calculatePostionThrottled = useMemo( + () => throttle(calculatePostion, 100), + [calculatePostion], + ) + + useMount(() => { + if (forwardedRef) { + typeof forwardedRef === 'function' + ? forwardedRef(ref.current) + : (forwardedRef.current = ref.current) } - } - - switch (direction) { - case 'top': - imgPosX = cardRect.x + cardRect.width / 2 - imgWidth / 2 - imgPosY = cardRect.top - visualPadding - imgHeight - fixX() - break - case 'bottom': - imgPosX = cardRect.x + cardRect.width / 2 - imgWidth / 2 - imgPosY = cardRect.bottom + visualPadding - fixX() - break - case 'right': - imgPosX = cardRect.right + visualPadding - imgPosY = cardRect.y + cardRect.height / 2 - imgHeight / 2 - fixY() - break - case 'left': - imgPosX = cardRect.left - visualPadding - imgWidth - imgPosY = cardRect.y + cardRect.height / 2 - imgHeight / 2 - fixY() - break - } - - setVisible(true) - setPosition({ direction, imgWidth, imgHeight, imgPosX, imgPosY }) - }) - - const calculatePostionThrottled = useMemo( - () => throttle(calculatePostion, 100), - [calculatePostion], - ) - - useMount(() => { - calculatePostion() - }) - - useEventListener('resize', calculatePostionThrottled, { target: window }) - useEventListener('scroll', calculatePostionThrottled, { target: window }) - - return ( -
- {visible && children} -
- ) -} + position && + css` + position: fixed; + z-index: 90000; + width: ${position.elWidth}px; + height: ${position.elHeight}px; + top: ${position.elPosY}px; + left: ${position.elPosX}px; + `, + ]} + > + {visible && children} + + ) + }, +) diff --git a/src/components/VideoCard/index.tsx b/src/components/VideoCard/index.tsx index d265b504..35fd712a 100644 --- a/src/components/VideoCard/index.tsx +++ b/src/components/VideoCard/index.tsx @@ -29,13 +29,14 @@ import { isFirefox, isSafari } from '$ua' import { antNotification } from '$utility/antd' import type { CssProp } from '$utility/type' import { css } from '@emotion/react' -import { useLockFn } from 'ahooks' +import { useHover, useLockFn } from 'ahooks' import { Dropdown } from 'antd' -import type { CSSProperties, MouseEventHandler, ReactNode } from 'react' +import type { ComponentRef, CSSProperties, MouseEventHandler, ReactNode } from 'react' import { videoCardBorderRadiusValue } from '../css-vars' import { useInNormalCardCss } from './card-border-css' import type { VideoData } from './card.service' import { fetchVideoData, isVideoshotDataValid } from './card.service' +import { LargePreview } from './child-components/LargePreviewImage' import { SimplePregressBar } from './child-components/PreviewImage' import { VideoCardActionStyle } from './child-components/VideoCardActions' import { VideoCardBottom } from './child-components/VideoCardBottom' @@ -170,6 +171,9 @@ const VideoCardInner = memo(function VideoCardInner({ style: { videoCard: { useBorder: cardUseBorder, useBorderOnlyOnHover: cardUseBorderOnlyOnHover }, }, + videoCard: { + __internal: { useLargePreview }, + }, } = useSettingsSnapshot() const authed = !!accessKey @@ -177,6 +181,7 @@ const VideoCardInner = memo(function VideoCardInner({ // video avid, bvid, + cid, goto, href, title, @@ -200,18 +205,17 @@ const VideoCardInner = memo(function VideoCardInner({ } const videoDataBox = useRefStateBox(null) - const videoshotData = videoDataBox.state?.videoshotJson?.data const tryFetchVideoData = useLockFn(async () => { if (!bvid) return // no bvid if (!bvid.startsWith('BV')) return // bvid invalid if (goto !== 'av') return // scrrenshot only for video if (isVideoshotDataValid(videoDataBox.value?.videoshotJson?.data)) return // already fetched - const data = await fetchVideoData(bvid) + const data = await fetchVideoData(bvid, cid) videoDataBox.set(data) if (!isWebApiSuccess(data.videoshotJson)) { - warnNoPreview(data.videoshotJson) + warnNoPreview(data.videoshotJson!) } }) @@ -243,6 +247,7 @@ const VideoCardInner = memo(function VideoCardInner({ // flag isHovering, isHoveringAfterDelay, + isHoveringDeferred, // el previewImageRef, previewImageEl, @@ -536,14 +541,32 @@ const VideoCardInner = memo(function VideoCardInner({ ) + const largePreviewRef = useRef>(null) + const isHoveringOnLargePreview = useHover(largePreviewRef) const extraContent = ( <> - {/* a large preview */} + {/* large preview + PreviewImage */} {/* {shouldShowPreview && previewImgProps && ( )} */} + {useLargePreview && + (isHoveringDeferred || isHoveringOnLargePreview) && + videoDataBox.state?.playUrl && ( + + + )} ) diff --git a/src/components/VideoCard/process/normalize.tsx b/src/components/VideoCard/process/normalize.tsx index 4c5b7abc..6cd64c0b 100644 --- a/src/components/VideoCard/process/normalize.tsx +++ b/src/components/VideoCard/process/normalize.tsx @@ -57,6 +57,7 @@ export interface IVideoCardData { // video avid?: string bvid?: string + cid?: number goto: string href: string @@ -158,6 +159,7 @@ function apiAndroidAppAdapter(item: AndroidAppRecItemExtend): IVideoCardData { const avid = item.param const bvid = BvCode.av2bv(Number(item.param)) + const cid = item.player_args?.cid const href = (() => { // valid uri @@ -192,6 +194,7 @@ function apiAndroidAppAdapter(item: AndroidAppRecItemExtend): IVideoCardData { // video avid, bvid, + cid, goto: item.goto, href, title: item.title, @@ -262,6 +265,7 @@ function apiIpadAppAdapter(item: IpadAppRecItemExtend): IVideoCardData { const avid = item.param const bvid = item.bvid || BvCode.av2bv(Number(item.param)) + const cid = item.player_args?.cid const href = (() => { // valid uri @@ -313,6 +317,7 @@ function apiIpadAppAdapter(item: IpadAppRecItemExtend): IVideoCardData { // video avid, bvid, + cid, goto: item.goto, href, title: item.title, @@ -347,6 +352,7 @@ function apiPcAdapter(item: PcRecItemExtend): IVideoCardData { // video avid: String(item.id), bvid: item.bvid, + cid: item.cid, goto: item.goto, href: item.goto === 'av' ? `/video/${item.bvid}/` : item.uri, title: item.title, @@ -393,6 +399,7 @@ function apiDynamicAdapter(item: DynamicFeedItemExtend): IVideoCardData { // video avid: v.aid, bvid: v.bvid, + // cid: v. goto: 'av', href: `/video/${v.bvid}/`, title: v.title, @@ -439,6 +446,7 @@ function apiWatchlaterAdapter(item: WatchlaterItemExtend): IVideoCardData { // video avid: String(item.aid), bvid: item.bvid, + cid: item.cid, goto: 'av', href: `https://www.bilibili.com/list/watchlater?bvid=${item.bvid}&oid=${item.aid}`, title, @@ -510,6 +518,7 @@ function apiFavAdapter(item: FavItemExtend): IVideoCardData { // video avid: String(item.id), bvid: item.bvid, + // cid: item. goto: 'av', href: `/video/${item.bvid}/`, title: `【${belongsToTitle}】· ${item.title}`, @@ -549,6 +558,7 @@ function apiPopularGeneralAdapter(item: PopularGeneralItemExtend): IVideoCardDat // video avid: String(item.aid), bvid: item.bvid, + cid: item.cid, goto: 'av', href: `/video/${item.bvid}/`, title: item.title, @@ -583,6 +593,7 @@ function apiPopularWeeklyAdapter(item: PopularWeeklyItemExtend): IVideoCardData // video avid: String(item.aid), bvid: item.bvid, + cid: item.cid, goto: 'av', href: `/video/${item.bvid}/`, title: item.title, @@ -647,6 +658,7 @@ function apiRankingAdapter(item: RankingItemExtend): IVideoCardData { // video avid: String(item.aid), bvid: item.bvid, + cid: item.cid, goto: 'av', href: `/video/${item.bvid}/`, title: item.title, diff --git a/src/components/VideoCard/use/usePreviewAnimation.tsx b/src/components/VideoCard/use/usePreviewAnimation.tsx index 007bfe0c..fe6ccfac 100644 --- a/src/components/VideoCard/use/usePreviewAnimation.tsx +++ b/src/components/VideoCard/use/usePreviewAnimation.tsx @@ -39,7 +39,7 @@ export function usePreviewAnimation({ videoPreviewWrapperRef: RefObject }) { const hasVideoData = useMemoizedFn(() => { - const data = videoDataBox.value?.videoshotJson.data + const data = videoDataBox.value?.videoshotJson?.data return Boolean(data?.index?.length && data?.image?.length) }) @@ -55,6 +55,10 @@ export function usePreviewAnimation({ const isHoveringAfterDelayBox = useRefStateBox(false) const startByHoverBox = useRefBox(false) + const isHoveringDeferredBox = useRefStateBox(false) + const isHoveringDeferredOnTimer = useRef>(undefined) + const isHoveringDeferredOffTimer = useRef>(undefined) + // mouseenter cursor state const [mouseProgress, setMouseProgress] = useState(undefined) const updateMouseProgress = (e: MouseEvent) => { @@ -74,6 +78,13 @@ export function usePreviewAnimation({ isHoveringBox.set(true) updateMouseProgress(e) + clearTimeout(isHoveringDeferredOnTimer.current) + clearTimeout(isHoveringDeferredOffTimer.current) + isHoveringDeferredOnTimer.current = setTimeout(() => { + if (!isHoveringBox.value) return + isHoveringDeferredBox.set(true) + }, 1000) + // fetch data const p = tryFetchVideoData() @@ -108,6 +119,12 @@ export function usePreviewAnimation({ const _mouseleaveAction = useMemoizedFn(() => { isHoveringBox.set(false) isHoveringAfterDelayBox.set(false) + + clearTimeout(isHoveringDeferredOnTimer.current) + clearTimeout(isHoveringDeferredOffTimer.current) + isHoveringDeferredOffTimer.current = setTimeout(() => { + isHoveringDeferredBox.set(false) + }, 100) // <- this is the defer }) useEventListener('mouseleave', _mouseleaveAction, { target: videoPreviewWrapperRef }) useMittOn(emitter, 'mouseenter-other-card', (srcUniqId) => { @@ -229,6 +246,7 @@ export function usePreviewAnimation({ const isHovering = isHoveringBox.state const isHoveringAfterDelay = isHoveringAfterDelayBox.state + const isHoveringDeferred = isHoveringDeferredBox.state const videoshotData = videoDataBox.state?.videoshotJson?.data const shouldShowPreview = @@ -262,8 +280,10 @@ export function usePreviewAnimation({ mouseProgress, previewProgress, previewT, + // isHovering, isHoveringAfterDelay, + isHoveringDeferred, // el shouldShowPreview, previewImageRef, diff --git a/src/modules/bilibili/video/play-url-types.ts b/src/modules/bilibili/video/play-url-types.ts new file mode 100644 index 00000000..01212d23 --- /dev/null +++ b/src/modules/bilibili/video/play-url-types.ts @@ -0,0 +1,117 @@ +export interface VideoPlayUrlJson { + code: number + message: string + ttl: number + data: Data +} + +export interface Data { + from: string + result: string + message: string + quality: number + format: string + timelength: number + accept_format: string + accept_description: string[] + accept_quality: number[] + video_codecid: number + seek_param: string + seek_type: string + dash: Dash + support_formats: SupportFormat[] + high_format: null + volume: Volume + last_play_time: number + last_play_cid: number + view_info: null +} + +export interface Dash { + duration: number + minBufferTime: number + min_buffer_time: number + video: Audio[] + audio: Audio[] + dolby: Dolby + flac: null +} + +export interface Audio { + id: number + baseUrl: string + base_url: string + backupUrl: string[] + backup_url: string[] + bandwidth: number + mimeType: MIMEType + mime_type: MIMEType + codecs: string + width: number + height: number + frameRate: FrameRate + frame_rate: FrameRate + sar: Sar + startWithSap: number + start_with_sap: number + SegmentBase: SegmentBase + segment_base: SegmentBaseClass + codecid: number +} + +export interface SegmentBase { + Initialization: string + indexRange: string +} + +export enum FrameRate { + Empty = '', + The29968 = '29.968', +} + +export enum MIMEType { + AudioMp4 = 'audio/mp4', + VideoMp4 = 'video/mp4', +} + +export enum Sar { + Empty = '', + The11 = '1:1', + The640639 = '640:639', +} + +export interface SegmentBaseClass { + initialization: string + index_range: string +} + +export interface Dolby { + type: number + audio: null +} + +export interface SupportFormat { + quality: number + format: string + new_description: string + display_desc: string + superscript: string + codecs: string[] +} + +export interface Volume { + measured_i: number + measured_lra: number + measured_tp: number + measured_threshold: number + target_offset: number + target_i: number + target_tp: number + multi_scene_args: MultiSceneArgs +} + +export interface MultiSceneArgs { + high_dynamic_target_i: string + normal_target_i: string + undersized_target_i: string +} diff --git a/src/modules/bilibili/video/play-url.ts b/src/modules/bilibili/video/play-url.ts new file mode 100644 index 00000000..ae512a1c --- /dev/null +++ b/src/modules/bilibili/video/play-url.ts @@ -0,0 +1,68 @@ +/** + * see https://socialsisteryi.github.io/bilibili-API-collect/docs/video/videostream_url.html + */ + +import { request } from '$request' +import type { VideoPlayUrlJson } from './play-url-types' + +/** + * 7 AVC 编码 8K 视频不支持该格式 + * 12 HEVC 编码 + * 13 AV1 编码 + */ +export enum ECodecId { + AVC = 7, + HEVC = 12, + AV1 = 13, +} + +/** + * 6 240P 极速 仅 MP4 格式支持,仅platform=html5时有效 + * 16 360P 流畅 + * 32 480P 清晰 + * 64 720P 高清 WEB 端默认值,B站前端需要登录才能选择,但是直接发送请求可以不登录就拿到 720P 的取流地址,无 720P 时则为 720P60 + * 74 720P60 高帧率 登录认证 + * 80 1080P 高清 TV 端与 APP 端默认值,登录认证 + * 112 1080P+ 高码率 大会员认证 + * 116 1080P60 高帧率 大会员认证 + * 120 4K 超清 需要fnval&128=128且fourk=1,大会员认证 + * 125 HDR 真彩色 仅支持 DASH 格式,需要fnval&64=64,大会员认证 + * 126 杜比视界 仅支持 DASH 格式,需要fnval&512=512,大会员认证 + * 127 8K 超高清 仅支持 DASH 格式,需要fnval&1024=1024,大会员认证 + */ +export enum EResolution { + _240p = 6, + _360p = 16, + _480p = 32, + _720p = 64, + _1080p = 80, + _4k = 120, + _8k = 127, + HDR = 125, + DolbyVision = 126, +} + +export async function getVideoPlayUrl(videoId: string | number, cid: number) { + const params: Record = { + cid, + fnver: 0, + fnval: 16, + } + + const _videoId = videoId.toString() + if (_videoId.startsWith('BV')) { + params.bvid = _videoId + } else if (/^\d+$/.test(_videoId)) { + params.avid = _videoId + } else { + throw new Error('Invalid videoId provided, must be avid | bvid') + } + + const res = await request.get('/x/player/wbi/playurl', { params }) + const json = res.data as VideoPlayUrlJson + + const pickedVideos = json.data.dash.video.filter((v) => v.id === EResolution._480p) + const hevcUrl = pickedVideos.find((x) => x.codecid === ECodecId.HEVC)?.baseUrl + const avcUrl = pickedVideos.find((x) => x.codecid === ECodecId.AVC)?.baseUrl + return hevcUrl || avcUrl +} diff --git a/src/modules/bilibili/video/video-detail.ts b/src/modules/bilibili/video/video-detail.ts index 5e938d51..d157c7ee 100644 --- a/src/modules/bilibili/video/video-detail.ts +++ b/src/modules/bilibili/video/video-detail.ts @@ -20,3 +20,8 @@ export const getVideoDetail = wrapWithIdbCache({ tableName: 'video_detail', ttl: ms('3M'), }) + +export async function getVideoCid(bvid: string) { + const detail = await getVideoDetail(bvid) + return detail.cid +} diff --git a/src/modules/settings/index.ts b/src/modules/settings/index.ts index 434b9feb..6ec7ffaa 100644 --- a/src/modules/settings/index.ts +++ b/src/modules/settings/index.ts @@ -59,6 +59,11 @@ export const initialSettings = { /** * Video Card */ + videoCard: { + __internal: { + useLargePreview: false, + }, + }, // 自动开始预览: 按键选择 autoPreviewWhenKeyboardSelect: false,