// Global socketio variable for socketio operations var socket = ""; // yt-dlp-web-api url var apiurl = "localhost:8888"; // Notification count var notifications = 0; // Web page title, used to help with notifications var title = "yt-dlp web ui"; // Global videojs player variable for videojs operations var player = ""; // Sources for video being clipped var vsources = ""; // All the easy id3 fields to add to the easy id3 form var eid3fields = ["album", "bpm", "compilation", "composer", "encodedby", "lyricist", "length", "media", "mood", "title", "version", "artist", "albumartist", "conductor", "arranger", "discnumber", "tracknumber", "author", "albumartistsort", "albumsort", "composersort", "artistsort", "titlesort", "language", "genre", "date", "performer", "asin", "performer", "catalognumber"]; // Info for current video var ginfo = []; // Limits of the server var glimits = []; var streamPick = ""; // Predefined themes var themes = { "white": { "bgcolor": "#ffffff", "textcolor": "#000000" }, "yellow": { "bgcolor": "#ffff00", "textcolor": "#000000" }, "purple": { "bgcolor": "#b642f5", "textcolor": "#ffffff" }, "grayblue": { "bgcolor": "#636987", "textcolor": "#ffffff" }, "sky": { "bgcolor": "#aaaaff", "textcolor": "#ffffff" }, "black": { "bgcolor": "#000000", "textcolor": "#ffffff" } }; const DO_NOT_RETRY = "coi-dnr"; window.coi = { shouldRegister: () => { if (!window["sessionStorage"]) return false; let register = !window.sessionStorage.getItem(DO_NOT_RETRY); window.sessionStorage.removeItem(DO_NOT_RETRY); return register; }, coepCredentialless: () => { if (window["chrome"] !== undefined || window["netscape"] !== undefined) return true; const test = /firefox(\/(\d+))?/i; try { const match = test.exec(window.navigator.userAgent); if (match && match[2]) { const n = parseInt(match[2]); if (!isNaN(n) && n >= 119) return true; } } catch (e) { } return false; }, doReload: () => { window.sessionStorage.setItem(DO_NOT_RETRY, "true"); window.location.reload() } }; // Do when the document has loaded document.addEventListener("DOMContentLoaded", function () { // Check url for youtube link and if there's a youtube link prefill the Video/Playlist URL field with it var here = window.location.href; if (here.includes("?h")) { document.querySelector("#url").value = "https://youtube.com/watch?v=" + here.split("?v=")[1]; } // Load theme from last theme settings // If a gradient is set up load the gradient if (document.querySelector("#gradienttop").value != "none") { if (document.querySelector("#gradientbottom").value != "none") { setTheme(true); } else { setTheme(); } } else { setTheme(); } // Connect to the socketio server socket = io(apiurl); // Emit signal to query the limits of the server socket.emit("limits", {}); // Preset the step variable to 1, this makes sure step based functionality won't break document.querySelector("#step").value = 1; // Hide all warnings, in some cases this is unncessary, but better safe than sorry document.querySelector("#clipperWarning").style.display = "none"; document.querySelector("#clipperWarning2").style.display = "none"; document.querySelector("#clipperWarning3").style.display = "none"; document.querySelector("#clipperWarning4").style.display = "none"; document.querySelector('#urlWarning').style.display = 'none'; // Populate easy id3 configuration form with fields from the eid3fields variable for (it in eid3fields) { document.querySelector("#id3").innerHTML += ''; document.querySelector("#id3").innerHTML += ''; } // Adds submit button to easy id3 configuration form document.querySelector("#id3").innerHTML += '

