feat: improve media capture routing and refresh pomeranian icon set
This commit is contained in:
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user