feat: 添加Bilibili链接解析域逻辑
This commit is contained in:
parent
f8cd07e018
commit
7522b4c2b2
@ -0,0 +1,13 @@
|
|||||||
|
export type BilibiliVideoReference =
|
||||||
|
| {
|
||||||
|
canonicalVideoId: string;
|
||||||
|
kind: 'bvid';
|
||||||
|
sourceUrl: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
canonicalVideoId: string;
|
||||||
|
kind: 'aid';
|
||||||
|
sourceUrl: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
@ -0,0 +1,79 @@
|
|||||||
|
import type { BilibiliVideoReference } from './bilibili-card.types';
|
||||||
|
|
||||||
|
const BILIBILI_HOST = 'bilibili.com';
|
||||||
|
const B23_HOST = 'b23.tv';
|
||||||
|
const TRAILING_WRAPPERS = /[\s"'<>,。!?、;:))\]}]+$/u;
|
||||||
|
const LEADING_WRAPPERS = /^[\s"'<>(([{]+/u;
|
||||||
|
const BVID_PATTERN = /(?:^|\/)(BV[0-9A-Za-z]{10,})/;
|
||||||
|
const AID_PATTERN = /(?:^|\/)(?:av|AV)(\d+)(?:$|[/?#])/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes QQ card wrappers and punctuation that often stick to copied URLs.
|
||||||
|
* @param candidate - Raw string fragment found in text or card JSON.
|
||||||
|
* @returns Cleaned URL candidate ready for `URL` parsing.
|
||||||
|
*/
|
||||||
|
export function cleanBilibiliUrlCandidate(candidate: string) {
|
||||||
|
return candidate
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
.replace(LEADING_WRAPPERS, '')
|
||||||
|
.replace(TRAILING_WRAPPERS, '')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a URL belongs to the Bilibili domains this plugin is allowed to parse.
|
||||||
|
* @param candidate - URL string collected from a QQ message or redirect result.
|
||||||
|
* @returns `true` when the host is Bilibili-owned or `b23.tv`.
|
||||||
|
*/
|
||||||
|
export function isAllowedBilibiliUrl(candidate: string) {
|
||||||
|
try {
|
||||||
|
const url = new URL(cleanBilibiliUrlCandidate(candidate));
|
||||||
|
const hostname = url.hostname.toLowerCase();
|
||||||
|
return (
|
||||||
|
(url.protocol === 'http:' || url.protocol === 'https:') &&
|
||||||
|
(hostname === B23_HOST ||
|
||||||
|
hostname === BILIBILI_HOST ||
|
||||||
|
hostname.endsWith(`.${BILIBILI_HOST}`))
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a Bilibili video identifier from a supported URL.
|
||||||
|
* @param candidate - Direct Bilibili video URL or short-link URL that already embeds BV/av.
|
||||||
|
* @returns Parsed video reference, or `null` when the URL is not a supported video URL.
|
||||||
|
*/
|
||||||
|
export function parseBilibiliVideoReference(
|
||||||
|
candidate: string,
|
||||||
|
): BilibiliVideoReference | null {
|
||||||
|
const cleaned = cleanBilibiliUrlCandidate(candidate);
|
||||||
|
if (!isAllowedBilibiliUrl(cleaned)) return null;
|
||||||
|
|
||||||
|
const url = new URL(cleaned);
|
||||||
|
const probe = `${url.pathname}${url.search}${url.hash}`;
|
||||||
|
const bvid = probe.match(BVID_PATTERN)?.[1];
|
||||||
|
if (bvid) {
|
||||||
|
return {
|
||||||
|
canonicalVideoId: bvid,
|
||||||
|
kind: 'bvid',
|
||||||
|
sourceUrl: cleaned,
|
||||||
|
value: bvid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const aid = `${url.pathname}/`.match(AID_PATTERN)?.[1];
|
||||||
|
if (aid) {
|
||||||
|
return {
|
||||||
|
canonicalVideoId: `av${aid}`,
|
||||||
|
kind: 'aid',
|
||||||
|
sourceUrl: cleaned,
|
||||||
|
value: aid,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import {
|
||||||
|
cleanBilibiliUrlCandidate,
|
||||||
|
isAllowedBilibiliUrl,
|
||||||
|
parseBilibiliVideoReference,
|
||||||
|
} from '../../../../../src/modules/qqbot/plugins/bilibili-card/src/domain/bilibili-url-parser';
|
||||||
|
|
||||||
|
describe('Bilibili URL parser', () => {
|
||||||
|
it('parses BV video URLs while ignoring query, hash, and trailing punctuation', () => {
|
||||||
|
const reference = parseBilibiliVideoReference(
|
||||||
|
'https://www.bilibili.com/video/BV1xx411c7mD/?share_source=qq#reply。',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(reference).toEqual({
|
||||||
|
canonicalVideoId: 'BV1xx411c7mD',
|
||||||
|
kind: 'bvid',
|
||||||
|
sourceUrl:
|
||||||
|
'https://www.bilibili.com/video/BV1xx411c7mD/?share_source=qq#reply',
|
||||||
|
value: 'BV1xx411c7mD',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses av video URLs from mobile Bilibili links', () => {
|
||||||
|
expect(
|
||||||
|
parseBilibiliVideoReference('https://m.bilibili.com/video/av170001'),
|
||||||
|
).toMatchObject({
|
||||||
|
canonicalVideoId: 'av170001',
|
||||||
|
kind: 'aid',
|
||||||
|
value: '170001',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows only Bilibili and b23.tv hosts', () => {
|
||||||
|
expect(isAllowedBilibiliUrl('https://b23.tv/abc123')).toBe(true);
|
||||||
|
expect(isAllowedBilibiliUrl('https://space.bilibili.com/1')).toBe(true);
|
||||||
|
expect(isAllowedBilibiliUrl('https://example.com/video/BV1xx411c7mD')).toBe(
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cleans card wrappers, html entities, and trailing brackets', () => {
|
||||||
|
expect(
|
||||||
|
cleanBilibiliUrlCandidate(
|
||||||
|
'"https://www.bilibili.com/video/BV1xx411c7mD?p=1")',
|
||||||
|
),
|
||||||
|
).toBe('https://www.bilibili.com/video/BV1xx411c7mD?p=1');
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user