Merge pull request #116 from ekimekim/mike/thrimbletrimmer-things

A lot of changes to thrimbletrimmer and related stuff
pull/118/head
Mike Lang 5 years ago committed by GitHub
commit 095e391b60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -73,7 +73,7 @@ class Cutter(object):
ERROR_RETRY_INTERVAL = 5
RETRYABLE_UPLOAD_ERROR_WAIT_INTERVAL = 5
def __init__(self, upload_locations, dbmanager, stop, name, segments_path, tags, title_header, description_footer):
def __init__(self, upload_locations, dbmanager, stop, name, segments_path, tags):
"""upload_locations is a map {location name: upload location backend}
Conn is a database connection.
Stop is an Event triggering graceful shutdown when set.
@ -86,8 +86,6 @@ class Cutter(object):
self.stop = stop
self.segments_path = segments_path
self.tags = tags
self.title_header = title_header
self.description_footer = description_footer
self.logger = logging.getLogger(type(self).__name__)
self.refresh_conn()
@ -338,14 +336,8 @@ class Cutter(object):
# a second try around the whole thing.
try:
video_id, video_link = upload_backend.upload_video(
title=(
"{} - {}".format(self.title_header, job.video_title)
if self.title_header else job.video_title
),
description=(
"{}\n\n{}".format(job.video_description, self.description_footer)
if self.description_footer else job.video_description
),
title=job.video_title,
description=job.video_description,
# Add category and sheet_name as tags
tags=self.tags + [job.category, job.sheet_name],
data=upload_wrapper(),
@ -524,8 +516,6 @@ def main(
name=None,
base_dir=".",
tags='',
title_header="",
description_footer="",
metrics_port=8003,
backdoor_port=0,
):
@ -552,17 +542,6 @@ def main(
name defaults to hostname.
tags should be a comma-seperated list of tags to attach to all videos.
title_header will be prepended to all video titles, seperated by a " - ".
description_footer will be added as a seperate paragraph at the end of all video descriptions.
For example, with --title-header foo --description-footer 'A video of foo.',
then a video with title 'bar' and a description 'Bar with baz' would actually have:
title: foo - bar
description:
Bar with baz
A video of foo.
"""
common.PromLogCountsHandler.install()
common.install_stacksampler()
@ -620,7 +599,7 @@ def main(
if backend.needs_transcode and not no_transcode_check:
needs_transcode_check.append(backend)
cutter = Cutter(upload_locations, dbmanager, stop, name, base_dir, tags, title_header, description_footer)
cutter = Cutter(upload_locations, dbmanager, stop, name, base_dir, tags)
transcode_checkers = [
TranscodeChecker(backend, dbmanager, stop)
for backend in needs_transcode_check

@ -94,6 +94,7 @@
desertbus: {type: "youtube"},
unlisted: {type: "youtube", hidden: true, no_transcode_check: true},
},
default_location:: "desertbus",
// Fixed tags to add to all videos
video_tags:: ["DB13", "DB2019", "2019", "Desert Bus", "Desert Bus for Hope", "Child's Play Charity", "Child's Play", "Charity Fundraiser"],
@ -194,8 +195,6 @@
"--base-dir", "/mnt",
"--backdoor-port", std.toString($.backdoor_port),
"--tags", std.join(",", $.video_tags),
"--title-header", $.title_header,
"--description-footer", $.description_footer,
$.db_connect,
std.manifestJson($.cutter_config),
"/etc/wubloader-creds.json",
@ -219,6 +218,12 @@
// Args for the thrimshim: database connection string
command: [
"--backdoor-port", std.toString($.backdoor_port),
"--title-header", $.title_header,
"--description-footer", $.description_footer,
"--upload-locations", std.join(",", [$.default_location] + [
location for location in std.objectFields($.cutter_config)
if location != $.default_location
]),
$.db_connect,
$.channel,
$.bustime_start,

@ -9,7 +9,7 @@ generate_location() {
}
LOCATIONS=$(
echo "$SERVICES" | while read name port; do
[ -n "$SERVICES" ] && echo "$SERVICES" | while read name port; do
# restreamer is the catch-all
[ "$name" == "restreamer" ] && generate_location / "http://restreamer:$port"
# thrimshim takes any calls to thrimshim/
@ -17,7 +17,9 @@ LOCATIONS=$(
# all services have metrics under /metrics/SERVICE, except for thrimebletrimmer
generate_location "/metrics/$name" "http://$name:$port/metrics"
done
[ -n "$THRIMBLETRIMMER" ] && echo -e "\t\tlocation = / { return 301 /thrimbletrimmer/dashboard.html; }\n\t\tlocation /thrimbletrimmer { }"
[ -n "$THRIMBLETRIMMER" ] &&
echo -e "\t\tlocation = / { return 301 /thrimbletrimmer/dashboard.html; }" &&
echo -e "\t\tlocation /thrimbletrimmer { }"
)
cat > /etc/nginx/nginx.conf <<EOF

@ -41,8 +41,8 @@
</tr>
<tr>
<td><input id="StreamName" value="desertbus" /></td>
<td><input id="StreamStart" style="display:none;" class="UTCTimeInput" value="" /><input id="BusTimeStart" class="BusTimeInput" value="0:00" /></td>
<td><input id="StreamEnd" style="display:none;" class="UTCTimeInput" value="" /><input id="BusTimeEnd" class="BusTimeInput" value="1:00" /></td>
<td><input id="StreamStart" style="display:none;" class="UTCTimeInput" value="" /><input id="BusTimeStart" class="BusTimeInput" value="0:00:00" /></td>
<td><input id="StreamEnd" style="display:none;" class="UTCTimeInput" value="" /><input id="BusTimeEnd" class="BusTimeInput" value="0:00:00" /></td>
<td><input type="button" value="Load Playlist" onclick="loadPlaylist()" /></td>
</tr>
<tr>
@ -57,12 +57,12 @@
</table>
<table id="wubloaderAdvancedInputTable" style="display:none;">
<tr>
<td><a href="javascript:window.open('/files');">Streams</a></td>
<td><a href="javascript:window.open('/files/' + document.getElementById('StreamName').value + '/source', '_blank');">Hours</a></td>
<td><a href="#" onclick="window.open('/files');">Streams</a></td>
<td><a href="#" onclick="window.open('/files/' + document.getElementById('StreamName').value + '/source', '_blank');">Hours</a></td>
</tr>
<tr><td>Allow Holes: </td><td><input id="AllowHoles" type="checkbox" checked /></td></tr>
<tr><td>Allow Holes: </td><td><input id="AllowHoles" type="checkbox" /></td></tr>
<tr><td>Quality Level: </td><td><select id="qualityLevel"></select></td></tr>
<tr><td>Upload Location: </td><td><select id="uploadLocation"><option value="YouTube" selected>YouTube</option></select></td></tr>
<tr><td>Upload Location: </td><td><select id="uploadLocation"></select></td></tr>
<tr><td>Uploader Whitelist: </td><td><input id="uploaderWhitelist" title="Uploader Whitelist" /></td></tr>
<tr>
<td>ThrimShim ID:</td>
@ -78,9 +78,14 @@
</div>
<div id="EditorDetailsPane">
<div id="EditNotesPane" style="display:none">
Edit Notes: <br/>
<textarea id="EditNotes" disabled ></textarea>
</div>
<div>
Title: <br />
<input type="text" id="VideoTitle" value="DB2019 - " maxlength="91" />
<input type="text" id="VideoTitlePrefix" value="DB2019 - " disabled />
<input type="text" id="VideoTitle" value="" maxlength="82" />
</div>
<div>
Description:<br/>
@ -96,7 +101,7 @@
<a id="UltrawideButton" style="float:right;margin-right:10px;" href="JavaScript:toggleUltrawide();">Ultrawide</a>
</div>
<div id="ManualLinkPane" style="display:none">
<input id="ManualLink" /> <input type="button" onclick="thrimbletrimmerManualLink()" value="Set Link" />
<input id="ManualLink" /> <input type="button" id="ManualButton" onclick="thrimbletrimmerManualLink()" value="Set Link" />
</div>
<div id="HelpPane" style="display:none;">
<ul>
@ -126,11 +131,13 @@
}
function toggleTimeInput(toggleInput) {
if(toggleInput == "UTC") {
setStreamRange();
document.getElementById("BusTimeStart").style.display = "none";
document.getElementById("BusTimeEnd").style.display = "none";
document.getElementById("StreamStart").style.display = "";
document.getElementById("StreamEnd").style.display = "";
} else {
setBustimeRange();
document.getElementById("StreamStart").style.display = "none";
document.getElementById("StreamEnd").style.display = "none";
document.getElementById("BusTimeStart").style.display = "";

@ -4,50 +4,110 @@ pageSetup = function() {
//Get values from ThrimShim
if(/id=/.test(document.location.search)) {
var rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
fetch("/thrimshim/"+rowId).then(data => data.json()).then(function (data) {
fetch("/thrimshim/"+rowId).then(data => data.json()).then(function (data) {
if (!data) {
alert("No video available for stream.");
return;
}
//data = testThrimShim;
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;
document.getElementById("StreamName").value = data.video_channel ? data.video_channel:document.getElementById("StreamName").value;
// set stream start/end, then copy to bustime inputs
document.getElementById("StreamStart").value = data.event_start;
document.getElementById("BusTimeStart").value = (new Date(data.event_start+"Z") < desertBusStart ? "-":"") + videojs.formatTime(Math.abs((new Date(data.event_start+"Z") - desertBusStart)/1000), 600.01).padStart(7, "0:");
document.getElementById("StreamEnd").value = data.event_end;
document.getElementById("BusTimeEnd").value = (new Date(data.event_end+"Z") < desertBusStart ? "-":"") + videojs.formatTime(Math.abs((new Date(data.event_end+"Z") - desertBusStart)/1000), 600.01).padStart(7, "0:");
document.getElementById("VideoTitle").value = data.video_title ? data.video_title:document.getElementById("VideoTitle").value;
document.getElementById("VideoDescription").value = data.video_description ? data.video_description:data.description;
setBustimeRange();
// 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;
// If any edit notes, show them
if (data.notes.length > 0) {
document.getElementById("EditNotes").value = data.notes;
document.getElementById("EditNotesPane").style.display = "block";
}
loadPlaylist(data.video_start, data.video_end);
// 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 != data.upload_locations[0])
|| data.allow_holes
|| !!data.uploader_whitelist
) {
document.getElementById('wubloaderAdvancedInputTable').style.display = "block";
}
loadPlaylist(data.video_start, data.video_end, data.video_quality);
});
}
else {
document.getElementById('SubmitButton').disabled = true;
var startOfHour = new Date(new Date().setMinutes(0,0,0));
document.getElementById("StreamStart").value = new Date(startOfHour.getTime() - 1000*60*60).toISOString().substring(0,19);
document.getElementById("StreamEnd").value = startOfHour.toISOString().substring(0,19);
fetch("/thrimshim/defaults").then(data => data.json()).then(function (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;
setOptions('uploadLocation', data.upload_locations);
// Default time range to the last 10min. This is useful for giffers, immediate replay, etc.
document.getElementById("StreamStart").value = new Date(new Date().getTime() - 1000*60*10).toISOString().substring(0,19);
document.getElementById("StreamEnd").value = new Date().toISOString().substring(0,19);
setBustimeRange();
});
loadPlaylist();
}
};
loadPlaylist = function(startTrim, endTrim) {
timestampToBustime = function(ts) {
date = new Date(ts + "Z");
return (date < desertBusStart ? "-":"") + videojs.formatTime(Math.abs((date - desertBusStart)/1000), 600.01).padStart(7, "0:");
};
bustimeToTimestamp = function(bustime) {
direction = 1;
if(bustime.startsWith("-")) {
bustime = bustime.slice(1);
direction = -1;
}
parts = bustime.split(':')
bustime_ms = (parseInt(parts[0]) + parts[1]/60 + parts[2]/3600) * 1000 * 60 * 60;
return new Date(desertBusStart.getTime() + direction * bustime_ms).toISOString().substring(0, 19);
};
setBustimeRange = function() {
document.getElementById("BusTimeStart").value = timestampToBustime(document.getElementById("StreamStart").value);
document.getElementById("BusTimeEnd").value = timestampToBustime(document.getElementById("StreamEnd").value);
};
setStreamRange = function() {
document.getElementById("StreamStart").value = bustimeToTimestamp(document.getElementById("BusTimeStart").value);
document.getElementById("StreamEnd").value = bustimeToTimestamp(document.getElementById("BusTimeEnd").value);
}
// 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.
setOptions = function(element, options, selected) {
if (!selected && options.length > 0) {
selected = options[0]
}
options.forEach(function(option) {
document.getElementById(element).innerHTML += '<option value="'+option+'" '+(option==selected ? 'selected':'')+'>'+option+'</option>';
});
}
loadPlaylist = function(startTrim, endTrim, defaultQuality) {
var playlist = "/playlist/" + document.getElementById("StreamName").value + ".m3u8";
// If we're using bustime, update stream start/end from it first
if(document.getElementById("BusTimeToggleBus").checked) {
var streamStart = desertBusStart;
var busTimeStart = document.getElementById("BusTimeStart").value;
var busTimeEnd = document.getElementById("BusTimeEnd").value;
//Convert BusTime to milliseconds from start of stream
busTimeStart = (parseInt(busTimeStart.split(':')[0]) + busTimeStart.split(':')[1]/60) * 1000 * 60 * 60;
busTimeEnd = (parseInt(busTimeEnd.split(':')[0]) + busTimeEnd.split(':')[1]/60) * 1000 * 60 * 60;
document.getElementById("StreamStart").value = new Date(streamStart.getTime() + busTimeStart).toISOString().substring(0,19);
document.getElementById("StreamEnd").value = new Date(streamStart.getTime() + busTimeEnd).toISOString().substring(0,19);
setStreamRange();
}
var streamStart = document.getElementById("StreamStart").value ? "start="+document.getElementById("StreamStart").value:null;
@ -58,74 +118,78 @@ loadPlaylist = function(startTrim, endTrim) {
//Get quality levels for advanced properties.
document.getElementById('qualityLevel').innerHTML = "";
fetch('/files/' + document.getElementById('StreamName').value).then(data => data.json()).then(function (data) {
fetch('/files/' + document.getElementById('StreamName').value).then(data => data.json()).then(function (data) {
if (!data.length) {
console.log("Could not retrieve quality levels");
return;
}
var qualityLevels = data.sort().reverse();
qualityLevels.forEach(function(level, index) {
document.getElementById('qualityLevel').innerHTML += '<option value="'+level+'" '+(index==0 ? 'selected':'')+'>'+level+'</option>';
});
setOptions('qualityLevel', qualityLevels, defaultQuality);
if (!!defaultQuality && qualityLevels.length > 0 && defaultQuality != qualityLevels[0]) {
document.getElementById('wubloaderAdvancedInputTable').style.display = "block";
}
});
};
thrimbletrimmerSubmit = function(state) {
document.getElementById('SubmitButton').disabled = true;
if(player.trimmingControls().options.startTrim >= player.trimmingControls().options.endTrim) {
alert("End Time must be greater than Start Time");
document.getElementById('SubmitButton').disabled = false;
} else {
var discontinuities = mapDiscontinuities();
var discontinuities = mapDiscontinuities();
var wubData = {
video_start:getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.startTrim).replace('Z',''),
video_end:getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.endTrim).replace('Z',''),
video_title:document.getElementById("VideoTitle").value,
video_description:document.getElementById("VideoDescription").value,
allow_holes:String(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,
token: user.getAuthResponse().id_token
};
// state_columns = ['state', 'uploader', 'error', 'video_link']
console.log(wubData);
console.log(JSON.stringify(wubData));
//Submit to thrimshim
var 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 => { if (!response.ok) { throw Error(response.statusText); }; return response; })
.then(data => { console.log(data); setTimeout(() => { window.location.href = '/thrimbletrimmer/dashboard.html'; }, 500); })
.catch(error => { console.log(error); alert(error); });
var wubData = {
video_start:getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.startTrim).replace('Z',''),
video_end:getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.endTrim).replace('Z',''),
video_title:document.getElementById("VideoTitle").value,
video_description:document.getElementById("VideoDescription").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,
};
if (!!user) {
wubData.token = user.getAuthResponse().id_token
}
console.log(wubData);
console.log(JSON.stringify(wubData));
//Submit to thrimshim
var 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) {
error = response.statusText + ": " + text;
console.log(error);
alert(error);
} else if (state == 'EDITED') {
// Only return to dashboard if submitted, not for save draft
setTimeout(() => { window.location.href = '/thrimbletrimmer/dashboard.html'; }, 500);
return
}
document.getElementById('SubmitButton').disabled = false;
}));
};
thrimbletrimmerDownload = function() {
document.getElementById('SubmitButton').disabled = true;
if(player.trimmingControls().options.startTrim >= player.trimmingControls().options.endTrim) {
alert("End Time must be greater than Start Time");
document.getElementById('SubmitButton').disabled = false;
} else {
var discontinuities = mapDiscontinuities();
var downloadStart = getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.startTrim);
var downloadEnd = getRealTimeForPlayerTime(discontinuities, player.trimmingControls().options.endTrim);
var targetURL = "/cut/" + document.getElementById("StreamName").value +
var targetURL = "/cut/" + document.getElementById("StreamName").value +
"/"+document.getElementById('qualityLevel').options[document.getElementById('qualityLevel').options.selectedIndex].value+".ts" +
"?start=" + downloadStart +
"&end=" + downloadEnd +
"?start=" + downloadStart +
"&end=" + downloadEnd +
"&allow_holes=" + String(document.getElementById('AllowHoles').checked);
console.log(targetURL);
document.getElementById('outputFile').src = targetURL;
@ -133,36 +197,66 @@ thrimbletrimmerDownload = function() {
};
thrimbletrimmerManualLink = function() {
document.getElementById("ManualButton").disabled = true;
var rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
body = {link: document.getElementById("ManualLink").value};
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({
link: document.getElementById("ManualLink").value,
token: user.getAuthResponse().id_token
})
body: JSON.stringify(body)
})
.then(response => { if (!response.ok) { throw Error(response.statusText); }; return response; })
.then(data => { console.log(data); setTimeout(() => { alert("Manual link set"); }, 500); })
.catch(error => { console.log(error); alert(error); });
.then(response => response.text().then(text => {
if (!response.ok) {
error = response.statusText + ": " + text;
console.log(error);
alert(error);
document.getElementById("ManualButton").disabled = false;
} else {
alert("Manual link set to " + body.link);
setTimeout(() => { window.location.href = '/thrimbletrimmer/dashboard.html'; }, 500);
}
}));
};
thrimbletrimmerResetLink = function() {
var rowId = /id=(.*)(?:&|$)/.exec(document.location.search)[1];
if(confirm('Are you sure you want to reset this event?')) {
fetch("/thrimshim/reset/"+rowId, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({token: user.getAuthResponse().id_token})
})
.then(response => { if (!response.ok) { throw Error(response.statusText); }; return response; })
.then(data => { console.log(data); setTimeout(() => { window.location.reload() }, 500); })
.catch(error => { console.log(error); alert(error); });
if(!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;
body = {}
if (!!user) {
body.token = user.getAuthResponse().id_token;
}
fetch("/thrimshim/reset/"+rowId, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})
.then(response => response.text().then(text => {
if (!response.ok) {
error = response.statusText + ": " + text;
console.log(error);
alert(error);
document.getElementById("ResetButton").disabled = false;
} else {
alert("Row has been reset. Reloading...");
setTimeout(() => { window.location.reload(); }, 500);
}
}));
};

@ -6,6 +6,7 @@ document.addEventListener('keypress', (event) => {
player.currentTime(player.currentTime()-10);
break;
case "k":
case " ": // also pause on space
player.paused() ? player.play():player.pause();
break;
case "l":

@ -53,7 +53,6 @@ function setupPlayer(source, startTrim, endTrim) {
this.on('error', function() {
videojs.log("Could not load video stream");
alert("No video available for stream.");
document.getElementById("my-player").style.display = "none";
})
});
var hlsQS = player.hlsQualitySelector();

@ -28,7 +28,19 @@ body.ultrawide .my-player-dimensions { width:100% !important; }
#EditorDetailsPane {
margin-top:100px;
}
#VideoTitle, #VideoDescription {
#EditNotes {
width: 100%;
background-color: pink;
color: black;
border: none;
}
#VideoTitle {
width:90%
}
#VideoTitlePrefix {
width:5%;
}
#VideoDescription {
width:100%;
}

