carch-web-ui/static/js/custom.js

654 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);
}