Implement fetching of video titles

Fixes #2
master
Dessa Simpson 2020-11-09 23:43:16 -07:00
parent 5ab11f69f6
commit 173c22b90a
10 changed files with 134 additions and 13 deletions

View File

@ -0,0 +1,6 @@
CREATE TABLE requestMetadata (
url varchar NOT NULL UNIQUE,
videoTitle varchar DEFAULT NULL,
PRIMARY KEY (url),
FOREIGN KEY (url) REFERENCES requests(url) ON DELETE CASCADE
);

View File

@ -1,5 +1,6 @@
CREATE OR REPLACE VIEW requests_vw AS CREATE OR REPLACE VIEW requests_vw AS
SELECT url,displayName AS requester,imageUrl,state,score,reqTimestamp FROM requests SELECT url,COALESCE(videoTitle,url) AS title,displayName AS requester,imageUrl,state,score,reqTimestamp FROM requests
JOIN requestMetadata USING (url)
JOIN scores USING (url) JOIN scores USING (url)
JOIN users ON requests.requester = users.userid JOIN users ON requests.requester = users.userid
ORDER BY score DESC, reqTimestamp ASC; ORDER BY score DESC, reqTimestamp ASC;
@ -7,6 +8,7 @@ CREATE OR REPLACE VIEW requests_vw AS
CREATE OR REPLACE FUNCTION get_requests_voted(votinguserid int) CREATE OR REPLACE FUNCTION get_requests_voted(votinguserid int)
RETURNS TABLE ( RETURNS TABLE (
url varchar, url varchar,
title varchar,
requester varchar, requester varchar,
imageUrl varchar, imageUrl varchar,
state varchar, state varchar,
@ -16,7 +18,7 @@ CREATE OR REPLACE FUNCTION get_requests_voted(votinguserid int)
) )
LANGUAGE SQL LANGUAGE SQL
AS $$ AS $$
SELECT url,requester,imageUrl,state,score,reqTimestamp, SELECT url,title,requester,imageUrl,state,score,reqTimestamp,
(CASE WHEN votes.userid IS NULL THEN FALSE ELSE TRUE END) AS voted (CASE WHEN votes.userid IS NULL THEN FALSE ELSE TRUE END) AS voted
FROM requests_vw FROM requests_vw
LEFT JOIN votes ON (requests_vw.url = votes.requesturl AND votes.userid = votinguserid) LEFT JOIN votes ON (requests_vw.url = votes.requesturl AND votes.userid = votinguserid)

View File

@ -10,6 +10,7 @@ CREATE OR REPLACE PROCEDURE add_request(url varchar,requester int)
LANGUAGE SQL LANGUAGE SQL
AS $$ AS $$
INSERT INTO requests (url,requester) VALUES (url,requester); INSERT INTO requests (url,requester) VALUES (url,requester);
INSERT INTO requestMetadata (url) VALUES (url);
INSERT INTO scores (url) VALUES (url); INSERT INTO scores (url) VALUES (url);
INSERT INTO votes (requesturl,userid) VALUES (url,requester); INSERT INTO votes (requesturl,userid) VALUES (url,requester);
CALL update_scores(); CALL update_scores();

View File