@ -25,6 +25,10 @@ psycopg2.extras.register_uuid()
app = flask.Flask('thrimshim')
app.after_request(after_request)
MAX_TITLE_LENGTH = 100 # Youtube only allows 100-character titles
def cors(app):
"""WSGI middleware that sets CORS headers"""
HEADERS = [
@ -116,6 +120,19 @@ def get_all_rows():
return json.dumps(rows)
@app.route('/thrimshim/defaults')
@request_stats
def get_defaults():
"""Get default info needed by thrimbletrimmer when not loading a specific row."""
return json.dumps({
"video_channel": app.default_channel,
"bustime_start": app.bustime_start,
"title_prefix": app.title_header,
"title_max_length": MAX_TITLE_LENGTH - len(app.title_header),
"upload_locations": app.upload_locations,
})
@app.route('/thrimshim/<uuid:ident>', methods=['GET'])
@request_stats
def get_row(ident):
@ -140,7 +157,25 @@ def get_row(ident):
}
if response["video_channel"] is None:
response["video_channel"] = app.default_channel
response["title_prefix"] = app.title_header
response["title_max_length"] = MAX_TITLE_LENGTH - len(app.title_header)
response["bustime_start"] = app.bustime_start
response["upload_locations"] = app.upload_locations
# remove any added headers or footers so round-tripping is a no-op
if (
app.title_header
and response["video_title"] is not None
and response["video_title"].startswith(app.title_header)
):
response["video_title"] = response["video_title"][len(app.title_header):]
if (
app.description_footer
and response["video_description"] is not None
and response["video_description"].endswith(app.description_footer)
):
response["video_description"] = response["video_description"][:-len(app.description_footer)]
logging.info('Row {} fetched'.format(ident))
return json.dumps(response)
@ -168,9 +203,15 @@ def update_row(ident, editor=None):
for extra in extras:
del new_row[extra]
#validate title length - YouTube titles are limited to 100 characters.
if len(new_row['video_title']) > 100:
return 'Title must be 100 characters or less', 400
# Include headers and footers
if 'video_title' in new_row:
new_row['video_title'] = app.title_header + new_row['video_title']
if 'video_description' in new_row:
new_row['video_description'] += app.description_footer
#validate title length
if len(new_row['video_title']) > MAX_TITLE_LENGTH:
return 'Title must be {} characters or less, including prefix'.format(MAX_TITLE_LENGTH), 400
#validate start time is less than end time
if new_row['video_start'] > new_row['video_end']:
return 'Video Start must be less than Video End.', 400
@ -198,6 +239,10 @@ def update_row(ident, editor=None):
missing.append(column)
if missing:
return 'Fields {} must be non-null for video to be cut'.format(', '.join(missing)), 400
if len(new_row.get('video_title', '')) <= len(app.title_header):
return 'Video title must not be blank', 400
if len(new_row.get('video_description', '')) <= len(app.description_footer):
return 'Video description must not be blank. If you have nothing else to say, just repeat the title.', 400
elif new_row['state'] != 'UNEDITED':
return 'Invalid state {}'.format(new_row['state']), 400
new_row['uploader'] = None
@ -275,14 +320,20 @@ def reset_row(ident, editor=None):
@argh.arg('bustime-start', help='The start time in UTC for the event, for UTC-Bustime conversion')
@argh.arg('--backdoor-port', help='Port for gevent.backdoor access. By default disabled.')
@argh.arg('--no-authentication', help='Do not authenticate')
@argh.arg('--title-header', help='A header to prefix all titles with, seperated from the submitted title by " - "')
@argh.arg('--description-footer', help='A footer to suffix all descriptions with, seperated from the submitted description by a blank line.')
@argh.arg('--upload-locations', help='A comma-seperated list of valid upload locations, to pass to thrimbletrimmer. The first is the default. Note this is NOT validated on write.')
def main(connection_string, default_channel, bustime_start, host='0.0.0.0', port=8004, backdoor_port=0,
no_authentication=False):
no_authentication=False, title_header=None, description_footer=None, upload_locations=''):
"""Thrimshim service."""
server = WSGIServer((host, port), cors(app))
app.no_authentication = no_authentication
app.default_channel = default_channel
app.bustime_start = bustime_start
app.title_header = "" if title_header is None else "{} - ".format(title_header)
app.description_footer = "" if description_footer is None else "\n\n{}".format(description_footer)
app.upload_locations = upload_locations.split(',') if upload_locations else []
stopping = gevent.event.Event()
def stop():

Loading…
Cancel
Save