B站视频基本信息获取,支持分享图生成

This commit is contained in:
目棃
2024-12-16 16:08:14 +08:00
parent 582d2cffb8
commit c6a9548a43
13 changed files with 195 additions and 148 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -35,31 +35,59 @@
"core:window:default", "core:window:default",
{ {
"identifier": "fs:allow-exists", "identifier": "fs:allow-exists",
"allow": [{ "path": "**" }] "allow": [
{
"path": "**"
}
]
}, },
{ {
"identifier": "fs:allow-mkdir", "identifier": "fs:allow-mkdir",
"allow": [{ "path": "**" }] "allow": [
{
"path": "**"
}
]
}, },
{ {
"identifier": "fs:allow-read-dir", "identifier": "fs:allow-read-dir",
"allow": [{ "path": "**" }] "allow": [
{
"path": "**"
}
]
}, },
{ {
"identifier": "fs:allow-read-text-file", "identifier": "fs:allow-read-text-file",
"allow": [{ "path": "**" }] "allow": [
{
"path": "**"
}
]
}, },
{ {
"identifier": "fs:allow-remove", "identifier": "fs:allow-remove",
"allow": [{ "path": "**" }] "allow": [
{
"path": "**"
}
]
}, },
{ {
"identifier": "fs:allow-write-file", "identifier": "fs:allow-write-file",
"allow": [{ "path": "**" }] "allow": [
{
"path": "**"
}
]
}, },
{ {
"identifier": "fs:allow-write-text-file", "identifier": "fs:allow-write-text-file",
"allow": [{ "path": "**" }] "allow": [
{
"path": "**"
}
]
}, },
{ {
"identifier": "http:default", "identifier": "http:default",
@@ -76,6 +104,9 @@
{ {
"url": "https://*.bilibili.com/*" "url": "https://*.bilibili.com/*"
}, },
{
"url": "http://*.hdslb.com/*"
},
{ {
"url": "https://*.hoyoverse.com/*" "url": "https://*.hoyoverse.com/*"
}, },

View File

@@ -124,6 +124,7 @@
{ "url": "https://*.mihoyo.com/*" }, { "url": "https://*.mihoyo.com/*" },
{ "url": "https://*.mihoyogift.com/*" }, { "url": "https://*.mihoyogift.com/*" },
{ "url": "https://*.bilibili.com/*" }, { "url": "https://*.bilibili.com/*" },
{ "url": "http://*.hdslb.com/*" },
{ "url": "https://*.hoyoverse.com/*" }, { "url": "https://*.hoyoverse.com/*" },
{ "url": "https://*.genshinnet.com/*" } { "url": "https://*.genshinnet.com/*" }
] ]

View File

