mirror of
				https://git.sr.ht/~cadence/cloudtube
				synced 2025-11-03 21:15:36 +00:00 
			
		
		
		
	Update feeds in background
This commit is contained in:
		
							parent
							
								
									4a3c1e2ac3
								
							
						
					
					
						commit
						643f1e0889
					
				@ -20,8 +20,8 @@ module.exports = [
 | 
			
		||||
				})
 | 
			
		||||
				if (subscriptions.length) {
 | 
			
		||||
					hasSubscriptions = true
 | 
			
		||||
					const all = await Promise.all(subscriptions.map(id => fetchChannelLatest(id)))
 | 
			
		||||
					videos = all.flat(1).sort((a, b) => b.published - a.published).slice(0, 60)
 | 
			
		||||
					const template = Array(subscriptions.length).fill("?").join(", ")
 | 
			
		||||
					videos = db.prepare(`SELECT * FROM Videos WHERE authorId IN (${template}) ORDER BY published DESC LIMIT 60`).all(subscriptions)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return render(200, "pug/subscriptions.pug", {hasSubscriptions, videos, channels})
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ const constants = {
 | 
			
		||||
	user_settings: {
 | 
			
		||||
		instance: {
 | 
			
		||||
			type: "string",
 | 
			
		||||
			default: "https://invidious.snopyta.org"
 | 
			
		||||
			default: "https://second.cadence.moe"
 | 
			
		||||
		},
 | 
			
		||||
		save_history: {
 | 
			
		||||
			type: "boolean",
 | 
			
		||||
@ -11,7 +11,9 @@ const constants = {
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	caching: {
 | 
			
		||||
		csrf_time: 4*60*60*1000
 | 
			
		||||
		csrf_time: 4*60*60*1000,
 | 
			
		||||
		seen_token_subscriptions_eligible: 40*60*60*1000,
 | 
			
		||||
		subscriptions_refresh_loop_min: 5*60*1000,
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	regex: {
 | 
			
		||||
 | 
			
		||||
@ -6,16 +6,19 @@ const db = require("./db")
 | 
			
		||||
function getToken(req, responseHeaders) {
 | 
			
		||||
	if (!req.headers.cookie) req.headers.cookie = ""
 | 
			
		||||
	const cookie = parseCookie(req.headers.cookie)
 | 
			
		||||
	const token = cookie.token
 | 
			
		||||
	if (token) return token
 | 
			
		||||
	if (responseHeaders) { // we should create a token
 | 
			
		||||
		const setCookie = responseHeaders["set-cookie"] || []
 | 
			
		||||
		const token = crypto.randomBytes(18).toString("base64").replace(/\W/g, "_")
 | 
			
		||||
		setCookie.push(`token=${token}; Path=/; Max-Age=2147483648; HttpOnly; SameSite=Lax`)
 | 
			
		||||
		responseHeaders["set-cookie"] = setCookie
 | 
			
		||||
		return token
 | 
			
		||||
	let token = cookie.token
 | 
			
		||||
	if (!token) {
 | 
			
		||||
		if (responseHeaders) { // we should create a token
 | 
			
		||||
			const setCookie = responseHeaders["set-cookie"] || []
 | 
			
		||||
			token = crypto.randomBytes(18).toString("base64").replace(/\W/g, "_")
 | 
			
		||||
			setCookie.push(`token=${token}; Path=/; Max-Age=2147483648; HttpOnly; SameSite=Lax`)
 | 
			
		||||
			responseHeaders["set-cookie"] = setCookie
 | 
			
		||||
		} else {
 | 
			
		||||
			return null
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return null
 | 
			
		||||
	db.prepare("REPLACE INTO SeenTokens (token, seen) VALUES (?, ?)").run([token, Date.now()])
 | 
			
		||||
	return token
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class User {
 | 
			
		||||
 | 
			
		||||
@ -13,14 +13,4 @@ async function fetchChannel(ucid, instance) {
 | 
			
		||||
	return channel
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fetchChannelLatest(ucid) {
 | 
			
		||||
	return fetch(`http://localhost:3000/api/v1/channels/${ucid}/latest`).then(res => res.json()).then(root => {
 | 
			
		||||
		root.forEach(video => {
 | 
			
		||||
			video.descriptionHtml = video.descriptionHtml.replace(/<a /g, '<a tabindex="-1" ')
 | 
			
		||||
		})
 | 
			
		||||
		return root
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports.fetchChannel = fetchChannel
 | 
			
		||||
module.exports.fetchChannelLatest = fetchChannelLatest
 | 
			
		||||
 | 
			
		||||
@ -1,2 +1,94 @@
 | 
			
		||||
const Denque = require("denque")
 | 
			
		||||
const fetch = require("node-fetch")
 | 
			
		||||
const constants = require("../api/utils/constants")
 | 
			
		||||
const db = require("../api/utils/db")
 | 
			
		||||
 | 
			
		||||
const prepared = {
 | 
			
		||||
	video_insert: db.prepare(
 | 
			
		||||
		"INSERT OR IGNORE INTO Videos"
 | 
			
		||||
			+ " ( videoId,  title,  author,  authorId,  published,  publishedText,  viewCountText,  descriptionHtml)"
 | 
			
		||||
			+ " VALUES"
 | 
			
		||||
			+ " (@videoId, @title, @author, @authorId, @published, @publishedText, @viewCountText, @descriptionHtml)"
 | 
			
		||||
	)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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(
 | 
			
		||||
			"SELECT DISTINCT Subscriptions.ucid FROM SeenTokens INNER JOIN Subscriptions ON SeenTokens.token = Subscriptions.token AND SeenTokens.seen > ? ORDER BY SeenTokens.seen DESC"
 | 
			
		||||
		).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() {
 | 
			
		||||
		const item = this.queue.shift()
 | 
			
		||||
		this.set.delete(item)
 | 
			
		||||
		return item
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const refreshQueue = new RefreshQueue()
 | 
			
		||||
 | 
			
		||||
function refreshChannel(ucid) {
 | 
			
		||||
	return fetch(`http://localhost:3000/api/v1/channels/${ucid}/latest`).then(res => res.json()).then(root => {
 | 
			
		||||
		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)
 | 
			
		||||
			})
 | 
			
		||||
			console.log(`updated ${root.length} videos for channel ${ucid}`)
 | 
			
		||||
		} else if (root.identifier === "PUBLISHED_DATES_NOT_PROVIDED") {
 | 
			
		||||
			return [] // nothing we can do. skip this iteration.
 | 
			
		||||
		} else {
 | 
			
		||||
			throw new Error(root.error)
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function refreshNext() {
 | 
			
		||||
	if (refreshQueue.isEmpty()) {
 | 
			
		||||
		const timeSinceLastLoop = Date.now() - refreshQueue.lastLoadTime
 | 
			
		||||
		if (timeSinceLastLoop < constants.caching.subscriptions_refresh_loop_min) {
 | 
			
		||||
			const timeToWait = constants.caching.subscriptions_refresh_loop_min - timeSinceLastLoop
 | 
			
		||||
			console.log(`waiting ${timeToWait} before next loop`)
 | 
			
		||||
			return setTimeout(refreshNext, timeToWait)
 | 
			
		||||
		} else {
 | 
			
		||||
			refreshQueue.load()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const ucid = refreshQueue.next()
 | 
			
		||||
	refreshChannel(ucid).then(refreshNext)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
refreshNext()
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										5
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -217,6 +217,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
 | 
			
		||||
      "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o="
 | 
			
		||||
    },
 | 
			
		||||
    "denque": {
 | 
			
		||||
      "version": "1.4.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
 | 
			
		||||
      "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "detect-libc": {
 | 
			
		||||
      "version": "1.0.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
 | 
			
		||||
 | 
			
		||||
@ -12,6 +12,7 @@
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "better-sqlite3": "^7.1.0",
 | 
			
		||||
    "cookie": "^0.4.1",
 | 
			
		||||
    "denque": "^1.4.1",
 | 
			
		||||
    "node-fetch": "^2.6.0",
 | 
			
		||||
    "pinski": "git+https://git.sr.ht/~cadence/nodejs-pinski#04cd72482574f689b78670b7f2b80209f5449a21"
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ mixin video_list_item(video)
 | 
			
		||||
  - let link = `/watch?v=${video.videoId}`
 | 
			
		||||
  a(href=link tabindex="-1").thumbnail
 | 
			
		||||
    img(src=`https://i.ytimg.com/vi/${video.videoId}/mqdefault.jpg` width=320 height=180 alt="").image
 | 
			
		||||
    if video.second__lengthText !== undefined
 | 
			
		||||
    if video.second__lengthText != undefined
 | 
			
		||||
      span.duration= video.second__lengthText
 | 
			
		||||
  .info
 | 
			
		||||
    div.title: a(href=link).title-link= video.title
 | 
			
		||||
 | 
			
		||||
@ -70,4 +70,4 @@ block content
 | 
			
		||||
    main.video-error-page
 | 
			
		||||
      h2 Error
 | 
			
		||||
      != message
 | 
			
		||||
      p: a(href=`https://www.youtube.com/watch?v=${video.videoId}`) Watch on YouTube →
 | 
			
		||||
      p: a(href=`https://www.youtube.com/watch?v=${video.videoId}#cloudtube`) Watch on YouTube →
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user