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

@@ -1,7 +1,7 @@
{
"name": "gomdown-helper",
"private": true,
"version": "0.0.11",
"version": "0.0.16",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,6 +1,6 @@
{
"../../../../../@crx/manifest": {
"file": "assets/crx-manifest.js-B0cyAIB1.js",
"file": "assets/crx-manifest.js-mSo6-ym3.js",
"name": "crx-manifest.js",
"src": "../../../../../@crx/manifest",
"isEntry": true
@@ -20,9 +20,9 @@
"file": "assets/downloadIntent-Dv31jC2S.js",
"name": "downloadIntent"
},
"_index.ts-loader-BHtfStLc.js": {
"file": "assets/index.ts-loader-BHtfStLc.js",
"src": "_index.ts-loader-BHtfStLc.js"
"_index.ts-loader-Bju9eGS_.js": {
"file": "assets/index.ts-loader-Bju9eGS_.js",
"src": "_index.ts-loader-Bju9eGS_.js"
},
"_settings-Bo6W9Drl.js": {
"file": "assets/settings-Bo6W9Drl.js",
@@ -32,7 +32,7 @@
]
},
"src/background/index.ts": {
"file": "assets/index.ts-BAxKsZ8F.js",
"file": "assets/index.ts-BljhweV3.js",
"name": "index.ts",
"src": "src/background/index.ts",
"isEntry": true,
@@ -57,7 +57,7 @@
]
},
"src/content/index.ts": {
"file": "assets/index.ts-CMnPQ13j.js",
"file": "assets/index.ts-C6ePCen1.js",
"name": "index.ts",
"src": "src/content/index.ts",
"isEntry": true,
@@ -67,7 +67,7 @@
]
},
"src/popup/index.html": {
"file": "assets/index.html-Tb8yZAds.js",
"file": "assets/index.html-BLzIyLM-.js",
"name": "index.html",
"src": "src/popup/index.html",
"isEntry": true,

View File

@@ -0,0 +1 @@
import{c as k,j as t,R as r}from"./client-CBvt1tWS.js";import{b as i}from"./browser-polyfill-CZ_dLIqp.js";import{g as N,s as m}from"./settings-Bo6W9Drl.js";function b(){const[s,d]=r.useState(null),[x,n]=r.useState(""),[l,u]=r.useState([]);r.useEffect(()=>{N().then(d)},[]),r.useEffect(()=>{let e=null;const a=async()=>{const o=await i.runtime.sendMessage({type:"media:list"});o?.ok&&Array.isArray(o.items)&&u(o.items.slice(0,10))};return a(),e=window.setInterval(()=>{a()},2e3),()=>{e!==null&&window.clearInterval(e)}},[]);const c=(e,a)=>{d(o=>o&&{...o,[e]:a})},h=async e=>{if(!s)return;const a={...s,extensionStatus:e};d(a),await m({extensionStatus:e}),n(e?"Extension ON":"Extension OFF"),window.setTimeout(()=>n(""),1200)},g=async()=>{s&&(await m(s),n("Saved"),window.setTimeout(()=>n(""),1200))},p=async()=>{const e=i.runtime.getURL("src/config/index.html");await i.tabs.create({url:e})},j=async()=>{const e=i.runtime.getURL("src/history/index.html");await i.tabs.create({url:e})},v=async()=>{const e=await i.runtime.sendMessage({type:"page:enqueue-ytdlp"});n(e?.ok?"Active tab sent to gdown (yt-dlp)":`Send failed: ${e?.error||"unknown error"}`),window.setTimeout(()=>n(""),1800)},w=async e=>{const a=await i.runtime.sendMessage({type:"media:enqueue",url:e.url,referer:e.referer||"",kind:e.kind,suggestedOut:e.suggestedOut||"",cookie:e.cookie||"",userAgent:e.userAgent||""});n(a?.ok?"Media sent to gdown":`Send failed: ${a?.error||"unknown error"}`),window.setTimeout(()=>n(""),1600)},y=async()=>{await i.runtime.sendMessage({type:"media:clear"}),u([]),n("Captured media cleared"),window.setTimeout(()=>n(""),1200)};return s?t.jsxs("div",{className:"container",children:[t.jsx("h1",{children:"Gomdown Helper"}),t.jsxs("div",{className:"field",children:[t.jsx("label",{children:"RPC Secret"}),t.jsx("input",{type:"text",value:s.motrixAPIkey,onChange:e=>c("motrixAPIkey",e.target.value),placeholder:"aria2 rpc secret"})]}),t.jsxs("div",{className:"field",children:[t.jsx("label",{children:"RPC Port"}),t.jsx("input",{type:"number",value:s.motrixPort,onChange:e=>c("motrixPort",Number(e.target.value||16800))})]}),t.jsxs("label",{className:"toggle",children:["Extension Enabled",t.jsx("input",{type:"checkbox",checked:s.extensionStatus,onChange:e=>{h(e.target.checked)}})]}),t.jsxs("label",{className:"toggle",children:["Use Native Host",t.jsx("input",{type:"checkbox",checked:s.useNativeHost,onChange:e=>c("useNativeHost",e.target.checked)})]}),t.jsxs("label",{className:"toggle",children:["Activate gdown App",t.jsx("input",{type:"checkbox",checked:s.activateAppOnDownload,onChange:e=>c("activateAppOnDownload",e.target.checked)})]}),t.jsx("button",{onClick:g,children:"Save"}),t.jsx("button",{onClick:p,children:"Settings"}),t.jsx("button",{onClick:j,children:"History"}),t.jsx("button",{onClick:()=>{v()},children:"Send Active Tab (yt-dlp)"}),t.jsxs("div",{className:"media-panel",children:[t.jsxs("div",{className:"media-head",children:[t.jsx("strong",{children:"Captured Media"}),t.jsx("button",{className:"mini ghost",onClick:y,children:"Clear"})]}),l.length===0?t.jsx("div",{className:"empty",children:"No media captured yet"}):t.jsx("div",{className:"media-list",children:l.map(e=>t.jsxs("div",{className:"media-item",children:[t.jsxs("div",{className:"media-meta",children:[t.jsx("span",{className:"kind",children:e.kind.toUpperCase()}),e.pageTitle?t.jsx("span",{className:"url",children:e.pageTitle}):null,t.jsx("span",{className:"url",children:e.url})]}),t.jsx("button",{className:"mini",onClick:()=>{w(e)},children:"Add"})]},e.id))})]}),t.jsx("div",{className:"status",children:x})]}):t.jsx("div",{className:"container",children:"Loading..."})}k.createRoot(document.getElementById("root")).render(t.jsx(r.StrictMode,{children:t.jsx(b,{})}));

View File

@@ -1 +0,0 @@
import{c as k,j as t,R as r}from"./client-CBvt1tWS.js";import{b as i}from"./browser-polyfill-CZ_dLIqp.js";import{g as N,s as m}from"./settings-Bo6W9Drl.js";function b(){const[s,d]=r.useState(null),[x,n]=r.useState(""),[l,u]=r.useState([]);r.useEffect(()=>{N().then(d)},[]),r.useEffect(()=>{let e=null;const a=async()=>{const o=await i.runtime.sendMessage({type:"media:list"});o?.ok&&Array.isArray(o.items)&&u(o.items.slice(0,10))};return a(),e=window.setInterval(()=>{a()},2e3),()=>{e!==null&&window.clearInterval(e)}},[]);const c=(e,a)=>{d(o=>o&&{...o,[e]:a})},h=async e=>{if(!s)return;const a={...s,extensionStatus:e};d(a),await m({extensionStatus:e}),n(e?"Extension ON":"Extension OFF"),window.setTimeout(()=>n(""),1200)},p=async()=>{s&&(await m(s),n("Saved"),window.setTimeout(()=>n(""),1200))},g=async()=>{const e=i.runtime.getURL("src/config/index.html");await i.tabs.create({url:e})},j=async()=>{const e=i.runtime.getURL("src/history/index.html");await i.tabs.create({url:e})},v=async()=>{const e=await i.runtime.sendMessage({type:"page:enqueue-ytdlp"});n(e?.ok?"Active tab sent to gdown (yt-dlp)":`Send failed: ${e?.error||"unknown error"}`),window.setTimeout(()=>n(""),1800)},w=async e=>{const a=await i.runtime.sendMessage({type:"media:enqueue",url:e.url,referer:e.referer||""});n(a?.ok?"Media sent to gdown":`Send failed: ${a?.error||"unknown error"}`),window.setTimeout(()=>n(""),1600)},y=async()=>{await i.runtime.sendMessage({type:"media:clear"}),u([]),n("Captured media cleared"),window.setTimeout(()=>n(""),1200)};return s?t.jsxs("div",{className:"container",children:[t.jsx("h1",{children:"Gomdown Helper"}),t.jsxs("div",{className:"field",children:[t.jsx("label",{children:"RPC Secret"}),t.jsx("input",{type:"text",value:s.motrixAPIkey,onChange:e=>c("motrixAPIkey",e.target.value),placeholder:"aria2 rpc secret"})]}),t.jsxs("div",{className:"field",children:[t.jsx("label",{children:"RPC Port"}),t.jsx("input",{type:"number",value:s.motrixPort,onChange:e=>c("motrixPort",Number(e.target.value||16800))})]}),t.jsxs("label",{className:"toggle",children:["Extension Enabled",t.jsx("input",{type:"checkbox",checked:s.extensionStatus,onChange:e=>{h(e.target.checked)}})]}),t.jsxs("label",{className:"toggle",children:["Use Native Host",t.jsx("input",{type:"checkbox",checked:s.useNativeHost,onChange:e=>c("useNativeHost",e.target.checked)})]}),t.jsxs("label",{className:"toggle",children:["Activate gdown App",t.jsx("input",{type:"checkbox",checked:s.activateAppOnDownload,onChange:e=>c("activateAppOnDownload",e.target.checked)})]}),t.jsx("button",{onClick:p,children:"Save"}),t.jsx("button",{onClick:g,children:"Settings"}),t.jsx("button",{onClick:j,children:"History"}),t.jsx("button",{onClick:()=>{v()},children:"Send Active Tab (yt-dlp)"}),t.jsxs("div",{className:"media-panel",children:[t.jsxs("div",{className:"media-head",children:[t.jsx("strong",{children:"Captured Media"}),t.jsx("button",{className:"mini ghost",onClick:y,children:"Clear"})]}),l.length===0?t.jsx("div",{className:"empty",children:"No media captured yet"}):t.jsx("div",{className:"media-list",children:l.map(e=>t.jsxs("div",{className:"media-item",children:[t.jsxs("div",{className:"media-meta",children:[t.jsx("span",{className:"kind",children:e.kind.toUpperCase()}),t.jsx("span",{className:"url",children:e.url})]}),t.jsx("button",{className:"mini",onClick:()=>{w(e)},children:"Add"})]},e.id))})]}),t.jsx("div",{className:"status",children:x})]}):t.jsx("div",{className:"container",children:"Loading..."})}k.createRoot(document.getElementById("root")).render(t.jsx(r.StrictMode,{children:t.jsx(b,{})}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
import{b as m}from"./browser-polyfill-CZ_dLIqp.js";import{i as a,n as k}from"./downloadIntent-Dv31jC2S.js";const E=8e3,i=new Map;function v(){const e=Date.now();for(const[r,t]of i.entries())t<=e&&i.delete(r)}async function u(e,r){const t=k(e,window.location.href);if(!t)return!1;if(v(),i.has(t))return!0;i.set(t,Date.now()+E);try{if((await m.runtime.sendMessage({type:"capture-link-download",url:t,referer:r||document.referrer||window.location.href}))?.ok)return!0}catch{}return i.delete(t),!1}function p(e){return e?e instanceof HTMLAnchorElement?e:e instanceof Element?e.closest("a[href]"):null:null}function h(e){return!!(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)}async function g(e){if(e.defaultPrevented||h(e))return;const r=p(e.target);if(!r)return;const t=r.href||"";!t||!a(t,window.location.href)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),await u(t,document.referrer||window.location.href))}function b(e){const r=p(e.target);if(!r)return;const t=r.href||"";!t||!a(t,window.location.href)||h(e)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),u(t,document.referrer||window.location.href))}document.addEventListener("pointerdown",e=>{e.button===0&&b(e)},!0);document.addEventListener("mousedown",e=>{e.button===0&&b(e)},!0);document.addEventListener("click",e=>{e.button===0&&g(e)},!0);document.addEventListener("keydown",e=>{if(e.key!=="Enter"||e.defaultPrevented||h(e))return;const r=p(e.target);if(!r)return;const t=r.href||"";!t||!a(t,window.location.href)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),u(t,document.referrer||window.location.href))},!0);document.addEventListener("auxclick",e=>{e.button===1&&g(e)},!0);function C(){try{const e=window.open.bind(window);window.open=function(t,n,x){const d=String(t||"").trim();return d&&a(d,window.location.href)?(u(d,window.location.href),null):e(t,n,x)}}catch{}try{const e=HTMLAnchorElement.prototype.click;HTMLAnchorElement.prototype.click=function(){const t=this.href||this.getAttribute("href")||"";if(t&&a(t,window.location.href)){u(t,document.referrer||window.location.href);return}e.call(this)}}catch{}}C();let l=null,o=null,w=!1,y=null,s=window.location.href;function L(e){try{const r=new URL(e);return r.hostname!=="www.youtube.com"&&r.hostname!=="youtube.com"?!1:r.pathname==="/watch"&&r.searchParams.has("v")}catch{return!1}}function c(e,r="idle"){o&&(o.textContent=e,r==="ok"?o.style.color="#8ff0a4":r==="error"?o.style.color="#ff9b9b":o.style.color="#aeb7d8")}async function P(){if(!w){w=!0,c("gdown으로 전송 중...");try{const e=await m.runtime.sendMessage({type:"page:enqueue-ytdlp-url",url:window.location.href,referer:window.location.href});e?.ok?c("다운로드 모달로 전송됨","ok"):c(`전송 실패: ${e?.error||"unknown error"}`,"error")}catch(e){c(`전송 실패: ${String(e)}`,"error")}finally{w=!1}}}function I(){l&&(l.remove(),l=null,o=null)}function f(){if(window.top!==window.self)return;if(!L(window.location.href)){I();return}if(l)return;const e=document.createElement("div");e.id="gomdown-youtube-overlay",e.style.position="fixed",e.style.right="20px",e.style.bottom="24px",e.style.zIndex="2147483647",e.style.background="rgba(17, 21, 32, 0.94)",e.style.border="1px solid rgba(133, 148, 195, 0.35)",e.style.borderRadius="12px",e.style.padding="10px",e.style.boxShadow="0 8px 24px rgba(0, 0, 0, 0.28)",e.style.backdropFilter="blur(6px)",e.style.width="220px",e.style.fontFamily="ui-sans-serif, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif",e.style.color="#e8edff";const r=document.createElement("div");r.textContent="Gdown Helper",r.style.fontSize="12px",r.style.fontWeight="700",r.style.marginBottom="8px";const t=document.createElement("button");t.type="button",t.textContent="이 영상 다운로드",t.style.width="100%",t.style.height="34px",t.style.border="1px solid #5a69f0",t.style.borderRadius="8px",t.style.background="#5a69f0",t.style.color="#ffffff",t.style.fontSize="12px",t.style.fontWeight="700",t.style.cursor="pointer",t.addEventListener("click",()=>{P()});const n=document.createElement("div");n.textContent="클릭 시 gdown 다운로드 모달로 연결",n.style.fontSize="11px",n.style.marginTop="8px",n.style.lineHeight="1.35",n.style.color="#aeb7d8",e.appendChild(r),e.appendChild(t),e.appendChild(n),document.documentElement.appendChild(e),l=e,o=n}function S(){y===null&&(y=window.setInterval(()=>{const e=window.location.href;e!==s&&(s=e,f())},800),window.addEventListener("popstate",()=>{s=window.location.href,f()}),document.addEventListener("yt-navigate-finish",()=>{s=window.location.href,f()}))}f();S();