@ -17,13 +17,13 @@ function getRequests(count,allRequests) {
} }
function buildTable() { function buildTable() {
var requestsDivHTML = '<table><tr><th class="request-url">URL</th><th class="request-requester">Requester</th><th class="request-score">Score</th>'; var requestsDivHTML = '<table><tr><th class="request-link">Song</th><th class="request-requester">Requester</th><th class="request-score">Score</th>';
requestsDivHTML += '<th class="request-state">State</td>'; requestsDivHTML += '<th class="request-state">State</td>';
if (window.loggedIn) requestsDivHTML += '<th class="request-vote">Vote</td>'; if (window.loggedIn) requestsDivHTML += '<th class="request-vote">Vote</td>';
if (window.isStreamer) requestsDivHTML += '<th class="request-update">Update</th>' if (window.isStreamer) requestsDivHTML += '<th class="request-update">Update</th>'
requestsDivHTML += "</tr>"; requestsDivHTML += "</tr>";
for (request of requests) { for (request of requests) {
requestsDivHTML += `<tr><td class="request-url"><a href="${request.url}" target="_blank">${request.url}</a></td>\ requestsDivHTML += `<tr><td class="request-link"><a href="${request.url}" target="_blank">${request.title}</a></td>\
<td class="request-requester">${request.imageurl ? `<img src="${request.imageurl}" class="table-userpic"/>` : ''}${request.requester}</td>\ <td class="request-requester">${request.imageurl ? `<img src="${request.imageurl}" class="table-userpic"/>` : ''}${request.requester}</td>\
<td class="request-score">${request.score}</td>`; <td class="request-score">${request.score}</td>`;
requestsDivHTML += `<td class="request-state">${request.state}</td>`; requestsDivHTML += `<td class="request-state">${request.state}</td>`;
@ -112,7 +112,7 @@ function closeAddRequestModal() {
} }
function openUpdateRequestModal(tr) { function openUpdateRequestModal(tr) {
var url = tr.getElementsByClassName('request-url')[0].innerText; var url = tr.getElementsByClassName('request-link')[0].firstChild.href;
var score = tr.getElementsByClassName('request-score')[0].innerText; var score = tr.getElementsByClassName('request-score')[0].innerText;
var state = tr.getElementsByClassName('request-state')[0].innerText; var state = tr.getElementsByClassName('request-state')[0].innerText;
document.getElementById("updateRequestUrl").href = url; document.getElementById("updateRequestUrl").href = url;
@ -244,6 +244,14 @@ function updateRequestScoreModifier(url,scoreDiff) {
}); });
} }
function updateRequestMetadata(url) {
fetch("/api/updateRequestMetadata?url=" + url)
.then(response => {
updateTable();
response.text().then(showMessage);
});
}
function deleteRequest(url) { function deleteRequest(url) {
fetch("/api/deleteRequest", { method: 'POST', body: new URLSearchParams({ fetch("/api/deleteRequest", { method: 'POST', body: new URLSearchParams({
url: url url: url

View File

@ -172,7 +172,7 @@ div#nav-userpic {
text-align: right; text-align: right;
} }
#scoreModifierHelp { .helptext {
font-size: 75%; font-size: 75%;
} }

View File

@ -129,6 +129,31 @@ app.post("/api/updateRequestState", async (request, response) => {
.catch((e: any) => errorHandler(request,response,e)); .catch((e: any) => errorHandler(request,response,e));
}); });
app.get("/api/updateRequestMetadata", async (request, response) => {
if (request.session) await validateApiToken(request.session);
if (!request.session || !request.session.user) {
response.status(401);
response.send("Session expired; please log in again");
return;
}
var streamerid = await db.query(queries.getStreamerId).then((result: pg.QueryResult) => result.rows[0]['userid']);
if (request.session.user.id != streamerid) {
response.status(401);
response.send("You are not the streamer");
return;
}
if (!request.query.url) {
response.status(400);
response.send("Missing url");
return;
}
requests.updateRequestMetadata(request.query.url as string).then((val: [number,string]) => {
response.status(val[0]);
response.send(val[1]);
})
.catch((e: any) => errorHandler(request,response,e));
});
app.post("/api/updateRequestScoreModifier", async (request, response) => { app.post("/api/updateRequestScoreModifier", async (request, response) => {
if (request.session) await validateApiToken(request.session); if (request.session) await validateApiToken(request.session);
if (!request.session || !request.session.user) { if (!request.session || !request.session.user) {
@ -349,6 +374,18 @@ async function processBannedUsers() {
setTimeout(processBannedUsers,600000+Math.floor(Math.random()*600000)) setTimeout(processBannedUsers,600000+Math.floor(Math.random()*600000))
async function processEmptyMetadata() {
log(LogLevel.INFO,"processEmptyMetadata run at " + new Date().toISOString());
var result = await db.query(queries.getRequestsWithEmptyMetadata);
for (var row of result.rows) {
log(LogLevel.DEBUG,"Processing empty metadata for request: " + row['url']);
requests.updateRequestMetadata(row['url']);
}
setTimeout(processEmptyMetadata,3600000+Math.floor(Math.random()*900000)) // Run every 1-1.25 hours to balance load
}
processEmptyMetadata();
// Check version then listen // Check version then listen
version.checkVersion().then(_ => app.listen(config.port, () => { version.checkVersion().then(_ => app.listen(config.port, () => {
console.log(`Listening on port ${config.port}`); console.log(`Listening on port ${config.port}`);

View File

@ -77,6 +77,16 @@ export const updateRequestState = {
text: "UPDATE requests SET state = $2 WHERE url = $1" text: "UPDATE requests SET state = $2 WHERE url = $1"
} }
export const getRequestsWithEmptyMetadata = {
name: "getRequestsWithEmptyMetadata",
text: "SELECT url FROM requestMetadata WHERE videoTitle IS NULL"
}
export const updateRequestMetadata = {
name: "updateRequestState",
text: "UPDATE requestMetadata SET videoTitle = $2 WHERE url = $1"
}
export const updateRequestScoreModifier = { export const updateRequestScoreModifier = {
name: "updateRequestScoreModifier", name: "updateRequestScoreModifier",
text: "CALL update_request_score_modifier($1,$2)" text: "CALL update_request_score_modifier($1,$2)"

View File

@ -1,4 +1,6 @@
import * as queries from "./queries" import * as queries from "./queries"
import * as youtube from "./youtube"
import { log, LogLevel } from "./logging"
import pg from "pg"; import pg from "pg";
import db from "./db"; import db from "./db";
@ -30,6 +32,32 @@ const validUrlRegexes = [
/^https:\/\/www\.youtube\.com\/watch\?v=[a-zA-Z0-9_-]{11}$/ /^https:\/\/www\.youtube\.com\/watch\?v=[a-zA-Z0-9_-]{11}$/
]; ];
async function checkRequestExists(url: string) {
var query = Object.assign(queries.checkRequestExists, { values: [url] });
var result = await db.query(query);
if (result.rowCount > 0) {
return result;
} else {
return false;
}
}
async function retrieveYoutubeMetadata(url: string) {
var videoId = url.match(/^https:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]{11})$/)![1];
try {
var ytResponse = await youtube.apiRequest('/videos', new URLSearchParams({ id: videoId, part: 'snippet' }));
if (ytResponse) {
var title = ytResponse['items'][0]['snippet']['title'];
db.query(Object.assign(queries.updateRequestMetadata, { values: [url,title] }));
}
} catch(e) {
log(LogLevel.ERROR,"Failed to fetch YouTube metadata");
log(LogLevel.ERROR,"Video ID: " + videoId);
log(LogLevel.ERROR,"Error info:");
log(LogLevel.ERROR,e);
}
}
export async function addRequest(url: string, requester: string): Promise<[number,string]> { export async function addRequest(url: string, requester: string): Promise<[number,string]> {
var validUrl = false; var validUrl = false;
for (var regex of validUrlRegexes) { for (var regex of validUrlRegexes) {
@ -39,16 +67,30 @@ export async function addRequest(url: string, requester: string): Promise<[numbe
} }
} }
if (!validUrl) return [400, "Invalid song URL."]; if (!validUrl) return [400, "Invalid song URL."];
var query = Object.assign(queries.checkRequestExists, { values: [url] }); var result = await checkRequestExists(url)
var result = await db.query(query); if (result) {
if (result.rowCount > 0) {
return [200,`Song already requested by ${result.rows[0].requester}. State: ${result.rows[0].state}`] return [200,`Song already requested by ${result.rows[0].requester}. State: ${result.rows[0].state}`]
} }
var query = Object.assign(queries.addRequest, { values: [url,requester] }); var query = Object.assign(queries.addRequest, { values: [url,requester] });
return db.query(query) return db.query(query)
.then(() => [201,"Song request added."] as [number,string]); .then(async () => {
if (url.includes('youtube')){
retrieveYoutubeMetadata(url);
}
return [201,"Song request added."] as [number,string];
})
}; };
export async function updateRequestMetadata(url: string): Promise <[number,string]> {
if (!checkRequestExists(url)) {
return [400,"Request does not exist."];
}
if (url.includes('youtube')){
retrieveYoutubeMetadata(url);
}
return [200,"Metadata update requested."];
}
export async function updateRequestState(url: string, state: string): Promise<[number,string]> { export async function updateRequestState(url: string, state: string): Promise<[number,string]> {
var query = Object.assign(queries.checkValidState, { values: [state] }); var query = Object.assign(queries.checkValidState, { values: [state] });
var result = await db.query(query); var result = await db.query(query);
@ -56,6 +98,10 @@ export async function updateRequestState(url: string, state: string): Promise<[n
return [400,"Invalid state"] return [400,"Invalid state"]
} }
if (!checkRequestExists(url)) {
return [400,"Request does not exist."];
}
var query = Object.assign(queries.updateRequestState, { values: [url,state] }); var query = Object.assign(queries.updateRequestState, { values: [url,state] });
return db.query(query) return db.query(query)
.then(() => [200,"Song request state updated."] as [number,string]); .then(() => [200,"Song request state updated."] as [number,string]);

View File

@ -3,9 +3,9 @@ import { log, LogLevel } from "./logging"
import fetch, { Response as FetchResponse } from "node-fetch"; import fetch, { Response as FetchResponse } from "node-fetch";
export async function apiRequest(endpoint: string, parameters: URLSearchParams): Promise <any> { export async function apiRequest(endpoint: string, parameters: URLSearchParams): Promise <any> {
log(LogLevel.DEBUG,`Call: youtube.apiRequest(${endpoint})`); log(LogLevel.DEBUG,`Call: youtube.apiRequest(${endpoint},${parameters})`);
parameters.set('key',config.youtubeSecret); parameters.set('key',config.youtubeSecret);
var requestUrl = "https://www.googleapis.com/youtube/v3/" + endpoint + "?" + parameters.toString(); var requestUrl = "https://www.googleapis.com/youtube/v3" + endpoint + "?" + parameters.toString();
return fetch(requestUrl) return fetch(requestUrl)
.then(async (res: FetchResponse) => { .then(async (res: FetchResponse) => {
if (res.status == 200) { if (res.status == 200) {

View File

@ -75,13 +75,24 @@
document.getElementById('updateRequestUrl').innerText, document.getElementById('updateRequestUrl').innerText,
document.getElementById('scoreModifierInput').value)">Submit</button> document.getElementById('scoreModifierInput').value)">Submit</button>
<br> <br>
<span id="scoreModifierHelp"> <span id="scoreModifierHelp" class="helptext">
Enter a number to add to (or negative to subtract from) the score of Enter a number to add to (or negative to subtract from) the score of
a request. Use this for things like donations and channel points a request. Use this for things like donations and channel points
redemptions. redemptions.
</span> </span>
</div> </div>
<br> <br>
<div>
<a id="updateMetadataLink" href="#" onclick="updateRequestMetadata(
document.getElementById('updateRequestUrl').innerText
)">Update Request Metadata</a>
<br>
<span id="updateMetadataHelp" class="helptext">
Use this to update metadata about a request (i.e. title) if it
is missing (shows URL instead of name) or is outdated.
</span>
</div>
<br>
<div> <div>
<a id="deleteRequestLink" href="#" onclick="openDeleteRequestModal( <a id="deleteRequestLink" href="#" onclick="openDeleteRequestModal(
document.getElementById('updateRequestUrl').innerText document.getElementById('updateRequestUrl').innerText