From aa8b5cfd4b5d836d9c258144c5b4237a9fbc3b55 Mon Sep 17 00:00:00 2001 From: Fijxu Date: Thu, 16 Oct 2025 17:09:38 -0300 Subject: [PATCH] feat: Add Video Cache compression Video information from youtube always weight about ~60KB uncompressed. When using Deflate to compress that information, it gets compressed down to ~15KB, so now you will be able to store up to 4x entries more with caching enabled. For context, nadeko.net Invidious instance stores 54.7k keys, since we also store the `time` in a different key associated to the video ID, the real count of videos cached would be 27.3k. With compression enabled, the Redis database will be able to store up to 4 times more videos in cache, which is 109.2k videos cached. Pretty cool huh. --- config/config.example.yml | 7 ++++ src/invidious/config.cr | 6 +++ src/invidious/database/videos.cr | 69 ++++++++++++++++++++++++++++++-- 3 files changed, 79 insertions(+), 3 deletions(-) diff --git a/config/config.example.yml b/config/config.example.yml index c63fe7ec..ee013bd6 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1158,6 +1158,13 @@ video_cache: ## lru_max_size: 18432 + ## Compress cache with Deflate to save RAM. Only works with Redis (1) and + ## LRU (2) cache. + ## + ## Accepted values: false, true + ## Default: false + ## + compress: false ## ## Maximum resolution that is going to be displayed diff --git a/src/invidious/config.cr b/src/invidious/config.cr index cdc18c63..dd46db05 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -238,6 +238,8 @@ class Config property backend : Int32 = 1 # Max quantity of keys that can be held on the LRU cache property lru_max_size : Int32 = 18432 # ~512MB + # Compress cache with Deflate + property compress : Bool = false end property check_backends_interval : Int32 = 30 @@ -453,6 +455,10 @@ class Config puts "1 (Redis compatible DB) (Default)" puts "2 (In memory LRU)" end + if config.video_cache.compress && config.video_cache.backend == 0 + puts "Video Cache compression can only be enabled when using backend 1 (Redis) or 2 (LRU)" + exit(1) + end end # Check if the socket configuration is valid diff --git a/src/invidious/database/videos.cr b/src/invidious/database/videos.cr index 2e232488..f5a85360 100644 --- a/src/invidious/database/videos.cr +++ b/src/invidious/database/videos.cr @@ -13,6 +13,37 @@ module Invidious::Database::Videos end end + module CacheCompression + extend self + + def compress(video_info : String) : String + compressed = IO::Memory.new + uncompressed = IO::Memory.new + uncompressed << video_info + uncompressed.rewind + Compress::Deflate::Writer.open(compressed, Compress::Deflate::BEST_SPEED) do |deflate| + IO.copy(uncompressed, deflate) + end + compressed.rewind + return compressed.gets_to_end + end + + def decompress(video_info_compressed : String, id : String) : String? + compressed = IO::Memory.new + compressed << video_info_compressed + compressed.rewind + decompressed = Compress::Deflate::Reader.new(compressed, sync_close: true) + begin + return decompressed.gets_to_end + rescue Compress::Deflate::Error + # If there is an error when decompressing the video data, + # delete the video from the cache to fetch it again. + VideoCache.del(id) + return nil + end + end + end + class Cache def initialize case CONFIG.video_cache.backend @@ -68,6 +99,15 @@ module Invidious::Database::Videos info = self[id] time = self[id + ":time"] if info && time + # With the { we identify if it's a JSON or not. In that way, compressed + # video info keeps working after setting video_cache.compress to false + # and new videos inserted will be uncompressed. + if info[0] != '{' + info = CacheCompression.decompress(info, id) + if info.nil? + return nil + end + end return Video.new({ id: id, info: JSON.parse(info).as_h, @@ -136,6 +176,13 @@ module Invidious::Database::Videos info = @redis.get(id) time = @redis.get(id + ":time") if info && time + # With the { we identify if it's a JSON or not + if info[0] != '{' + info = CacheCompression.decompress(info, id) + if info.nil? + return nil + end + end return Video.new({ id: id, info: JSON.parse(info).as_h, @@ -177,7 +224,19 @@ module Invidious::Database::Videos WHERE id = $1 SQL - return PG_DB.query_one?(request, id, as: Video) + data = PG_DB.query_one?(request, id, as: VideoCacheInfo) + + if data + if data.info && data.updated + return Video.new({ + id: id, + info: JSON.parse(data.info).as_h, + updated: Time.parse(data.updated, "%Y-%m-%d %H:%M:%S %z", Time::Location::UTC), + }) + else + return nil + end + end end end end @@ -185,7 +244,11 @@ module Invidious::Database::Videos extend self def insert(video : Video) - video_cache_info = VideoCacheInfo.new(video.info.to_json, video.id, video.updated.to_s) + video_info = video.info.to_json + if CONFIG.video_cache.compress + video_info = CacheCompression.compress(video_info) + end + video_cache_info = VideoCacheInfo.new(video_info, video.id, video.updated.to_s) VideoCache.set(video: video_cache_info, expire_time: 14400) if CONFIG.video_cache.enabled end @@ -194,7 +257,7 @@ module Invidious::Database::Videos end def select(id : String) : Video? - return VideoCache.get(id) + VideoCache.get(id) end def delete_expired