2020-09-23 11:45:02 +00:00
const Denque = require ( "denque" )
2021-05-11 09:27:57 +00:00
/** @type {import("node-fetch").default} */
// @ts-ignore
2020-09-23 11:45:02 +00:00
const fetch = require ( "node-fetch" )
2020-09-23 12:05:02 +00:00
const constants = require ( "../utils/constants" )
const db = require ( "../utils/db" )
2020-08-31 13:22:16 +00:00
2020-09-23 11:45:02 +00:00
const prepared = {
video _insert : db . prepare (
"INSERT OR IGNORE INTO Videos"
2020-09-23 12:05:02 +00:00
+ " ( videoId, title, author, authorId, published, viewCountText, descriptionHtml)"
2020-09-23 11:45:02 +00:00
+ " VALUES"
2020-09-23 12:05:02 +00:00
+ " (@videoId, @title, @author, @authorId, @published, @viewCountText, @descriptionHtml)"
2020-09-23 12:48:32 +00:00
) ,
channel _refreshed _update : db . prepare (
"UPDATE Channels SET refreshed = ? WHERE ucid = ?"
2020-12-06 02:40:04 +00:00
) ,
2022-01-10 01:18:45 +00:00
channel _mark _as _missing : db . prepare (
"UPDATE Channels SET missing = 1, missing_reason = ? WHERE ucid = ?"
2020-09-23 11:45:02 +00:00
)
}
class RefreshQueue {
constructor ( ) {
this . set = new Set ( )
this . queue = new Denque ( )
this . lastLoadTime = 0
}
isEmpty ( ) {
return this . queue . isEmpty ( )
}
load ( ) {
// get the next set of scheduled channels to refresh
const afterTime = Date . now ( ) - constants . caching . seen _token _subscriptions _eligible
const channels = db . prepare (
2022-01-10 01:18:45 +00:00
"SELECT DISTINCT Subscriptions.ucid FROM SeenTokens INNER JOIN Subscriptions ON SeenTokens.token = Subscriptions.token INNER JOIN Channels ON Channels.ucid = Subscriptions.ucid WHERE Channels.missing = 0 AND SeenTokens.seen > ? ORDER BY SeenTokens.seen DESC"
2020-09-23 11:45:02 +00:00
) . pluck ( ) . all ( afterTime )
this . addLast ( channels )
this . lastLoadTime = Date . now ( )
}
addNext ( items ) {
for ( const i of items ) {
this . queue . unshift ( i )
this . set . add ( i )
}
}
addLast ( items ) {
for ( const i of items ) {
this . queue . push ( i )
this . set . add ( i )
}
}
next ( ) {
2020-09-23 12:05:02 +00:00
if ( this . isEmpty ( ) ) {
throw new Error ( "Cannot get next of empty refresh queue" )
}
2020-09-23 11:45:02 +00:00
const item = this . queue . shift ( )
this . set . delete ( item )
return item
}
}
2020-10-02 12:32:22 +00:00
class Refresher {
constructor ( ) {
this . sym = constants . symbols . refresher
this . refreshQueue = new RefreshQueue ( )
this . state = this . sym . ACTIVE
this . waitingTimeout = null
2022-01-10 01:18:45 +00:00
this . lastFakeNotFoundTime = 0
2020-10-02 12:32:22 +00:00
this . next ( )
}
2020-09-23 11:45:02 +00:00
2020-10-02 12:32:22 +00:00
refreshChannel ( ucid ) {
2022-01-10 01:18:45 +00:00
return fetch ( ` ${ constants . server _setup . local _instance _origin } /api/v1/channels/ ${ ucid } /latest ` ) . then ( res => res . json ( ) ) . then ( /** @param {any} root */ root => {
2020-10-02 12:32:22 +00:00
if ( Array . isArray ( root ) ) {
root . forEach ( video => {
// organise
video . descriptionHtml = video . descriptionHtml . replace ( /<a /g , '<a tabindex="-1" ' ) // should be safe
video . viewCountText = null //TODO?
// store
prepared . video _insert . run ( video )
} )
// update channel refreshed
prepared . channel _refreshed _update . run ( Date . now ( ) , ucid )
2021-08-18 00:17:12 +00:00
// console.log(`updated ${root.length} videos for channel ${ucid}`)
2020-10-02 12:32:22 +00:00
} else if ( root . identifier === "PUBLISHED_DATES_NOT_PROVIDED" ) {
2022-01-10 01:18:45 +00:00
// nothing we can do. skip this iteration.
2020-12-06 02:40:04 +00:00
} else if ( root . identifier === "NOT_FOUND" ) {
2022-01-10 01:18:45 +00:00
// YouTube sometimes returns not found for absolutely no reason.
// There is no way to distinguish between a fake missing channel and a real missing channel without requesting the real endpoint.
// These fake missing channels often happen in bursts, which is why there is a cooldown.
const timeSinceLastFakeNotFound = Date . now ( ) - this . lastFakeNotFoundTime
if ( timeSinceLastFakeNotFound >= constants . caching . subscriptions _refesh _fake _not _found _cooldown ) {
// We'll request the real endpoint to verify.
fetch ( ` ${ constants . server _setup . local _instance _origin } /api/v1/channels/ ${ ucid } ` ) . then ( res => res . json ( ) ) . then ( /** @param {any} root */ root => {
if ( root . error && ( root . identifier === "NOT_FOUND" || root . identifier === "ACCOUNT_TERMINATED" ) ) {
// The channel is really gone, and we should mark it as missing for everyone.
prepared . channel _mark _as _missing . run ( root . error , ucid )
} else {
// The channel is not actually gone and YouTube is trolling us.
this . lastFakeNotFoundTime = Date . now ( )
}
} )
} // else youtube is currently trolling us, skip this until later.
2020-10-02 12:32:22 +00:00
} else {
throw new Error ( root . error )
}
} )
}
next ( ) {
if ( this . refreshQueue . isEmpty ( ) ) {
const timeSinceLastLoop = Date . now ( ) - this . refreshQueue . lastLoadTime
if ( timeSinceLastLoop < constants . caching . subscriptions _refresh _loop _min ) {
const timeToWait = constants . caching . subscriptions _refresh _loop _min - timeSinceLastLoop
2021-08-18 00:17:12 +00:00
// console.log(`waiting ${timeToWait} before next loop`)
2020-10-02 12:32:22 +00:00
this . state = this . sym . WAITING
this . waitingTimeout = setTimeout ( ( ) => this . next ( ) , timeToWait )
return
} else {
this . refreshQueue . load ( )
}
2020-09-23 11:45:02 +00:00
}
2020-10-02 12:32:22 +00:00
if ( ! this . refreshQueue . isEmpty ( ) ) {
this . state = this . sym . ACTIVE
const ucid = this . refreshQueue . next ( )
2021-08-18 00:15:34 +00:00
this . refreshChannel ( ucid ) . then ( ( ) => this . next ( ) ) . catch ( error => {
// Problems related to fetching from the instance?
// All we can do is retry later.
// However, skip this channel this time in case the problem will occur every time.
2021-08-18 00:17:12 +00:00
console . error ( "Error in background refresh:\n" , error )
2021-08-18 00:15:34 +00:00
setTimeout ( ( ) => {
this . next ( )
} , 10e3 )
} )
2020-09-23 11:45:02 +00:00
} else {
2020-10-02 12:32:22 +00:00
this . state = this . sym . EMPTY
2020-09-23 11:45:02 +00:00
}
}
2020-10-02 12:32:22 +00:00
skipWaiting ( ) {
if ( this . state !== this . sym . ACTIVE ) {
clearTimeout ( this . waitingTimeout )
this . refreshQueue . lastLoadTime = 0
this . next ( )
}
}
2020-09-23 11:45:02 +00:00
}
2020-10-02 12:32:22 +00:00
const refresher = new Refresher ( )
module . exports . refresher = refresher