diff --git a/package.json b/package.json index 705bdc9..bf3b2d3 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gomdown-helper", "private": true, - "version": "0.0.11", + "version": "0.0.16", "type": "module", "scripts": { "dev": "vite", diff --git a/packages/chrome/.vite/manifest.json b/packages/chrome/.vite/manifest.json index 561a9de..e14a1c7 100644 --- a/packages/chrome/.vite/manifest.json +++ b/packages/chrome/.vite/manifest.json @@ -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, diff --git a/packages/chrome/assets/index.html-BLzIyLM-.js b/packages/chrome/assets/index.html-BLzIyLM-.js new file mode 100644 index 0000000..87126ce --- /dev/null +++ b/packages/chrome/assets/index.html-BLzIyLM-.js @@ -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,{})})); diff --git a/packages/chrome/assets/index.html-Tb8yZAds.js b/packages/chrome/assets/index.html-Tb8yZAds.js deleted file mode 100644 index 65be8cb..0000000 --- a/packages/chrome/assets/index.html-Tb8yZAds.js +++ /dev/null @@ -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,{})})); diff --git a/packages/chrome/assets/index.ts-BAxKsZ8F.js b/packages/chrome/assets/index.ts-BAxKsZ8F.js deleted file mode 100644 index 6a56ba5..0000000 --- a/packages/chrome/assets/index.ts-BAxKsZ8F.js +++ /dev/null @@ -1 +0,0 @@ -import{b as i}from"./browser-polyfill-CZ_dLIqp.js";import{n as E,a as B}from"./downloadIntent-Dv31jC2S.js";import{g as m}from"./settings-Bo6W9Drl.js";const N="org.gdown.nativehost";async function W(e){return i.runtime.sendNativeMessage(N,{action:"addUri",...e})}async function Y(){return i.runtime.sendNativeMessage(N,{action:"focus"})}const b="history";async function K(){const t=(await i.storage.local.get([b]))[b];return Array.isArray(t)?t:[]}async function Q(e){await i.storage.local.set({[b]:e.slice(0,300)})}async function V(e){const t=await K(),n=t.findIndex(r=>r.gid===e.gid);n>=0?t[n]=e:t.unshift(e),await Q(t)}function F(e,t){const n=Array.isArray(e?.responseHeaders)?e.responseHeaders:[],r=t.toLowerCase(),o=n.find(s=>String(s?.name||"").toLowerCase()===r);return String(o?.value||"")}function X(e){const t=e.toLowerCase();return t?t.includes("application/vnd.apple.mpegurl")||t.includes("application/x-mpegurl")||t.includes("audio/mpegurl")?"m3u8":t.includes("video/mp4")?"mp4":t.includes("application/octet-stream")&&t.includes("m3u8")?"m3u8":t.includes("hls")?"hls":"unknown":"unknown"}function j(e){const t=String(e||"").toLowerCase();return t.includes(".m3u8")?"m3u8":t.includes(".m3u")?"m3u":t.includes(".mp4")?"mp4":t.includes("m3u8")?"m3u8":t.includes("hls")?"hls":"unknown"}function P(e,t){const n=X(t);return n!=="unknown"?n:j(e)}function J(e){if(!e?.url)return!1;const t=String(e?.method||"").toUpperCase();if(t&&t!=="GET")return!1;const n=Number(e?.statusCode||0);if(n>0&&(n<200||n>299))return!1;const r=String(e?.type||"");if(!["xmlhttprequest","media","other","main_frame","sub_frame","fetch"].includes(r))return!1;const o=F(e,"content-type");return P(e.url,o)!=="unknown"}function Z(e,t=""){const n=F(e,"content-type"),r=String(e?.url||""),o=P(r,n),s=Number.isInteger(e?.tabId)?Number(e.tabId):-1,a=Date.now();return{id:`${a}:${s}:${o}:${r}`,url:r,kind:o,tabId:s,pageUrl:String(e?.documentUrl||e?.initiator||""),referer:String(t||e?.documentUrl||e?.initiator||""),contentType:n,detectedAt:a}}function q(e){try{const t=new URL(e);return`${t.protocol}//${t.host}${t.pathname}`.toLowerCase()}catch{return String(e||"").toLowerCase()}}const y="media_candidates",ee=200;async function G(){const t=(await i.storage.local.get([y]))[y];return Array.isArray(t)?t:[]}async function te(e){const t=[...e].sort((n,r)=>r.detectedAt-n.detectedAt);await i.storage.local.set({[y]:t.slice(0,ee)})}async function ne(e,t){const n=await G(),r=n.findIndex(o=>{try{const s=new URL(o.url);return`${s.protocol}//${s.host}${s.pathname}`.toLowerCase()===t}catch{return o.url.toLowerCase()===t}});r>=0?n[r]={...n[r],...e,detectedAt:Date.now()}:n.unshift(e),await te(n)}async function re(){await i.storage.local.set({[y]:[]})}const h=8e3,oe=7e3,C="gomdown-helper-download-context-menu-option",d=new Map,v=new Map,M=new Map,x=new Map,f=new Map,I=new Map,L=new Map;let T=!1,D=!1,$=!1,l=null;function S(e){try{const t=new URL(e),n=(t.pathname||"/").replace(/\/+$/,"")||"/";return`${t.protocol}//${t.host}${n}`.toLowerCase()}catch{return String(e||"").toLowerCase()}}function c(e){const t=Date.now();for(const[n,r]of e.entries())r<=t&&e.delete(n)}function O(e){const t=Date.now()+h;v.set(E(e),t),M.set(S(e),t)}function z(e){!Number.isInteger(e)||(e??-1)<0||I.set(e,Date.now()+h)}function ie(e){c(v),c(M);const t=E(e);return v.has(t)||M.has(S(e))}function se(e){return c(I),!Number.isInteger(e)||(e??-1)<0?!1:I.has(e)}function ae(e){return c(x),x.has(S(e))}function ue(e){x.set(S(e),Date.now()+oe)}function ce(e){return c(f),!!e&&f.has(e)}function le(e){e&&f.set(e,Date.now()+h)}function de(e){c(L);const t=q(e);return L.has(t)}function fe(e){L.set(q(e),Date.now()+h)}async function pe(){try{await Y()}catch{}}async function U(e){await i.notifications.create(`gomdown-notice-${Date.now()}`,{type:"basic",iconUrl:"/images/icon-large.png",title:"Gomdown Helper",message:e}).catch(()=>null)}async function u(e,t="",n,r){if(ae(e))return{ok:!1,error:"duplicate transfer suppressed"};const o=await m();if(!o.extensionStatus)return{ok:!1,error:"extension disabled"};if(!o.motrixAPIkey)return{ok:!1,error:"motrixAPIkey is not set"};try{const s=await W({url:e,rpcPort:o.motrixPort,rpcSecret:o.motrixAPIkey,referer:t,split:64,extractor:n==="yt-dlp"?"yt-dlp":void 0,format:n==="yt-dlp"?r||"bestvideo*+bestaudio/best":void 0});if(!s?.ok)return{ok:!1,error:s?.error||"native host addUri failed"};o.activateAppOnDownload&&await pe(),ue(e);const a=String(s?.gid||s?.requestId||`pending-${Date.now()}`),g=(()=>{try{return new URL(e).pathname}catch{return""}})().split("/").filter(Boolean).pop()||e;return await V({gid:a,downloader:"native",startTime:new Date().toISOString(),icon:"/images/32.png",name:decodeURIComponent(g),path:null,status:s?.pending?"queued":"downloading",size:0,downloaded:0}),o.enableNotifications&&await i.notifications.create(`gomdown-transfer-${Date.now()}`,{type:"basic",iconUrl:"/images/icon-large.png",title:"Gomdown Helper",message:"Download sent to gdown"}),{ok:!0}}catch(s){return{ok:!1,error:String(s)}}}async function me(e){if(e.type!=="main_frame"||(e.method||"").toUpperCase()!=="GET"||typeof e.statusCode=="number"&&(e.statusCode<200||e.statusCode>299))return!1;const t=await m();if(!t.extensionStatus||!t.motrixAPIkey)return!1;const n=String(Array.isArray(e?.responseHeaders)&&e.responseHeaders.find(o=>String(o?.name||"").toLowerCase()==="content-length")?.value||""),r=Number(n||0);return t.minFileSize>0&&r>0&&r{await w();const t=e,n=t.finalUrl||t.url||"";!ie(n)&&!se(t.tabId)||(await i.downloads.cancel(e.id).catch(()=>null),await i.downloads.erase({id:e.id}).catch(()=>null),await i.downloads.removeFile(e.id).catch(()=>null))}))}function A(){T||(T=!0,i.webRequest.onSendHeaders.addListener(e=>{d.set(e.requestId,e)},{urls:[""]},["requestHeaders","extraHeaders"]),i.webRequest.onErrorOccurred.addListener(e=>{d.delete(e.requestId),f.delete(String(e.requestId))},{urls:[""]}),i.webRequest.onCompleted.addListener(e=>{d.delete(e.requestId),f.delete(String(e.requestId))},{urls:[""]}),i.webRequest.onHeadersReceived.addListener(e=>{we(e),ge(e)},{urls:[""]},["responseHeaders"]))}async function _(e,t){console.log("[gomdown-helper] context menu clicked",{menuItemId:e?.menuItemId,linkUrl:e?.linkUrl,srcUrl:e?.srcUrl,frameUrl:e?.frameUrl,pageUrl:e?.pageUrl,tabUrl:t?.url});const n=e?.menuItemId;if(n!=null&&String(n)!==C)return;const r=String(e?.linkUrl||e?.srcUrl||"").trim(),o=String(e?.frameUrl||e?.pageUrl||t?.url||"").trim(),a=String(r||o||"").trim();if(!a||/^(about:|chrome:|chrome-extension:|edge:|brave:)/i.test(a)){await U("다운로드 가능한 URL을 찾지 못했습니다.");return}const k=!r&&!!o,g=await u(a,String(e?.pageUrl||t?.url||""),k?"yt-dlp":"aria2");if(!g.ok){await U(`전송 실패: ${g.error||"unknown error"}`);return}await U(k?"페이지 URL을 yt-dlp로 gdown에 전송했습니다.":"gdown으로 전송했습니다.")}function ye(){if(typeof chrome>"u"||!chrome.contextMenus?.create){i.contextMenus.create({id:C,title:"Download with Gomdown",visible:!0,contexts:["all"]});return}chrome.contextMenus.create({id:C,title:"Download with Gomdown",contexts:["all"]},()=>{chrome.runtime.lastError})}function H(){$||(typeof chrome<"u"&&chrome.contextMenus?.onClicked?chrome.contextMenus.onClicked.addListener((e,t)=>{_(e,t)}):i.contextMenus.onClicked.addListener((e,t)=>{_(e,t)}),$=!0)}async function he(){const e=await m();if(!e.extensionStatus||!e.showContextOption){await i.contextMenus.removeAll().catch(()=>null);return}await i.contextMenus.removeAll().catch(()=>null),ye()}function p(){return l||(l=he().finally(()=>{l=null}),l)}i.runtime.onMessage.addListener((e,t)=>{if(e?.type==="capture-link-download"){const n=String(e?.url||"").trim();if(!n)return Promise.resolve({ok:!1,error:"url is empty"});const r=Number(t?.tab?.id);return u(n,String(e?.referer||"")).then(o=>(o.ok&&(O(n),z(r)),o))}if(e?.type==="media:list")return G().then(n=>({ok:!0,items:n}));if(e?.type==="media:clear")return re().then(()=>({ok:!0}));if(e?.type==="media:enqueue"){const n=String(e?.url||"").trim();return n?u(n,String(e?.referer||"")).then(r=>r):Promise.resolve({ok:!1,error:"url is empty"})}if(e?.type==="page:enqueue-ytdlp")return i.tabs.query({active:!0,currentWindow:!0}).then(async n=>{const r=n[0],o=String(r?.url||"").trim();return o?u(o,o,"yt-dlp"):{ok:!1,error:"active tab url is empty"}});if(e?.type==="page:enqueue-ytdlp-url"){const n=String(e?.url||"").trim(),r=String(e?.referer||n).trim();return n?u(n,r||n,"yt-dlp"):Promise.resolve({ok:!1,error:"url is empty"})}});i.runtime.onInstalled.addListener(()=>{console.log("[gomdown-helper] onInstalled"),A(),R(),H(),p(),w()});i.runtime.onStartup.addListener(()=>{console.log("[gomdown-helper] onStartup"),A(),R(),H(),p(),w()});i.storage.onChanged.addListener((e,t)=>{t==="sync"&&((e.hideChromeBar||e.useNativeHost||e.extensionStatus)&&(w(),p()),e.showContextOption&&p())});A();R();H();p();w();console.log("[gomdown-helper] service worker initialized"); diff --git a/packages/chrome/assets/index.ts-BljhweV3.js b/packages/chrome/assets/index.ts-BljhweV3.js new file mode 100644 index 0000000..9311254 --- /dev/null +++ b/packages/chrome/assets/index.ts-BljhweV3.js @@ -0,0 +1 @@ +import{b as i}from"./browser-polyfill-CZ_dLIqp.js";import{n as P,a as j}from"./downloadIntent-Dv31jC2S.js";import{g as h}from"./settings-Bo6W9Drl.js";const z="org.gdown.nativehost";async function J(e){return i.runtime.sendNativeMessage(z,{action:"addUri",...e})}async function Z(){return i.runtime.sendNativeMessage(z,{action:"focus"})}const C="history";async function ee(){const t=(await i.storage.local.get([C]))[C];return Array.isArray(t)?t:[]}async function te(e){await i.storage.local.set({[C]:e.slice(0,300)})}async function ne(e){const t=await ee(),n=t.findIndex(r=>r.gid===e.gid);n>=0?t[n]=e:t.unshift(e),await te(t)}function G(e,t){const n=Array.isArray(e?.responseHeaders)?e.responseHeaders:[],r=t.toLowerCase(),o=n.find(s=>String(s?.name||"").toLowerCase()===r);return String(o?.value||"")}function re(e){const t=e.toLowerCase();return t?t.includes("application/vnd.apple.mpegurl")||t.includes("application/x-mpegurl")||t.includes("audio/mpegurl")?"m3u8":t.includes("video/mp4")?"mp4":t.includes("application/octet-stream")&&t.includes("m3u8")?"m3u8":t.includes("hls")?"hls":"unknown":"unknown"}function oe(e){const t=String(e||"").toLowerCase();return t.includes(".m3u8")?"m3u8":t.includes(".m3u")?"m3u":t.includes(".mp4")?"mp4":t.includes("m3u8")?"m3u8":t.includes("hls")?"hls":"unknown"}function B(e,t){const n=re(t);return n!=="unknown"?n:oe(e)}function ie(e){if(!e?.url)return!1;const t=String(e?.method||"").toUpperCase();if(t&&t!=="GET")return!1;const n=Number(e?.statusCode||0);if(n>0&&(n<200||n>299))return!1;const r=String(e?.type||"");if(!["xmlhttprequest","media","other","main_frame","sub_frame","fetch"].includes(r))return!1;const o=G(e,"content-type");return B(e.url,o)!=="unknown"}function se(e,t=""){const n=G(e,"content-type"),r=String(e?.url||""),o=B(r,n),s=Number.isInteger(e?.tabId)?Number(e.tabId):-1,a=Date.now();return{id:`${a}:${s}:${o}:${r}`,url:r,kind:o,tabId:s,pageUrl:String(e?.documentUrl||e?.initiator||""),referer:String(t||e?.documentUrl||e?.initiator||""),contentType:n,detectedAt:a}}function q(e){try{const t=new URL(e);return`${t.protocol}//${t.host}${t.pathname}`.toLowerCase()}catch{return String(e||"").toLowerCase()}}const b="media_candidates",ae=200;async function W(){const t=(await i.storage.local.get([b]))[b];return Array.isArray(t)?t:[]}async function ue(e){const t=[...e].sort((n,r)=>r.detectedAt-n.detectedAt);await i.storage.local.set({[b]:t.slice(0,ae)})}async function ce(e,t){const n=await W(),r=n.findIndex(o=>{try{const s=new URL(o.url);return`${s.protocol}//${s.host}${s.pathname}`.toLowerCase()===t}catch{return o.url.toLowerCase()===t}});r>=0?n[r]={...n[r],...e,detectedAt:Date.now()}:n.unshift(e),await ue(n)}async function le(){await i.storage.local.set({[b]:[]})}const k=8e3,de=7e3,U="gomdown-helper-download-context-menu-option",m=new Map,M=new Map,I=new Map,L=new Map,w=new Map,R=new Map,A=new Map;let $=!1,E=!1,_=!1,p=null;const K="bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/best",fe=[{hosts:["youtube.com","www.youtube.com","m.youtube.com","youtu.be"],extractor:"yt-dlp",format:K}];function x(e){try{const t=new URL(e),n=(t.pathname||"/").replace(/\/+$/,"")||"/";return`${t.protocol}//${t.host}${n}`.toLowerCase()}catch{return String(e||"").toLowerCase()}}function f(e){const t=Date.now();for(const[n,r]of e.entries())r<=t&&e.delete(n)}function Y(e){const t=Date.now()+k;M.set(P(e),t),I.set(x(e),t)}function Q(e){!Number.isInteger(e)||(e??-1)<0||R.set(e,Date.now()+k)}function pe(e){f(M),f(I);const t=P(e);return M.has(t)||I.has(x(e))}function me(e){return f(R),!Number.isInteger(e)||(e??-1)<0?!1:R.has(e)}function we(e){return f(L),L.has(x(e))}function ge(e){L.set(x(e),Date.now()+de)}function he(e){return f(w),!!e&&w.has(e)}function ye(e){e&&w.set(e,Date.now()+k)}function Se(e){f(A);const t=q(e);return A.has(t)}function be(e){A.set(q(e),Date.now()+k)}async function ke(){try{await Z()}catch{}}async function v(e){await i.notifications.create(`gomdown-notice-${Date.now()}`,{type:"basic",iconUrl:"/images/icon-large.png",title:"Gomdown Helper",message:e}).catch(()=>null)}async function d(e,t="",n,r,o,s){if(we(e))return{ok:!1,error:"duplicate transfer suppressed"};const a=await h();if(!a.extensionStatus)return{ok:!1,error:"extension disabled"};if(!a.motrixAPIkey)return{ok:!1,error:"motrixAPIkey is not set"};try{const u=await J({url:e,rpcPort:a.motrixPort,rpcSecret:a.motrixAPIkey,referer:t,split:64,out:o?.trim()||void 0,cookie:s?.cookie?.trim()||void 0,userAgent:s?.userAgent?.trim()||void 0,authorization:s?.authorization?.trim()||void 0,proxy:s?.proxy?.trim()||void 0,extractor:n==="yt-dlp"?"yt-dlp":void 0,format:n==="yt-dlp"?r||K:void 0});if(!u?.ok)return{ok:!1,error:u?.error||"native host addUri failed"};a.activateAppOnDownload&&await ke(),ge(e);const c=String(u?.gid||u?.requestId||`pending-${Date.now()}`),l=(()=>{try{return new URL(e).pathname}catch{return""}})().split("/").filter(Boolean).pop()||e;return await ne({gid:c,downloader:"native",startTime:new Date().toISOString(),icon:"/images/32.png",name:decodeURIComponent(l),path:null,status:u?.pending?"queued":"downloading",size:0,downloaded:0}),a.enableNotifications&&await i.notifications.create(`gomdown-transfer-${Date.now()}`,{type:"basic",iconUrl:"/images/icon-large.png",title:"Gomdown Helper",message:"Download sent to gdown"}),{ok:!0}}catch(u){return{ok:!1,error:String(u)}}}function F(e){try{return new URL(e).hostname.toLowerCase()}catch{return""}}function V(e,t="",n=""){const r=[F(e),F(t)].filter(Boolean);for(const s of fe)if(r.some(a=>s.hosts.includes(a)))return{extractor:s.extractor,format:s.format};const o=n.toLowerCase();return o==="m3u8"||o==="m3u"||o==="hls"?{extractor:"yt-dlp",format:"best"}:o==="mp4"?{extractor:"aria2"}:{extractor:"aria2"}}function O(e,t){const n=t.toLowerCase(),r=e.find(o=>String(o?.name||"").toLowerCase()===n);return String(r?.value||"").trim()}async function xe(e){if(e.type!=="main_frame"||(e.method||"").toUpperCase()!=="GET"||typeof e.statusCode=="number"&&(e.statusCode<200||e.statusCode>299))return!1;const t=await h();if(!t.extensionStatus||!t.motrixAPIkey)return!1;const n=String(Array.isArray(e?.responseHeaders)&&e.responseHeaders.find(o=>String(o?.name||"").toLowerCase()==="content-length")?.value||""),r=Number(n||0);return t.minFileSize>0&&r>0&&r=0){const X=await i.tabs.get(u.tabId).catch(()=>null);c=String(X?.title||"").trim()}const S=Ie(u.url,u.kind,c),l={...u,pageTitle:c||void 0,cookie:s||void 0,userAgent:a||void 0,suggestedOut:S||void 0};await ce(l,q(l.url)),be(l.url),l.tabId>=0&&await i.tabs.sendMessage(l.tabId,{type:"media:captured",kind:l.kind,url:l.url,suggestedOut:l.suggestedOut||""}).catch(()=>null)}function Ue(e){let t=e.trim().replace(/[\\/:*?"<>|]/g,"_").replace(/\s+/g," ").replace(/^\.+/,"").replace(/\.+$/,"");return t.length>180&&(t=t.slice(0,180).trim()),t}function Me(e){try{return new URL(e).pathname.toLowerCase().match(/\.([a-z0-9]{2,6})(?:$|[?#])/)?.[1]||""}catch{return""}}function Ie(e,t,n){const r=Ue(n||""),s=Me(e)||(t==="mp4"||t==="m3u8"||t==="m3u"||t==="hls"?"mp4":"");return r?!s||r.toLowerCase().endsWith(`.${s}`)?r:`${r}.${s}`:""}async function y(){const e=i.downloads;if(!e.setShelfEnabled)return;const t=await h();if(!t.extensionStatus)return;const n=t.useNativeHost?!1:!t.hideChromeBar;await e.setShelfEnabled(n)}function H(){E||(E=!0,i.downloads.onCreated.addListener(async e=>{await y();const t=e,n=t.finalUrl||t.url||"";!pe(n)&&!me(t.tabId)||(await i.downloads.cancel(e.id).catch(()=>null),await i.downloads.erase({id:e.id}).catch(()=>null),await i.downloads.removeFile(e.id).catch(()=>null))}))}function T(){$||($=!0,i.webRequest.onSendHeaders.addListener(e=>{m.set(e.requestId,e)},{urls:[""]},["requestHeaders","extraHeaders"]),i.webRequest.onErrorOccurred.addListener(e=>{m.delete(e.requestId),w.delete(String(e.requestId))},{urls:[""]}),i.webRequest.onCompleted.addListener(e=>{m.delete(e.requestId),w.delete(String(e.requestId))},{urls:[""]}),i.webRequest.onHeadersReceived.addListener(e=>{ve(e),Ce(e)},{urls:[""]},["responseHeaders"]))}async function N(e,t){console.log("[gomdown-helper] context menu clicked",{menuItemId:e?.menuItemId,linkUrl:e?.linkUrl,srcUrl:e?.srcUrl,frameUrl:e?.frameUrl,pageUrl:e?.pageUrl,tabUrl:t?.url});const n=e?.menuItemId;if(n!=null&&String(n)!==U)return;const r=String(e?.linkUrl||e?.srcUrl||"").trim(),o=String(e?.frameUrl||e?.pageUrl||t?.url||"").trim(),a=String(r||o||"").trim();if(!a||/^(about:|chrome:|chrome-extension:|edge:|brave:)/i.test(a)){await v("다운로드 가능한 URL을 찾지 못했습니다.");return}const u=V(a,String(e?.pageUrl||t?.url||""),""),c=await d(a,String(e?.pageUrl||t?.url||""),u.extractor,u.format);if(!c.ok){await v(`전송 실패: ${c.error||"unknown error"}`);return}await v(u.extractor==="yt-dlp"?"페이지 URL을 yt-dlp로 gdown에 전송했습니다.":"gdown으로 전송했습니다.")}function Le(){if(typeof chrome>"u"||!chrome.contextMenus?.create){i.contextMenus.create({id:U,title:"Download with Gomdown",visible:!0,contexts:["all"]});return}chrome.contextMenus.create({id:U,title:"Download with Gomdown",contexts:["all"]},()=>{chrome.runtime.lastError})}function D(){_||(typeof chrome<"u"&&chrome.contextMenus?.onClicked?chrome.contextMenus.onClicked.addListener((e,t)=>{N(e,t)}):i.contextMenus.onClicked.addListener((e,t)=>{N(e,t)}),_=!0)}async function Re(){const e=await h();if(!e.extensionStatus||!e.showContextOption){await i.contextMenus.removeAll().catch(()=>null);return}await i.contextMenus.removeAll().catch(()=>null),Le()}function g(){return p||(p=Re().finally(()=>{p=null}),p)}i.runtime.onMessage.addListener((e,t)=>{if(e?.type==="capture-link-download"){const n=String(e?.url||"").trim();if(!n)return Promise.resolve({ok:!1,error:"url is empty"});const r=Number(t?.tab?.id);return d(n,String(e?.referer||"")).then(o=>(o.ok&&(Y(n),Q(r)),o))}if(e?.type==="media:list")return W().then(n=>({ok:!0,items:n}));if(e?.type==="media:clear")return le().then(()=>({ok:!0}));if(e?.type==="media:enqueue"){const n=String(e?.url||"").trim(),r=String(e?.kind||"").trim(),o=String(e?.suggestedOut||"").trim(),s=String(e?.referer||"").trim(),a=String(e?.cookie||"").trim(),u=String(e?.userAgent||"").trim();if(!n)return Promise.resolve({ok:!1,error:"url is empty"});const c=V(n,s,r);return d(n,s,c.extractor,c.format,o,{cookie:a,userAgent:u}).then(S=>S)}if(e?.type==="page:enqueue-ytdlp")return i.tabs.query({active:!0,currentWindow:!0}).then(async n=>{const r=n[0],o=String(r?.url||"").trim();return o?d(o,o,"yt-dlp"):{ok:!1,error:"active tab url is empty"}});if(e?.type==="page:enqueue-ytdlp-url"){const n=String(e?.url||"").trim(),r=String(e?.referer||n).trim();return n?d(n,r||n,"yt-dlp"):Promise.resolve({ok:!1,error:"url is empty"})}});i.runtime.onInstalled.addListener(()=>{console.log("[gomdown-helper] onInstalled"),T(),H(),D(),g(),y()});i.runtime.onStartup.addListener(()=>{console.log("[gomdown-helper] onStartup"),T(),H(),D(),g(),y()});i.storage.onChanged.addListener((e,t)=>{t==="sync"&&((e.hideChromeBar||e.useNativeHost||e.extensionStatus)&&(y(),g()),e.showContextOption&&g())});T();H();D();g();y();console.log("[gomdown-helper] service worker initialized"); diff --git a/packages/chrome/assets/index.ts-C6ePCen1.js b/packages/chrome/assets/index.ts-C6ePCen1.js new file mode 100644 index 0000000..2ee71d0 --- /dev/null +++ b/packages/chrome/assets/index.ts-C6ePCen1.js @@ -0,0 +1 @@ +import{b as m}from"./browser-polyfill-CZ_dLIqp.js";import{i as s,n as C}from"./downloadIntent-Dv31jC2S.js";const S=8e3,i=new Map;function v(){const e=Date.now();for(const[n,t]of i.entries())t<=e&&i.delete(n)}async function a(e,n){const t=C(e,window.location.href);if(!t)return!1;if(v(),i.has(t))return!0;i.set(t,Date.now()+S);try{if((await m.runtime.sendMessage({type:"capture-link-download",url:t,referer:n||document.referrer||window.location.href}))?.ok)return!0}catch{}return i.delete(t),!1}function g(e){return e?e instanceof HTMLAnchorElement?e:e instanceof Element?e.closest("a[href]"):null:null}function b(e){return!!(e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)}async function k(e){if(e.defaultPrevented||b(e))return;const n=g(e.target);if(!n)return;const t=n.href||"";!t||!s(t,window.location.href)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),await a(t,document.referrer||window.location.href))}function E(e){const n=g(e.target);if(!n)return;const t=n.href||"";!t||!s(t,window.location.href)||b(e)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),a(t,document.referrer||window.location.href))}document.addEventListener("pointerdown",e=>{e.button===0&&E(e)},!0);document.addEventListener("mousedown",e=>{e.button===0&&E(e)},!0);document.addEventListener("click",e=>{e.button===0&&k(e)},!0);document.addEventListener("keydown",e=>{if(e.key!=="Enter"||e.defaultPrevented||b(e))return;const n=g(e.target);if(!n)return;const t=n.href||"";!t||!s(t,window.location.href)||(e.preventDefault(),e.stopImmediatePropagation(),e.stopPropagation(),a(t,document.referrer||window.location.href))},!0);document.addEventListener("auxclick",e=>{e.button===1&&k(e)},!0);function L(){try{const e=window.open.bind(window);window.open=function(t,o,u){const y=String(t||"").trim();return y&&s(y,window.location.href)?(a(y,window.location.href),null):e(t,o,u)}}catch{}try{const e=HTMLAnchorElement.prototype.click;HTMLAnchorElement.prototype.click=function(){const t=this.href||this.getAttribute("href")||"";if(t&&s(t,window.location.href)){a(t,document.referrer||window.location.href);return}e.call(this)}}catch{}}L();let l=null,r=null,w=!1,x=null,d=window.location.href;function T(e){try{const n=new URL(e);return n.hostname!=="www.youtube.com"&&n.hostname!=="youtube.com"?!1:n.pathname==="/watch"&&n.searchParams.has("v")}catch{return!1}}function c(e,n="idle"){r&&(r.textContent=e,n==="ok"?r.style.color="#8ff0a4":n==="error"?r.style.color="#ff9b9b":r.style.color="#aeb7d8")}async function I(){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 M(){l&&(l.remove(),l=null,r=null)}function p(){if(window.top!==window.self)return;if(!T(window.location.href)){M();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 n=document.createElement("div");n.textContent="Gdown Helper",n.style.fontSize="12px",n.style.fontWeight="700",n.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",()=>{I()});const o=document.createElement("div");o.textContent="클릭 시 gdown 다운로드 모달로 연결",o.style.fontSize="11px",o.style.marginTop="8px",o.style.lineHeight="1.35",o.style.color="#aeb7d8",e.appendChild(n),e.appendChild(t),e.appendChild(o),document.documentElement.appendChild(e),l=e,r=o}function P(){x===null&&(x=window.setInterval(()=>{const e=window.location.href;e!==d&&(d=e,p())},800),window.addEventListener("popstate",()=>{d=window.location.href,p()}),document.addEventListener("yt-navigate-finish",()=>{d=window.location.href,p()}))}p();P();let h=null,f=null;function O(){if(h)return h;const e=document.createElement("div");return e.id="gomdown-media-toast",e.style.position="fixed",e.style.left="18px",e.style.bottom="18px",e.style.zIndex="2147483647",e.style.maxWidth="360px",e.style.padding="10px 12px",e.style.borderRadius="10px",e.style.border="1px solid rgba(128, 140, 180, 0.42)",e.style.background="rgba(18, 21, 31, 0.95)",e.style.color="#dce4fa",e.style.fontSize="12px",e.style.lineHeight="1.35",e.style.fontFamily="ui-sans-serif, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif",e.style.boxShadow="0 10px 24px rgba(0, 0, 0, 0.28)",e.style.display="none",document.documentElement.appendChild(e),h=e,e}function A(e){const n=O(),t=String(e?.kind||"media").toUpperCase(),o=String(e?.suggestedOut||"").trim(),u=String(e?.url||"").trim().slice(0,96);n.textContent=o?`캡처됨 [${t}] ${o}`:`캡처됨 [${t}] ${u}${u.length>=96?"…":""}`,n.style.display="block",f!==null&&window.clearTimeout(f),f=window.setTimeout(()=>{n.style.display="none",f=null},2200)}m.runtime.onMessage.addListener(e=>{e?.type==="media:captured"&&A({kind:e?.kind,url:e?.url,suggestedOut:e?.suggestedOut})}); diff --git a/packages/chrome/assets/index.ts-CMnPQ13j.js b/packages/chrome/assets/index.ts-CMnPQ13j.js deleted file mode 100644 index cc8d4c8..0000000 --- a/packages/chrome/assets/index.ts-CMnPQ13j.js +++ /dev/null @@ -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(); diff --git a/packages/chrome/assets/index.ts-loader-BHtfStLc.js b/packages/chrome/assets/index.ts-loader-Bju9eGS_.js similarity index 82% rename from packages/chrome/assets/index.ts-loader-BHtfStLc.js rename to packages/chrome/assets/index.ts-loader-Bju9eGS_.js index de55a3c..08be718 100644 --- a/packages/chrome/assets/index.ts-loader-BHtfStLc.js +++ b/packages/chrome/assets/index.ts-loader-Bju9eGS_.js @@ -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); diff --git a/packages/chrome/images/128.png b/packages/chrome/images/128.png index eaae62a..d547e24 100644 Binary files a/packages/chrome/images/128.png and b/packages/chrome/images/128.png differ diff --git a/packages/chrome/images/16.png b/packages/chrome/images/16.png index 504fccd..34dbc5c 100644 Binary files a/packages/chrome/images/16.png and b/packages/chrome/images/16.png differ diff --git a/packages/chrome/images/32.png b/packages/chrome/images/32.png index d77f929..3214db3 100644 Binary files a/packages/chrome/images/32.png and b/packages/chrome/images/32.png differ diff --git a/packages/chrome/images/48.png b/packages/chrome/images/48.png index eb9f4b6..a72d4f5 100644 Binary files a/packages/chrome/images/48.png and b/packages/chrome/images/48.png differ diff --git a/packages/chrome/images/dwld.png b/packages/chrome/images/dwld.png index 029ced1..3214db3 100644 Binary files a/packages/chrome/images/dwld.png and b/packages/chrome/images/dwld.png differ diff --git a/packages/chrome/images/icon-large.png b/packages/chrome/images/icon-large.png index 42ee5bb..dcf91f0 100644 Binary files a/packages/chrome/images/icon-large.png and b/packages/chrome/images/icon-large.png differ diff --git a/packages/chrome/images/pomeranian-bw.svg b/packages/chrome/images/pomeranian-bw.svg new file mode 100644 index 0000000..2355b21 --- /dev/null +++ b/packages/chrome/images/pomeranian-bw.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/packages/chrome/manifest.json b/packages/chrome/manifest.json index 422b4f4..4aeff20 100644 --- a/packages/chrome/manifest.json +++ b/packages/chrome/manifest.json @@ -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": [ "" @@ -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 } diff --git a/packages/chrome/service-worker-loader.js b/packages/chrome/service-worker-loader.js index 8d9dbfa..f7c4242 100644 --- a/packages/chrome/service-worker-loader.js +++ b/packages/chrome/service-worker-loader.js @@ -1 +1 @@ -import './assets/index.ts-BAxKsZ8F.js'; +import './assets/index.ts-BljhweV3.js'; diff --git a/packages/chrome/src/popup/index.html b/packages/chrome/src/popup/index.html index 3e07ee0..091206d 100644 --- a/packages/chrome/src/popup/index.html +++ b/packages/chrome/src/popup/index.html @@ -4,7 +4,7 @@ Gomdown Helper - + diff --git a/packages/gomdown-helper.v0.0.12.chrome.zip b/packages/gomdown-helper.v0.0.12.chrome.zip new file mode 100644 index 0000000..37712df Binary files /dev/null and b/packages/gomdown-helper.v0.0.12.chrome.zip differ diff --git a/packages/gomdown-helper.v0.0.13.chrome.zip b/packages/gomdown-helper.v0.0.13.chrome.zip new file mode 100644 index 0000000..dd67676 Binary files /dev/null and b/packages/gomdown-helper.v0.0.13.chrome.zip differ diff --git a/packages/gomdown-helper.v0.0.14.chrome.zip b/packages/gomdown-helper.v0.0.14.chrome.zip new file mode 100644 index 0000000..ffbd694 Binary files /dev/null and b/packages/gomdown-helper.v0.0.14.chrome.zip differ diff --git a/packages/gomdown-helper.v0.0.15.chrome.zip b/packages/gomdown-helper.v0.0.15.chrome.zip new file mode 100644 index 0000000..288685d Binary files /dev/null and b/packages/gomdown-helper.v0.0.15.chrome.zip differ diff --git a/packages/gomdown-helper.v0.0.16.chrome.zip b/packages/gomdown-helper.v0.0.16.chrome.zip new file mode 100644 index 0000000..97ce5b8 Binary files /dev/null and b/packages/gomdown-helper.v0.0.16.chrome.zip differ diff --git a/public/images/128.png b/public/images/128.png index eaae62a..d547e24 100644 Binary files a/public/images/128.png and b/public/images/128.png differ diff --git a/public/images/16.png b/public/images/16.png index 504fccd..34dbc5c 100644 Binary files a/public/images/16.png and b/public/images/16.png differ diff --git a/public/images/32.png b/public/images/32.png index d77f929..3214db3 100644 Binary files a/public/images/32.png and b/public/images/32.png differ diff --git a/public/images/48.png b/public/images/48.png index eb9f4b6..a72d4f5 100644 Binary files a/public/images/48.png and b/public/images/48.png differ diff --git a/public/images/dwld.png b/public/images/dwld.png index 029ced1..3214db3 100644 Binary files a/public/images/dwld.png and b/public/images/dwld.png differ diff --git a/public/images/icon-large.png b/public/images/icon-large.png index 42ee5bb..dcf91f0 100644 Binary files a/public/images/icon-large.png and b/public/images/icon-large.png differ diff --git a/public/images/pomeranian-bw.svg b/public/images/pomeranian-bw.svg new file mode 100644 index 0000000..2355b21 --- /dev/null +++ b/public/images/pomeranian-bw.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/src/background/index.ts b/src/background/index.ts index 7777baa..8f217bb 100644 --- a/src/background/index.ts +++ b/src/background/index.ts @@ -22,6 +22,19 @@ let downloadHooked = false let contextMenuHooked = false let contextMenuUpdateInFlight: Promise | 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 { if (details.type !== 'main_frame') return false if ((details.method || '').toUpperCase() !== 'GET') return false @@ -223,9 +279,66 @@ async function interceptMediaByWebRequest(details: any): Promise { 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 { @@ -311,17 +424,18 @@ async function handleContextMenuClick(data: any, tab?: any): Promise { 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') { diff --git a/src/content/index.ts b/src/content/index.ts index 5813684..011a271 100644 --- a/src/content/index.ts +++ b/src/content/index.ts @@ -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, + }) + } +}) diff --git a/src/lib/mediaCapture.ts b/src/lib/mediaCapture.ts index e3299c4..f75243c 100644 --- a/src/lib/mediaCapture.ts +++ b/src/lib/mediaCapture.ts @@ -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 } diff --git a/src/popup/main.tsx b/src/popup/main.tsx index 2b42123..1e2a58e 100644 --- a/src/popup/main.tsx +++ b/src/popup/main.tsx @@ -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 {
{item.kind.toUpperCase()} + {item.pageTitle ? {item.pageTitle} : null} {item.url}