fbcc3df2cf
- 新增 shortlink_resolver.py 模块 * 检测 B站短链接(b23.tv)和其他短链接服务 * 提供友好的用户引导信息 * 支持 Playwright 浏览器自动化解析(可选) - 集成到 handler.py * 短链接自动检测和提示 * 智能区分短链接和完整 URL - 更新 add_video.py * 支持短链接解析 * 添加 --playwright 参数启用浏览器解析 - 新增 test_shortlink.py 测试脚本 * 测试单个短链接检测 * 测试多个短链接检测 * 测试混合内容场景 * 所有测试通过 - 更新文档 * SKILL.md: 添加短链接说明 * AUTO_TRIGGER.md: 添加短链接章节 * obsidian-kb-improvements.md: 标记任务2为已完成 功能特点: - 方案A(用户引导):已实现并作为默认行为 - 方案B(浏览器自动化):提供 PlaywrightResolver 类,可选使用 - 方案C(代理池):未实现(复杂度高,收益低) 验收标准达成: ✅ 短链接能够友好处理(显示引导信息) ✅ 用户不会感到困惑 ✅ 支持 b23.tv 和其他短链接服务(检测) ✅ 集成到 handler.py ✅ 测试各种短链接场景
356 lines
10 KiB
Python
Executable File
356 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
Short link resolver for obsidian-kb skill.
|
||
|
||
Provides utilities for detecting and resolving short links, particularly
|
||
Bilibili short links (b23.tv) which may be blocked by anti-scraping measures.
|
||
|
||
Supported short link services:
|
||
- Bilibili: b23.tv
|
||
- Generic URL shorteners (detection only)
|
||
"""
|
||
|
||
import re
|
||
from typing import Optional, Dict, List, Tuple
|
||
from urllib.parse import urlparse
|
||
|
||
|
||
class ShortLinkResolver:
|
||
"""Short link detection and resolution handler"""
|
||
|
||
# Pattern for Bilibili short links
|
||
BILIBILI_SHORT_PATTERN = re.compile(r'https?://b23\.tv/([a-zA-Z0-9]+)')
|
||
|
||
# Pattern for other short link services
|
||
OTHER_SHORT_PATTERNS = {
|
||
'bit.ly': re.compile(r'https?://bit\.ly/\w+'),
|
||
't.cn': re.compile(r'https?://t\.cn/\w+'),
|
||
'dwz.cn': re.compile(r'https?://dwz\.cn/\w+'),
|
||
'goo.gl': re.compile(r'https?://goo\.gl/\w+'),
|
||
'tinyurl.com': re.compile(r'https?://tinyurl\.com/\w+'),
|
||
}
|
||
|
||
@staticmethod
|
||
def detect_short_links(text: str) -> List[Dict[str, str]]:
|
||
"""
|
||
Detect short links in text.
|
||
|
||
Args:
|
||
text: Input text to search
|
||
|
||
Returns:
|
||
List of detected short links with metadata:
|
||
[
|
||
{
|
||
'url': 'https://b23.tv/xxxxx',
|
||
'platform': 'bilibili',
|
||
'is_short': True
|
||
},
|
||
...
|
||
]
|
||
"""
|
||
detected = []
|
||
|
||
# Check for Bilibili short links
|
||
for match in ShortLinkResolver.BILIBILI_SHORT_PATTERN.finditer(text):
|
||
detected.append({
|
||
'url': match.group(0),
|
||
'code': match.group(1),
|
||
'platform': 'bilibili',
|
||
'is_short': True
|
||
})
|
||
|
||
# Check for other short link services
|
||
for platform, pattern in ShortLinkResolver.OTHER_SHORT_PATTERNS.items():
|
||
for match in pattern.finditer(text):
|
||
detected.append({
|
||
'url': match.group(0),
|
||
'platform': platform,
|
||
'is_short': True
|
||
})
|
||
|
||
return detected
|
||
|
||
@staticmethod
|
||
def is_bilibili_short_link(url: str) -> bool:
|
||
"""
|
||
Check if URL is a Bilibili short link.
|
||
|
||
Args:
|
||
url: URL to check
|
||
|
||
Returns:
|
||
True if URL is a b23.tv short link
|
||
"""
|
||
return bool(ShortLinkResolver.BILIBILI_SHORT_PATTERN.match(url))
|
||
|
||
@staticmethod
|
||
def format_short_link_help(short_link: Optional[str] = None) -> str:
|
||
"""
|
||
Format help message for short links.
|
||
|
||
Args:
|
||
short_link: Optional short link URL to mention
|
||
|
||
Returns:
|
||
Help message
|
||
"""
|
||
lines = [
|
||
"🔗 检测到短链接",
|
||
"",
|
||
"⚠️ 由于反爬虫保护,短链接无法自动解析",
|
||
""
|
||
]
|
||
|
||
if short_link:
|
||
lines.append(f"📍 短链接: {short_link}")
|
||
lines.append("")
|
||
|
||
lines.extend([
|
||
"📱 手机用户:",
|
||
" 1. 点击/打开链接",
|
||
" 2. 在浏览器地址栏复制完整 URL",
|
||
" 3. 发送给助手",
|
||
"",
|
||
"💻 桌面用户:",
|
||
" 1. 发送 BV 号(如:BV1jcFqz7EFW)",
|
||
" 2. 或使用完整 URL",
|
||
"",
|
||
"💡 提示:完整 URL 格式为:https://www.bilibili.com/video/BVxxxxx"
|
||
])
|
||
|
||
return "\n".join(lines)
|
||
|
||
@staticmethod
|
||
def format_short_link_warning(short_links: List[Dict[str, str]]) -> str:
|
||
"""
|
||
Format warning message for multiple short links.
|
||
|
||
Args:
|
||
short_links: List of short link metadata
|
||
|
||
Returns:
|
||
Warning message
|
||
"""
|
||
if not short_links:
|
||
return ""
|
||
|
||
lines = [
|
||
"⚠️ 检测到短链接",
|
||
""
|
||
]
|
||
|
||
for i, link in enumerate(short_links, 1):
|
||
platform = link.get('platform', 'unknown')
|
||
url = link['url']
|
||
|
||
if platform == 'bilibili':
|
||
lines.append(f" [{i}] B站短链接: {url}")
|
||
lines.append(" 需要完整URL或BV号")
|
||
else:
|
||
lines.append(f" [{i}] {platform}: {url}")
|
||
lines.append(" 需要完整URL")
|
||
|
||
lines.append("")
|
||
|
||
lines.extend([
|
||
"💡 建议:",
|
||
" - 在浏览器打开链接后复制完整URL",
|
||
" - 或者直接发送BV号(仅限B站)"
|
||
])
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
class PlaywrightResolver:
|
||
"""
|
||
Browser-based short link resolver using Playwright.
|
||
|
||
This is an optional implementation for environments where Playwright
|
||
is available. It can bypass anti-scraping measures by simulating
|
||
a real browser.
|
||
|
||
Requirements:
|
||
pip install playwright
|
||
playwright install chromium
|
||
"""
|
||
|
||
def __init__(self, headless: bool = True, timeout: int = 10000):
|
||
"""
|
||
Initialize Playwright resolver.
|
||
|
||
Args:
|
||
headless: Whether to run browser in headless mode
|
||
timeout: Page navigation timeout in milliseconds
|
||
"""
|
||
self.headless = headless
|
||
self.timeout = timeout
|
||
self._playwright = None
|
||
|
||
def _check_playwright_available(self) -> bool:
|
||
"""Check if Playwright is installed and available"""
|
||
try:
|
||
from playwright.sync_api import sync_playwright
|
||
return True
|
||
except ImportError:
|
||
return False
|
||
|
||
def resolve(self, short_url: str) -> Optional[str]:
|
||
"""
|
||
Resolve short link using Playwright browser automation.
|
||
|
||
Args:
|
||
short_url: Short URL to resolve
|
||
|
||
Returns:
|
||
Final URL after redirects, or None if resolution fails
|
||
"""
|
||
if not self._check_playwright_available():
|
||
raise ImportError(
|
||
"Playwright not installed. Install with: pip install playwright && playwright install chromium"
|
||
)
|
||
|
||
from playwright.sync_api import sync_playwright
|
||
|
||
try:
|
||
with sync_playwright() as p:
|
||
# Launch browser
|
||
browser = p.chromium.launch(headless=self.headless)
|
||
context = browser.new_context(
|
||
user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||
viewport={'width': 1920, 'height': 1080}
|
||
)
|
||
page = context.new_page()
|
||
|
||
# Navigate to short URL
|
||
page.goto(short_url, timeout=self.timeout, wait_until='networkidle')
|
||
|
||
# Get final URL
|
||
final_url = page.url
|
||
|
||
# Close browser
|
||
browser.close()
|
||
|
||
return final_url
|
||
|
||
except Exception as e:
|
||
print(f"Playwright resolution failed: {e}")
|
||
return None
|
||
|
||
def resolve_bilibili_short(self, short_url: str) -> Optional[str]:
|
||
"""
|
||
Resolve Bilibili short link and extract BV ID.
|
||
|
||
Args:
|
||
short_url: Bilibili short URL (e.g., https://b23.tv/xxxxx)
|
||
|
||
Returns:
|
||
Full Bilibili URL with BV ID, or None if resolution fails
|
||
"""
|
||
final_url = self.resolve(short_url)
|
||
if not final_url:
|
||
return None
|
||
|
||
# Check if final URL contains BV ID
|
||
if 'bilibili.com/video/' in final_url:
|
||
return final_url
|
||
|
||
# Extract BV ID if present in URL
|
||
bv_pattern = re.compile(r'(BV[a-zA-Z0-9]{10})')
|
||
match = bv_pattern.search(final_url)
|
||
if match:
|
||
return f"https://www.bilibili.com/video/{match.group(1)}"
|
||
|
||
return None
|
||
|
||
|
||
def get_resolver() -> ShortLinkResolver:
|
||
"""
|
||
Get the default resolver instance.
|
||
|
||
Returns:
|
||
ShortLinkResolver instance
|
||
"""
|
||
return ShortLinkResolver()
|
||
|
||
|
||
def detect_and_format(text: str) -> Tuple[bool, Optional[str]]:
|
||
"""
|
||
Detect short links in text and format appropriate message.
|
||
|
||
Args:
|
||
text: Input text to search
|
||
|
||
Returns:
|
||
Tuple of (has_short_links, formatted_message)
|
||
- has_short_links: True if short links detected
|
||
- formatted_message: Help/warning message if short links found, None otherwise
|
||
"""
|
||
resolver = ShortLinkResolver()
|
||
short_links = resolver.detect_short_links(text)
|
||
|
||
if not short_links:
|
||
return False, None
|
||
|
||
# Determine appropriate message based on number and type of links
|
||
if len(short_links) == 1:
|
||
link = short_links[0]
|
||
if link['platform'] == 'bilibili':
|
||
return True, resolver.format_short_link_help(link['url'])
|
||
else:
|
||
return True, resolver.format_short_link_warning([link])
|
||
else:
|
||
return True, resolver.format_short_link_warning(short_links)
|
||
|
||
|
||
# CLI interface for testing
|
||
def main():
|
||
"""CLI for testing the short link resolver."""
|
||
import sys
|
||
|
||
if len(sys.argv) < 2:
|
||
print("Usage: python3 shortlink_resolver.py 'text to test'")
|
||
print("\nExamples:")
|
||
print(' python3 shortlink_resolver.py "Check this: https://b23.tv/oLBZZfK"')
|
||
print(' python3 shortlink_resolver.py "https://bit.ly/xxxxx"')
|
||
print('\nOptions:')
|
||
print(' --playwright Try to resolve short links using Playwright')
|
||
sys.exit(1)
|
||
|
||
text = ' '.join(sys.argv[1:])
|
||
|
||
# Check if we should try Playwright
|
||
use_playwright = '--playwright' in sys.argv
|
||
if use_playwright:
|
||
text = text.replace('--playwright', '').strip()
|
||
resolver = PlaywrightResolver()
|
||
|
||
# Find short links
|
||
short_links = ShortLinkResolver.detect_short_links(text)
|
||
|
||
for link in short_links:
|
||
url = link['url']
|
||
print(f"Resolving: {url}")
|
||
|
||
if link['platform'] == 'bilibili':
|
||
resolved = resolver.resolve_bilibili_short(url)
|
||
else:
|
||
resolved = resolver.resolve(url)
|
||
|
||
if resolved:
|
||
print(f" → {resolved}")
|
||
else:
|
||
print(f" ✗ Resolution failed")
|
||
else:
|
||
# Just detect and show help
|
||
has_links, message = detect_and_format(text)
|
||
|
||
if has_links:
|
||
print(message)
|
||
else:
|
||
print("No short links detected")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|