Merge branch 'master' into feature/getuserinfo

This commit is contained in:
Nanobyte
2020-09-15 19:08:56 +02:00
committed by GitHub
32 changed files with 1427 additions and 216 deletions

View File

@@ -2,6 +2,7 @@ var express = require('express');
// Create a service (the app object is just a callback).
var app = express();
var config = require('./config.js');
var redis = require('./utils/redis.js');
// Middleware
var corsMiddleware = require('./middleware/cors.js');
@@ -11,6 +12,7 @@ const userCounter = require('./middleware/userCounter.js');
// Routes
var getSkipSegments = require('./routes/getSkipSegments.js').endpoint;
var postSkipSegments = require('./routes/postSkipSegments.js');
var getSkipSegmentsByHash = require('./routes/getSkipSegmentsByHash.js');
var voteOnSponsorTime = require('./routes/voteOnSponsorTime.js');
var viewedVideoSponsorTime = require('./routes/viewedVideoSponsorTime.js');
var setUsername = require('./routes/setUsername.js');
@@ -23,6 +25,8 @@ var getTopUsers = require('./routes/getTopUsers.js');
var getTotalStats = require('./routes/getTotalStats.js');
var getDaysSavedFormatted = require('./routes/getDaysSavedFormatted.js');
var getUserInfo = require('./routes/getUserInfo.js');
var postNoSegments = require('./routes/postNoSegments.js');
var getIsUserVIP = require('./routes/getIsUserVIP.js');
// Old Routes
var oldGetVideoSponsorTimes = require('./routes/oldGetVideoSponsorTimes.js');
@@ -52,6 +56,9 @@ app.post('/api/postVideoSponsorTimes', oldSubmitSponsorTimes);
app.get('/api/skipSegments', getSkipSegments);
app.post('/api/skipSegments', postSkipSegments);
// add the privacy protecting skip segments functions
app.get('/api/skipSegments/:prefix', getSkipSegmentsByHash);
//voting endpoint
app.get('/api/voteOnSponsorTime', voteOnSponsorTime.endpoint);
app.post('/api/voteOnSponsorTime', voteOnSponsorTime.endpoint);
@@ -92,6 +99,13 @@ app.get('/api/getUserInfo', getUserInfo);
//send out a formatted time saved total
app.get('/api/getDaysSavedFormatted', getDaysSavedFormatted);
//submit video containing no segments
app.post('/api/noSegments', postNoSegments);
//get if user is a vip
app.get('/api/isUserVIP', getIsUserVIP);
app.get('/database.db', function (req, res) {
res.sendFile("./databases/sponsorTimes.db", { root: "./" });
});

View File

