Yt-dl Addon for Windows
+114
-39
| /* | ||
| On startup, connect to the 'firefox_command_runner' app. | ||
| Bridges the popup and the yt-dlp native host. | ||
| The popup is short-lived, so all download state lives here and is replayed to | ||
| the popup whenever it reconnects. | ||
| */ | ||
| console.log('Starting firefox command runner!') | ||
| var port = browser.runtime.connectNative('firefox_command_runner'); | ||
| const HOST_NAME = "firefox_command_runner"; | ||
| let hostPort = null; | ||
| let popupPort = null; | ||
| let lastDirectory = null; | ||
| let state = { phase: "idle" }; | ||
| let progressCount = 0; | ||
| function log(...args) { | ||
| console.log("[yt-dlp]", new Date().toLocaleTimeString(), ...args); | ||
| } | ||
| function setState(next) { | ||
| state = next; | ||
| log("state ->", state.phase, popupPort ? "(popup open)" : "(popup closed)"); | ||
| if (popupPort) { | ||
| popupPort.postMessage(state); | ||
| } | ||
| } | ||
| function notify(title, message) { | ||
| browser.notifications.create({ | ||
| type: "basic", | ||
| iconUrl: browser.runtime.getURL("icons/YT_icon.png"), | ||
| title, | ||
| message, | ||
| }); | ||
| } | ||
| /* | ||
| Listen for messages from the app. | ||
| Native host connection. Reconnected lazily so a host crash doesn't wedge the | ||
| add-on until the browser restarts. | ||
| */ | ||
| port.onMessage.addListener((response) => { | ||
| console.log("Received: "); | ||
| console.log(response); | ||
| if(response[0]=="END"){ | ||
| browser.notifications.create({ | ||
| 'type': 'basic', | ||
| 'iconUrl': browser.extension.getURL('icons/YT_icon.png'), | ||
| 'title': 'Download Completed !', | ||
| 'message': response[1], | ||
| function host() { | ||
| if (hostPort) { | ||
| return hostPort; | ||
| } | ||
| log("connecting to native host", HOST_NAME); | ||
| hostPort = browser.runtime.connectNative(HOST_NAME); | ||
| hostPort.onMessage.addListener(onHostMessage); | ||
| hostPort.onDisconnect.addListener((port) => { | ||
| hostPort = null; | ||
| log("host disconnected", port.error ? port.error.message : "(clean)"); | ||
| if (port.error) { | ||
| const message = `Native host disconnected: ${port.error.message}`; | ||
| notify("Download failed", message); | ||
| setState({ phase: "error", message }); | ||
| } | ||
| }); | ||
| browser.runtime.sendMessage("Complete") | ||
| return hostPort; | ||
| } | ||
| else if(response[0]=="PROG" && response[1][2]=="of"){ | ||
| values=["PROG",response[1][1],response[1][3],response[1][5]] | ||
| browser.runtime.sendMessage(values) | ||
| function onHostMessage(message) { | ||
| if (message.type === "progress") { | ||
| // One in 20 keeps the console readable at ~5 progress messages a second | ||
| progressCount += 1; | ||
| if (progressCount % 20 === 1 || message.status !== "downloading") { | ||
| log(`host -> progress #${progressCount}`, JSON.stringify(message)); | ||
| } | ||
| } else { | ||
| progressCount = 0; | ||
| log("host ->", JSON.stringify(message)); | ||
| } | ||
| else if(response[0]=="FORMAT"){ | ||
| browser.runtime.sendMessage(response); | ||
| switch (message.type) { | ||
| case "formats": | ||
| setState({ phase: "formats", formats: message.formats }); | ||
| break; | ||
| case "progress": | ||
| setState({ phase: "downloading", progress: message }); | ||
| break; | ||
| case "done": { | ||
| lastDirectory = message.dir; | ||
| const names = message.files.slice(0, 4); | ||
| if (message.files.length > names.length) { | ||
| names.push(`… and ${message.files.length - names.length} more`); | ||
| } | ||
| notify( | ||
| "Download complete", | ||
| names.length ? names.join("\n") : "Already downloaded" | ||
| ); | ||
| setState({ phase: "done", files: message.files, dir: message.dir }); | ||
| break; | ||
| } | ||
| case "error": | ||
| notify("Download failed", message.message); | ||
| setState({ phase: "error", message: message.message }); | ||
| break; | ||
| } | ||
| }); | ||
| } | ||
| /* | ||
| Listen for messages from the content script. | ||
| Popup connection. | ||
| */ | ||
| browser.runtime.onMessage.addListener(function(message) { | ||
| if(message!="Complete"){ | ||
| console.log('getting button message :' + message) | ||
| function logTabs(tabs) { | ||
| let tab = tabs[0]; // Safe to assume there will only be one result | ||
| console.log(tab.url); | ||
| port.postMessage(message+tab.url); | ||
| } | ||
| browser.tabs.query({currentWindow: true, active: true}).then(logTabs, console.error); | ||
| } | ||
| browser.runtime.onConnect.addListener((port) => { | ||
| log("popup connected, replaying state:", state.phase); | ||
| popupPort = port; | ||
| port.postMessage(state); | ||
| port.onMessage.addListener(onPopupMessage); | ||
| port.onDisconnect.addListener(() => { | ||
| log("popup closed"); | ||
| popupPort = null; | ||
| }); | ||
| }); | ||
| async function onPopupMessage(message) { | ||
| try { | ||
| const [tab] = await browser.tabs.query({ currentWindow: true, active: true }); | ||
| log("popup ->", JSON.stringify(message), "on", tab.url); | ||
| if (message.action === "formats") { | ||
| setState({ phase: "loading-formats" }); | ||
| } else if (message.action === "download") { | ||
| setState({ phase: "downloading", progress: null }); | ||
| } | ||
| host().postMessage({ ...message, url: tab.url }); | ||
| } catch (error) { | ||
| log("relay failed:", error); | ||
| setState({ phase: "error", message: String(error) }); | ||
| } | ||
| } | ||
| /* | ||
| Notification actions | ||
| */ | ||
| browser.notifications.onClicked.addListener((response) => { | ||
| console.log("Open Download folder") | ||
| port.postMessage("OpenDL"); | ||
| }); | ||
| browser.notifications.onClicked.addListener(() => { | ||
| log("notification clicked, opening", lastDirectory); | ||
| host().postMessage({ action: "open", path: lastDirectory }); | ||
| }); |
+101
-21
@@ -1,29 +0,109 @@ | ||
| /* Paddings */ | ||
| .center-text{text-align:center!important;padding:2%} | ||
| .hidden{ | ||
| display: none; | ||
| :root { | ||
| --bg: #ffffff; | ||
| --fg: #15141a; | ||
| --muted: #5b5b66; | ||
| --button-bg: #f0f0f4; | ||
| --button-hover: #e0e0e6; | ||
| --border: #cfcfd8; | ||
| --error: #c50042; | ||
| } | ||
| .full-width{width:100%} | ||
| /* Rounded progress bar */ | ||
| .container:after,.container:before,.panel:after,.panel:before,.row:after,.row:before,.row-padding:after,.row-padding:before, | ||
| .round-small{border-radius:2px}.round,.round-medium{border-radius:4px}.round-large{border-radius:8px}.round-xlarge{border-radius:16px}.round-xxlarge{border-radius:32px} | ||
| @media (prefers-color-scheme: dark) { | ||
| :root { | ||
| --bg: #2b2a33; | ||
| --fg: #fbfbfe; | ||
| --muted: #b1b1bb; | ||
| --button-bg: #52525e; | ||
| --button-hover: #5b5b66; | ||
| --border: #6f6f7a; | ||
| --error: #ff9aa2; | ||
| } | ||
| } | ||
| /* Colors */ | ||
| .light-grey,.hover-light-grey:hover,.light-gray,.hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} | ||
| .blue,.hover-blue:hover{color:#fff!important;background-color:#2196F3!important} | ||
| .red,.hover-red:hover{color:#fff!important;background-color:#f44336!important;font-size: 100%;} | ||
| /* The display rules below would otherwise beat the hidden attribute's UA style */ | ||
| [hidden] { | ||
| display: none !important; | ||
| } | ||
| /* Text size */ | ||
| .text-80{font-size:80%} | ||
| .text-60{font-size:60%} | ||
| .text-40{font-size:40%} | ||
| .text-20{font-size:20%} | ||
| body { | ||
| width: 260px; | ||
| margin: 0; | ||
| padding: 10px; | ||
| background: var(--bg); | ||
| color: var(--fg); | ||
| font: 13px system-ui, sans-serif; | ||
| } | ||
| .buttons{ | ||
| display:flex; | ||
| width:100%; | ||
| justify-content: space-between; | ||
| button, select { | ||
| font: inherit; | ||
| color: inherit; | ||
| background: var(--button-bg); | ||
| border: 1px solid var(--border); | ||
| border-radius: 4px; | ||
| padding: 6px 10px; | ||
| cursor: pointer; | ||
| } | ||
| button:hover { | ||
| background: var(--button-hover); | ||
| } | ||
| .row { | ||
| display: flex; | ||
| gap: 6px; | ||
| } | ||
| .row > * { | ||
| flex: 1; | ||
| } | ||
| .wide, #format-picker { | ||
| display: flex; | ||
| width: 100%; | ||
| box-sizing: border-box; | ||
| gap: 6px; | ||
| } | ||
| #format-picker { | ||
| margin-top: 6px; | ||
| } | ||
| #playlist-row { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 6px; | ||
| margin-top: 8px; | ||
| cursor: pointer; | ||
| } | ||
| #dropdown { | ||
| flex: 1; | ||
| min-width: 0; | ||
| } | ||
| progress { | ||
| width: 100%; | ||
| height: 8px; | ||
| } | ||
| p { | ||
| margin: 8px 0; | ||
| } | ||
| .center { | ||
| text-align: center; | ||
| } | ||
| .muted { | ||
| color: var(--muted); | ||
| font-size: 92%; | ||
| } | ||
| #error { | ||
| color: var(--error); | ||
| word-wrap: break-word; | ||
| } | ||
| #result { | ||
| word-wrap: break-word; | ||
| } |
+26
-38
| <!DOCTYPE html> | ||
| <html> | ||
@@ -10,44 +9,33 @@ <head> | ||
| <body> | ||
| <div id="opt-in-prompt"> | ||
| <p><b>This addon extracts the current URL and send it to your local yt-dl program.</b><br> | ||
| Please support the developer of this add-on by enabling this feature. | ||
| </p> | ||
| <p><a href="https://mozilla.org/privacy/websites/" target="_blank"> | ||
| Privacy information</a></p> | ||
| <div id="button-container"> | ||
| <button id="button-enable">Enable</button> | ||
| <button id="button-cancel" autofocus="true">Cancel</button> | ||
| <div id="picker"> | ||
| <div class="row"> | ||
| <button data-format="m4a">M4A</button> | ||
| <button data-format="mp3">MP3</button> | ||
| <button data-format="mp4">MP4</button> | ||
| </div> | ||
| </div> | ||
| <div id="after-opt-in" hidden="true"></div> | ||
| <div id="main" class="buttons"> | ||
| <button id='audio_M4a'>M4a</button> | ||
| <button id='audio_mp3'>Mp3</button> | ||
| <button id='video'>Mp4</button> | ||
| <p class="muted center">OR</p> | ||
| <button id="list-formats" class="wide">Other video formats</button> | ||
| <div id="format-picker" hidden> | ||
| <select id="dropdown"></select> | ||
| <button id="download-selected">Download</button> | ||
| </div> | ||
| <p class="center-text text-80">OR</p> | ||
| <div id="aux"> | ||
| <button id="format" class="full-width">Other formats</button> | ||
| <div id="load-format" class="hidden center-text full-width">Loading...</div> | ||
| <div id="ddMenu" class="hidden"> | ||
| <div id="ddMenu" class="buttons"> | ||
| <select id="dropdown" class="full-width"> | ||
| </select> | ||
| <button id="DL">DL</button> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div id="progress-bar" class="hidden full-width"> | ||
| <div id="speed" class="center-text text-80"> Downloading...</div> | ||
| <div> | ||
| <progress id="prog" max="100" value="0" class="buttons"></progress> | ||
| <div id="perc" class="center-text">0%</div> | ||
| </div> | ||
| </div> | ||
| <label id="playlist-row" class="muted" hidden> | ||
| <input type="checkbox" id="playlist-toggle"> | ||
| Download the whole playlist | ||
| </label> | ||
| </div> | ||
| <p id="loading" class="muted center" hidden>Loading formats...</p> | ||
| <div id="progress" hidden> | ||
| <p id="progress-status" class="muted center">Starting...</p> | ||
| <progress id="progress-bar" max="100" value="0"></progress> | ||
| <p id="progress-detail" class="center"></p> | ||
| </div> | ||
| <p id="result" class="center" hidden></p> | ||
| <p id="error" hidden></p> | ||
| <script src="script.js"></script> | ||
| </body> | ||
| </html> |
+16
-13
| { | ||
| "description": "Run yt-dl command-line program from firefox", | ||
| "description": "Download audio and video from the current tab with yt-dlp", | ||
| "manifest_version": 2, | ||
| "name": "Yt-dl Addon for Windows", | ||
| "version": "1.3", | ||
| "name": "yt-dlp Downloader", | ||
| "version": "2.0", | ||
| "icons": { | ||
| "128": "icons/YT_icon.png" | ||
| }, | ||
| "browser_specific_settings": { | ||
| "gecko": { | ||
| "id": "yt_dl_firefox_command_runner@test.com", | ||
| "strict_min_version": "57.0" | ||
| "strict_min_version": "140.0", | ||
| "data_collection_permissions": { | ||
| "required": [ | ||
| "none" | ||
| ] | ||
| } | ||
| } | ||
| }, | ||
| "background": { | ||
| "scripts": ["background.js"] | ||
| "scripts": [ | ||
| "background.js" | ||
| ] | ||
| }, | ||
| "browser_action": { | ||
| "default_icon": "icons/YT_icon.png", | ||
| "default_title": "Youtube-dl", | ||
| "default_popup" :"index.html" | ||
| "default_title": "yt-dlp Downloader", | ||
| "default_popup": "index.html" | ||
| }, | ||
| "permissions": [ | ||
@@ -31,4 +34,4 @@ "nativeMessaging", | ||
| "activeTab", | ||
| "storage" | ||
| "tabs" | ||
| ] | ||
| } | ||
| } |
+145
-106
@@ -1,114 +0,153 @@ | ||
| // Addon Opt-in actions | ||
| window.addEventListener("load", function() { | ||
| // set up the appearance of the popup depending on the outcome of the opt-in | ||
| browser.storage.local.get("optInShown", function(result) { | ||
| console.log("Setting up UI. result.optInShown:" + result.optInShown); | ||
| document.getElementById("opt-in-prompt").hidden = result.optInShown; | ||
| document.getElementById("after-opt-in").hidden = !result.optInShown; | ||
| }); | ||
| document.getElementById("button-enable").addEventListener( | ||
| "click", | ||
| function() { | ||
| browser.storage.local.set({ "optIn" : true, "optInShown" : true }); | ||
| window.close(); | ||
| }); | ||
| document.getElementById("button-cancel").addEventListener( | ||
| "click", | ||
| function() { | ||
| browser.storage.local.set({ "optIn" : false, "optInShown" : false }); | ||
| window.close(); | ||
| }); | ||
| /* | ||
| Popup UI. Owns no state: it renders whatever snapshot the background sends, | ||
| so closing and reopening it mid-download picks the progress bar back up. | ||
| */ | ||
| const port = browser.runtime.connect({ name: "popup" }); | ||
| const sections = { | ||
| picker: document.getElementById("picker"), | ||
| loading: document.getElementById("loading"), | ||
| progress: document.getElementById("progress"), | ||
| result: document.getElementById("result"), | ||
| error: document.getElementById("error"), | ||
| }; | ||
| const formatPicker = document.getElementById("format-picker"); | ||
| const dropdown = document.getElementById("dropdown"); | ||
| const playlistRow = document.getElementById("playlist-row"); | ||
| const playlistToggle = document.getElementById("playlist-toggle"); | ||
| // Offer the playlist checkbox only when the page belongs to one | ||
| browser.tabs.query({ currentWindow: true, active: true }).then(([tab]) => { | ||
| let url; | ||
| try { | ||
| url = new URL(tab.url); | ||
| } catch { | ||
| return; | ||
| } | ||
| if (url.pathname === "/playlist") { | ||
| // A playlist page has no single video to fall back to | ||
| playlistRow.hidden = false; | ||
| playlistToggle.checked = true; | ||
| playlistToggle.disabled = true; | ||
| } else if (url.searchParams.has("list")) { | ||
| playlistRow.hidden = false; | ||
| } | ||
| }); | ||
| function requestDownload(format) { | ||
| port.postMessage({ | ||
| action: "download", | ||
| format, | ||
| playlist: playlistToggle.checked, | ||
| }); | ||
| } | ||
| ////////// Plugin Event listeners ////////// | ||
| // 3 Main formats | ||
| document.getElementById('audio_M4a').addEventListener('click', function(){ | ||
| browser.runtime.sendMessage("m4a"); | ||
| document.getElementById("aux").classList.add("hidden"); | ||
| document.querySelector("p").classList.add("hidden"); | ||
| document.querySelector("#progress-bar").classList.remove("hidden"); | ||
| }) | ||
| document.getElementById('audio_mp3').addEventListener('click', function(){ | ||
| browser.runtime.sendMessage("mp3"); | ||
| document.getElementById("aux").classList.add("hidden"); | ||
| document.querySelector("p").classList.add("hidden"); | ||
| document.querySelector("#progress-bar").classList.remove("hidden"); | ||
| }) | ||
| document.getElementById('video').addEventListener('click', function(){ | ||
| browser.runtime.sendMessage("mp4"); | ||
| document.getElementById("aux").classList.add("hidden"); | ||
| document.querySelector("p").classList.add("hidden"); | ||
| document.querySelector("#progress-bar").classList.remove("hidden"); | ||
| }) | ||
| document.querySelectorAll("[data-format]").forEach((button) => { | ||
| button.addEventListener("click", () => requestDownload(button.dataset.format)); | ||
| }); | ||
| // Other formats | ||
| document.getElementById('format').addEventListener('click', function(){ | ||
| browser.runtime.sendMessage("format"); | ||
| document.getElementById('load-format').classList.remove("hidden"); | ||
| document.getElementById('format').classList.add("hidden"); | ||
| }) | ||
| document.getElementById('DL').addEventListener('click', function(){ | ||
| var e = document.getElementById("dropdown"); | ||
| var strUser = e.value; | ||
| console.log(strUser) | ||
| browser.runtime.sendMessage(strUser) | ||
| document.querySelector("#main").classList.remove("buttons"); | ||
| document.querySelector("#main").classList.add("hidden"); | ||
| document.querySelector("p").classList.add("hidden"); | ||
| document.querySelector("#progress-bar").classList.remove("hidden"); | ||
| }) | ||
| document.getElementById("list-formats").addEventListener("click", () => { | ||
| port.postMessage({ action: "formats" }); | ||
| }); | ||
| ////////// Listen for background script message ////////// | ||
| browser.runtime.onMessage.addListener((response) => { | ||
| if (response=="Complete"){ | ||
| document.querySelector("#progress-bar").classList.add("hidden"); | ||
| } | ||
| if (response[0]=="PROG"){ | ||
| console.log(response) | ||
| // Progression percent | ||
| percent = response[1]; | ||
| percent2 = percent.substring(0,response[1].length-1); | ||
| percentFloat=parseFloat(percent2); | ||
| // Total DL size | ||
| sizeDL = response[2]; | ||
| sizeDLFloat = parseFloat(sizeDL.substring(0,sizeDL.length-3)) | ||
| partDL = (sizeDLFloat*percentFloat/100).toFixed(2) | ||
| // Real-time Speed value | ||
| speedDL=response[3] | ||
| if(speedDL!=null){ | ||
| valSpeed="Downloading... "+ speedDL; | ||
| } | ||
| document.getElementById("download-selected").addEventListener("click", () => { | ||
| if (dropdown.value) { | ||
| requestDownload(dropdown.value); | ||
| } | ||
| }); | ||
| // Update HTML | ||
| document.querySelector("#prog").value=percentFloat; | ||
| document.querySelector("#perc").textContent=partDL + " / "+ sizeDL; | ||
| document.querySelector("#speed").textContent=valSpeed; | ||
| // Verifier si meilleure astuce | ||
| document.querySelector("#progress-bar").classList.remove("hidden"); | ||
| } | ||
| if (response[0]=="FORMAT"){ | ||
| formats = response[1] | ||
| // Add dropdown options | ||
| var x = document.getElementById("dropdown"); | ||
| for(a in formats){ | ||
| //console.log(formats[a]) | ||
| desc = formats[a][1]+" ("+formats[a][2]+")"+" - "+formats[a][3]; | ||
| console.log(desc) | ||
| var option = document.createElement("option"); | ||
| option.text = desc; | ||
| x.add(option, x[0]); | ||
| option.value = formats[a][0] | ||
| } | ||
| option.selected = "selected" | ||
| // Reveal Dropdown | ||
| document.getElementById('load-format').classList.add("hidden"); | ||
| document.getElementById("ddMenu").classList.remove("hidden"); | ||
| } | ||
| function show(...visible) { | ||
| for (const [name, element] of Object.entries(sections)) { | ||
| element.hidden = !visible.includes(name); | ||
| } | ||
| } | ||
| }) | ||
| function mib(bytes) { | ||
| return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; | ||
| } | ||
| function eta(seconds) { | ||
| const minutes = Math.floor(seconds / 60); | ||
| return `${minutes}:${String(seconds % 60).padStart(2, "0")}`; | ||
| } | ||
| function renderProgress(progress) { | ||
| const status = document.getElementById("progress-status"); | ||
| const bar = document.getElementById("progress-bar"); | ||
| const detail = document.getElementById("progress-detail"); | ||
| if (!progress) { | ||
| status.textContent = "Starting..."; | ||
| bar.removeAttribute("value"); | ||
| detail.textContent = ""; | ||
| return; | ||
| } | ||
| if (progress.status === "finished") { | ||
| status.textContent = "Converting..."; | ||
| bar.removeAttribute("value"); | ||
| detail.textContent = ""; | ||
| return; | ||
| } | ||
| status.textContent = progress.speed | ||
| ? `Downloading at ${mib(progress.speed)}/s` | ||
| : "Downloading..."; | ||
| if (progress.total) { | ||
| bar.value = (progress.downloaded / progress.total) * 100; | ||
| detail.textContent = `${mib(progress.downloaded)} / ${mib(progress.total)}`; | ||
| } else { | ||
| bar.removeAttribute("value"); | ||
| detail.textContent = mib(progress.downloaded); | ||
| } | ||
| if (progress.eta) { | ||
| detail.textContent += ` - ${eta(progress.eta)} left`; | ||
| } | ||
| } | ||
| function renderFormats(formats) { | ||
| dropdown.replaceChildren(); | ||
| for (const format of formats) { | ||
| const option = document.createElement("option"); | ||
| option.value = format.selection; | ||
| option.textContent = `${format.label} - ${format.size}`; | ||
| dropdown.add(option); | ||
| } | ||
| document.getElementById("list-formats").hidden = formats.length > 0; | ||
| formatPicker.hidden = formats.length === 0; | ||
| if (!formats.length) { | ||
| sections.error.hidden = false; | ||
| sections.error.textContent = "No mp4 formats found on this page"; | ||
| } | ||
| } | ||
| port.onMessage.addListener((state) => { | ||
| if (state.phase !== "downloading") { | ||
| console.log("[popup] state:", JSON.stringify(state)); | ||
| } | ||
| switch (state.phase) { | ||
| case "loading-formats": | ||
| show("picker", "loading"); | ||
| break; | ||
| case "formats": | ||
| show("picker"); | ||
| renderFormats(state.formats); | ||
| break; | ||
| case "downloading": | ||
| show("progress"); | ||
| renderProgress(state.progress); | ||
| break; | ||
| case "done": | ||
| show("picker", "result"); | ||
| sections.result.textContent = state.files.length | ||
| ? `Saved to ${state.dir}` | ||
| : "Already downloaded"; | ||
| break; | ||
| case "error": | ||
| show("picker", "error"); | ||
| sections.error.textContent = state.message; | ||
| break; | ||
| default: | ||
| show("picker"); | ||
| } | ||
| }); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet