You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wubloader/thrimbletrimmer/scripts/IO.js

510 lines
16 KiB
JavaScript

let desertBusStart = new Date("1970-01-01T00:00:00Z");
let timeFormat = "AGO";
function pageSetup(isEditor) {
//Get values from ThrimShim
if (isEditor && /id=/.test(document.location.search)) {
const rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
fetch(`/thrimshim/${rowId}`)
.then(data => data.json())
.then(function (data) {
if (!data) {
alert("No video available for stream.");
return;
}
document.data = data;
desertBusStart = new Date(data.bustime_start);
document.getElementById("VideoTitlePrefix").value = data.title_prefix;
document.getElementById("VideoTitle").setAttribute("maxlength", data.title_max_length);
document.getElementById("StreamName").value = data.video_channel;
document.getElementById("hiddenSubmissionID").value = data.id;
// for editor, switch to bustime since that's the default
timeFormat = "BUSTIME";
// Apply padding - start 1min early, finish 2min late because these times are generally
// rounded down to the minute, so if something ends at "00:10" it might actually end
// at 00:10:59 so we should pad to 00:12:00.
const start = data.event_start
? new Date(fromTimestamp(data.event_start).getTime() - 60 * 1000)
: null;
const end = data.event_end
? new Date(fromTimestamp(data.event_end).getTime() + 2 * 60 * 1000)
: null;
setTimeRange(start, end);
// title and description both default to row description
document.getElementById("VideoTitle").value = data.video_title
? data.video_title
: data.description;
document.getElementById("VideoDescription").value = data.video_description
? data.video_description
: data.description;
// tags default to tags from sheet
document.getElementById("VideoTags").value = tags_list_to_string(
data.video_tags ? data.video_tags : data.tags
);
// If any edit notes, show them
if (data.notes.length > 0) {
document.getElementById("EditNotes").value = data.notes;
document.getElementById("EditNotesPane").style.display = "block";
}
// Restore advanced options. If any of these are non-default, automatically expand the advanced options pane.
setOptions("uploadLocation", data.upload_locations, data.upload_location);
document.getElementById("AllowHoles").checked = data.allow_holes;
document.getElementById("uploaderWhitelist").value = !!data.uploader_whitelist
? data.uploader_whitelist.join(",")
: "";
if (
(data.upload_locations.length > 0 &&
data.upload_location != null &&
data.upload_location != data.upload_locations[0]) ||
data.allow_holes ||
!!data.uploader_whitelist
) {
document.getElementById("wubloaderAdvancedInputTable").style.display = "block";
}
loadPlaylist(isEditor, data.video_start, data.video_end, data.video_quality);
});
} else {
if (isEditor) {
document.getElementById("SubmitButton").disabled = true;
}
fetch("/thrimshim/defaults")
.then(data => data.json())
.then(function (data) {
if (!data) {
alert("Editor results call failed, is thrimshim running?");
return;
}
desertBusStart = new Date(data.bustime_start);
document.getElementById("StreamName").value = data.video_channel;
if (isEditor) {
document.getElementById("VideoTitlePrefix").value = data.title_prefix;
document.getElementById("VideoTitle").setAttribute("maxlength", data.title_max_length);
setOptions("uploadLocation", data.upload_locations);
}
// Default time format changes depending on mode.
// But in both cases the default input value is 10min ago / "",
// it's just for editor we convert it before the user sees.
if (isEditor) {
toggleTimeInput("BUSTIME");
}
loadPlaylist(isEditor);
});
}
}
// Time-formatting functions
function parseDuration(duration) {
let direction = 1;
if (duration.startsWith("-")) {
duration = duration.slice(1);
direction = -1;
}
const [hours, mins, secs] = duration.split(":");
return (3600 * parseInt(hours) + 60 * (mins || "0") + (secs || "0")) * direction;
}
function toBustime(date) {
return (
(date < desertBusStart ? "-" : "") +
videojs.formatTime(Math.abs((date - desertBusStart) / 1000), 600.01).padStart(7, "0:")
);
}
function fromBustime(bustime) {
return new Date(desertBusStart.getTime() + 1000 * parseDuration(bustime));
}
function toTimestamp(date) {
return date.toISOString().substring(0, 19);
}
function fromTimestamp(ts) {
return new Date(ts + "Z");
}
function toAgo(date) {
now = new Date();
return (
(date < now ? "" : "-") +
videojs.formatTime(Math.abs((date - now) / 1000), 600.01).padStart(7, "0:")
);
}
function fromAgo(ago) {
return new Date(new Date().getTime() - 1000 * parseDuration(ago));
}
// Set the stream start/end range from a pair of Dates using the current format
// If given null, sets to blank.
function setTimeRange(start, end) {
const toFunc = {
UTC: toTimestamp,
BUSTIME: toBustime,
AGO: toAgo,
}[timeFormat];
document.getElementById("StreamStart").value = start ? toFunc(start) : "";
document.getElementById("StreamEnd").value = end ? toFunc(end) : "";
}
// Get the current start/end range as Dates using the current format
// Returns an object containing 'start' and 'end' fields.
// If either is empty / invalid, returns null.
function getTimeRange() {
const fromFunc = {
UTC: fromTimestamp,
BUSTIME: fromBustime,
AGO: fromAgo,
}[timeFormat];
function convert(value) {
if (!value) {
return null;
}
const date = fromFunc(value);
return isNaN(date) ? null : date;
}
return {
start: convert(document.getElementById("StreamStart").value),
end: convert(document.getElementById("StreamEnd").value),
};
}
function getTimeRangeAsTimestamp() {
const range = getTimeRange();
return {
// if not null, format as timestamp
start: range.start && toTimestamp(range.start),
end: range.end && toTimestamp(range.end),
};
}
function toggleHiddenPane(paneID) {
const pane = document.getElementById(paneID);
pane.style.display = pane.style.display === "none" ? "block" : "none";
}
function toggleUltrawide() {
const body = document.getElementsByTagName("Body")[0];
body.classList.contains("ultrawide")
? body.classList.remove("ultrawide")
: body.classList.add("ultrawide");
}
function toggleTimeInput(toggleInput) {
// Get times using current format, then change format, then write them back
const range = getTimeRange();
timeFormat = toggleInput;
setTimeRange(range.start, range.end);
}
// For a given select input element id, add the given list of options.
// If selected is given, it should be the name of an option to select.
// Otherwise the first one is used.
function setOptions(element, options, selected) {
if (!selected && options.length > 0) {
selected = options[0];
}
options.forEach(option => {
const maybeSelected = option == selected ? "selected" : "";
const optionHTML = `<option value="${option}" ${maybeSelected}>${option}</option>`;
document.getElementById(element).innerHTML += optionHTML;
});
}
function buildQuery(params) {
return Object.keys(params)
.filter(key => params[key] !== null)
.map(key => encodeURIComponent(key) + "=" + encodeURIComponent(params[key]))
.join("&");
}
function loadPlaylist(isEditor, startTrim, endTrim, defaultQuality) {
const stream = document.getElementById("StreamName").value;
const playlist = `/playlist/${stream}.m3u8`;
const range = getTimeRangeAsTimestamp();
const queryString = buildQuery(range);
// Preserve existing edit times
if (player && player.trimmingControls && player.vhs.playlists.master) {
const discontinuities = mapDiscontinuities();
if (!startTrim) {
startTrim = getRealTimeForPlayerTime(
discontinuities,
player.trimmingControls().options.startTrim
);
if (startTrim) {
startTrim = startTrim.replace("Z", "");
}
}
if (!endTrim) {
endTrim = getRealTimeForPlayerTime(
discontinuities,
player.trimmingControls().options.endTrim
);
if (endTrim) {
endTrim = endTrim.replace("Z", "");
}
}
}
setupPlayer(isEditor, playlist + "?" + queryString, startTrim, endTrim);
//Get quality levels for advanced properties / download
document.getElementById("qualityLevel").innerHTML = "";
fetch(`/files/${stream}`)
.then(data => data.json())
.then(function (data) {
if (!data.length) {
console.log("Could not retrieve quality levels");
return;
}
var qualityLevels = data.sort().reverse();
setOptions("qualityLevel", qualityLevels, defaultQuality);
if (!!defaultQuality && qualityLevels.length > 0 && defaultQuality != qualityLevels[0]) {
document.getElementById("wubloaderAdvancedInputTable").style.display = "block";
}
});
// Get audio waveform. Note we assume "source" is a valid quality.
const waveformURL = `/waveform/${stream}/source.png?${buildQuery(range)}`;
document.getElementById("Waveform").setAttribute("src", waveformURL);
}
function thrimbletrimmerSubmit(state, override_changes = false) {
document.getElementById("SubmitButton").disabled = true;
const discontinuities = mapDiscontinuities();
let start = getRealTimeForPlayerTime(
discontinuities,
player.trimmingControls().options.startTrim
);
if (start) {
start = start.replace("Z", "");
}
let end = getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.endTrim);
if (end) {
end = end.replace("Z", "");
}
const wubData = {
video_start: start,
video_end: end,
video_title: document.getElementById("VideoTitle").value,
video_description: document.getElementById("VideoDescription").value,
video_tags: tags_string_to_list(document.getElementById("VideoTags").value),
allow_holes: document.getElementById("AllowHoles").checked,
upload_location: document.getElementById("uploadLocation").value,
video_channel: document.getElementById("StreamName").value,
video_quality:
document.getElementById("qualityLevel").options[
document.getElementById("qualityLevel").options.selectedIndex
].value,
uploader_whitelist: document.getElementById("uploaderWhitelist").value
? document.getElementById("uploaderWhitelist").value.split(",")
: null,
state: state,
//pass back the sheet columns to check if any have changed
sheet_name: document.data.sheet_name,
event_start: document.data.event_start,
event_end: document.data.event_end,
category: document.data.category,
description: document.data.description,
notes: document.data.notes,
tags: document.data.tags,
};
if (!!user) {
wubData.token = user.getAuthResponse().id_token;
}
if (override_changes) {
wubData.override_changes = true;
}
console.log("Submitting", wubData);
if (!wubData.video_start) {
alert("No start time set");
return;
}
if (!wubData.video_end) {
alert("No end time set");
return;
}
//Submit to thrimshim
const rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
fetch(`/thrimshim/${rowId}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(wubData),
}).then(response =>
response.text().then(text => {
if (!response.ok) {
if (response.status == 409) {
dialogue = text + "\nClick Ok to submit anyway; Click Cancel to return to editing";
if (confirm(dialogue)) {
thrimbletrimmerSubmit(state, true);
}
} else {
const error = `${response.statusText}: ${text}`;
console.log("Failed to submit:", error);
alert(error);
}
} else if (state == "EDITED") {
alert(`Edit submitted for video from ${start} to ${end}`);
} else {
alert("Draft saved");
}
document.getElementById("SubmitButton").disabled = false;
})
);
}
function thrimbletrimmerDownload(isEditor) {
const range = getTimeRangeAsTimestamp();
if (isEditor) {
if (player.trimmingControls().options.startTrim >= player.trimmingControls().options.endTrim) {
alert("End Time must be greater than Start Time");
return;
}
const discontinuities = mapDiscontinuities();
range.start = getRealTimeForPlayerTime(
discontinuities,
player.trimmingControls().options.startTrim
);
range.end = getRealTimeForPlayerTime(
discontinuities,
player.trimmingControls().options.endTrim
);
}
const stream = document.getElementById("StreamName").value;
const quality =
document.getElementById("qualityLevel").options[
document.getElementById("qualityLevel").options.selectedIndex
].value;
const query = buildQuery({
start: range.start,
end: range.end,
// In non-editor, always use rough cut. They don't have the edit controls to do
// fine time selection anyway.
type: isEditor
? document.getElementById("DownloadType").options[
document.getElementById("DownloadType").options.selectedIndex
].value
: "rough",
// Always allow holes in non-editor, accidentially including holes isn't important
allow_holes: isEditor ? String(document.getElementById("AllowHoles").checked) : "true",
});
const targetURL = `/cut/${stream}/${quality}.ts?${query}`;
document.getElementById("DownloadLink").href = targetURL;
document.getElementById("DownloadLink").style.display = "";
}
function thrimbletrimmerManualLink() {
document.getElementById("ManualButton").disabled = true;
const rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
const upload_location = document.getElementById("ManualYoutube").checked
? "youtube-manual"
: "manual";
const body = {
link: document.getElementById("ManualLink").value,
upload_location: upload_location,
};
if (!!user) {
body.token = user.getAuthResponse().id_token;
}
fetch(`/thrimshim/manual-link/${rowId}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then(response => response.text())
.then(text => {
if (!response.ok) {
const error = `${response.statusText}: ${text}`;
console.log("Failed to set manual link:", error);
alert(error);
document.getElementById("ManualButton").disabled = false;
} else {
alert(`Manual link set to ${body.link}`);
setTimeout(() => {
window.location.href = "/thrimbletrimmer/dashboard.html";
}, 500);
}
});
}
function thrimbletrimmerResetLink(force) {
const rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
if (
force &&
!confirm(
"Are you sure you want to reset this event? " +
"This will set the row back to UNEDITED and forget about any video that already may exist. " +
"It is intended as a last-ditch command to clear a malfunctioning cutter, " +
"or if a video needs to be re-edited and replaced. " +
"IT IS YOUR RESPONSIBILITY TO DEAL WITH ANY VIDEO THAT MAY HAVE ALREADY BEEN UPLOADED. "
)
) {
return;
}
document.getElementById("ResetButton").disabled = true;
document.getElementById("CancelButton").disabled = true;
const body = {};
if (!!user) {
body.token = user.getAuthResponse().id_token;
}
fetch(`/thrimshim/reset/${rowId}?force=${force}`, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
})
.then(response => response.text())
.then(text => {
if (!response.ok) {
const error = `${response.statusText}: ${text}`;
console.log("Failed to reset:", error);
alert(error);
document.getElementById("ResetButton").disabled = false;
document.getElementById("CancelButton").disabled = true;
} else {
alert(`Row has been ${force ? "reset" : "cancelled"}. Reloading...`);
setTimeout(() => {
window.location.reload();
}, 500);
}
});
}
function tags_list_to_string(tag_list) {
return tag_list.join(", ");
}
function tags_string_to_list(tag_string) {
return tag_string
.split(",")
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
}
function round_trip_tag_string() {
const element = document.getElementById("VideoTags");
element.value = tags_list_to_string(tags_string_to_list(element.value));
}