'; // All emissions return here socket.on("done", (data) => { // If necessary add a notification to the tab of the site notify(); // If there's an error in the result just make the row with the given data, logic for what to actually show is in makeRow if (data["error"]) { var tr = makeRow(data); document.querySelector("#results").prepend(tr); } else if (data["method"] == "limits") { // For the limits method we don't want to show snything in the results table, but the limits table at the top of the page var limits = data["limits"]; glimits = limits; // Populate the limits table for (it in limits) { var lv = Number(limits[it]["limitvalue"]); var curl = ""; if (limits[it]["limitid"].includes("Length")) { curl = "" + (Math.floor(lv / 60)).toString().padStart(2, "0") + ":" + (lv % 60).toString().padStart(2, "0"); } else { curl = lv.toString(); } document.querySelector("#" + limits[it]["limitid"]).innerHTML = curl; } } else { // Get method var method = data["method"]; // If done with step 1 of subtitles increment to step 2, otherwise reset to step 1 if (method == "subtitles") { if ("step" in data) { document.querySelector("#step").value = 2; } else { document.querySelector("#step").value = 1; } } // When done with clipping reset to step 1 if (method == "clip") { document.querySelector("#step").value = 1; } // When done wih getting streams set global video info if (method == "streams") { ginfo = data["info"]; } // When done with step 1 of clipping (getting the tracks), populate the sources for the videojs player and reload it if (method == "getClipperTracks") { var sources = []; for (i in data["info"]["formats"]) { var cob = data["info"]["formats"][i]; if (cob["vcodec"] != "none") { var codec = cob["video_ext"]; sources.push({ "type": "video/" + codec, "src": cob["url"], "resolution": cob["resolution"], "format_id": cob["format_id"] }); } } loadVideo(sources); } else { // For any other case just make a row with the given data var tr = makeRow(data); document.querySelector("#results").prepend(tr); } } // Remove the loading spinner if it exists if (data["method"] != "limits") { document.querySelector("[id='" + data["spinnerid"] + "']").remove(); } }); // When the tab is viewed the notifications are reset to 0 and the notifications are removed from the tab title document.addEventListener("visibilitychange", () => { if (!document.hidden) { document.title = title; notifications = 0; } }); }); // When downloading a stream from the select the Get all download links for video method provides just open the link in a new tab function dlStream(sid) { if (document.getElementById("combineCheck").checked == true) { //var aurl = ginfo["formats"][document.querySelector("#a_" + sid).value]["url"]; //var vurl = ginfo["formats"][document.querySelector("#v_" + sid).value]["url"]; //combineStreams(aurl, vurl); var vid = ginfo["formats"][document.querySelector("#v_" + sid).value]["format_id"]; var aid = ginfo["formats"][document.querySelector("#a_" + sid).value]["format_id"]; combineStreamsHard(vid, aid); } else { window.open(ginfo["formats"][streamPick]["url"], "_blank"); } } const { fetchFile } = FFmpegUtil; const { FFmpeg } = FFmpegWASM; let ffmpeg = null; // On hold, ffmpeg-wasm needs more testing // import { FFmpeg } from '@ffmpeg/ffmpeg'; // import { fetchFile, toBlobURL } from '@ffmpeg/util'; async function combineStreams(aurl, vurl, sid) { console.log("loading ffmpeg"); try { if (ffmpeg === null) { ffmpeg = new FFmpeg(); ffmpeg.on("log", ({ message }) => { console.log(message); }) ffmpeg.on("progress", ({ progress, time }) => { console.log(`${progress * 100} %, time: ${time / 1000000} s`); }); // TODO: Providing necessary headers // TODO: Figure out what needs to be done to get this working live await ffmpeg.load({ coreURL: '/node_modules/@ffmpeg/core/dist/umd/ffmpeg-core.js', //wasmURL: '/node_modules/@ffmpeg/core-mt/dist/umd/ffmpeg-core.wasm', //workerURL: '/node_modules/@ffmpeg/core-mt/dist/umd/ffmpeg-core.worker.js', }); } console.log("loaded ffmpeg"); console.log("fetching audio"); await ffmpeg.writeFile('inputaudio.mp3', await fetchFile(aurl)); console.log("fetching video"); console.log(vurl); await ffmpeg.writeFile('inputvideo.mp4', await fetchFile(vurl)); // Todo: codec matching for more efficient mixing console.log("mixing"); await ffmpeg.exec(['-i', 'inputvideo.mp4', '-i', 'inputaudio.mp3', '-c:v', 'mp4', '-c:a', 'aac', 'output.mp4']); console.log("done"); const data = await ffmpeg.readFile('output.mp4'); window.open(URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' })), "_blank"); } catch (e){ console.trace(); } } function combineStreamsHard(format_id, format_id_audio){ var spinnerid = genSpinner(); var data = { "url": ginfo["original_url"], "format_id": format_id, "format_id_audio": format_id_audio, "spinnerid": spinnerid } socket.emit("combine", data); } // Method for showing notifications in title function notify() { notifications += 1; document.title = "(" + notifications.toString() + ") " + title; } // Very important, the logic here decides what is returned to the user via the results tables function makeRow(data) { // Create a table row element var tr = document.createElement("tr"); // Create variable to store generated html for later adding to the tr element var lhtml = ""; // Columns of the table const cols = ["status", "title", "link", "details"]; // Iterate through each column of the table and construct and add the html for each column for (col in cols) { col = cols[col]; // If the column is in the data or it's status then... if ((col in data) || (col == "status")) { // Starts the column html lhtml += ""; // If the column is status, display either an error indicator or a success indicator if (col == "status") { if (data["error"]) { lhtml += ""; } else { lhtml += ""; } } else if (col == "link") { // If the column is link then wrap the link in the requisite html to be clickable lhtml += "" + data[col] + ""; } else if (col == "details") { // If the column is details first display whatever data has been set for this column lhtml += "

" + data[col] + "

"; // If select is in our data keys... if ("select" in data) { // Generate specially formatted selects based on whether this is for streams or another method (subtitles) if (data["method"] == "streams") { // Used to differentiate selects when there are multiple var iid = crypto.randomUUID(); ahtml = "
Audio"; chtml = "Both"; for (it in ginfo["formats"]) { if (ginfo["formats"][it]["audio_ext"] == "none") { if (ginfo["formats"][it]["video_ext"] == "none") { ohtml += ""; } else { vhtml += ""; } } else if (ginfo["formats"][it]["video_ext"] == "none") { ahtml += ""; } else { chtml += ""; } } lhtml += ahtml + "" + vhtml + "" + chtml + "" + ohtml + ""; // TODO: Split dropdown into audio/video/both dropdowns Done // TODO: Combine and download button for audio/video edit: checkbox lhtml += "
" + ``; lhtml += "
"; } else { var iid = crypto.randomUUID(); lhtml += ""; lhtml += ''; lhtml += ''; lhtml += '
'; } } } else { // Anything else just display the data directly lhtml += "

" + data[col] + "

"; } // Close out the column lhtml += ""; } else { // Otherwise write the row with nothing in it so all the other rows display properly lhtml += "" } } // Add html to tr element and return it for display tr.innerHTML = lhtml; return tr; } // Generate a turretcss spinner with a random id (so it can be removed later) and append it to the spinners div (just above the results table) function genSpinner() { var spinnerid = crypto.randomUUID(); var sp = document.createElement("button"); sp.setAttribute("id", spinnerid); sp.setAttribute("class", "spinner"); sp.setAttribute("title", "Loading"); document.querySelector("#spinners").append(sp); return spinnerid; } // When an ID3 submission is made we use a submit button, this way we run the code we need and return false, meaning that the form won't actually post // Constructs an object with all the id3 form values to use with toMP3, which will in turn set these values to be added to the metadata of the MP3 function id3submit() { var form = document.getElementById('id3'); var values = {}; for (var it in form.elements) { values[form.elements[it].id] = form.elements[it].value; } var url = document.querySelector('#url').value; toMP3(url, values); return false; } // Hide/show the ID3 form function toggleID3Form() { var form = document.querySelector("#id3"); var button = document.querySelector("#id3toggle"); if (form.style.display == "none") { form.style.display = "block"; button.textContent = "Hide ID3 form"; } else { form.style.display = "none"; button.textContent = "Show ID3 form"; } } // Generate spinner and emit toMP3 with the spinnerid (to remove the spinner later) to download mp3 and optionally add metadata to the file function toMP3(url, id3data = null) { var spinnerid = genSpinner(); socket.emit("toMP3", { "url": url, "spinnerid": spinnerid, "id3": id3data }); } // Emit playlist to download a playlist as MP3s and zip them for download function playlist(url) { var spinnerid = genSpinner(); socket.emit("playlist", { "url": url, "spinnerid": spinnerid }); } // Emit either step of subtitles // Step 1 will give us a list of the subtitle languages // Step 2 will give us a download of the selected subtitle function subtitles(url, iid = null) { var spinnerid = genSpinner(); var step = Number(document.querySelector("#step").value); var data = { "url": url, "spinnerid": spinnerid, "step": step } if (step == 2) { data["languageCode"] = document.querySelector('[data-iid="' + iid + '"]#langSelect').value; data["autoSub"] = document.querySelector('[data-iid="' + iid + '"]#autoSubs').checked; } socket.emit("subtitles", data); } // Generic getInfo method to get all the info for a given link function getInfo(url, method) { var spinnerid = genSpinner() var data = { "url": url, "spinnerid": spinnerid, "method": method }; socket.emit("getInfoEvent", data); } // Clip method, ultimately produces a clip of a video or a gif of that clip function clip(url, directURL = null, gif = null, format_id = null, resolution = null) { // Get the start and end times for the clip var timeA = document.querySelector("#timeA").value; var timeB = document.querySelector("#timeB").value; // If there is a set resolution, check to make sure that resolution is not larger then the servers limit // Otherwise display an error message if (resolution != null) { if (resolution > glimits[4]["limitvalue"]) { document.querySelector("#clipperWarning4").style.display = "block"; } } else if (gif && ((timeB - timeA) > glimits[3]["limitvalue"])) { // If this is a gif check if the length of the clip is longer than the servers limit // If so, display an error message document.querySelector("#clipperWarning3").style.display = "block"; } else { // If the start of the clip is after the end of the clip display an error message if (Number(timeA) > Number(timeB)) { document.querySelector("#clipperWarning2").style.display = "block"; } else { var spinnerid = genSpinner(); // Set up the required values for the clip method and then set up additional values if they are not null var data = { "url": url, "spinnerid": spinnerid, "timeA": timeA, "timeB": timeB }; if (format_id != null) { data["format_id"] = format_id } if (directURL != null) { data["directURL"] = directURL } if (gif != null) { data["gif"] = true; } socket.emit("clip", data); } } } // This loads a video into videojs with multiple sources and generates a list of options in a select for the user to choose a stream // setSource handles actually setting the stream function loadVideo(sources) { // Clear the video container, to be populated later document.querySelector("#videoContainer").innerHTML = ""; // Create video element to be used to create videojs player var video = document.createElement("video"); // Sources object for videojs initialization var sdata = { "sources": sources }; // Set global sources for the video vsources = sources; // Set some basic attributes for the videojs player video.setAttribute("id", "clipPlayer"); video.setAttribute("class", "video-js vjs-default-skin"); video.setAttribute("controls", ""); video.setAttribute("width", "720"); // Add the video to the DOM document.querySelector("#videoContainer").append(video); // Clear the source selector document.querySelector("#srcSelect").innerHTML = ""; // Populate source selector for (it in sources) { document.querySelector("#srcSelect").innerHTML += ""; } // If the videojs player object isn't empty then dispose it if (player != "") { player.dispose(); } // Test if this is doing anything //document.querySelector("#clipPlayer").innerHTML = ""; // Initialize the videojs player player = videojs('clipPlayer', sdata, function onPlayerReady() { this.on("loadedmetadata", function () { // If the video is too long display a warning if (this.duration() < glimits[0]["limitvalue"]) { document.querySelector("#timeA").max = this.duration(); document.querySelector("#timeB").max = this.duration(); document.querySelector("#clipper").style.display = "block"; } else { document.querySelector("#clipper").innerHTML = ""; document.querySelector("#clipperWarning").style.display = "block"; } }); }); } // Set the current source of the videojs player based on the selected source function setSource(srcIndex) { player.src(vsources[Number(srcIndex)]); } // Set the current time of the video player when the time sliders change, format the time display to minutes and seconds function updatePlayerTime(range) { player.currentTime(range.value); if (range.id == "timeA") { document.querySelector("#timeStart").value = (Math.floor(range.value / 60)).toString().padStart(2, "0") + ":" + (range.value % 60).toString().padStart(2, "0"); } else if (range.id == "timeB") { document.querySelector("#timeEnd").value = (Math.floor(range.value / 60)).toString().padStart(2, "0") + ":" + (range.value % 60).toString().padStart(2, "0"); } } // Submit the clip to be cut function clipSecond() { document.querySelector("#step").value = 2; process(); } // Semi generic method tying buttons to emissions // This should be reworked and the extraSteps stuff should be removed completely // Recommendation: just a set of if elses for the exact method that was passed function process(button = null, extraSteps = null) { // Get method from method select var method = document.querySelector('#method').value; // Get the url from the Video/Playlist URL field var url = document.querySelector('#url').value; // Check if the url is valid if (isValidUrl(url)) { // Hide warnings until needed document.querySelector('#urlWarning').style.display = 'none'; document.querySelector("#clipperWarning").style.display = "none"; // Run toMP3 function if (method == "toMP3") { toMP3(url); } else if (method == "playlist") { // Run playlist function playlist(url); } else if (method == "subtitles") { // Run subtitles function // If a button has been passed (in order to identify the select) pass the iid to subtitles for selected subtitles retrieval if (button == null) { subtitles(url); } else { subtitles(url, iid = button.getAttribute('data-iid')); } } else if (method == "streams") { // Run getInfo function with the streams method getInfo(url, "streams"); } else if (method == "clip") { // Get the step of clipping we're at var step = Number(document.querySelector("#step").value); // Add extraSteps if specified if (extraSteps != null) { extraSteps = Number(extraSteps); step += extraSteps; } // If we're at step 1 get a list of tracks for the video if (step == 1) { getInfo(url, "getClipperTracks"); } else if (step == 2) { // If we're at step 2 get some info and clip the video var format = vsources[Number(document.querySelector("#srcSelect").value)]["format_id"] var format_id = format["format_id"]; var resolution = format["width"] if (document.querySelector("#toGif").checked) { clip(url, null, true, format_id = format_id, resolution = resolution); } else { clip(url, null, null, format_id = format_id, resolution = resolution); } } } } else { // If the url isn't valid display a warning document.querySelector('#urlWarning').style.display = 'block'; } } // Checks if an entered url is valid // https://www.freecodecamp.org/news/check-if-a-javascript-string-is-a-url/ const isValidUrl = urlString => { try { return Boolean(new URL(urlString)); } catch (e) { return false; } } // Get a random integer in an inclusive range function randomInt(min, max) { return Math.floor(Math.random() * (max - min + 1) + min) } // Set theme based on the selected theme or custom values function setTheme(customgradient = false) { var r = document.querySelector(':root'); var theme = document.querySelector("#theme").value; if (theme == "custom") { theme = { "bgcolor": document.querySelector("#bgcolor").value, "textcolor": document.querySelector("#textcolor").value } if (customgradient) { // Set the gradient with the custom values from Top/Bottom color of gradient theme["gradienttop"] = document.querySelector("#gradienttop").value; theme["gradientbottom"] = document.querySelector("#gradientbottom").value; } else { // Set the gradient to a solid color theme["gradienttop"] = theme["bgcolor"]; theme["gradientbottom"] = theme["bgcolor"]; } } else { var themename = theme; theme = themes[theme]; if (themename != "sky") { theme["gradienttop"] = theme["bgcolor"]; theme["gradientbottom"] = theme["bgcolor"]; } else { // Generate two light blues colors in hex format and set the top and bottom of the gradient to these var top0 = randomInt(50, 99).toString().padStart(2, "0"); top0 = "#" + top0 + top0 + "ff"; var bottom0 = randomInt(50, 99).toString().padStart(2, "0"); bottom0 = "#" + bottom0 + bottom0 + "ff"; theme["gradienttop"] = top0; theme["gradientbottom"] = bottom0; } } for (it in theme) { r.style.setProperty('--' + it, theme[it]); } var rs = getComputedStyle(r); } // Generic toggle method, requires an element with an id and a element that will be used for toggling the visibility of the main element with an id of the main element + _toggle function toggle(id) { if (document.querySelector(id).style.display == "none") { document.querySelector(id).style.display = "block"; document.querySelector(id + "_toggle").innerHTML = "➖ Hide"; } else { document.querySelector(id).style.display = "none"; document.querySelector(id + "_toggle").innerHTML = "➕ Show"; } } function setStreamPick(sid) { streamPick = Number(document.getElementById(sid).value); }