// 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 += "
" + 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 = "" + data[col] + "
"; } // Close out the column lhtml += ""; } else { // Otherwise write the row with nothing in it so all the other rows display properly lhtml += "