536 lines
23 KiB
JavaScript
536 lines
23 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 = [];
|
||
// 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"
|
||
}
|
||
};
|
||
// 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) {
|
||
document.querySelector("#" + limits[it]["limitid"]).innerHTML = limits[it]["limitvalue"];
|
||
}
|
||
} 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(selectID) {
|
||
var index = document.getElementById(selectID).value;
|
||
window.open(ginfo["formats"][Number(index)]["url"], "_blank");
|
||
}
|
||
|
||
// 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();
|
||
lhtml += "<br><select id='" + iid + "'>";
|
||
for (it in ginfo["formats"]) {
|
||
lhtml += "<option value='" + it.toString() + "'>" + ginfo["formats"][it]["format"] + " / " + ginfo["formats"][it]["ext"] + " / audio: " + ginfo["formats"][it]["audio_ext"] + "</option>";
|
||
}
|
||
lhtml += "</select>";
|
||
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";
|
||
}
|
||
}
|