diff --git a/src/playlist.js b/src/playlist.js index c1a1189..6a25119 100644 --- a/src/playlist.js +++ b/src/playlist.js @@ -1,766 +1,655 @@ -const {clipboard, shell, ipcRenderer} = require("electron"); +const {clipboard, ipcRenderer} = require("electron"); const {default: YTDlpWrap} = require("yt-dlp-wrap-plus"); const path = require("path"); const os = require("os"); const fs = require("fs"); const {execSync} = require("child_process"); const {constants} = require("fs/promises"); -let url; -const ytDlp = localStorage.getItem("ytdlp"); -console.log(`yt-dlp: ${ytDlp}`); -const ytdlp = new YTDlpWrap(`"${ytDlp}"`); -const downloadsDir = path.join(os.homedir(), "Downloads"); -let downloadDir = localStorage.getItem("downloadPath") || downloadsDir; - -document.addEventListener("translations-loaded", () => { - window.i18n.translatePage(); -}); - -try { - fs.accessSync(downloadDir, constants.W_OK); - downloadDir = downloadDir; -} catch (err) { - console.log( - "Unable to write to download directory. Switching to default one." - ); - console.log("Err:", err); - downloadDir = downloadsDir; - localStorage.setItem("downloadPath", downloadsDir); -} - -getId("path").textContent = downloadDir; -let cookieArg = ""; -let browser = ""; -const formats = { - 144: 160, - 240: 133, - 360: 134, - 480: 135, - 720: 136, - 1080: 299, - 1440: 400, - 2160: 401, - 4320: 571, -}; -let originalCount = 0; -let ffmpeg; -let ffmpegPath; - -if (os.platform() === "win32") { - ffmpegPath = `${__dirname}\\..\\ffmpeg.exe`; -} else if (os.platform() === "freebsd") { - try { - ffmpegPath = execSync("which ffmpeg") - .toString("utf8") - .split("\n")[0] - .trim(); - } catch (error) { - console.log(error); - } -} else { - ffmpegPath = `${__dirname}/../ffmpeg`; -} - -let denoPath = getJsRuntimePath(); - -if (process.env.YTDOWNLOADER_FFMPEG_PATH) { - ffmpegPath = `${process.env.YTDOWNLOADER_FFMPEG_PATH}`; - - if (fs.existsSync(process.env.YTDOWNLOADER_FFMPEG_PATH)) { - console.log("Using YTDOWNLOADER_FFMPEG_PATH"); - } else { - console.error("No ffmpeg found in " + ffmpeg); - } -} - -ffmpeg = `"${ffmpegPath}"`; - -console.log("ffmpeg:", ffmpeg); - -if (localStorage.getItem("preferredVideoQuality")) { - const preferredVideoQuality = localStorage.getItem("preferredVideoQuality"); - getId("select").value = preferredVideoQuality; -} -if (localStorage.getItem("preferredAudioQuality")) { - const preferredAudioQuality = localStorage.getItem("preferredAudioQuality"); - getId("audioSelect").value = preferredAudioQuality; -} - -let foldernameFormat = "%(playlist_title)s"; -let filenameFormat = "%(playlist_index)s.%(title)s.%(ext)s"; -let playlistIndex = 1; -let playlistEnd = ""; -let proxy = ""; -let playlistName = ""; - -/** - * - * @param {string} id - * @returns {any} - */ -function getId(id) { - return document.getElementById(id); -} - -function pasteLink() { - url = clipboard.readText(); - getId("link").textContent = " " + url; - getId("options").style.display = "block"; - getId("incorrectMsgPlaylist").textContent = ""; - getId("errorBtn").style.display = "none"; -} - -getId("pasteLink").addEventListener("click", () => { - pasteLink(); -}); - -document.addEventListener("keydown", (event) => { - if (event.ctrlKey && event.key == "v") { - pasteLink(); - } -}); - -// Patterns -const playlistTxt = "Downloading playlist: "; -const videoIndex = "Downloading item "; -const oldVideoIndex = "Downloading video "; - -/** - * @param {string} type - */ -function download(type) { - // Config file - let configArg = ""; - let configTxt = ""; - if (localStorage.getItem("configPath")) { - configArg = "--config-location"; - configTxt = `"${localStorage.getItem("configPath")}"`; - } - proxy = localStorage.getItem("proxy") || ""; - console.log("Proxy:", proxy); - - nameFormatting(); - originalCount = 0; - - // Playlist download range - managePlaylistRange(); - - // Whether to use browser cookies or not - if (localStorage.getItem("browser")) { - browser = localStorage.getItem("browser"); - } - if (browser) { - cookieArg = "--cookies-from-browser"; - } else { - cookieArg = ""; - } - let count = 0; - let subs, subLangs; - - // If subtitles are checked - if (getId("subChecked").checked) { - subs = "--write-subs"; - subLangs = "--sub-langs all"; - console.log("Downloading with subtitles"); - } else { - subs = ""; - subLangs = ""; - } - - playlistName = ""; - - hideOptions(); - - let quality, format, downloadProcess, videoType, audioQuality; - if (type === "video") { - quality = getId("select").value; - videoType = getId("videoTypeSelect").value; - const formatId = formats[quality]; + +const playlistDownloader = { + // State and config + state: { + url: null, + downloadDir: null, + ytDlpPath: null, + ytDlpWrap: null, + ffmpegPath: null, + denoPath: null, + playlistName: "", + originalCount: 0, + currentDownloadProcess: null, + }, + + config: { + foldernameFormat: "%(playlist_title)s", + filenameFormat: "%(playlist_index)s.%(title)s.%(ext)s", + proxy: "", + cookie: { + browser: "", + arg: "", + }, + configFile: { + arg: "", + path: "", + }, + playlistRange: { + start: 1, + end: "", + }, + }, + + // DOM elements + ui: { + pasteLinkBtn: document.getElementById("pasteLink"), + linkDisplay: document.getElementById("link"), + optionsContainer: document.getElementById("options"), + downloadList: document.getElementById("list"), + + downloadVideoBtn: document.getElementById("download"), + downloadAudioBtn: document.getElementById("audioDownload"), + downloadThumbnailsBtn: document.getElementById("downloadThumbnails"), + saveLinksBtn: document.getElementById("saveLinks"), + + selectLocationBtn: document.getElementById("selectLocation"), + pathDisplay: document.getElementById("path"), + openDownloadsBtn: document.getElementById("openDownloads"), + + videoToggle: document.getElementById("videoToggle"), + audioToggle: document.getElementById("audioToggle"), + advancedToggle: document.getElementById("advancedToggle"), + videoBox: document.getElementById("videoBox"), + audioBox: document.getElementById("audioBox"), + videoQualitySelect: document.getElementById("select"), + videoTypeSelect: document.getElementById("videoTypeSelect"), + typeSelectBox: document.getElementById("typeSelectBox"), + audioTypeSelect: document.getElementById("audioSelect"), + audioQualitySelect: document.getElementById("audioQualitySelect"), + + advancedMenu: document.getElementById("advancedMenu"), + playlistIndexInput: document.getElementById("playlistIndex"), + playlistEndInput: document.getElementById("playlistEnd"), + subtitlesCheckbox: document.getElementById("subChecked"), + closeHiddenBtn: document.getElementById("closeHidden"), + + playlistNameDisplay: document.getElementById("playlistName"), + errorMsgDisplay: document.getElementById("incorrectMsgPlaylist"), + errorBtn: document.getElementById("errorBtn"), + errorDetails: document.getElementById("errorDetails"), + + menuIcon: document.getElementById("menuIcon"), + menu: document.getElementById("menu"), + preferenceWinBtn: document.getElementById("preferenceWin"), + aboutWinBtn: document.getElementById("aboutWin"), + historyWinBtn: document.getElementById("historyWin"), + homeWinBtn: document.getElementById("homeWin"), + compressorWinBtn: document.getElementById("compressorWin"), + }, + + init() { + this.loadInitialConfig(); + this.initEventListeners(); + + // Set initial UI state + this.ui.pathDisplay.textContent = this.state.downloadDir; + this.ui.videoToggle.style.backgroundColor = "var(--box-toggleOn)"; + this.updateVideoTypeVisibility(); + + // Load translations when ready + document.addEventListener("translations-loaded", () => { + window.i18n.translatePage(); + }); + + console.log(`yt-dlp path: ${this.state.ytDlpPath}`); + console.log(`ffmpeg path: ${this.state.ffmpegPath}`); + }, + + loadInitialConfig() { + // yt-dlp path + this.state.ytDlpPath = localStorage.getItem("ytdlp"); + this.state.ytDlpWrap = new YTDlpWrap(this.state.ytDlpPath); + + const defaultDownloadsDir = path.join(os.homedir(), "Downloads"); + let preferredDir = + localStorage.getItem("downloadPath") || defaultDownloadsDir; + try { + fs.accessSync(preferredDir, constants.W_OK); + this.state.downloadDir = preferredDir; + } catch (err) { + console.error( + "Unable to write to preferred download directory. Reverting to default.", + err + ); + this.state.downloadDir = defaultDownloadsDir; + localStorage.setItem("downloadPath", defaultDownloadsDir); + } + + // ffmpeg and deno path setup + this.state.ffmpegPath = this.getFfmpegPath(); + this.state.denoPath = this.getJsRuntimePath(); + + if (localStorage.getItem("preferredVideoQuality")) { + this.ui.videoQualitySelect.value = localStorage.getItem( + "preferredVideoQuality" + ); + } + if (localStorage.getItem("preferredAudioQuality")) { + this.ui.audioQualitySelect.value = localStorage.getItem( + "preferredAudioQuality" + ); + } + }, + + initEventListeners() { + this.ui.pasteLinkBtn.addEventListener("click", () => this.pasteLink()); + document.addEventListener("keydown", (event) => { + if (event.ctrlKey && event.key === "v") this.pasteLink(); + }); + + this.ui.downloadVideoBtn.addEventListener("click", () => + this.startDownload("video") + ); + this.ui.downloadAudioBtn.addEventListener("click", () => + this.startDownload("audio") + ); + this.ui.downloadThumbnailsBtn.addEventListener("click", () => + this.startDownload("thumbnails") + ); + this.ui.saveLinksBtn.addEventListener("click", () => + this.startDownload("links") + ); + + this.ui.videoToggle.addEventListener("click", () => + this.toggleDownloadType("video") + ); + this.ui.audioToggle.addEventListener("click", () => + this.toggleDownloadType("audio") + ); + this.ui.advancedToggle.addEventListener("click", () => + this.toggleAdvancedMenu() + ); + this.ui.videoQualitySelect.addEventListener("change", () => + this.updateVideoTypeVisibility() + ); + this.ui.selectLocationBtn.addEventListener("click", () => + ipcRenderer.send("select-location-main", "") + ); + this.ui.openDownloadsBtn.addEventListener("click", () => + this.openDownloadsFolder() + ); + this.ui.closeHiddenBtn.addEventListener("click", () => + this.hideOptions(true) + ); + + this.ui.preferenceWinBtn.addEventListener("click", () => + this.navigate("page", "/preferences.html") + ); + this.ui.aboutWinBtn.addEventListener("click", () => + this.navigate("page", "/about.html") + ); + this.ui.historyWinBtn.addEventListener("click", () => + this.navigate("page", "/history.html") + ); + this.ui.homeWinBtn.addEventListener("click", () => + this.navigate("win", "/index.html") + ); + this.ui.compressorWinBtn.addEventListener("click", () => + this.navigate("win", "/compressor.html") + ); + + ipcRenderer.on("downloadPath", (_event, downloadPath) => { + if (downloadPath && downloadPath[0]) { + this.ui.pathDisplay.textContent = downloadPath[0]; + this.state.downloadDir = downloadPath[0]; + } + }); + }, + + startDownload(type) { + if (!this.state.url) { + this.showError("URL is missing. Please paste a link first."); + return; + } + this.updateDynamicConfig(); + this.hideOptions(); + + const controller = new AbortController(); + const baseArgs = this.buildBaseArgs(); + let specificArgs = []; + + switch (type) { + case "video": + specificArgs = this.getVideoArgs(); + break; + case "audio": + specificArgs = this.getAudioArgs(); + break; + case "thumbnails": + specificArgs = this.getThumbnailArgs(); + break; + case "links": + specificArgs = this.getLinkArgs(); + break; + } + + const allArgs = [ + ...baseArgs, + ...specificArgs, + `"${this.state.url}"`, + ].filter(Boolean); + + console.log(`Command: ${this.state.ytDlpPath}`, allArgs.join(" ")); + this.state.currentDownloadProcess = this.state.ytDlpWrap.exec( + allArgs, + {shell: true, detached: false}, + controller.signal + ); + + this.handleDownloadEvents(this.state.currentDownloadProcess, type); + }, + + buildBaseArgs() { + const {start, end} = this.config.playlistRange; + const outputPath = `"${path.join( + this.state.downloadDir, + this.config.foldernameFormat, + this.config.filenameFormat + )}"`; + + return [ + "--yes-playlist", + "-o", + outputPath, + "-I", + `"${start}:${end}"`, + "--ffmpeg-location", + `"${this.state.ffmpegPath}"`, + ...(this.state.denoPath + ? ["--no-js-runtimes", "--js-runtime", this.state.denoPath] + : []), + this.config.cookie.arg, + this.config.cookie.browser, + this.config.configFile.arg, + this.config.configFile.path, + ...(this.config.proxy + ? ["--no-check-certificate", "--proxy", this.config.proxy] + : []), + "--compat-options", + "no-youtube-unavailable-videos", + ].filter(Boolean); + }, + + getVideoArgs() { + const quality = this.ui.videoQualitySelect.value; + const videoType = this.ui.videoTypeSelect.value; + let formatArgs = []; + if (quality === "best") { - format = "-f bv*+ba/best"; + formatArgs = ["-f", "bv*+ba/best"]; } else if (quality === "worst") { - format = "-f wv+wa/worst"; + formatArgs = ["-f", "wv+wa/worst"]; } else if (quality === "useConfig") { - format = ""; + formatArgs = []; } else { if (videoType === "mp4") { - format = `-f "bestvideo[height<=1080]+bestaudio[ext=m4a]/best[height<=1080]/best" --merge-output-format "mp4" --recode-video "mp4"`; + formatArgs = [ + "-f", + `"bestvideo[height<=1080]+bestaudio[ext=m4a]/best[height<=1080]/best"`, + "--merge-output-format", + "mp4", + "--recode-video", + "mp4", + ]; } else if (videoType === "webm") { - format = `-f "bestvideo[height<=1080]+bestaudio[ext=webm]/best[height<=1080]/best" --merge-output-format "webm" --recode-video "webm"`; + formatArgs = [ + "-f", + `"bestvideo[height<=1080]+bestaudio[ext=webm]/best[height<=1080]/best"`, + "--merge-output-format", + "webm", + "--recode-video", + "webm", + ]; } else { - format = `-f "bv*[height=${quality}]+ba/best[height=${quality}]/best[height<=${quality}]"`; + formatArgs = [ + "-f", + `"bv*[height=${quality}]+ba/best[height=${quality}]/best[height<=${quality}]"`, + ]; } } - } else { - format = getId("audioSelect").value; - audioQuality = getId("audioQualitySelect").value; - console.log("Quality:", audioQuality); - } - console.log("Format:", format); - const controller = new AbortController(); + const isYouTube = + this.state.url.includes("youtube.com/") || + this.state.url.includes("youtu.be/"); + const canEmbedThumb = os.platform() !== "darwin"; - if (type === "video") { - const args = [ - format, - "--yes-playlist", - "-o", - `"${path.join(downloadDir, foldernameFormat, filenameFormat)}"`, - "-I", - `"${playlistIndex}:${playlistEnd}"`, - "--ffmpeg-location", - ffmpeg, - denoPath ? `--no-js-runtimes --js-runtime ${denoPath}` : "", - cookieArg, - browser, - configArg, - configTxt, + return [ + ...formatArgs, "--embed-metadata", - subs, - subLangs, - videoType == "mp4" && - (url.includes("youtube.com/") || url.includes("youtu.be/")) && - os.platform() !== "darwin" + this.ui.subtitlesCheckbox.checked ? "--write-subs" : "", + this.ui.subtitlesCheckbox.checked ? "--sub-langs" : "", + this.ui.subtitlesCheckbox.checked ? "all" : "", + videoType === "mp4" && isYouTube && canEmbedThumb ? "--embed-thumbnail" : "", - proxy ? "--no-check-certificate" : "", - proxy ? "--proxy" : "", - proxy, - "--compat-options", - "no-youtube-unavailable-videos", - `"${url}"`, - ].filter((item) => item); - - downloadProcess = ytdlp.exec( - args, - {shell: true, detached: false}, - controller.signal - ); - } else { - // Youtube provides m4a as audio, so no need to convert - if ( - (url.includes("youtube.com/") || url.includes("youtu.be/")) && - format === "m4a" && - audioQuality === "auto" - ) { - console.log("Downloading m4a without extracting"); - - const args = [ - "--yes-playlist", - "--no-warnings", + ].filter(Boolean); + }, + + getAudioArgs() { + const format = this.ui.audioTypeSelect.value; + const quality = this.ui.audioQualitySelect.value; + const isYouTube = + this.state.url.includes("youtube.com/") || + this.state.url.includes("youtu.be/"); + const canEmbedThumb = os.platform() !== "darwin"; + + if (isYouTube && format === "m4a" && quality === "auto") { + return [ "-f", `ba[ext=${format}]/ba`, - "-o", - `"${path.join(downloadDir, foldernameFormat, filenameFormat)}"`, - "-I", - `"${playlistIndex}:${playlistEnd}"`, - "--ffmpeg-location", - ffmpeg, - denoPath ? `--no-js-runtimes --js-runtime ${denoPath}` : "", - cookieArg, - browser, - configArg, - configTxt, "--embed-metadata", - subs, - subLangs, - os.platform() !== "darwin" ? "--embed-thumbnail" : "", - proxy ? "--no-check-certificate" : "", - proxy ? "--proxy" : "", - proxy, - "--compat-options", - "no-youtube-unavailable-videos", - `"${url}"`, - ].filter((item) => item); - - downloadProcess = ytdlp.exec( - args, - {shell: true, detached: false}, - controller.signal - ); - } else { - console.log("Extracting audio"); - - const args = [ - "--yes-playlist", - "--no-warnings", - "-x", - "--audio-format", - format, - "--audio-quality", - audioQuality, - "-o", - `"${path.join(downloadDir, foldernameFormat, filenameFormat)}"`, - "-I", - `"${playlistIndex}:${playlistEnd}"`, - "--ffmpeg-location", - ffmpeg, - denoPath ? `--no-js-runtimes --js-runtime ${denoPath}` : "", - cookieArg, - browser, - configArg, - configTxt, - "--embed-metadata", - subs, - subLangs, - format === "mp3" || - (format === "m4a" && - (url.includes("youtube.com/") || - url.includes("youtu.be/")) && - os.platform() !== "darwin") - ? "--embed-thumbnail" - : "", - proxy ? "--no-check-certificate" : "", - proxy ? "--proxy" : "", - proxy, - "--compat-options", - "no-youtube-unavailable-videos", - `"${url}"`, - ].filter((item) => item); - - downloadProcess = ytdlp.exec( - args, - {shell: true, detached: false}, - controller.signal - ); - } - } - - // getId("finishBtn").addEventListener("click", () => { - // controller.abort("user_finished") - // try { - // process.kill(downloadProcess.ytDlpProcess.pid, 'SIGINT') - // } catch (_error) {} - // }) - - downloadProcess.on("ytDlpEvent", (_eventType, eventData) => { - // console.log(eventData); - - if (eventData.includes(playlistTxt)) { - playlistName = eventData.split("playlist:")[1].slice(1); - getId("playlistName").textContent = - i18n.__("downloadingPlaylist") + " " + playlistName; - console.log(playlistName); + canEmbedThumb ? "--embed-thumbnail" : "", + ]; } - if ( - (eventData.includes(videoIndex) || - eventData.includes(oldVideoIndex)) && - !eventData.includes("thumbnail") - ) { - count += 1; - originalCount += 1; - let itemTitle; - if (type === "video") { - itemTitle = i18n.__("video") + " " + originalCount; - } else { - itemTitle = i18n.__("audio") + " " + originalCount; + return [ + "-x", + "--audio-format", + format, + "--audio-quality", + quality, + "--embed-metadata", + (format === "mp3" || (format === "m4a" && isYouTube)) && + canEmbedThumb + ? "--embed-thumbnail" + : "", + ]; + }, + + getThumbnailArgs() { + return [ + "--write-thumbnail", + "--convert-thumbnails", + "png", + "--skip-download", + ]; + }, + + getLinkArgs() { + const linksFilePath = `"${path.join( + this.state.downloadDir, + this.config.foldernameFormat, + "links.txt" + )}"`; + return [ + "--skip-download", + "--print-to-file", + "webpage_url", + linksFilePath, + ]; + }, + + // yt-dlp event handling + handleDownloadEvents(process, type) { + let count = 0; + + process.on("ytDlpEvent", (_eventType, eventData) => { + const playlistTxt = "Downloading playlist: "; + if (eventData.includes(playlistTxt)) { + this.state.playlistName = eventData + .split(playlistTxt)[1] + .trim() + .replaceAll("|", "|") + this.ui.playlistNameDisplay.textContent = `${window.i18n.__( + "downloadingPlaylist" + )} ${this.state.playlistName}`; } - if (count > 1) { - getId(`p${count - 1}`).textContent = i18n.__("fileSaved"); + const videoIndexTxt = "Downloading item "; + const oldVideoIndexTxt = "Downloading video "; + if ( + (eventData.includes(videoIndexTxt) || + eventData.includes(oldVideoIndexTxt)) && + !eventData.includes("thumbnail") + ) { + count++; + this.state.originalCount++; + this.updatePlaylistUI(count, type); } + }); - const item = `
${itemTitle}
-${i18n.__("downloading")}
-${itemTitle}
+${window.i18n.__( + "downloading" + )}
+${itemTitle}
-${i18n.__("Downloading...")}
-${itemTitle}
-${i18n.__("downloading")}
-