From c2510d302a3934a8c895c03b943403721b6df763 Mon Sep 17 00:00:00 2001 From: Martijn van der Ven Date: Sat, 23 May 2020 21:17:47 +0200 Subject: [PATCH] Limit overlapping segments to just one through weighted randomness --- src/routes/getSkipSegmentsByHash.js | 90 +++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 18 deletions(-) diff --git a/src/routes/getSkipSegmentsByHash.js b/src/routes/getSkipSegmentsByHash.js index 3672eed..8bb5fcc 100644 --- a/src/routes/getSkipSegmentsByHash.js +++ b/src/routes/getSkipSegmentsByHash.js @@ -12,24 +12,64 @@ const getIP = require('../utils/getIP.js'); * @property {string} UUID Unique identifier for the specific segment */ +/** + * @typedef {Object} Row + * @property {string} videoID + * @property {number} startTime + * @property {number} endTime + * @property {number} votes + * @property {string} UUID + * @property {string} category + * @property {number} shadowHidden + */ + +/** + * Input an array of database records and get only one back, weighed on votes. + * The logic is taken from getWeightedRandomChoice, just simplified input and output to not work on indices only. + * + * @param {Row[]} rows + * @returns {?Row} + */ +function pickWeightedRandomRow(rows) { + if (rows.length === 0) { + return null; + } else if (rows.length === 1) { + return rows[0]; + } + + const sqrtWeightsList = []; + let totalSqrtWeights = 0; + for (const row of rows) { + let sqrtVote = Math.sqrt((row.votes + 3) * 10); + sqrtWeightsList.push(sqrtVote); + totalSqrtWeights += sqrtVote; + } + + const randomNumber = Math.random(); + let currentVoteNumber = 0; + for (let i = 0; i < sqrtWeightsList.length; i++) { + if (randomNumber > currentVoteNumber / totalSqrtWeights && randomNumber < (currentVoteNumber + sqrtWeightsList[i]) / totalSqrtWeights) { + return rows[i]; + } + currentVoteNumber += sqrtWeightsList[i]; + } +} /** * @param {string} prefix Lowercased hexadecimal hash prefix * @param {string} hashedIP Custom hash of the visitor’s IP address - * @returns {Segment[]} + * @returns {Object.} */ function getSkipSegmentsByHash(prefix, hashedIP) { - /** - * @constant - * @type {Segment[]} - * @default - */ - const segments = []; - - const rows = db.prepare('SELECT videoID, startTime, endTime, UUID, category, shadowHidden FROM sponsorTimes WHERE votes >= -1 AND hashedVideoID LIKE ? ORDER BY startTime') + /** @type Row[] */ + const rows = db.prepare('SELECT videoID, startTime, endTime, votes, UUID, category, shadowHidden FROM sponsorTimes WHERE votes >= -1 AND hashedVideoID LIKE ? ORDER BY videoID, startTime') .all(prefix + '%'); - + /** @type {string[]} */ const onlyForCurrentUser = privateDB.prepare('SELECT videoID FROM sponsorTimes WHERE hashedIP = ?').all(hashedIP).map(row => row.videoID); + /** @type {Object.} */ + const rowGroupsPerVideo = {}; + let previousVideoID = null; + let previousEndTime = null; for (const row of rows) { /** @TODO check if this logic does what is expected. */ if (row.shadowHidden === 1 && onlyForCurrentUser.indexOf(row.videoID) === -1) { @@ -37,16 +77,30 @@ function getSkipSegmentsByHash(prefix, hashedIP) { // Do not send shadowHidden segments to them. continue; } - - segments.push({ - videoID: row.videoID, - segment: [row.startTime, row.endTime], - category: row.category, - UUID: row.UUID - }); + // Split up the rows per video and group overlapping segments together. + if (!(row.videoID in rowGroupsPerVideo)) { + rowGroupsPerVideo[row.videoID] = []; + } + if (previousVideoID === row.videoID && row.startTime <= previousEndTime) { + rowGroupsPerVideo[row.videoID][rowGroupsPerVideo[row.videoID].length - 1].push(row); + previousEndTime = Math.max(previousEndTime, row.endTime); + } else { + rowGroupsPerVideo[row.videoID].push([row]); + previousVideoID = row.videoID; + previousEndTime = row.endTime; + } } - return segments; + /** @type {Object.} */ + const output = {}; + for (const videoID in rowGroupsPerVideo) { + const pickedVideosForVideoID = []; + for (const group of rowGroupsPerVideo[videoID]) { + pickedVideosForVideoID.push(pickWeightedRandomRow(group)); + } + output[videoID] = pickedVideosForVideoID.map(row => ({ videoID: row.videoID, segment: [row.startTime, row.endTime], category: row.category, UUID: row.UUID })); + } + return output; } const minimumPrefix = config.minimumPrefix || '3';