@@ -1,6 +1,6 @@
var fs = require('fs');
var config = undefined;
const fs = require('fs');
let config = {};
// Check to see if launched in test mode
if (process.env.npm_lifecycle_script === 'node test.js') {
@@ -9,4 +9,27 @@ if (process.env.npm_lifecycle_script === 'node test.js') {
config = JSON.parse(fs.readFileSync('config.json'));
}
module.exports = config;
addDefaults(config, {
"port": 80,
"behindProxy": "X-Forwarded-For",
"db": "./databases/sponsorTimes.db",
"privateDB": "./databases/private.db",
"createDatabaseIfNotExist": true,
"schemaFolder": "./databases",
"dbSchema": "./databases/_sponsorTimes.db.sql",
"privateDBSchema": "./databases/_private.db.sql",
"readOnly": false,
"webhooks": [],
"categoryList": ["sponsor", "intro", "outro", "interaction", "selfpromo", "music_offtopic"]
})
module.exports = config;
// Add defaults
function addDefaults(config, defaults) {
for (const key in defaults) {
if(!config.hasOwnProperty(key)) {
config[key] = defaults[key];
}
}
};

View File

@@ -1,5 +1,4 @@
var MysqlInterface = require('sync-mysql');
var config = require('../config.js');
const logger = require('../utils/logger.js');
class Mysql {

View File

@@ -3,7 +3,9 @@ var Sqlite3 = require('better-sqlite3');
var fs = require('fs');
var path = require('path');
var Sqlite = require('./Sqlite.js')
var Mysql = require('./Mysql.js')
var Mysql = require('./Mysql.js');
const logger = require('../utils/logger.js');
const getHash = require('../utils/getHash.js');
let options = {
readonly: config.readOnly,
@@ -33,6 +35,10 @@ if (config.mysql) {
}
if (!config.readOnly) {
db.function("sha256", (string) => {
return getHash(string, 1);
});
// Upgrade database if required
ugradeDB(db, "sponsorTimes");
ugradeDB(privateDB, "private")
@@ -60,12 +66,16 @@ if (config.mysql) {
let versionCodeInfo = db.prepare("SELECT value FROM config WHERE key = ?").get("version");
let versionCode = versionCodeInfo ? versionCodeInfo.value : 0;
let path = config.schemaFolder + "/_upgrade_" + prefix + "_" + (versionCode + 1) + ".sql";
let path = config.schemaFolder + "/_upgrade_" + prefix + "_" + (parseInt(versionCode) + 1) + ".sql";
logger.debug('db update: trying ' + path);
while (fs.existsSync(path)) {
logger.debug('db update: updating ' + path);
db.exec(fs.readFileSync(path).toString());
versionCode = db.prepare("SELECT value FROM config WHERE key = ?").get("version").value;
path = config.schemaFolder + "/_upgrade_" + prefix + "_" + (versionCode + 1) + ".sql";
path = config.schemaFolder + "/_upgrade_" + prefix + "_" + (parseInt(versionCode) + 1) + ".sql";
logger.debug('db update: trying ' + path);
}
logger.debug('db update: no file ' + path);
}
}

View File

@@ -0,0 +1,31 @@
var db = require('../databases/databases.js').db;
var getHash = require('../utils/getHash.js');
const logger = require('../utils/logger.js');
const isUserVIP = require('../utils/isUserVIP.js');
module.exports = (req, res) => {
let userID = req.query.userID;
if (userID == undefined) {
//invalid request
res.sendStatus(400);
return;
}
//hash the userID
userID = getHash(userID);
try {
let vipState = isUserVIP(userID);
res.status(200).json({
hashedUserID: userID,
vip: vipState
});
} catch (err) {
logger.error(err);
res.sendStatus(500);
return;
}
}

View File

@@ -1,4 +1,3 @@
var fs = require('fs');
var config = require('../config.js');
var databases = require('../databases/databases.js');
@@ -9,45 +8,101 @@ var logger = require('../utils/logger.js');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
function cleanGetSegments(req, videoID, categories) {
let userHashedIP, shadowHiddenSegments;
let segments = [];
try {
for (const category of categories) {
const categorySegments = db
.prepare(
'all',
'SELECT startTime, endTime, votes, UUID, shadowHidden FROM sponsorTimes WHERE videoID = ? and category = ? ORDER BY startTime',
[videoID, category]
)
.filter(segment => {
if (segment.votes < -1) {
return false; //too untrustworthy, just ignore it
}
//check if shadowHidden
//this means it is hidden to everyone but the original ip that submitted it
if (segment.shadowHidden != 1) {
return true;
}
if (shadowHiddenSegments === undefined) {
shadowHiddenSegments = privateDB.prepare('all', 'SELECT hashedIP FROM sponsorTimes WHERE videoID = ?', [videoID]);
}
//if this isn't their ip, don't send it to them
return shadowHiddenSegments.some(shadowHiddenSegment => {
if (userHashedIP === undefined) {
//hash the IP only if it's strictly necessary
userHashedIP = getHash(getIP(req) + config.globalSalt);
}
return shadowHiddenSegment.hashedIP === userHashedIP;
});
});
chooseSegments(categorySegments).forEach(chosenSegment => {
segments.push({
category,
segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID,
});
});
}
return segments;
} catch (err) {
if (err) {
logger.error(err);
return null;
}
}
}
//gets a weighted random choice from the choices array based on their `votes` property.
//amountOfChoices specifies the maximum amount of choices to return, 1 or more.
//choices are unique
function getWeightedRandomChoice(choices, amountOfChoices) {
//trivial case: no need to go through the whole process
if (amountOfChoices >= choices.length) {
return choices;
}
//assign a weight to each choice
let totalWeight = 0;
choices = choices.map(choice => {
//The 3 makes -2 the minimum votes before being ignored completely
//https://www.desmos.com/calculator/c1duhfrmts
//this can be changed if this system increases in popularity.
const weight = Math.exp((choice.votes + 3), 0.85);
totalWeight += weight;
return { ...choice, weight };
});
//iterate and find amountOfChoices choices
const chosen = [];
while (amountOfChoices-- > 0) {
//weighted random draw of one element of choices
const randomNumber = Math.random() * totalWeight;
let stackWeight = choices[0].weight;
let i = 0;
while (stackWeight < randomNumber) {
stackWeight += choices[++i].weight;
//trivial case: no need to go through the whole process
if (amountOfChoices >= choices.length) {
return choices;
}
//add it to the chosen ones and remove it from the choices before the next iteration
chosen.push(choices[i]);
totalWeight -= choices[i].weight;
choices.splice(i, 1);
}
//assign a weight to each choice
let totalWeight = 0;
choices = choices.map(choice => {
//The 3 makes -2 the minimum votes before being ignored completely
//https://www.desmos.com/calculator/c1duhfrmts
//this can be changed if this system increases in popularity.
const weight = Math.exp((choice.votes + 3), 0.85);
totalWeight += weight;
return chosen;
return { ...choice, weight };
});
//iterate and find amountOfChoices choices
const chosen = [];
while (amountOfChoices-- > 0) {
//weighted random draw of one element of choices
const randomNumber = Math.random() * totalWeight;
let stackWeight = choices[0].weight;
let i = 0;
while (stackWeight < randomNumber) {
stackWeight += choices[++i].weight;
}
//add it to the chosen ones and remove it from the choices before the next iteration
chosen.push(choices[i]);
totalWeight -= choices[i].weight;
choices.splice(i, 1);
}
return chosen;
}
//This function will find segments that are contained inside of eachother, called similar segments
@@ -55,34 +110,34 @@ function getWeightedRandomChoice(choices, amountOfChoices) {
//This allows new less voted items to still sometimes appear to give them a chance at getting votes.
//Segments with less than -1 votes are already ignored before this function is called
function chooseSegments(segments) {
//Create groups of segments that are similar to eachother
//Segments must be sorted by their startTime so that we can build groups chronologically:
//1. As long as the segments' startTime fall inside the currentGroup, we keep adding them to that group
//2. If a segment starts after the end of the currentGroup (> cursor), no other segment will ever fall
// inside that group (because they're sorted) so we can create a new one
const similarSegmentsGroups = [];
let currentGroup;
let cursor = -1; //-1 to make sure that, even if the 1st segment starts at 0, a new group is created
segments.forEach(segment => {
if (segment.startTime > cursor) {
currentGroup = { segments: [], votes: 0 };
similarSegmentsGroups.push(currentGroup);
}
//Create groups of segments that are similar to eachother
//Segments must be sorted by their startTime so that we can build groups chronologically:
//1. As long as the segments' startTime fall inside the currentGroup, we keep adding them to that group
//2. If a segment starts after the end of the currentGroup (> cursor), no other segment will ever fall
// inside that group (because they're sorted) so we can create a new one
const similarSegmentsGroups = [];
let currentGroup;
let cursor = -1; //-1 to make sure that, even if the 1st segment starts at 0, a new group is created
segments.forEach(segment => {
if (segment.startTime > cursor) {
currentGroup = { segments: [], votes: 0 };
similarSegmentsGroups.push(currentGroup);
}
currentGroup.segments.push(segment);
//only if it is a positive vote, otherwise it is probably just a sponsor time with slightly wrong time
if (segment.votes > 0) {
currentGroup.votes += segment.votes;
}
currentGroup.segments.push(segment);
//only if it is a positive vote, otherwise it is probably just a sponsor time with slightly wrong time
if (segment.votes > 0) {
currentGroup.votes += segment.votes;
}
cursor = Math.max(cursor, segment.endTime);
});
cursor = Math.max(cursor, segment.endTime);
});
//if there are too many groups, find the best 8
return getWeightedRandomChoice(similarSegmentsGroups, 8).map(
//randomly choose 1 good segment per group and return them
group => getWeightedRandomChoice(group.segments, 1)[0]
);
//if there are too many groups, find the best 8
return getWeightedRandomChoice(similarSegmentsGroups, 8).map(
//randomly choose 1 good segment per group and return them
group => getWeightedRandomChoice(group.segments, 1)[0]
);
}
/**
@@ -96,91 +151,39 @@ function chooseSegments(segments) {
* @returns
*/
function handleGetSegments(req, res) {
const videoID = req.query.videoID;
// Default to sponsor
// If using params instead of JSON, only one category can be pulled
const categories = req.query.categories
? JSON.parse(req.query.categories)
: req.query.category
? [req.query.category]
: ['sponsor'];
const videoID = req.query.videoID;
// Default to sponsor
// If using params instead of JSON, only one category can be pulled
const categories = req.query.categories
? JSON.parse(req.query.categories)
: req.query.category
? [req.query.category]
: ['sponsor'];
/**
* @type {Array<{
* segment: number[],
* category: string,
* UUID: string
* }>
* }
*/
const segments = [];
let segments = cleanGetSegments(req, videoID, categories);
let userHashedIP, shadowHiddenSegments;
try {
for (const category of categories) {
const categorySegments = db
.prepare(
'all',
'SELECT startTime, endTime, votes, UUID, shadowHidden FROM sponsorTimes WHERE videoID = ? and category = ? ORDER BY startTime',
[videoID, category]
)
.filter(segment => {
if (segment.votes < -1) {
return false; //too untrustworthy, just ignore it
}
//check if shadowHidden
//this means it is hidden to everyone but the original ip that submitted it
if (segment.shadowHidden != 1) {
return true;
}
if (shadowHiddenSegments === undefined) {
shadowHiddenSegments = privateDB.prepare('all', 'SELECT hashedIP FROM sponsorTimes WHERE videoID = ?', [videoID]);
}
//if this isn't their ip, don't send it to them
return shadowHiddenSegments.some(shadowHiddenSegment => {
if (userHashedIP === undefined) {
//hash the IP only if it's strictly necessary
userHashedIP = getHash(getIP(req) + config.globalSalt);
}
return shadowHiddenSegment.hashedIP === userHashedIP;
});
});
chooseSegments(categorySegments).forEach(chosenSegment => {
segments.push({
category,
segment: [chosenSegment.startTime, chosenSegment.endTime],
UUID: chosenSegment.UUID,
});
});
if (segments === null || segments === undefined) {
res.sendStatus(500);
return false;
}
if (segments.length == 0) {
res.sendStatus(404);
return false;
res.sendStatus(404);
return false;
}
return segments;
} catch (error) {
logger.error(error);
res.sendStatus(500);
return false;
}
}
module.exports = {
handleGetSegments,
endpoint: function (req, res) {
let segments = handleGetSegments(req, res);
handleGetSegments,
cleanGetSegments,
endpoint: function (req, res) {
let segments = handleGetSegments(req, res);
if (segments) {
//send result
res.send(segments);
}
},
if (segments) {
//send result
res.send(segments);
}
},
};

View File

@@ -0,0 +1,37 @@
const hashPrefixTester = require('../utils/hashPrefixTester.js');
const getSegments = require('./getSkipSegments.js').cleanGetSegments;
const databases = require('../databases/databases.js');
const logger = require('../utils/logger.js');
const db = databases.db;
module.exports = async function (req, res) {
let hashPrefix = req.params.prefix;
if (!hashPrefixTester(req.params.prefix)) {
res.status(400).send("Hash prefix does not match format requirements."); // Exit early on faulty prefix
return;
}
const categories = req.query.categories
? JSON.parse(req.query.categories)
: req.query.category
? [req.query.category]
: ['sponsor'];
// Get all video id's that match hash prefix
const videoIds = db.prepare('all', 'SELECT DISTINCT videoId, hashedVideoID from sponsorTimes WHERE hashedVideoID LIKE ?', [hashPrefix+'%']);
if (videoIds.length === 0) {
res.sendStatus(404);
return;
}
let segments = videoIds.map((video) => {
return {
videoID: video.videoID,
hash: video.hashedVideoID,
segments: getSegments(req, video.videoID, categories)
};
});
res.status(200).json(segments);
}

View File

@@ -0,0 +1,74 @@
const db = require('../databases/databases.js').db;
const getHash = require('../utils/getHash.js');
const isUserVIP = require('../utils/isUserVIP.js');
const logger = require('../utils/logger.js');
module.exports = (req, res) => {
// Collect user input data
let videoID = req.body.videoID;
let userID = req.body.userID;
let categories = req.body.categories;
// Check input data is valid
if (!videoID
|| !userID
|| !categories
|| !Array.isArray(categories)
|| categories.length === 0
) {
res.status(400).json({
message: 'Bad Format'
});
return;
}
// Check if user is VIP
userID = getHash(userID);
let userIsVIP = isUserVIP(userID);
if (!userIsVIP) {
res.status(403).json({
message: 'Must be a VIP to mark videos.'
});
return;
}
// Get existing no segment markers
let noSegmentList = db.prepare('all', 'SELECT category from noSegments where videoID = ?', [videoID]);
if (!noSegmentList || noSegmentList.length === 0) {
noSegmentList = [];
} else {
noSegmentList = noSegmentList.map((obj) => {
return obj.category;
});
}
// get user categories not already submitted that match accepted format
let categoriesToMark = categories.filter((category) => {
return !!category.match(/^[_a-zA-Z]+$/);
}).filter((category) => {
return noSegmentList.indexOf(category) === -1;
});
// remove any duplicates
categoriesToMark = categoriesToMark.filter((category, index) => {
return categoriesToMark.indexOf(category) === index;
});
// create database entry
categoriesToMark.forEach((category) => {
try {
db.prepare('run', "INSERT INTO noSegments (videoID, userID, category) VALUES(?, ?, ?)", [videoID, userID, category]);
} catch (err) {
logger.error("Error submitting 'noSegment' marker for category '" + category + "' for video '" + videoID + "'");
logger.error(err);
res.status(500).json({
message: "Internal Server Error: Could not write marker to the database."
});
}
});
res.status(200).json({
submitted: categoriesToMark
});
};

View File

@@ -49,10 +49,7 @@ function sendWebhooks(userID, videoID, UUID, segmentInfo) {
if (config.youtubeAPIKey !== null) {
let userSubmissionCountRow = db.prepare('get', "SELECT count(*) as submissionCount FROM sponsorTimes WHERE userID = ?", [userID]);
YouTubeAPI.videos.list({
part: "snippet",
id: videoID
}, function (err, data) {
YouTubeAPI.listVideos(videoID, "snippet", (err, data) => {
if (err || data.items.length === 0) {
err && logger.error(err);
return;
@@ -156,10 +153,7 @@ async function autoModerateSubmission(submission) {
// Get the video information from the youtube API
if (config.youtubeAPIKey !== null) {
let {err, data} = await new Promise((resolve, reject) => {
YouTubeAPI.videos.list({
part: "contentDetails,snippet",
id: submission.videoID
}, (err, data) => resolve({err, data}));
YouTubeAPI.listVideos(submission.videoID, "contentDetails,snippet", (err, data) => resolve({err, data}));
});
if (err) {
@@ -224,7 +218,6 @@ async function autoModerateSubmission(submission) {
}
}
}
} else {
logger.debug("Skipped YouTube API");
@@ -238,9 +231,9 @@ function proxySubmission(req) {
request.post(config.proxySubmission + '/api/skipSegments?userID='+req.query.userID+'&videoID='+req.query.videoID, {json: req.body}, (err, result) => {
if (config.mode === 'development') {
if (!err) {
logger.error('Proxy Submission: ' + result.statusCode + ' ('+result.body+')');
logger.debug('Proxy Submission: ' + result.statusCode + ' ('+result.body+')');
} else {
logger.debug("Proxy Submission: Failed to make call");
logger.error("Proxy Submission: Failed to make call");
}
}
});
@@ -277,6 +270,8 @@ module.exports = async function postSkipSegments(req, res) {
//hash the ip 5000 times so no one can get it from the database
let hashedIP = getHash(getIP(req) + config.globalSalt);
let noSegmentList = db.prepare('all', 'SELECT category from noSegments where videoID = ?', [videoID]).map((list) => { return list.category });
//check if this user is on the vip list
let isVIP = db.prepare("get", "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]).userCount > 0;
@@ -295,6 +290,18 @@ module.exports = async function postSkipSegments(req, res) {
return;
}
// Reject segemnt if it's in the no segments list
if (noSegmentList.indexOf(segments[i].category) !== -1) {
// TODO: Do something about the fradulent submission
logger.warn("Caught a no-segment submission. userID: '" + userID + "', videoID: '" + videoID + "', category: '" + segments[i].category + "'");
res.status(403).send(
"Request rejected by auto moderator: This video has been reported as not containing any segments with the category '"
+ segments[i].category + "'. If you believe this is incorrect, contact someone on Discord."
);
return;
}
let startTime = parseFloat(segments[i].segment[0]);
let endTime = parseFloat(segments[i].segment[1]);
@@ -394,11 +401,13 @@ module.exports = async function postSkipSegments(req, res) {
segmentInfo.segment[1] + segmentInfo.category + userID, 1);
try {
db.prepare('run', "INSERT INTO sponsorTimes " +
"(videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden)" +
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [videoID, segmentInfo.segment[0],
segmentInfo.segment[1], startingVotes, UUID, userID, timeSubmitted, 0, segmentInfo.category, shadowBanned]);
db.prepare('run', "INSERT INTO sponsorTimes " +
"(videoID, startTime, endTime, votes, UUID, userID, timeSubmitted, views, category, shadowHidden, hashedVideoID)" +
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
videoID, segmentInfo.segment[0], segmentInfo.segment[1], startingVotes, UUID, userID, timeSubmitted, 0, segmentInfo.category, shadowBanned, getHash(videoID, 1)
]
);
//add to private db as well
privateDB.prepare('run', "INSERT INTO sponsorTimes VALUES(?, ?, ?)", [videoID, hashedIP, timeSubmitted]);
} catch (err) {

View File

@@ -1,11 +1,10 @@
var fs = require('fs');
var config = require('../config.js');
var getHash = require('../utils/getHash.js');
var getIP = require('../utils/getIP.js');
var getFormattedTime = require('../utils/getFormattedTime.js');
var isUserTrustworthy = require('../utils/isUserTrustworthy.js');
const {getVoteAuthor, getVoteAuthorRaw, dispatchEvent} = require('../utils/webhookUtils.js');
const { getVoteAuthor, getVoteAuthorRaw, dispatchEvent } = require('../utils/webhookUtils.js');
var databases = require('../databases/databases.js');
var db = databases.db;
@@ -49,10 +48,7 @@ function sendWebhooks(voteData) {
}
if (config.youtubeAPIKey !== null) {
YouTubeAPI.videos.list({
part: "snippet",
id: submissionInfoRow.videoID
}, function (err, data) {
YouTubeAPI.listVideos(submissionInfoRow.videoID, "snippet", (err, data) => {
if (err || data.items.length === 0) {
err && logger.error(err);
return;
@@ -375,8 +371,6 @@ async function voteOnSponsorTime(req, res) {
}
module.exports = {
voteOnSponsorTime,
endpoint: function (req, res) {
voteOnSponsorTime(req, res);
},
};
voteOnSponsorTime,
endpoint: voteOnSponsorTime
};

View File

@@ -0,0 +1,10 @@
const config = require('../config.js');
const minimumPrefix = config.minimumPrefix || '3';
const maximumPrefix = config.maximumPrefix || '32'; // Half the hash.
const prefixChecker = new RegExp('^[\\da-f]{' + minimumPrefix + ',' + maximumPrefix + '}$', 'i');
module.exports = (prefix) => {
return prefixChecker.test(prefix);
};

8
src/utils/isUserVIP.js Normal file
View File

@@ -0,0 +1,8 @@
const databases = require('../databases/databases.js');
const db = databases.db;
module.exports = (userID) => {
return db.prepare('get', "SELECT count(*) as userCount FROM vipUsers WHERE userID = ?", [userID]).userCount > 0;
}

View File

@@ -46,7 +46,7 @@ if (config.mode === 'development') {
settings.INFO = true;
settings.DEBUG = true;
} else if (config.mode === 'test') {
settings.WARN = false;
settings.WARN = false;
}
function log(level, string) {

18
src/utils/redis.js Normal file
View File

@@ -0,0 +1,18 @@
const config = require('../config.js');
const logger = require('./logger.js');
if (config.redis) {
const redis = require('redis');
logger.info('Connected to redis');
const client = redis.createClient(config.redis);
module.exports = client;
} else {
module.exports = {
get: (key, callback) => {
callback(false);
},
set: (key, value, callback) => {
callback(false);
}
};
}

View File

@@ -2,6 +2,8 @@ var config = require('../config.js');
// YouTube API
const YouTubeAPI = require("youtube-api");
const redis = require('./redis.js');
const logger = require('./logger.js');
var exportObject;
// If in test mode, return a mocked youtube object
@@ -14,6 +16,41 @@ if (config.mode === "test") {
key: config.youtubeAPIKey
});
exportObject = YouTubeAPI;
// YouTubeAPI.videos.list wrapper with cacheing
exportObject.listVideos = (videoID, part, callback) => {
let redisKey = "youtube.video." + videoID + "." + part;
redis.get(redisKey, (getErr, result) => {
if (getErr || !result) {
logger.debug("redis: no cache for video information: " + videoID);
YouTubeAPI.videos.list({
part,
id: videoID
}, (ytErr, data) => {
if (!ytErr) {
// Only set cache if data returned
if (data.items.length > 0) {
redis.set(redisKey, JSON.stringify(data), (setErr) => {
if(setErr) {
logger.warn(setErr);
} else {
logger.debug("redis: video information cache set for: " + videoID);
}
callback(false, data); // don't fail
});
} else {
callback(false, data); // don't fail
}
} else {
callback(ytErr, data)
}
});
} else {
logger.debug("redis: fetched video information from cache: " + videoID);
callback(getErr, JSON.parse(result));
}
});
};
}
module.exports = exportObject;