654 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			654 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
// 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 += '<label for="' + eid3fields[it] + '">' + eid3fields[it] + '</label>';
 | 
						||
        document.querySelector("#id3").innerHTML += '<input type="text" id="' + eid3fields[it] + '" />';
 | 
						||
    }
 | 
						||
    // Adds submit button to easy id3 configuration form
 | 
						||
    document.querySelector("#id3").innerHTML += '<br><button onclick="return id3submit()">Submit</button><br>';
 | 
						||
    // 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 += "<td>";
 | 
						||
            // If the column is status, display either an error indicator or a success indicator
 | 
						||
            if (col == "status") {
 | 
						||
                if (data["error"]) {
 | 
						||
                    lhtml += "<button class='button error'>Error</button>";
 | 
						||
                } else {
 | 
						||
                    lhtml += "<button class='button success'>Success</button></td>";
 | 
						||
                }
 | 
						||
            } else if (col == "link") {
 | 
						||
                // If the column is link then wrap the link in the requisite html to be clickable
 | 
						||
                lhtml += "<a href='" + data[col] + "'>" + data[col] + "</a>";
 | 
						||
            } else if (col == "details") {
 | 
						||
                // If the column is details first display whatever data has been set for this column
 | 
						||
                lhtml += "<p>" + data[col] + "</p>";
 | 
						||
                // 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 = "<br>Audio<select onchange='setStreamPick(this.id)' id='a_" + iid + "'>";
 | 
						||
                        vhtml = "Video<select onchange='setStreamPick(this.id)' id='v_" + iid + "'>";
 | 
						||
                        chtml = "Both<select onchange='setStreamPick(this.id)' id='c_" + iid + "'>";
 | 
						||
                        ohtml = "Misc<select onchange='setStreamPick(this.id)' id='m_" + iid + "'>";
 | 
						||
                        for (it in ginfo["formats"]) {
 | 
						||
                            if (ginfo["formats"][it]["audio_ext"] == "none") {
 | 
						||
                                if (ginfo["formats"][it]["video_ext"] == "none") {
 | 
						||
                                    ohtml += "<option value='" + it.toString() + "'>" + ginfo["formats"][it]["format"] + "</option>";
 | 
						||
                                } else {
 | 
						||
                                    vhtml += "<option value='" + it.toString() + "'>" + ginfo["formats"][it]["format"] + " / " + ginfo["formats"][it]["video_ext"] + "</option>";
 | 
						||
                                }
 | 
						||
                            } else if (ginfo["formats"][it]["video_ext"] == "none") {
 | 
						||
                                ahtml += "<option value='" + it.toString() + "'>" + ginfo["formats"][it]["format"] + " / " + ginfo["formats"][it]["audio_ext"] + "</option>";
 | 
						||
                            } else {
 | 
						||
                                chtml += "<option value='" + it.toString() + "'>" + ginfo["formats"][it]["format"] + " / " + ginfo["formats"][it]["ext"] + "</option>";
 | 
						||
                            }
 | 
						||
                        }
 | 
						||
                        lhtml += ahtml + "</select>" + vhtml + "</select>" + chtml + "</select>" + ohtml + "</select>";
 | 
						||
                        // TODO: Split dropdown into audio/video/both dropdowns Done
 | 
						||
                        // TODO: Combine and download button for audio/video edit: checkbox
 | 
						||
                        lhtml += "<br>" + `<label class="control checkbox">
 | 
						||
                        <input type="checkbox" name="combineCheck" id="combineCheck" />
 | 
						||
                        <span class="control-indicator"></span>
 | 
						||
                        <span class="control-label">Combine audio and video selections?</span>
 | 
						||
                      </label>`;
 | 
						||
                        lhtml += "<br><button onclick='dlStream(`" + iid + "`)'>Download selected</button>";
 | 
						||
                    } else {
 | 
						||
                        var iid = crypto.randomUUID();
 | 
						||
                        lhtml += "<select data-iid='" + iid + "' id='langSelect'>";
 | 
						||
                        for (it in data["select"]) {
 | 
						||
                            lhtml += "<option value='" + data["select"][it] + "'>" + data["select"][it] + "</option>";
 | 
						||
                        }
 | 
						||
                        lhtml += "</select>";
 | 
						||
                        lhtml += '<input id="autoSubs" data-iid="' + iid + '" type="checkbox" name="checkbox" />';
 | 
						||
                        lhtml += '<label for="autoSubs">Download automatic subs?</label>';
 | 
						||
                        lhtml += '<br><button data-iid="' + iid + '" onclick="process(this)">Get subtitles</button>';
 | 
						||
                    }
 | 
						||
                }
 | 
						||
            } else {
 | 
						||
                // Anything else just display the data directly
 | 
						||
                lhtml += "<p>" + data[col] + "</p>";
 | 
						||
            }
 | 
						||
            // Close out the column
 | 
						||
            lhtml += "</td>";
 | 
						||
        } else {
 | 
						||
            // Otherwise write the row with nothing in it so all the other rows display properly
 | 
						||
            lhtml += "<td></td>"
 | 
						||
        }
 | 
						||
    }
 | 
						||
    // 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 += "<option value='" + it + "'>" + sources[it]["resolution"] + " " + sources[it]["type"] + "</option>";
 | 
						||
    }
 | 
						||
    // 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);
 | 
						||
} |