@@ -1,6 +1,5 @@
<!-- todo 优化 -->
<template> <template>
<div class="tp-video-box"> <div class="tp-video-box" v-if="videoData">
<!-- todo https://socialsisteryi.github.io/bilibili-API-collect/docs/video/videostream_url.html#%E8%A7%86%E9%A2%91%E4%BC%B4%E9%9F%B3%E9%9F%B3%E8%B4%A8%E4%BB%A3%E7%A0%81 --> <!-- todo https://socialsisteryi.github.io/bilibili-API-collect/docs/video/videostream_url.html#%E8%A7%86%E9%A2%91%E4%BC%B4%E9%9F%B3%E9%9F%B3%E8%B4%A8%E4%BB%A3%E7%A0%81 -->
<iframe <iframe
class="tp-video-container" class="tp-video-container"
@@ -9,26 +8,32 @@
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
sandbox="allow-forms allow-same-origin allow-popups allow-presentation allow-scripts" sandbox="allow-forms allow-same-origin allow-popups allow-presentation allow-scripts"
:id="`tp-video-${props.data.insert.video}`" :id="`tp-video-${props.data.insert.video}`"
> />
</iframe> <div class="tp-video-share">
<div class="tp-video-cover" v-if="videoCover"> <img alt="cover" :src="videoCover" class="tp-video-cover" />
<img alt="cover" :src="videoCover" /> <img alt="icon" src="/source/UI/video_play_bili.png" class="tp-video-icon" />
<img src="/source/UI/video_play.svg" alt="icon" /> <div class="tp-video-info">
<span>{{ getVideoTime() }}</span> <span>{{ videoData.bvid }}|{{ timestampToDate(videoData.ctime * 1000) }}</span>
<span>{{ videoData.title }}</span>
</div>
<div class="tp-video-view">
<v-icon size="12">mdi-eye</v-icon>
<span>{{ videoData.stat.view }}</span>
</div>
<div class="tp-video-time">
<v-icon size="12">mdi-clock-time-four-outline</v-icon>
<span>{{ getVideoDuration(videoData.duration) }}</span>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
// todo https://artplayer.org/?libs=https://cdnjs.cloudflare.com/ajax/libs/dashjs/4.5.2/dash.all.min.js&example=dash
// todo flv
// https://artplayer.org/document/library/flv.html
// https://api.bilibili.com/x/player/playurl?avid=666064953&cid=1400018762&qn=64&otype=json
import Bili from "@Bili/index.js"; import Bili from "@Bili/index.js";
import { getCurrentWindow } from "@tauri-apps/api/window"; import showSnackbar from "@comp/func/snackbar.js";
import { onBeforeMount, onMounted, onUnmounted, ref, shallowRef, useTemplateRef } from "vue"; import { onMounted, onUnmounted, ref, shallowRef } from "vue";
import TGLogger from "@/utils/TGLogger.js"; import { getImageBuffer } from "@/utils/TGShare.js";
import { saveImgLocal } from "@/utils/TGShare.js"; import { getVideoDuration, timestampToDate } from "@/utils/toolFunc.js";
type TpVideo = { insert: { video: string } }; type TpVideo = { insert: { video: string } };
type TpVideoProps = { data: TpVideo }; type TpVideoProps = { data: TpVideo };
@@ -37,60 +42,28 @@ const props = defineProps<TpVideoProps>();
const videoAspectRatio = ref<number>(16 / 9); const videoAspectRatio = ref<number>(16 / 9);
const videoCover = ref<string>(); const videoCover = ref<string>();
const videoData = shallowRef<TGApp.Plugins.Bili.Video.ViewData>(); const videoData = shallowRef<TGApp.Plugins.Bili.Video.ViewData>();
const videoRef = useTemplateRef<HTMLIFrameElement>(`#tp-video-${props.data.insert.video}`); const coverBuffer = shallowRef<Uint8Array | null>(null);
console.log("tpVideo", props.data.insert.video); console.log("tpVideo", props.data.insert.video);
onBeforeMount(async () => { onMounted(async () => {
const url = new URL(props.data.insert.video); const url = new URL(props.data.insert.video);
const aid = url.searchParams.get("aid") ?? undefined; const aid = url.searchParams.get("aid") ?? undefined;
const bvid = url.searchParams.get("bvid") ?? undefined; const bvid = url.searchParams.get("bvid") ?? undefined;
try { videoData.value = await Bili.video.view(aid, bvid);
videoData.value = await Bili.video.view(aid, bvid);
} catch (e) {
if (e instanceof Error) {
await TGLogger.Error(`获取视频信息失败: ${e.message}`);
} else await TGLogger.Error(`获取视频信息失败: ${e}`);
}
if (!videoData.value) { if (!videoData.value) {
console.error("videoData is null"); showSnackbar.error(`获取B站视频信息失败${props.data.insert.video}`);
return; return;
} }
const meta = videoData.value.dimension; const meta = videoData.value.dimension;
if (meta.width > meta.height) videoAspectRatio.value = meta.width / meta.height; if (meta.width > meta.height) videoAspectRatio.value = meta.width / meta.height;
else videoAspectRatio.value = meta.height / meta.width; else videoAspectRatio.value = meta.height / meta.width;
coverBuffer.value = await getImageBuffer(videoData.value.pic);
videoCover.value = URL.createObjectURL(new Blob([coverBuffer.value], { type: "image/png" }));
}); });
onMounted(async () => {
if (videoData.value && videoData.value.pic) {
videoCover.value = await saveImgLocal(videoData.value.pic);
}
if (videoRef.value === null) return;
videoRef.value.addEventListener("fullscreenchange", listenFullScreen);
});
async function listenFullScreen(): Promise<void> {
if (document.fullscreenElement) await getCurrentWindow().setFullscreen(true);
else await getCurrentWindow().setFullscreen(false);
}
function getVideoTime(): string {
const duration = videoData.value?.duration ?? 0;
const seconds = duration % 60;
const minutes = Math.floor(duration / 60) % 60;
const hours = Math.floor(duration / 3600);
let result = "";
if (hours > 0) result += `${hours.toString().padStart(2, "0")}:`;
result += `${minutes.toString().padStart(2, "0")}:`;
result += `${seconds.toString().padStart(2, "0")}`;
return result;
}
onUnmounted(() => { onUnmounted(() => {
if (videoCover.value) URL.revokeObjectURL(videoCover.value); if (videoCover.value) URL.revokeObjectURL(videoCover.value);
if (videoRef.value !== null) {
videoRef.value.removeEventListener("fullscreenchange", listenFullScreen);
}
}); });
</script> </script>
<style lang="css" scoped> <style lang="css" scoped>
@@ -102,6 +75,8 @@ onUnmounted(() => {
} }
.tp-video-container { .tp-video-container {
position: relative;
z-index: 1;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@@ -110,7 +85,7 @@ onUnmounted(() => {
aspect-ratio: v-bind(videoAspectRatio); aspect-ratio: v-bind(videoAspectRatio);
} }
.tp-video-cover { .tp-video-share {
position: absolute; position: absolute;
z-index: -1; z-index: -1;
top: 0; top: 0;
@@ -124,28 +99,64 @@ onUnmounted(() => {
aspect-ratio: v-bind(videoAspectRatio); aspect-ratio: v-bind(videoAspectRatio);
} }
.tp-video-cover :first-child { .tp-video-cover {
width: 100%; position: absolute;
max-width: 100%;
object-fit: cover; object-fit: cover;
} }
.tp-video-cover :nth-child(2) { .tp-video-icon {
position: absolute; position: absolute;
top: calc(50% - 40px); top: 50%;
left: calc(50% - 40px); left: 50%;
width: 80px; width: 80px;
height: 80px; height: 80px;
transform: translate(-50%, -50%);
} }
.tp-video-cover :nth-child(3) { .tp-video-time {
position: absolute; position: absolute;
right: 10px;
bottom: 10px; bottom: 10px;
padding: 0 5px; left: 10px;
display: flex;
align-items: center;
padding: 2px 5px;
border-radius: 5px; border-radius: 5px;
background: rgb(0 0 0/50%); background: rgb(0 0 0/50%);
color: var(--tgc-white-4); color: var(--tgc-white-4);
font-family: var(--font-title); font-family: var(--font-title);
font-size: 12px; font-size: 12px;
gap: 5px;
}
.tp-video-info {
position: absolute;
top: 10px;
left: 10px;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
padding: 5px;
border-radius: 5px;
background: rgb(0 0 0 / 50%);
color: var(--tgc-white-1);
font-family: var(--font-title);
font-size: 12px;
}
.tp-video-view {
position: absolute;
top: 10px;
right: 10px;
display: flex;
align-items: center;
padding: 2px 5px;
border-radius: 5px;
background: rgb(0 0 0/50%);
color: var(--tgc-white-4);
font-family: var(--font-title);
font-size: 12px;
gap: 5px;
} }
</style> </style>

View File

@@ -1,19 +1,24 @@
/** /**
* @file plugins/Bili/request/getNav.ts * @file plugins/Bili/request/getNav.ts
* @description Bili 插件导航请求文件 * @description Bili 插件导航请求文件
* @since Beta v0.5.0 * @since Beta v0.5.7
*/ */
import headerBili from "@Bili/utils/getHeader.js";
import TGHttp from "@/utils/TGHttp.js"; import TGHttp from "@/utils/TGHttp.js";
/** /**
* @description Bili 插件导航请求 * @description Bili 插件导航请求
* @since Beta v0.5.0 * @since Beta v0.5.7
* @return {Promise<TGApp.Plugins.Bili.Nav.NavData>} Bili 插件导航请求返回 * @return {Promise<TGApp.Plugins.Bili.Nav.Data>} Bili 插件导航请求返回
*/ */
async function getNav(): Promise<TGApp.Plugins.Bili.Nav.NavData> { async function getNav(): Promise<TGApp.Plugins.Bili.Nav.Data> {
const url = "https://api.bilibili.com/x/web-interface/nav"; const url = "https://api.bilibili.com/x/web-interface/nav";
const resp = await TGHttp<TGApp.Plugins.Bili.Nav.NavResponse>(url, { method: "GET" }); const resp = await TGHttp<TGApp.Plugins.Bili.Nav.Response>(url, {
method: "GET",
headers: headerBili,
});
return resp.data; return resp.data;
} }

View File

@@ -4,6 +4,7 @@
* @since Beta v0.5.0 * @since Beta v0.5.0
*/ */
import headerBili from "@Bili/utils/getHeader.js";
import getWrid from "@Bili/utils/getWrid.js"; import getWrid from "@Bili/utils/getWrid.js";
import TGHttp from "@/utils/TGHttp.js"; import TGHttp from "@/utils/TGHttp.js";
@@ -18,22 +19,13 @@ import TGHttp from "@/utils/TGHttp.js";
*/ */
async function getVideoUrl(cid: number, bvid: string): Promise<TGApp.Plugins.Bili.Video.UrlData> { async function getVideoUrl(cid: number, bvid: string): Promise<TGApp.Plugins.Bili.Video.UrlData> {
const url = "https://api.bilibili.com/x/player/playurl"; const url = "https://api.bilibili.com/x/player/playurl";
let params: Record<string, string> = { let params: Record<string, string> = { bvid, cid: cid.toString(), fnval: "16", platform: "pc" };
bvid,
cid: cid.toString(),
fnval: "16",
platform: "pc",
};
const wridRes = await getWrid(params); const wridRes = await getWrid(params);
params = { params = { ...params, wts: wridRes[0], w_rid: wridRes[1] };
...params,
wts: wridRes[0],
wrid: wridRes[1],
};
const resp = await TGHttp<TGApp.Plugins.Bili.Video.UrlResponse>(url, { const resp = await TGHttp<TGApp.Plugins.Bili.Video.UrlResponse>(url, {
method: "GET", method: "GET",
query: params, query: params,
headers: { referer: "https://www.bilibili.com/" }, headers: headerBili,
}); });
return resp.data; return resp.data;
} }

View File

@@ -4,7 +4,11 @@
* @since Beta v0.5.0 * @since Beta v0.5.0
*/ */
import headerBili from "@Bili/utils/getHeader.js";
import getWrid from "@Bili/utils/getWrid.js";
import TGHttp from "@/utils/TGHttp.js"; import TGHttp from "@/utils/TGHttp.js";
import TGLogger from "@/utils/TGLogger.js";
/** /**
* @description 获取视频基本信息 * @description 获取视频基本信息
@@ -18,26 +22,24 @@ async function getVideoView(
bvid?: string, bvid?: string,
): Promise<TGApp.Plugins.Bili.Video.ViewData> { ): Promise<TGApp.Plugins.Bili.Video.ViewData> {
const url = "https://api.bilibili.com/x/web-interface/wbi/view"; const url = "https://api.bilibili.com/x/web-interface/wbi/view";
const params: Record<string, string | number | boolean> = { let params: Record<string, string | number | boolean> = { need_view: 1, isGaiaAvoided: true };
need_view: 1, if (aid) params.aid = aid;
isGaiaAoided: true, if (bvid) params.bvid = bvid;
}; if (!aid && !bvid) throw new Error("aid和bVid不能同时为空");
if (aid) { const wrid = await getWrid(params);
params.aid = aid; params = { ...params, wts: wrid[0], w_rid: wrid[1] };
} else if (bvid) { try {
params.bvid = bvid; const resp = await TGHttp<TGApp.Plugins.Bili.Video.ViewResponse>(url, {
} else { method: "GET",
throw new Error("参数错误"); query: params,
headers: headerBili,
});
return resp.data;
} catch (error) {
if (error instanceof Error) await TGLogger.Error(`获取视频基本信息失败: ${error.message}`);
else await TGLogger.Error(`获取视频基本信息失败: ${error}`);
} }
const resp = await TGHttp<TGApp.Plugins.Bili.Video.ViewResponse>(url, { throw new Error("获取视频基本信息失败");
method: "GET",
query: params,
}).catch((err) => {
console.error(err);
return err;
});
console.warn(resp.data);
return resp.data;
} }
export default getVideoView; export default getVideoView;

View File

@@ -4,12 +4,6 @@
* @since Beta v0.4.0 * @since Beta v0.4.0
*/ */
/**
* @description Bili 插件基础类型
* @since Beta v0.4.0
* @namespace Base
* @memberof TGApp.Plugins.Bili
*/
declare namespace TGApp.Plugins.Bili.Base { declare namespace TGApp.Plugins.Bili.Base {
/** /**
* @description Bili Response 统一接口 * @description Bili Response 统一接口
@@ -21,10 +15,5 @@ declare namespace TGApp.Plugins.Bili.Base {
* @property {any} data 数据 * @property {any} data 数据
* @return Response * @return Response
*/ */
interface Response { type Response = { code: number; message: string; ttl: number; data: unknown };
code: number;
message: string;
ttl: number;
data: any;
}
} }

View File

@@ -1,42 +1,29 @@
/** /**
* @file plugins/Bili/types/Nav.d.ts * @file plugins/Bili/types/Nav.d.ts
* @description Bili 插件导航类型定义文件 * @description Bili 插件导航类型定义文件
* @since Beta v0.4.0 * @since Beta v0.5.7
*/ */
/**
* @description Bili 插件导航类型
* @since Beta v0.4.0
* @namespace Nav
* @memberof TGApp.Plugins.Bili
*/
declare namespace TGApp.Plugins.Bili.Nav { declare namespace TGApp.Plugins.Bili.Nav {
/** /**
* @description Bili 导航基本信息返回 * @description Bili 导航基本信息返回
* @since Beta v0.4.0 * @since Beta v0.5.7
* @interface NavResponse * @interface Response
* @extends {TGApp.Plugins.Bili.Base.Response} * @extends {TGApp.Plugins.Bili.Base.Response}
* @property {NavData} data 导航基本信息 * @property {Data} data 导航基本信息
* @return NavResponse * @return NavResponse
*/ */
interface NavResponse extends TGApp.Plugins.Bili.Base.Response { type Response = TGApp.Plugins.Bili.Base.Response & { data: Data };
data: NavData;
}
/** /**
* @description Bili 导航基本信息 * @description Bili 导航基本信息
* @since Beta v0.4.0 * @since Beta v0.5.7
* @interface NavData * @interface Data
* @see https://api.bilibili.com/x/web-interface/nav * @see https://api.bilibili.com/x/web-interface/nav
* @desc 只写了用到的部分 * @desc 只写了用到的部分
* @property {string} wbi_img.img_url 网站图标 * @property {string} wbi_img.img_url 网站图标
* @property {string} wbi_img.sub_url 网站图标(小) * @property {string} wbi_img.sub_url 网站图标(小)
* @return NavData * @return Data
*/ */
interface NavData { type Data = { wbi_img: { img_url: string; sub_url: string } };
wbi_img: {
img_url: string;
sub_url: string;
};
}
} }

View File

@@ -187,7 +187,7 @@ declare namespace TGApp.Plugins.Bili.Video {
* @property {string} seek_type 视频跳转类型 * @property {string} seek_type 视频跳转类型
* @property {UrlDash} dash 视频播放地址 * @property {UrlDash} dash 视频播放地址
* @property {UrlDurl[]} durl 视频播放地址 * @property {UrlDurl[]} durl 视频播放地址
* @property {UrlFormats} support_formats 视频支持格式 * @property {UrlFormat[]} support_formats 视频支持格式
* @property {unknown} high_format 视频高清格式 * @property {unknown} high_format 视频高清格式
* @property {number} last_play_time 视频上次播放时间 * @property {number} last_play_time 视频上次播放时间
* @property {number} last_play_cid 视频上次播放分P号 * @property {number} last_play_cid 视频上次播放分P号
@@ -208,7 +208,7 @@ declare namespace TGApp.Plugins.Bili.Video {
seek_type: string; seek_type: string;
dash: UrlDash; // dash 返回 dash: UrlDash; // dash 返回
durl: UrlDurl[]; // mp4 返回 durl: UrlDurl[]; // mp4 返回
support_formats: UrlFormats; support_formats: UrlFormat[];
high_format: unknown; high_format: unknown;
last_play_time: number; last_play_time: number;
last_play_cid: number; last_play_cid: number;
@@ -331,7 +331,7 @@ declare namespace TGApp.Plugins.Bili.Video {
* @property {unknown} codecs 视频编码 * @property {unknown} codecs 视频编码
* @return UrlFormats * @return UrlFormats
*/ */
interface UrlFormats { interface UrlFormat {
quality: number; quality: number;
format: string; format: string;
new_description: string; new_description: string;

View File

@@ -0,0 +1,13 @@
/**
* @file plugins/Bili/utils/getHeader.ts
* @description 获取请求头
* @since Beta v0.5.7
*/
const headerBili = {
cookie: "",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
origin: "https://www.bilibili.com",
};
export default headerBili;

View File

@@ -43,13 +43,12 @@ async function getMixinKey(): Promise<string> {
* @param {Record<string,string|number>} params 请求参数 * @param {Record<string,string|number>} params 请求参数
* @returns {Promise<[string|string]>} wrid * @returns {Promise<[string|string]>} wrid
*/ */
async function getWrid(params: Record<string, string | number>): Promise<[string, string]> { async function getWrid(
params: Record<string, string | number | boolean>,
): Promise<[string, string]> {
const mixin_key = await getMixinKey(); const mixin_key = await getMixinKey();
const wts = Math.floor(Date.now() / 1000); const wts = Math.floor(Date.now() / 1000);
const obj: Record<string, string | number> = { const obj: Record<string, string | number> = { ...params, wts };
...params,
wts,
};
const keys = Object.keys(obj).sort(); const keys = Object.keys(obj).sort();
let md5Str = ""; let md5Str = "";
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {

View File

@@ -232,3 +232,20 @@ export function getZhElement(element: string): string {
return "未知"; return "未知";
} }
} }
/**
* @description 获取视频时长
* @since Beta v0.5.7
* @param {number} duration - 视频时长(秒)
* @returns {string} 视频时长
*/
export function getVideoDuration(duration: number): string {
const seconds = duration % 60;
const minutes = Math.floor(duration / 60) % 60;
const hours = Math.floor(duration / 3600);
let result = "";
if (hours > 0) result += `${hours.toString().padStart(2, "0")}:`;
result += `${minutes.toString().padStart(2, "0")}:`;
result += `${seconds.toString().padStart(2, "0")}`;
return result;
}