feat: improve media capture routing and refresh pomeranian icon set

This commit is contained in:
tongki078
2026-02-26 13:02:37 +09:00
parent b223ebd945
commit b9a4faa5f1
35 changed files with 248 additions and 25 deletions

View File

@@ -22,6 +22,19 @@ let downloadHooked = false
let contextMenuHooked = false
let contextMenuUpdateInFlight: Promise<void> | null = null
const MP4_PREFERRED_FORMAT = 'bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best'
const SITE_STRATEGIES: Array<{
hosts: string[]
extractor: 'yt-dlp' | 'aria2'
format?: string
}> = [
{
hosts: ['youtube.com', 'www.youtube.com', 'm.youtube.com', 'youtu.be'],
extractor: 'yt-dlp',
format: MP4_PREFERRED_FORMAT,
},
]
function urlFingerprint(raw: string): string {
try {
const u = new URL(raw)
@@ -115,7 +128,9 @@ async function transferUrlToGdown(
url: string,
referer = '',
extractor?: 'yt-dlp' | 'aria2',
format?: string
format?: string,
out?: string,
extra?: { cookie?: string; userAgent?: string; authorization?: string; proxy?: string }
): Promise<{ ok: boolean; error?: string }> {
if (shouldSuppressDuplicateTransfer(url)) {
return { ok: false, error: 'duplicate transfer suppressed' }
@@ -132,8 +147,13 @@ async function transferUrlToGdown(
rpcSecret: settings.motrixAPIkey,
referer,
split: 64,
out: out?.trim() || undefined,
cookie: extra?.cookie?.trim() || undefined,
userAgent: extra?.userAgent?.trim() || undefined,
authorization: extra?.authorization?.trim() || undefined,
proxy: extra?.proxy?.trim() || undefined,
extractor: extractor === 'yt-dlp' ? 'yt-dlp' : undefined,
format: extractor === 'yt-dlp' ? format || 'bestvideo*+bestaudio/best' : undefined,
format: extractor === 'yt-dlp' ? format || MP4_PREFERRED_FORMAT : undefined,
})
if (!nativeResult?.ok) {
@@ -180,6 +200,42 @@ async function transferUrlToGdown(
}
}
function hostOf(raw: string): string {
try {
return new URL(raw).hostname.toLowerCase()
} catch {
return ''
}
}
function resolveStrategy(
url: string,
referer = '',
kind = ''
): { extractor: 'yt-dlp' | 'aria2'; format?: string } {
const candidates = [hostOf(url), hostOf(referer)].filter(Boolean)
for (const rule of SITE_STRATEGIES) {
if (candidates.some((host) => rule.hosts.includes(host))) {
return { extractor: rule.extractor, format: rule.format }
}
}
const mediaKind = kind.toLowerCase()
if (mediaKind === 'm3u8' || mediaKind === 'm3u' || mediaKind === 'hls') {
return { extractor: 'yt-dlp', format: 'best' }
}
if (mediaKind === 'mp4') {
return { extractor: 'aria2' }
}
return { extractor: 'aria2' }
}
function getHeaderFromRequestHeaders(headers: any[], name: string): string {
const lower = name.toLowerCase()
const found = headers.find((item: any) => String(item?.name || '').toLowerCase() === lower)
return String(found?.value || '').trim()
}
async function shouldCaptureRequest(details: any): Promise<boolean> {
if (details.type !== 'main_frame') return false
if ((details.method || '').toUpperCase() !== 'GET') return false
@@ -223,9 +279,66 @@ async function interceptMediaByWebRequest(details: any): Promise<void> {
const req = pendingRequests.get(details.requestId)
const referer = String(req?.documentUrl || req?.originUrl || req?.initiator || req?.url || '')
const requestHeaders = Array.isArray(req?.requestHeaders) ? req.requestHeaders : []
const cookie = getHeaderFromRequestHeaders(requestHeaders, 'cookie')
const userAgent = getHeaderFromRequestHeaders(requestHeaders, 'user-agent')
const candidate = makeMediaCandidate(details, referer)
await upsertMediaCandidate(candidate, mediaFingerprint(candidate.url))
rememberMediaFingerprint(candidate.url)
let pageTitle = ''
if (candidate.tabId >= 0) {
const tab = await browser.tabs.get(candidate.tabId).catch(() => null)
pageTitle = String(tab?.title || '').trim()
}
const suggestedOut = suggestMediaOut(candidate.url, candidate.kind, pageTitle)
const enriched = {
...candidate,
pageTitle: pageTitle || undefined,
cookie: cookie || undefined,
userAgent: userAgent || undefined,
suggestedOut: suggestedOut || undefined,
}
await upsertMediaCandidate(enriched, mediaFingerprint(enriched.url))
rememberMediaFingerprint(enriched.url)
if (enriched.tabId >= 0) {
await browser.tabs
.sendMessage(enriched.tabId, {
type: 'media:captured',
kind: enriched.kind,
url: enriched.url,
suggestedOut: enriched.suggestedOut || '',
})
.catch(() => null)
}
}
function sanitizeOut(value: string): string {
let next = value
.trim()
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, ' ')
.replace(/^\.+/, '')
.replace(/\.+$/, '')
if (next.length > 180) next = next.slice(0, 180).trim()
return next
}
function extFromUrl(url: string): string {
try {
const path = new URL(url).pathname.toLowerCase()
const match = path.match(/\.([a-z0-9]{2,6})(?:$|[?#])/)
return match?.[1] || ''
} catch {
return ''
}
}
function suggestMediaOut(url: string, kind: string, pageTitle: string): string {
const title = sanitizeOut(pageTitle || '')
const ext = extFromUrl(url)
const baseExt = ext || (kind === 'mp4' ? 'mp4' : kind === 'm3u8' || kind === 'm3u' || kind === 'hls' ? 'mp4' : '')
if (!title) return ''
if (!baseExt) return title
if (title.toLowerCase().endsWith(`.${baseExt}`)) return title
return `${title}.${baseExt}`
}
async function applyShelfVisibility(): Promise<void> {
@@ -311,17 +424,18 @@ async function handleContextMenuClick(data: any, tab?: any): Promise<void> {
return
}
const useYtDlp = !linkUrl && !!pageUrl
const strategy = resolveStrategy(url, String(data?.pageUrl || tab?.url || ''), '')
const result = await transferUrlToGdown(
url,
String(data?.pageUrl || tab?.url || ''),
useYtDlp ? 'yt-dlp' : 'aria2'
strategy.extractor,
strategy.format
)
if (!result.ok) {
await notify(`전송 실패: ${result.error || 'unknown error'}`)
return
}
await notify(useYtDlp ? '페이지 URL을 yt-dlp로 gdown에 전송했습니다.' : 'gdown으로 전송했습니다.')
await notify(strategy.extractor === 'yt-dlp' ? '페이지 URL을 yt-dlp로 gdown에 전송했습니다.' : 'gdown으로 전송했습니다.')
}
function createContextMenuSafe(): void {
@@ -404,8 +518,17 @@ browser.runtime.onMessage.addListener((message: any, sender: any) => {
if (message?.type === 'media:enqueue') {
const url = String(message?.url || '').trim()
const kind = String(message?.kind || '').trim()
const suggestedOut = String(message?.suggestedOut || '').trim()
const referer = String(message?.referer || '').trim()
const cookie = String(message?.cookie || '').trim()
const userAgent = String(message?.userAgent || '').trim()
if (!url) return Promise.resolve({ ok: false, error: 'url is empty' })
return transferUrlToGdown(url, String(message?.referer || '')).then((result) => result)
const strategy = resolveStrategy(url, referer, kind)
return transferUrlToGdown(url, referer, strategy.extractor, strategy.format, suggestedOut, {
cookie,
userAgent,
}).then((result) => result)
}
if (message?.type === 'page:enqueue-ytdlp') {