mioku-plugin-media 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mioku Lab
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # mioku-plugin-media
2
+
3
+ 流媒体解析插件,自动识别并解析哔哩哔哩、抖音、小红书和快手平台的视频/图文链接。
4
+
5
+ ## 功能
6
+
7
+ - 自动检测消息中的平台链接,无需命令前缀
8
+ - 支持哔哩哔哩、抖音、小红书、快手四大平台
9
+ - 返回封面图、标题、作者、简介和视频文件
10
+ - 可选配置各平台 Cookies 以获取更高质量的视频
11
+
12
+ ## 支持平台
13
+
14
+ ### 哔哩哔哩
15
+ - 域名:bilibili.com、b23.tv、t.bilibili.com、bili2233.cn
16
+ - 支持纯 BV 号(BV...)和 av 号(av...)格式
17
+
18
+ ### 抖音
19
+ - 域名:douyin.com、iesdouyin.com 及子域名(www, v, jx, m, jingxuan)
20
+
21
+ ### 快手
22
+ - 域名:kuaishou.com、v.kuaishou.com
23
+ - 支持 APP 分享文本格式(如"快手...快手")
24
+
25
+ ### 小红书
26
+ - 域名:xiaohongshu.com、xhslink.com
27
+
28
+ ## 配置
29
+
30
+ 通过 WebUI 配置页面或直接编辑配置文件:
31
+
32
+ ```json
33
+ {
34
+ "cookies": {
35
+ "bilibili": "",
36
+ "douyin": "",
37
+ "kuaishou": "",
38
+ "xiaohongshu": ""
39
+ },
40
+ "debug": false
41
+ }
42
+ ```
package/config.md ADDED
@@ -0,0 +1,29 @@
1
+ ---
2
+ title: 媒体解析配置
3
+ description: 配置各平台 Cookie 和调试选项
4
+ fields:
5
+ - key: base.cookies.bilibili
6
+ label: 哔哩哔哩 Cookie
7
+ type: textarea
8
+ description: B站 Cookie,配置后可获取更高质量的视频流。留空则使用基础解析。
9
+
10
+ - key: base.cookies.douyin
11
+ label: 抖音 Cookie
12
+ type: textarea
13
+ description: 抖音 Cookie,配置后可获取无水印视频。留空则使用基础解析。
14
+
15
+ - key: base.cookies.kuaishou
16
+ label: 快手 Cookie
17
+ type: textarea
18
+ description: 快手 Cookie,配置后可获取更完整的视频信息。留空则使用基础解析。
19
+
20
+ - key: base.cookies.xiaohongshu
21
+ label: 小红书 Cookie
22
+ type: textarea
23
+ description: 小红书 Cookie,配置后可获取笔记详情和视频。留空则使用基础解析。
24
+
25
+ - key: base.debug
26
+ label: 调试模式
27
+ type: switch
28
+ description: 开启后解析失败时直接发送错误信息,不走 AI 通知
29
+ ---
package/config.ts ADDED
@@ -0,0 +1,19 @@
1
+ export interface MediaConfig {
2
+ cookies: {
3
+ bilibili: string;
4
+ douyin: string;
5
+ kuaishou: string;
6
+ xiaohongshu: string;
7
+ };
8
+ debug: boolean;
9
+ }
10
+
11
+ export const MEDIA_DEFAULTS: MediaConfig = {
12
+ cookies: {
13
+ bilibili: "",
14
+ douyin: "",
15
+ kuaishou: "",
16
+ xiaohongshu: "",
17
+ },
18
+ debug: false,
19
+ };
package/index.ts ADDED
@@ -0,0 +1,124 @@
1
+ import { definePlugin, type MiokiContext } from "mioki";
2
+ import type { ConfigService } from "../../src/services/config/tpyes";
3
+ import type { MediaConfig } from "./types";
4
+ import { MEDIA_DEFAULTS } from "./config";
5
+ import { createMediaAmagiClient } from "./platforms/amagi-client";
6
+ import { extractMediaUrlFromEvent, resolveShortUrl, isShortUrl } from "./platforms/url-parser";
7
+ import { resolveMedia } from "./platforms/resolvers";
8
+ import { sendMediaResult } from "./utils/message";
9
+ import { handleMediaError } from "./utils/error-handler";
10
+ import { setMediaRuntimeState, resetMediaRuntimeState } from "./runtime";
11
+
12
+ const REACTION_EMOJI_ID = 60;
13
+
14
+ function cloneConfig<T>(value: T): T {
15
+ return JSON.parse(JSON.stringify(value)) as T;
16
+ }
17
+
18
+ async function addReaction(bot: any, messageId: number | string): Promise<void> {
19
+ if (!bot || messageId == null) return;
20
+ try {
21
+ await bot.api("set_msg_emoji_like", {
22
+ message_id: messageId,
23
+ emoji_id: REACTION_EMOJI_ID,
24
+ set: true,
25
+ });
26
+ } catch {
27
+ // ignore reaction errors
28
+ }
29
+ }
30
+
31
+ export default definePlugin({
32
+ name: "media",
33
+ version: "1.0.0",
34
+ description: "流媒体解析插件,支持哔哩哔哩、抖音、小红书和快手平台",
35
+
36
+ async setup(ctx: MiokiContext) {
37
+ const configService = ctx.services?.config as ConfigService | undefined;
38
+ let config = cloneConfig(MEDIA_DEFAULTS);
39
+
40
+ if (configService) {
41
+ await configService.registerConfig("media", "base", config);
42
+ const saved = await configService.getConfig("media", "base");
43
+ if (saved) {
44
+ config = saved as MediaConfig;
45
+ }
46
+ } else {
47
+ ctx.logger.warn("config-service 未加载,media 插件将使用默认配置");
48
+ }
49
+
50
+ let amagiClient = createMediaAmagiClient(config);
51
+
52
+ setMediaRuntimeState({ config, amagiClient });
53
+
54
+ const disposers: Array<() => void> = [];
55
+ if (configService) {
56
+ disposers.push(
57
+ configService.onConfigChange("media", "base", (next) => {
58
+ config = next as MediaConfig;
59
+ amagiClient = createMediaAmagiClient(config);
60
+ setMediaRuntimeState({ config, amagiClient });
61
+ }),
62
+ );
63
+ }
64
+
65
+ ctx.handle("message", async (event: any) => {
66
+ if (event.user_id === event.self_id) return;
67
+
68
+ const parsed = extractMediaUrlFromEvent(event);
69
+ if (!parsed) return;
70
+
71
+ const messageId = event.message_id ?? event.message_seq;
72
+
73
+ const platformLabel =
74
+ parsed.platform === "bilibili"
75
+ ? "B站"
76
+ : parsed.platform === "douyin"
77
+ ? "抖音"
78
+ : parsed.platform === "kuaishou"
79
+ ? "快手"
80
+ : "小红书";
81
+
82
+ ctx.logger.info(`[media] 检测到${platformLabel}链接,开始解析...`);
83
+
84
+ const selfId = event?.self_id != null ? Number(event.self_id) : undefined;
85
+ const bot =
86
+ selfId != null && typeof ctx?.pickBot === "function"
87
+ ? ctx.pickBot(selfId)
88
+ : undefined;
89
+
90
+ await addReaction(bot, messageId);
91
+
92
+ try {
93
+ if (isShortUrl(parsed)) {
94
+ ctx.logger.info(`[media] 检测到短链接,正在解析: ${parsed.id}`);
95
+ const resolvedUrl = await resolveShortUrl(parsed.id);
96
+ const reParsed = extractMediaUrlFromEvent({ raw_message: resolvedUrl, message: [{ type: "text", data: { text: resolvedUrl } }] });
97
+ if (reParsed) {
98
+ Object.assign(parsed, reParsed);
99
+ } else {
100
+ ctx.logger.warn(`[media] 短链接解析后无法识别: ${resolvedUrl}`);
101
+ }
102
+ }
103
+
104
+ const result = await resolveMedia(amagiClient, parsed);
105
+ await sendMediaResult(ctx, event, parsed, result);
106
+ } catch (error) {
107
+ await handleMediaError({
108
+ ctx,
109
+ event,
110
+ error,
111
+ platform: platformLabel,
112
+ config,
113
+ });
114
+ }
115
+ });
116
+
117
+ return () => {
118
+ for (const dispose of disposers) {
119
+ dispose();
120
+ }
121
+ resetMediaRuntimeState();
122
+ };
123
+ },
124
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "mioku-plugin-media",
3
+ "version": "1.0.0",
4
+ "description": "流媒体解析插件,支持哔哩哔哩、抖音、小红书和快手平台",
5
+ "main": "index.ts",
6
+ "keywords": [
7
+ "mioku"
8
+ ],
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/mioku-lab/mioku-plugin-media.git"
12
+ },
13
+ "dependencies": {
14
+ "@ikenxuan/amagi": "^6.1.2"
15
+ },
16
+ "mioku": {
17
+ "services": [
18
+ "config",
19
+ "ai"
20
+ ],
21
+ "help": {
22
+ "title": "媒体解析",
23
+ "description": "自动识别并解析哔哩哔哩、抖音、小红书、快手的视频/图文链接",
24
+ "commands": [
25
+ {
26
+ "cmd": "发送包含平台链接的消息",
27
+ "desc": "自动识别并解析消息中的B站/抖音/小红书/快手链接,返回视频信息和视频文件",
28
+ "role": "member"
29
+ }
30
+ ]
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,24 @@
1
+ import { createAmagiClient } from "@ikenxuan/amagi";
2
+ import type { MediaConfig } from "../types";
3
+
4
+ export function createMediaAmagiClient(config: MediaConfig): any {
5
+ const cookies: Record<string, string> = {};
6
+
7
+ if (config.cookies.bilibili?.trim()) {
8
+ cookies.bilibili = config.cookies.bilibili.trim();
9
+ }
10
+ if (config.cookies.douyin?.trim()) {
11
+ cookies.douyin = config.cookies.douyin.trim();
12
+ }
13
+ if (config.cookies.kuaishou?.trim()) {
14
+ cookies.kuaishou = config.cookies.kuaishou.trim();
15
+ }
16
+ if (config.cookies.xiaohongshu?.trim()) {
17
+ cookies.xiaohongshu = config.cookies.xiaohongshu.trim();
18
+ }
19
+
20
+ return createAmagiClient({
21
+ cookies,
22
+ request: { timeout: 15000 },
23
+ });
24
+ }
@@ -0,0 +1,95 @@
1
+ import type { ParsedMediaResult } from "../../types";
2
+ import type { ParsedMediaUrl } from "../types";
3
+ import type { PlatformResolver } from "./types";
4
+
5
+ export class BilibiliResolver implements PlatformResolver {
6
+ async resolve(client: any, parsed: ParsedMediaUrl): Promise<ParsedMediaResult> {
7
+ let bvid = parsed.id;
8
+ let avid: number | undefined;
9
+
10
+ if (bvid.startsWith("av")) {
11
+ avid = parseInt(bvid.slice(2), 10);
12
+ }
13
+
14
+ if (bvid.startsWith("http")) {
15
+ const infoResult = await client.bilibili.fetcher.fetchVideoInfo({ bvid: "" });
16
+ if (!infoResult.success) {
17
+ throw new Error(`B站视频信息获取失败: ${infoResult.message}`);
18
+ }
19
+ const data = infoResult.data?.data || infoResult.data;
20
+ bvid = data?.bvid || "";
21
+ avid = data?.aid;
22
+ if (!bvid) {
23
+ throw new Error("B站短链接解析失败,无法获取BV号");
24
+ }
25
+ }
26
+
27
+ const infoResult = await client.bilibili.fetcher.fetchVideoInfo({ bvid });
28
+ if (!infoResult.success) {
29
+ throw new Error(`B站视频信息获取失败: ${infoResult.message}`);
30
+ }
31
+
32
+ const infoData = infoResult.data?.data || infoResult.data;
33
+ if (!infoData) {
34
+ throw new Error("B站视频信息为空");
35
+ }
36
+
37
+ const title = infoData.title || "未知标题";
38
+ const author = infoData.owner?.name || "未知作者";
39
+ const description = infoData.desc || "";
40
+ const coverUrl = infoData.pic || "";
41
+ const cid = infoData.cid || infoData.pages?.[0]?.cid;
42
+ const aid = infoData.aid || avid || 0;
43
+ const duration = infoData.duration;
44
+
45
+ let videoUrl = "";
46
+
47
+ if (aid && cid) {
48
+ try {
49
+ const streamResult = await client.bilibili.fetcher.fetchVideoStreamUrl({
50
+ avid: aid,
51
+ cid,
52
+ });
53
+
54
+ if (streamResult.success) {
55
+ const streamData = streamResult.data?.data || streamResult.data;
56
+ if (streamData?.durl?.length) {
57
+ videoUrl = streamData.durl[0].url || "";
58
+ } else if (streamData?.dash?.video?.length) {
59
+ const videoItem = streamData.dash.video[0];
60
+ const audioItem = streamData.dash.audio?.[0];
61
+ videoUrl = videoItem.baseUrl || videoItem.base_url || "";
62
+ if (!videoUrl && videoItem.backupUrl?.length) {
63
+ videoUrl = videoItem.backupUrl[0];
64
+ }
65
+ if (!videoUrl && videoItem.backup_url?.length) {
66
+ videoUrl = videoItem.backup_url[0];
67
+ }
68
+ }
69
+ }
70
+ } catch {
71
+ videoUrl = "";
72
+ }
73
+ }
74
+
75
+ const stat = infoData.stat || {};
76
+
77
+ return {
78
+ title,
79
+ author,
80
+ description,
81
+ coverUrl,
82
+ videoUrl,
83
+ duration,
84
+ stats: {
85
+ likes: stat.like,
86
+ coins: stat.coin,
87
+ favorites: stat.favorite,
88
+ shares: stat.share,
89
+ views: stat.view,
90
+ comments: stat.reply,
91
+ danmaku: stat.danmaku,
92
+ },
93
+ };
94
+ }
95
+ }
@@ -0,0 +1,80 @@
1
+ import type { ParsedMediaResult } from "../../types";
2
+ import type { ParsedMediaUrl } from "../types";
3
+ import type { PlatformResolver } from "./types";
4
+
5
+ export class DouyinResolver implements PlatformResolver {
6
+ async resolve(client: any, parsed: ParsedMediaUrl): Promise<ParsedMediaResult> {
7
+ const awemeId = parsed.id;
8
+
9
+ const result = await client.douyin.fetcher.parseWork({ aweme_id: awemeId });
10
+ if (!result.success) {
11
+ const err = new Error(`抖音作品解析失败: ${result.message}`);
12
+ (err as any).cause = result;
13
+ throw err;
14
+ }
15
+
16
+ const data = result.data;
17
+ if (!data) {
18
+ throw new Error("抖音作品数据为空");
19
+ }
20
+
21
+ const detail = data.aweme_detail || data;
22
+ const title = detail.desc || detail.item_title || "未知标题";
23
+ const author = detail.author?.nickname || "未知作者";
24
+ const description = detail.desc || "";
25
+ const coverUrl =
26
+ detail.video?.cover?.url_list?.[0] ||
27
+ detail.video?.origin_cover?.url_list?.[0] ||
28
+ detail.video?.dynamic_cover?.url_list?.[0] ||
29
+ "";
30
+ const duration = detail.duration;
31
+
32
+ let videoUrl = "";
33
+
34
+ if (detail.video) {
35
+ videoUrl =
36
+ detail.video?.play_addr?.url_list?.[0] ||
37
+ detail.video?.download_addr?.url_list?.[0] ||
38
+ "";
39
+
40
+ if (!videoUrl && detail.video?.bit_rate?.length) {
41
+ const bestBitrate = detail.video.bit_rate.reduce(
42
+ (best: any, curr: any) =>
43
+ (curr.bit_rate || 0) > (best.bit_rate || 0) ? curr : best,
44
+ detail.video.bit_rate[0],
45
+ );
46
+ videoUrl =
47
+ bestBitrate?.play_addr?.url_list?.[0] || "";
48
+ }
49
+ }
50
+
51
+ const images: string[] = [];
52
+ if (detail.images && Array.isArray(detail.images)) {
53
+ for (const img of detail.images) {
54
+ const url =
55
+ img?.url_list?.[0] ||
56
+ img?.download_addr?.url_list?.[0] ||
57
+ "";
58
+ if (url) images.push(url);
59
+ }
60
+ }
61
+
62
+ const statistics = detail.statistics || detail.stats || {};
63
+
64
+ return {
65
+ title,
66
+ author,
67
+ description,
68
+ coverUrl,
69
+ videoUrl,
70
+ duration,
71
+ stats: {
72
+ likes: statistics.digg_count,
73
+ favorites: statistics.collect_count,
74
+ shares: statistics.share_count,
75
+ comments: statistics.comment_count,
76
+ views: statistics.play_count,
77
+ },
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,27 @@
1
+ import type { ParsedMediaUrl } from "../types";
2
+ import type { ParsedMediaResult } from "../../types";
3
+ import type { PlatformResolver } from "./types";
4
+ import { BilibiliResolver } from "./bilibili";
5
+ import { DouyinResolver } from "./douyin";
6
+ import { KuaishouResolver } from "./kuaishou";
7
+ import { XiaohongshuResolver } from "./xiaohongshu";
8
+
9
+ const resolvers: Record<string, PlatformResolver> = {
10
+ bilibili: new BilibiliResolver(),
11
+ douyin: new DouyinResolver(),
12
+ kuaishou: new KuaishouResolver(),
13
+ xiaohongshu: new XiaohongshuResolver(),
14
+ };
15
+
16
+ export async function resolveMedia(
17
+ client: any,
18
+ parsed: ParsedMediaUrl,
19
+ ): Promise<ParsedMediaResult> {
20
+ const resolver = resolvers[parsed.platform];
21
+ if (!resolver) {
22
+ throw new Error(`不支持的平台: ${parsed.platform}`);
23
+ }
24
+ return resolver.resolve(client, parsed);
25
+ }
26
+
27
+ export { BilibiliResolver, DouyinResolver, KuaishouResolver, XiaohongshuResolver };
@@ -0,0 +1,58 @@
1
+ import type { ParsedMediaResult } from "../../types";
2
+ import type { ParsedMediaUrl } from "../types";
3
+ import type { PlatformResolver } from "./types";
4
+
5
+ export class KuaishouResolver implements PlatformResolver {
6
+ async resolve(client: any, parsed: ParsedMediaUrl): Promise<ParsedMediaResult> {
7
+ const photoId = parsed.id;
8
+
9
+ const result = await client.kuaishou.fetcher.fetchVideoWork({ photoId });
10
+ if (!result.success) {
11
+ throw new Error(`快手作品解析失败: ${result.message}`);
12
+ }
13
+
14
+ const data = result.data;
15
+ if (!data) {
16
+ throw new Error("快手作品数据为空");
17
+ }
18
+
19
+ const detail = data.data?.visionVideoDetail || data;
20
+ const photo = detail.photo || {};
21
+ const authorInfo = detail.author || {};
22
+
23
+ const title = photo.caption || "未知标题";
24
+ const author = authorInfo.name || "未知作者";
25
+ const description = photo.caption || "";
26
+ const coverUrl = photo.coverUrl || "";
27
+ const duration = photo.duration;
28
+
29
+ let videoUrl = "";
30
+
31
+ if (photo.photoUrl) {
32
+ videoUrl = photo.photoUrl;
33
+ } else if (photo.croppedPhotoUrl) {
34
+ videoUrl = photo.croppedPhotoUrl;
35
+ } else if (photo.videoResource?.h264?.adaptationSet?.[0]?.representation?.[0]?.url) {
36
+ videoUrl = photo.videoResource.h264.adaptationSet[0].representation[0].url;
37
+ } else if (photo.videoResource?.hevc?.adaptationSet?.[0]?.representation?.[0]?.url) {
38
+ videoUrl = photo.videoResource.hevc.adaptationSet[0].representation[0].url;
39
+ } else if (photo.manifest?.adaptationSet?.[0]?.representation?.[0]?.url) {
40
+ videoUrl = photo.manifest.adaptationSet[0].representation[0].url;
41
+ }
42
+
43
+ return {
44
+ title,
45
+ author,
46
+ description,
47
+ coverUrl,
48
+ videoUrl,
49
+ duration,
50
+ stats: {
51
+ likes: photo.likeCount || photo.likeCnt,
52
+ comments: photo.commentCount || photo.commentCnt,
53
+ views: photo.viewCount || photo.viewCnt,
54
+ shares: photo.shareCount || photo.shareCnt,
55
+ },
56
+ };
57
+ }
58
+ }
@@ -0,0 +1,6 @@
1
+ import type { ParsedMediaResult } from "../../types";
2
+ import type { ParsedMediaUrl } from "../types";
3
+
4
+ export interface PlatformResolver {
5
+ resolve(client: any, parsed: ParsedMediaUrl): Promise<ParsedMediaResult>;
6
+ }
@@ -0,0 +1,92 @@
1
+ import type { ParsedMediaResult } from "../../types";
2
+ import type { ParsedMediaUrl } from "../types";
3
+ import type { PlatformResolver } from "./types";
4
+
5
+ export class XiaohongshuResolver implements PlatformResolver {
6
+ async resolve(client: any, parsed: ParsedMediaUrl): Promise<ParsedMediaResult> {
7
+ const noteId = parsed.id;
8
+ const xsecToken = parsed.extra?.xsec_token || "";
9
+
10
+ if (!xsecToken && !parsed.id.startsWith("http")) {
11
+ throw new Error("小红书笔记解析需要 xsec_token,请确保链接包含完整参数");
12
+ }
13
+
14
+ const result = await client.xiaohongshu.fetcher.fetchNoteDetail({
15
+ note_id: noteId,
16
+ xsec_token: xsecToken,
17
+ });
18
+
19
+ if (!result.success) {
20
+ throw new Error(`小红书笔记解析失败: ${result.message}`);
21
+ }
22
+
23
+ const data = result.data;
24
+ if (!data) {
25
+ throw new Error("小红书笔记数据为空");
26
+ }
27
+
28
+ const items = data.data?.items || data.items || [];
29
+ const noteCard = items[0]?.note_card || items[0]?.note_card;
30
+
31
+ if (!noteCard) {
32
+ const noteData = data.data || data;
33
+ const title = noteData.title || "未知标题";
34
+ const author = noteData.user?.nickname || "未知作者";
35
+ const description = noteData.desc || "";
36
+ const coverUrl =
37
+ noteData.image_list?.[0]?.url ||
38
+ noteData.image_list?.[0]?.url_default ||
39
+ noteData.image_list?.[0]?.url_pre ||
40
+ "";
41
+
42
+ return {
43
+ title,
44
+ author,
45
+ description,
46
+ coverUrl,
47
+ videoUrl: "",
48
+ stats: buildXhsStats(noteData.interact_info),
49
+ };
50
+ }
51
+
52
+ const title = noteCard.title || "未知标题";
53
+ const author = noteCard.user?.nickname || "未知作者";
54
+ const description = noteCard.desc || "";
55
+ const coverUrl =
56
+ noteCard.image_list?.[0]?.url ||
57
+ noteCard.image_list?.[0]?.url_default ||
58
+ noteCard.image_list?.[0]?.url_pre ||
59
+ "";
60
+
61
+ let videoUrl = "";
62
+ if (noteCard.type === "video" && noteCard.video) {
63
+ const video = noteCard.video;
64
+ videoUrl =
65
+ video.media?.stream?.h264?.[0]?.master_url ||
66
+ video.media?.stream?.h264?.[0]?.backup_urls?.[0] ||
67
+ video.media?.stream?.h265?.[0]?.master_url ||
68
+ video.media?.stream?.h265?.[0]?.backup_urls?.[0] ||
69
+ video.consumer?.origin_video_key ||
70
+ "";
71
+ }
72
+
73
+ return {
74
+ title,
75
+ author,
76
+ description,
77
+ coverUrl,
78
+ videoUrl,
79
+ stats: buildXhsStats(noteCard.interact_info),
80
+ };
81
+ }
82
+ }
83
+
84
+ function buildXhsStats(interactInfo: any): import("../../types").MediaStats {
85
+ if (!interactInfo) return {};
86
+ return {
87
+ likes: parseInt(interactInfo.like_count, 10) || undefined,
88
+ favorites: parseInt(interactInfo.collect_count, 10) || undefined,
89
+ comments: parseInt(interactInfo.comment_count, 10) || undefined,
90
+ shares: parseInt(interactInfo.share_count, 10) || undefined,
91
+ };
92
+ }
@@ -0,0 +1,8 @@
1
+ export type Platform = "bilibili" | "douyin" | "kuaishou" | "xiaohongshu";
2
+
3
+ export interface ParsedMediaUrl {
4
+ platform: Platform;
5
+ id: string;
6
+ subtype?: "video" | "article" | "note" | "bangumi";
7
+ extra?: Record<string, string>;
8
+ }