View File

@@ -5,7 +5,7 @@
(async () => {
const { onExecute } = await import(
/* @vite-ignore */
chrome.runtime.getURL("assets/index.ts-CMnPQ13j.js")
chrome.runtime.getURL("assets/index.ts-C6ePCen1.js")
);
onExecute?.({ perf: { injectTime, loadTime: performance.now() - injectTime } });
})().catch(console.error);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,17 @@
<svg xmlns='http://www.w3.org/2000/svg' width='512' height='512' viewBox='0 0 512 512'>
<rect width='512' height='512' rx='92' fill='#ffffff'/>
<g transform='translate(0 6) scale(1.08) translate(-19 -20)' fill='none' stroke='#101010' stroke-linecap='round' stroke-linejoin='round'>
<path d='M150 204 L102 136 L200 164 Z' stroke-width='22' fill='#fff'/>
<path d='M362 204 L410 136 L312 164 Z' stroke-width='22' fill='#fff'/>
<circle cx='256' cy='282' r='146' stroke-width='24' fill='#fff'/>
<path d='M176 246 C171 220,188 202,210 205 C230 184,256 182,277 204 C299 201,318 220,314 245 C336 257,342 279,336 299 C341 324,327 344,303 352 C286 374,228 374,209 352 C185 344,171 324,176 299 C170 280,173 258,176 246 Z' stroke-width='16' fill='#fff'/>
<circle cx='214' cy='272' r='16' fill='#101010' stroke='none'/>
<circle cx='298' cy='272' r='16' fill='#101010' stroke='none'/>
<circle cx='209' cy='266' r='5' fill='#fff' stroke='none'/>
<circle cx='293' cy='266' r='5' fill='#fff' stroke='none'/>
<ellipse cx='256' cy='314' rx='24' ry='18' fill='#101010' stroke='none'/>
<path d='M220 340 C236 356,276 356,292 340' stroke-width='12'/>
<path d='M244 331 L232 344' stroke-width='8'/>
<path d='M268 331 L280 344' stroke-width='8'/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "Gomdown Helper",
"description": "Send browser downloads to gdown",
"version": "0.0.11",
"version": "0.0.16",
"default_locale": "en",
"icons": {
"16": "images/16.png",
@@ -28,7 +28,7 @@
"content_scripts": [
{
"js": [
"assets/index.ts-loader-BHtfStLc.js"
"assets/index.ts-loader-Bju9eGS_.js"
],
"matches": [
"<all_urls>"
@@ -59,7 +59,7 @@
"images/*",
"assets/browser-polyfill-CZ_dLIqp.js",
"assets/downloadIntent-Dv31jC2S.js",
"assets/index.ts-CMnPQ13j.js"
"assets/index.ts-C6ePCen1.js"
],
"use_dynamic_url": false
}

View File

@@ -1 +1 @@
import './assets/index.ts-BAxKsZ8F.js';
import './assets/index.ts-BljhweV3.js';

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Gomdown Helper</title>
<script type="module" crossorigin src="/assets/index.html-Tb8yZAds.js"></script>
<script type="module" crossorigin src="/assets/index.html-BLzIyLM-.js"></script>
<link rel="modulepreload" crossorigin href="/assets/browser-polyfill-CZ_dLIqp.js">
<link rel="modulepreload" crossorigin href="/assets/client-CBvt1tWS.js">
<link rel="modulepreload" crossorigin href="/assets/settings-Bo6W9Drl.js">

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 597 B

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,17 @@
<svg xmlns='http://www.w3.org/2000/svg' width='512' height='512' viewBox='0 0 512 512'>
<rect width='512' height='512' rx='92' fill='#ffffff'/>
<g transform='translate(0 6) scale(1.08) translate(-19 -20)' fill='none' stroke='#101010' stroke-linecap='round' stroke-linejoin='round'>
<path d='M150 204 L102 136 L200 164 Z' stroke-width='22' fill='#fff'/>
<path d='M362 204 L410 136 L312 164 Z' stroke-width='22' fill='#fff'/>
<circle cx='256' cy='282' r='146' stroke-width='24' fill='#fff'/>
<path d='M176 246 C171 220,188 202,210 205 C230 184,256 182,277 204 C299 201,318 220,314 245 C336 257,342 279,336 299 C341 324,327 344,303 352 C286 374,228 374,209 352 C185 344,171 324,176 299 C170 280,173 258,176 246 Z' stroke-width='16' fill='#fff'/>
<circle cx='214' cy='272' r='16' fill='#101010' stroke='none'/>
<circle cx='298' cy='272' r='16' fill='#101010' stroke='none'/>
<circle cx='209' cy='266' r='5' fill='#fff' stroke='none'/>
<circle cx='293' cy='266' r='5' fill='#fff' stroke='none'/>
<ellipse cx='256' cy='314' rx='24' ry='18' fill='#101010' stroke='none'/>
<path d='M220 340 C236 356,276 356,292 340' stroke-width='12'/>
<path d='M244 331 L232 344' stroke-width='8'/>
<path d='M268 331 L280 344' stroke-width='8'/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

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') {

View File

@@ -279,3 +279,56 @@ function watchYoutubeRouteChanges(): void {
ensureYoutubeOverlay()
watchYoutubeRouteChanges()
let mediaToastRoot: HTMLDivElement | null = null
let mediaToastTimer: number | null = null
function ensureMediaToastRoot(): HTMLDivElement {
if (mediaToastRoot) return mediaToastRoot
const root = document.createElement('div')
root.id = 'gomdown-media-toast'
root.style.position = 'fixed'
root.style.left = '18px'
root.style.bottom = '18px'
root.style.zIndex = '2147483647'
root.style.maxWidth = '360px'
root.style.padding = '10px 12px'
root.style.borderRadius = '10px'
root.style.border = '1px solid rgba(128, 140, 180, 0.42)'
root.style.background = 'rgba(18, 21, 31, 0.95)'
root.style.color = '#dce4fa'
root.style.fontSize = '12px'
root.style.lineHeight = '1.35'
root.style.fontFamily = 'ui-sans-serif, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif'
root.style.boxShadow = '0 10px 24px rgba(0, 0, 0, 0.28)'
root.style.display = 'none'
document.documentElement.appendChild(root)
mediaToastRoot = root
return root
}
function showMediaCapturedToast(payload: { kind?: string; url?: string; suggestedOut?: string }): void {
const root = ensureMediaToastRoot()
const kind = String(payload?.kind || 'media').toUpperCase()
const out = String(payload?.suggestedOut || '').trim()
const shortUrl = String(payload?.url || '').trim().slice(0, 96)
root.textContent = out
? `캡처됨 [${kind}] ${out}`
: `캡처됨 [${kind}] ${shortUrl}${shortUrl.length >= 96 ? '…' : ''}`
root.style.display = 'block'
if (mediaToastTimer !== null) window.clearTimeout(mediaToastTimer)
mediaToastTimer = window.setTimeout(() => {
root.style.display = 'none'
mediaToastTimer = null
}, 2200)
}
browser.runtime.onMessage.addListener((message: any) => {
if (message?.type === 'media:captured') {
showMediaCapturedToast({
kind: message?.kind,
url: message?.url,
suggestedOut: message?.suggestedOut,
})
}
})

View File

@@ -6,7 +6,11 @@ export type MediaCandidate = {
kind: MediaKind
tabId: number
pageUrl: string
pageTitle?: string
referer: string
cookie?: string
userAgent?: string
suggestedOut?: string
contentType: string
detectedAt: number
}

View File

@@ -9,6 +9,10 @@ type MediaCandidate = {
url: string
kind: 'mp4' | 'm3u8' | 'm3u' | 'hls' | 'unknown'
referer: string
pageTitle?: string
suggestedOut?: string
cookie?: string
userAgent?: string
detectedAt: number
}
@@ -85,6 +89,10 @@ function App(): JSX.Element {
type: 'media:enqueue',
url: item.url,
referer: item.referer || '',
kind: item.kind,
suggestedOut: item.suggestedOut || '',
cookie: item.cookie || '',
userAgent: item.userAgent || '',
})) as { ok?: boolean; error?: string }
setStatus(result?.ok ? 'Media sent to gdown' : `Send failed: ${result?.error || 'unknown error'}`)
window.setTimeout(() => setStatus(''), 1600)
@@ -171,6 +179,7 @@ function App(): JSX.Element {
<div key={item.id} className="media-item">
<div className="media-meta">
<span className="kind">{item.kind.toUpperCase()}</span>
{item.pageTitle ? <span className="url">{item.pageTitle}</span> : null}
<span className="url">{item.url}</span>
</div>
<button className="mini" onClick={() => void onSendMedia(item)}>Add</button>