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 = ``; 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)); }