From c55c5a2c45cfc2d2d46a8ba8c325fb1d10a153b6 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Fri, 21 Oct 2022 00:22:31 +0200 Subject: [PATCH 1/8] Shards: Add required dependencies and update lock file --- shard.lock | 8 ++++++++ shard.yml | 16 +++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/shard.lock b/shard.lock index efb60a59..76feb149 100644 --- a/shard.lock +++ b/shard.lock @@ -32,6 +32,10 @@ shards: git: https://github.com/will/crystal-pg.git version: 0.24.0 + pool: + git: https://github.com/ysbaddaden/pool.git + version: 0.2.4 + protodec: git: https://github.com/iv-org/protodec.git version: 0.1.5 @@ -40,6 +44,10 @@ shards: git: https://github.com/luislavena/radix.git version: 0.4.1 + redis: + git: https://github.com/stefanwille/crystal-redis.git + version: 2.8.3 + spectator: git: https://github.com/icy-arctic-fox/spectator.git version: 0.10.4 diff --git a/shard.yml b/shard.yml index be06a7df..1c36865c 100644 --- a/shard.yml +++ b/shard.yml @@ -10,25 +10,35 @@ targets: main: src/invidious.cr dependencies: + # Database pg: github: will/crystal-pg version: ~> 0.24.0 sqlite3: github: crystal-lang/crystal-sqlite3 version: ~> 0.18.0 + + # Web server kemal: github: kemalcr/kemal version: ~> 1.1.2 kilt: github: jeromegn/kilt version: ~> 0.6.1 - protodec: - github: iv-org/protodec - version: ~> 0.1.5 athena-negotiation: github: athena-framework/negotiation version: ~> 0.1.1 + # Youtube backend + protodec: + github: iv-org/protodec + version: ~> 0.1.5 + + # Caching + redis: + github: stefanwille/crystal-redis + version: ~> 2.8.3 + development_dependencies: spectator: github: icy-arctic-fox/spectator From 62f3f8702757f1b8987360456fe4f848e73e7f1d Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 3 Apr 2023 00:03:06 +0200 Subject: [PATCH 2/8] Config: Add scheme support to DBConfig --- src/invidious/config.cr | 13 +++---------- src/invidious/config/db.cr | 23 +++++++++++++++++++++++ 2 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 src/invidious/config/db.cr diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 09c2168b..cd8b839c 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -1,12 +1,5 @@ -struct DBConfig - include YAML::Serializable - - property user : String - property password : String - property host : String - property port : Int32 - property dbname : String -end +require "yaml" +require "./config/*" struct ConfigPreferences include YAML::Serializable @@ -69,7 +62,7 @@ class Config # Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr property log_level : LogLevel = LogLevel::Info # Database configuration with separate parameters (username, hostname, etc) - property db : DBConfig? = nil + property db : IV::Config::DBConfig? = nil # Database configuration using 12-Factor "Database URL" syntax @[YAML::Field(converter: Preferences::URIConverter)] diff --git a/src/invidious/config/db.cr b/src/invidious/config/db.cr new file mode 100644 index 00000000..7ee3b9c6 --- /dev/null +++ b/src/invidious/config/db.cr @@ -0,0 +1,23 @@ +module Invidious::Config + struct DBConfig + include YAML::Serializable + + property scheme : String + property user : String + property password : String + property host : String + property port : Int32 + property dbname : String + + def to_uri + return URI.new( + scheme: @scheme, + user: @user, + password: @password, + host: @host, + port: @port, + path: @dbname, + ) + end + end +end From d7bfdae7de80058871878300ca53727e0aa21330 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 3 Apr 2023 00:12:36 +0200 Subject: [PATCH 3/8] Config: clean up the various converters --- src/invidious/config.cr | 9 +- src/invidious/config/converters.cr | 74 ++++++++++++++ src/invidious/user/preferences.cr | 153 ----------------------------- 3 files changed, 79 insertions(+), 157 deletions(-) create mode 100644 src/invidious/config/converters.cr diff --git a/src/invidious/config.cr b/src/invidious/config.cr index cd8b839c..7c189abc 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -53,7 +53,7 @@ class Config # Number of threads to use for crawling videos from channels (for updating subscriptions) property channel_threads : Int32 = 1 # Time interval between two executions of the job that crawls channel videos (subscriptions update). - @[YAML::Field(converter: Preferences::TimeSpanConverter)] + @[YAML::Field(converter: IV::Config::TimeSpanConverter)] property channel_refresh_interval : Time::Span = 30.minutes # Number of threads to use for updating feeds property feed_threads : Int32 = 1 @@ -65,7 +65,7 @@ class Config property db : IV::Config::DBConfig? = nil # Database configuration using 12-Factor "Database URL" syntax - @[YAML::Field(converter: Preferences::URIConverter)] + @[YAML::Field(converter: IV::Config::URIConverter)] property database_url : URI = URI.parse("") # Use polling to keep decryption function up to date property decrypt_polling : Bool = false @@ -111,8 +111,9 @@ class Config property modified_source_code_url : String? = nil # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729) - @[YAML::Field(converter: Preferences::FamilyConverter)] + @[YAML::Field(converter: IV::Config::FamilyConverter)] property force_resolve : Socket::Family = Socket::Family::UNSPEC + # Port to listen for connections (overridden by command line argument) property port : Int32 = 3000 # Host to bind (overridden by command line argument) @@ -124,7 +125,7 @@ class Config property use_innertube_for_captions : Bool = false # Saved cookies in "name1=value1; name2=value2..." format - @[YAML::Field(converter: Preferences::StringToCookies)] + @[YAML::Field(converter: IV::Config::CookiesConverter)] property cookies : HTTP::Cookies = HTTP::Cookies.new # Playlist length limit diff --git a/src/invidious/config/converters.cr b/src/invidious/config/converters.cr new file mode 100644 index 00000000..62a04fc2 --- /dev/null +++ b/src/invidious/config/converters.cr @@ -0,0 +1,74 @@ +module Invidious::Config + module CookiesConverter + def self.to_yaml(value : HTTP::Cookies, yaml : YAML::Nodes::Builder) + (value.map { |c| "#{c.name}=#{c.value}" }).join("; ").to_yaml(yaml) + end + + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : HTTP::Cookies + unless node.is_a?(YAML::Nodes::Scalar) + node.raise "Expected scalar, not #{node.class}" + end + + cookies = HTTP::Cookies.new + node.value.split(";").each do |cookie| + next if cookie.strip.empty? + name, value = cookie.split("=", 2) + cookies << HTTP::Cookie.new(name.strip, value.strip) + end + + return cookies + end + end + + module FamilyConverter + def self.to_yaml(value : Socket::Family, yaml : YAML::Nodes::Builder) + case value + when Socket::Family::UNSPEC then yaml.scalar nil + when Socket::Family::INET then yaml.scalar "ipv4" + when Socket::Family::INET6 then yaml.scalar "ipv6" + when Socket::Family::UNIX then raise "Invalid socket family #{value}" + end + end + + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Socket::Family + if node.is_a?(YAML::Nodes::Scalar) + case node.value.downcase + when "ipv4" then Socket::Family::INET + when "ipv6" then Socket::Family::INET6 + else + Socket::Family::UNSPEC + end + else + node.raise "Expected scalar, not #{node.class}" + end + end + end + + module URIConverter + def self.to_yaml(value : URI, yaml : YAML::Nodes::Builder) + yaml.scalar value.normalize! + end + + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : URI + if node.is_a?(YAML::Nodes::Scalar) + URI.parse node.value + else + node.raise "Expected scalar, not #{node.class}" + end + end + end + + module TimeSpanConverter + def self.to_yaml(value : Time::Span, yaml : YAML::Nodes::Builder) + return yaml.scalar value.total_minutes.to_i32 + end + + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Time::Span + if node.is_a?(YAML::Nodes::Scalar) + return decode_interval(node.value) + else + node.raise "Expected scalar, not #{node.class}" + end + end + end +end diff --git a/src/invidious/user/preferences.cr b/src/invidious/user/preferences.cr index b3059403..c8c71f8a 100644 --- a/src/invidious/user/preferences.cr +++ b/src/invidious/user/preferences.cr @@ -1,6 +1,5 @@ struct Preferences include JSON::Serializable - include YAML::Serializable property annotations : Bool = CONFIG.default_user_preferences.annotations property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed @@ -8,17 +7,14 @@ struct Preferences property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect @[JSON::Field(converter: Preferences::StringToArray)] - @[YAML::Field(converter: Preferences::StringToArray)] property captions : Array(String) = CONFIG.default_user_preferences.captions @[JSON::Field(converter: Preferences::StringToArray)] - @[YAML::Field(converter: Preferences::StringToArray)] property comments : Array(String) = CONFIG.default_user_preferences.comments property continue : Bool = CONFIG.default_user_preferences.continue property continue_autoplay : Bool = CONFIG.default_user_preferences.continue_autoplay @[JSON::Field(converter: Preferences::BoolToString)] - @[YAML::Field(converter: Preferences::BoolToString)] property dark_mode : String = CONFIG.default_user_preferences.dark_mode property latest_only : Bool = CONFIG.default_user_preferences.latest_only property listen : Bool = CONFIG.default_user_preferences.listen @@ -78,27 +74,6 @@ struct Preferences end end end - - def self.to_yaml(value : String, yaml : YAML::Nodes::Builder) - yaml.scalar value - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String - unless node.is_a?(YAML::Nodes::Scalar) - node.raise "Expected scalar, not #{node.class}" - end - - case node.value - when "true" - "dark" - when "false" - "light" - when "" - CONFIG.default_user_preferences.dark_mode - else - node.value - end - end end module ClampInt @@ -109,58 +84,6 @@ struct Preferences def self.from_json(value : JSON::PullParser) : Int32 value.read_int.clamp(0, MAX_ITEMS_PER_PAGE).to_i32 end - - def self.to_yaml(value : Int32, yaml : YAML::Nodes::Builder) - yaml.scalar value - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Int32 - node.value.clamp(0, MAX_ITEMS_PER_PAGE) - end - end - - module FamilyConverter - def self.to_yaml(value : Socket::Family, yaml : YAML::Nodes::Builder) - case value - when Socket::Family::UNSPEC - yaml.scalar nil - when Socket::Family::INET - yaml.scalar "ipv4" - when Socket::Family::INET6 - yaml.scalar "ipv6" - when Socket::Family::UNIX - raise "Invalid socket family #{value}" - end - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Socket::Family - if node.is_a?(YAML::Nodes::Scalar) - case node.value.downcase - when "ipv4" - Socket::Family::INET - when "ipv6" - Socket::Family::INET6 - else - Socket::Family::UNSPEC - end - else - node.raise "Expected scalar, not #{node.class}" - end - end - end - - module URIConverter - def self.to_yaml(value : URI, yaml : YAML::Nodes::Builder) - yaml.scalar value.normalize! - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : URI - if node.is_a?(YAML::Nodes::Scalar) - URI.parse node.value - else - node.raise "Expected scalar, not #{node.class}" - end - end end module ProcessString @@ -171,14 +94,6 @@ struct Preferences def self.from_json(value : JSON::PullParser) : String HTML.escape(value.read_string[0, 100]) end - - def self.to_yaml(value : String, yaml : YAML::Nodes::Builder) - yaml.scalar value - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String - HTML.escape(node.value[0, 100]) - end end module StringToArray @@ -202,73 +117,5 @@ struct Preferences result end - - def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder) - yaml.sequence do - value.each do |element| - yaml.scalar element - end - end - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(String) - begin - unless node.is_a?(YAML::Nodes::Sequence) - node.raise "Expected sequence, not #{node.class}" - end - - result = [] of String - node.nodes.each do |item| - unless item.is_a?(YAML::Nodes::Scalar) - node.raise "Expected scalar, not #{item.class}" - end - - result << HTML.escape(item.value[0, 100]) - end - rescue ex - if node.is_a?(YAML::Nodes::Scalar) - result = [HTML.escape(node.value[0, 100]), ""] - else - result = ["", ""] - end - end - - result - end - end - - module StringToCookies - def self.to_yaml(value : HTTP::Cookies, yaml : YAML::Nodes::Builder) - (value.map { |c| "#{c.name}=#{c.value}" }).join("; ").to_yaml(yaml) - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : HTTP::Cookies - unless node.is_a?(YAML::Nodes::Scalar) - node.raise "Expected scalar, not #{node.class}" - end - - cookies = HTTP::Cookies.new - node.value.split(";").each do |cookie| - next if cookie.strip.empty? - name, value = cookie.split("=", 2) - cookies << HTTP::Cookie.new(name.strip, value.strip) - end - - cookies - end - end - - module TimeSpanConverter - def self.to_yaml(value : Time::Span, yaml : YAML::Nodes::Builder) - return yaml.scalar value.total_minutes.to_i32 - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Time::Span - if node.is_a?(YAML::Nodes::Scalar) - return decode_interval(node.value) - else - node.raise "Expected scalar, not #{node.class}" - end - end end end From 161fb6425902d40c38a1a60c21a3715017981c29 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 23 Oct 2022 14:15:16 +0200 Subject: [PATCH 4/8] Cache: Create the base of the caching subsystem --- config/config.example.yml | 18 +++++++++++++ src/invidious/cache.cr | 29 ++++++++++++++++++++ src/invidious/cache/cacheable_item.cr | 9 +++++++ src/invidious/cache/item_store.cr | 22 +++++++++++++++ src/invidious/cache/null_item_store.cr | 24 +++++++++++++++++ src/invidious/cache/redis_item_store.cr | 36 +++++++++++++++++++++++++ src/invidious/cache/store_type.cr | 6 +++++ src/invidious/config.cr | 12 +++------ src/invidious/config/cache.cr | 14 ++++++++++ 9 files changed, 162 insertions(+), 8 deletions(-) create mode 100644 src/invidious/cache.cr create mode 100644 src/invidious/cache/cacheable_item.cr create mode 100644 src/invidious/cache/item_store.cr create mode 100644 src/invidious/cache/null_item_store.cr create mode 100644 src/invidious/cache/redis_item_store.cr create mode 100644 src/invidious/cache/store_type.cr create mode 100644 src/invidious/config/cache.cr diff --git a/config/config.example.yml b/config/config.example.yml index 38085a20..74226d4e 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -42,6 +42,24 @@ db: +######################################### +# +# Cache configuration +# +######################################### + +cache: + ## + ## URL of the caching server. To not use a caching server, + ## set to an empty string or leave empty. + ## + ## Note: The same "long" format as the 'db' parameter is + ## also supported. + ## + url: "" + + + ######################################### # # Server config diff --git a/src/invidious/cache.cr b/src/invidious/cache.cr new file mode 100644 index 00000000..e7f2b966 --- /dev/null +++ b/src/invidious/cache.cr @@ -0,0 +1,29 @@ +require "./cache/*" + +module Invidious::Cache + extend self + + INSTANCE = self.init(CONFIG.cache) + + def init(cfg : Config::CacheConfig) : ItemStore + # Environment variable takes precedence over local config + url = ENV.fetch("INVIDIOUS_CACHE_URL", nil).try { |u| URI.parse(u) } + url ||= cfg.url + url ||= URI.new + + # Determine cache type from URL scheme + type = StoreType.parse?(url.scheme || "none") || StoreType::None + + case type + when .none? + return NullItemStore.new + when .redis? + if url.nil? + raise InvalidConfigException.new "Redis cache requires an URL." + end + return RedisItemStore.new(url) + else + raise InvalidConfigException.new "Invalid cache url. Only redis:// URL are currently supported." + end + end +end diff --git a/src/invidious/cache/cacheable_item.cr b/src/invidious/cache/cacheable_item.cr new file mode 100644 index 00000000..c1295a4a --- /dev/null +++ b/src/invidious/cache/cacheable_item.cr @@ -0,0 +1,9 @@ +require "json" + +module Invidious::Cache + # Including this module allows the includer object to be cached. + # The object will automatically inherit from JSON::Serializable. + module CacheableItem + include JSON::Serializable + end +end diff --git a/src/invidious/cache/item_store.cr b/src/invidious/cache/item_store.cr new file mode 100644 index 00000000..e4ec1201 --- /dev/null +++ b/src/invidious/cache/item_store.cr @@ -0,0 +1,22 @@ +require "./cacheable_item" + +module Invidious::Cache + # Abstract class from which any cached element should inherit + # Note: class is used here, instead of a module, in order to benefit + # from various compiler checks (e.g methods must be implemented) + abstract class ItemStore + # Retrieves an item from the store + # Returns nil if item wasn't found or is expired + abstract def fetch(key : String, *, as : T.class) + + # Stores a given item into cache + abstract def store(key : String, value : CacheableItem, expires : Time::Span) + + # Prematurely deletes item(s) from the cache + abstract def delete(key : String) + abstract def delete(keys : Array(String)) + + # Removes all the items stored in the cache + abstract def clear + end +end diff --git a/src/invidious/cache/null_item_store.cr b/src/invidious/cache/null_item_store.cr new file mode 100644 index 00000000..c26c0804 --- /dev/null +++ b/src/invidious/cache/null_item_store.cr @@ -0,0 +1,24 @@ +require "./item_store" + +module Invidious::Cache + class NullItemStore < ItemStore + def initialize + end + + def fetch(key : String, *, as : T.class) : T? forall T + return nil + end + + def store(key : String, value : CacheableItem, expires : Time::Span) + end + + def delete(key : String) + end + + def delete(keys : Array(String)) + end + + def clear + end + end +end diff --git a/src/invidious/cache/redis_item_store.cr b/src/invidious/cache/redis_item_store.cr new file mode 100644 index 00000000..ccf847a6 --- /dev/null +++ b/src/invidious/cache/redis_item_store.cr @@ -0,0 +1,36 @@ +require "./item_store" +require "json" +require "redis" + +module Invidious::Cache + class RedisItemStore < ItemStore + @redis : Redis::PooledClient + @node_name : String + + def initialize(url : URI, @node_name = "") + @redis = Redis::PooledClient.new url + end + + def fetch(key : String, *, as : T.class) : (T | Nil) forall T + value = @redis.get(key) + return nil if value.nil? + return T.from_json(JSON::PullParser.new(value)) + end + + def store(key : String, value : CacheableItem, expires : Time::Span) + @redis.set(key, value, ex: expires.to_i) + end + + def delete(key : String) + @redis.del(key) + end + + def delete(keys : Array(String)) + @redis.del(keys) + end + + def clear + @redis.flushdb + end + end +end diff --git a/src/invidious/cache/store_type.cr b/src/invidious/cache/store_type.cr new file mode 100644 index 00000000..39238715 --- /dev/null +++ b/src/invidious/cache/store_type.cr @@ -0,0 +1,6 @@ +module Invidious::Cache + enum StoreType + None + Redis + end +end diff --git a/src/invidious/config.cr b/src/invidious/config.cr index 7c189abc..acd5d3fa 100644 --- a/src/invidious/config.cr +++ b/src/invidious/config.cr @@ -74,6 +74,8 @@ class Config # Jobs config structure. See jobs.cr and jobs/base_job.cr property jobs = Invidious::Jobs::JobsConfig.new + # Cache configuration. See cache/cache.cr + property cache = Invidious::Config::CacheConfig.new # Used to tell Invidious it is behind a proxy, so links to resources should be https:// property https_only : Bool? @@ -208,14 +210,8 @@ class Config # Build database_url from db.* if it's not set directly if config.database_url.to_s.empty? if db = config.db - config.database_url = URI.new( - scheme: "postgres", - user: db.user, - password: db.password, - host: db.host, - port: db.port, - path: db.dbname, - ) + db.scheme = "postgres" + config.database_url = db.to_uri else puts "Config: Either database_url or db.* is required" exit(1) diff --git a/src/invidious/config/cache.cr b/src/invidious/config/cache.cr new file mode 100644 index 00000000..e15efcc0 --- /dev/null +++ b/src/invidious/config/cache.cr @@ -0,0 +1,14 @@ +require "../cache/store_type" + +module Invidious::Config + struct CacheConfig + include YAML::Serializable + + @[YAML::Field(converter: IV::Config::URIConverter)] + property url : URI? = URI.new + + # Required because of YAML serialization + def initialize + end + end +end From 700ca568c52fbf9a979b8abd899bd5ac79aad536 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Thu, 15 Jun 2023 19:40:58 +0200 Subject: [PATCH 5/8] Misc: Add an utility function to for 'region' URL parameter --- src/invidious/routes/api/manifest.cr | 2 +- src/invidious/routes/api/v1/channels.cr | 2 +- src/invidious/routes/api/v1/feeds.cr | 2 +- src/invidious/routes/api/v1/search.cr | 6 +++--- src/invidious/routes/api/v1/videos.cr | 8 ++++---- src/invidious/routes/feeds.cr | 2 +- src/invidious/routes/playlists.cr | 2 +- src/invidious/routes/preferences.cr | 2 +- src/invidious/routes/search.cr | 2 +- src/invidious/routes/video_playback.cr | 4 ++-- src/invidious/routes/watch.cr | 2 +- src/invidious/videos/regions.cr | 10 ++++++++++ src/invidious/videos/video_preferences.cr | 4 +++- 13 files changed, 30 insertions(+), 18 deletions(-) diff --git a/src/invidious/routes/api/manifest.cr b/src/invidious/routes/api/manifest.cr index 662d1002..31147d9c 100644 --- a/src/invidious/routes/api/manifest.cr +++ b/src/invidious/routes/api/manifest.cr @@ -6,7 +6,7 @@ module Invidious::Routes::API::Manifest local = env.params.query["local"]?.try &.== "true" id = env.params.url["id"] - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) # Since some implementations create playlists based on resolution regardless of different codecs, # we can opt to only add a source to a representation if it has a unique height within that representation diff --git a/src/invidious/routes/api/v1/channels.cr b/src/invidious/routes/api/v1/channels.cr index 67018660..263732e3 100644 --- a/src/invidious/routes/api/v1/channels.cr +++ b/src/invidious/routes/api/v1/channels.cr @@ -429,7 +429,7 @@ module Invidious::Routes::API::V1::Channels def self.search(env) locale = env.get("preferences").as(Preferences).locale - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) env.response.content_type = "application/json" diff --git a/src/invidious/routes/api/v1/feeds.cr b/src/invidious/routes/api/v1/feeds.cr index 41865f34..d55605f7 100644 --- a/src/invidious/routes/api/v1/feeds.cr +++ b/src/invidious/routes/api/v1/feeds.cr @@ -4,7 +4,7 @@ module Invidious::Routes::API::V1::Feeds env.response.content_type = "application/json" - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) trending_type = env.params.query["type"]? begin diff --git a/src/invidious/routes/api/v1/search.cr b/src/invidious/routes/api/v1/search.cr index 2922b060..a85daf9d 100644 --- a/src/invidious/routes/api/v1/search.cr +++ b/src/invidious/routes/api/v1/search.cr @@ -1,7 +1,7 @@ module Invidious::Routes::API::V1::Search def self.search(env) locale = env.get("preferences").as(Preferences).locale - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) env.response.content_type = "application/json" @@ -24,7 +24,7 @@ module Invidious::Routes::API::V1::Search def self.search_suggestions(env) preferences = env.get("preferences").as(Preferences) - region = env.params.query["region"]? || preferences.region + region = find_region(env.params.query["region"]?) || preferences.region env.response.content_type = "application/json" @@ -65,7 +65,7 @@ module Invidious::Routes::API::V1::Search page = env.params.query["page"]?.try &.to_i? || 1 locale = env.get("preferences").as(Preferences).locale - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) env.response.content_type = "application/json" begin diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 9281f4dd..7d22522f 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -5,7 +5,7 @@ module Invidious::Routes::API::V1::Videos env.response.content_type = "application/json" id = env.params.url["id"] - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) proxy = {"1", "true"}.any? &.== env.params.query["local"]? begin @@ -25,7 +25,7 @@ module Invidious::Routes::API::V1::Videos env.response.content_type = "application/json" id = env.params.url["id"] - region = env.params.query["region"]? || env.params.body["region"]? + region = find_region(env.params.query["region"]? || env.params.body["region"]?) if id.nil? || id.size != 11 || !id.matches?(/^[\w-]+$/) return error_json(400, "Invalid video ID") @@ -168,7 +168,7 @@ module Invidious::Routes::API::V1::Videos env.response.content_type = "application/json" id = env.params.url["id"] - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) begin video = get_video(id, region: region) @@ -297,7 +297,7 @@ module Invidious::Routes::API::V1::Videos def self.comments(env) locale = env.get("preferences").as(Preferences).locale - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) env.response.content_type = "application/json" diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index e20a7139..04bccf81 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -48,7 +48,7 @@ module Invidious::Routes::Feeds trending_type = env.params.query["type"]? trending_type ||= "Default" - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) region ||= env.get("preferences").as(Preferences).region begin diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index 9c6843e9..abd0e945 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -228,7 +228,7 @@ module Invidious::Routes::Playlists prefs = env.get("preferences").as(Preferences) locale = prefs.locale - region = env.params.query["region"]? || prefs.region + region = find_region(env.params.query["region"]?) || prefs.region user = env.get? "user" sid = env.get? "sid" diff --git a/src/invidious/routes/preferences.cr b/src/invidious/routes/preferences.cr index 112535bd..61856515 100644 --- a/src/invidious/routes/preferences.cr +++ b/src/invidious/routes/preferences.cr @@ -110,7 +110,7 @@ module Invidious::Routes::PreferencesRoute automatic_instance_redirect ||= "off" automatic_instance_redirect = automatic_instance_redirect == "on" - region = env.params.body["region"]?.try &.as(String) + region = find_region(env.params.body["region"]?) locale = env.params.body["locale"]?.try &.as(String) locale ||= CONFIG.default_user_preferences.locale diff --git a/src/invidious/routes/search.cr b/src/invidious/routes/search.cr index 5be33533..859e5ded 100644 --- a/src/invidious/routes/search.cr +++ b/src/invidious/routes/search.cr @@ -40,7 +40,7 @@ module Invidious::Routes::Search prefs = env.get("preferences").as(Preferences) locale = prefs.locale - region = env.params.query["region"]? || prefs.region + region = find_region(env.params.query["region"]?) || prefs.region query = Invidious::Search::Query.new(env.params.query, :regular, region) diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index ec18f3b8..74ad1d1d 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -9,7 +9,7 @@ module Invidious::Routes::VideoPlayback mns ||= [] of String if query_params["region"]? - region = query_params["region"] + region = find_region(query_params["region"]) query_params.delete("region") end @@ -265,7 +265,7 @@ module Invidious::Routes::VideoPlayback return error_template(400, "Invalid itag") end - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) local = (env.params.query["local"]? == "true") title = env.params.query["title"]? diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index aabe8dfc..410e625b 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -3,7 +3,7 @@ module Invidious::Routes::Watch def self.handle(env) locale = env.get("preferences").as(Preferences).locale - region = env.params.query["region"]? + region = find_region(env.params.query["region"]?) if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+") url = "/watch?" + env.params.query.to_s.gsub("%20", "").delete("+") diff --git a/src/invidious/videos/regions.cr b/src/invidious/videos/regions.cr index 575f8c25..7b1fd3f0 100644 --- a/src/invidious/videos/regions.cr +++ b/src/invidious/videos/regions.cr @@ -25,3 +25,13 @@ REGIONS = { "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW", } + +# Utility function that searches in the array above for a given input. +def find_region(reg : String?) : String? + return nil if reg.nil? + + # Normalize input + region = (reg || "").upcase[0..1] + + return REGIONS.find(&.== region) +end diff --git a/src/invidious/videos/video_preferences.cr b/src/invidious/videos/video_preferences.cr index 34cf7ff0..0abcd42d 100644 --- a/src/invidious/videos/video_preferences.cr +++ b/src/invidious/videos/video_preferences.cr @@ -38,7 +38,9 @@ def process_video_params(query, preferences) preferred_captions = query["subtitles"]?.try &.split(",").map(&.downcase) quality = query["quality"]? quality_dash = query["quality_dash"]? - region = query["region"]? + + region = find_region(query["region"]?) + related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe } speed = query["speed"]?.try &.rchop("x").to_f? video_loop = query["loop"]?.try { |q| (q == "true" || q == "1").to_unsafe } From ac60ed8e94eb38ae8681f5a9ec599f7ace255136 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 18 Jun 2023 13:09:12 +0200 Subject: [PATCH 6/8] temp fix, while cacheable item is not integrated --- src/invidious/cache/item_store.cr | 4 ++-- src/invidious/cache/null_item_store.cr | 4 ++-- src/invidious/cache/redis_item_store.cr | 14 ++++++-------- src/invidious/exceptions.cr | 4 ++++ 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/invidious/cache/item_store.cr b/src/invidious/cache/item_store.cr index e4ec1201..ebe5b1f9 100644 --- a/src/invidious/cache/item_store.cr +++ b/src/invidious/cache/item_store.cr @@ -7,10 +7,10 @@ module Invidious::Cache abstract class ItemStore # Retrieves an item from the store # Returns nil if item wasn't found or is expired - abstract def fetch(key : String, *, as : T.class) + abstract def fetch(key : String) # Stores a given item into cache - abstract def store(key : String, value : CacheableItem, expires : Time::Span) + abstract def store(key : String, value : CacheableItem | String, expires : Time::Span) # Prematurely deletes item(s) from the cache abstract def delete(key : String) diff --git a/src/invidious/cache/null_item_store.cr b/src/invidious/cache/null_item_store.cr index c26c0804..0f599564 100644 --- a/src/invidious/cache/null_item_store.cr +++ b/src/invidious/cache/null_item_store.cr @@ -5,11 +5,11 @@ module Invidious::Cache def initialize end - def fetch(key : String, *, as : T.class) : T? forall T + def fetch(key : String) : String? return nil end - def store(key : String, value : CacheableItem, expires : Time::Span) + def store(key : String, value : CacheableItem | String, expires : Time::Span) end def delete(key : String) diff --git a/src/invidious/cache/redis_item_store.cr b/src/invidious/cache/redis_item_store.cr index ccf847a6..bd5550ef 100644 --- a/src/invidious/cache/redis_item_store.cr +++ b/src/invidious/cache/redis_item_store.cr @@ -5,19 +5,17 @@ require "redis" module Invidious::Cache class RedisItemStore < ItemStore @redis : Redis::PooledClient - @node_name : String - def initialize(url : URI, @node_name = "") - @redis = Redis::PooledClient.new url + def initialize(url : URI) + @redis = Redis::PooledClient.new(url: url.to_s) end - def fetch(key : String, *, as : T.class) : (T | Nil) forall T - value = @redis.get(key) - return nil if value.nil? - return T.from_json(JSON::PullParser.new(value)) + def fetch(key : String) : String? + return @redis.get(key) end - def store(key : String, value : CacheableItem, expires : Time::Span) + def store(key : String, value : CacheableItem | String, expires : Time::Span) + value = value.to_json if value.is_a?(CacheableItem) @redis.set(key, value, ex: expires.to_i) end diff --git a/src/invidious/exceptions.cr b/src/invidious/exceptions.cr index 690db907..bdbc0c49 100644 --- a/src/invidious/exceptions.cr +++ b/src/invidious/exceptions.cr @@ -38,3 +38,7 @@ end # some important informations, and that the query should be sent again. class RetryOnceException < Exception end + +# Exception used to indicate that the config file contains some errors +class InvalidConfigException < Exception +end From 82d92f43142df8a27bfc454a4942a8bcef1edd0f Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Sun, 18 Jun 2023 13:10:39 +0200 Subject: [PATCH 7/8] Use new cache system for 'Video' objects --- src/invidious/database/base.cr | 1 - src/invidious/helpers/helpers.cr | 4 +- src/invidious/routes/api/manifest.cr | 2 +- src/invidious/routes/api/v1/authenticated.cr | 2 +- src/invidious/routes/api/v1/videos.cr | 6 +- src/invidious/routes/embed.cr | 2 +- src/invidious/routes/feeds.cr | 2 +- src/invidious/routes/playlists.cr | 2 +- src/invidious/routes/video_playback.cr | 2 +- src/invidious/routes/watch.cr | 2 +- src/invidious/user/imports.cr | 4 +- src/invidious/videos.cr | 90 ++++++++++---------- 12 files changed, 58 insertions(+), 61 deletions(-) diff --git a/src/invidious/database/base.cr b/src/invidious/database/base.cr index 0fb1b6af..3a6a15c6 100644 --- a/src/invidious/database/base.cr +++ b/src/invidious/database/base.cr @@ -18,7 +18,6 @@ module Invidious::Database Invidious::Database.check_table("nonces", Nonce) Invidious::Database.check_table("session_ids", SessionId) Invidious::Database.check_table("users", User) - Invidious::Database.check_table("videos", Video) if cfg.cache_annotations Invidious::Database.check_table("annotations", Annotation) diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 6add0237..80aa092b 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -74,7 +74,7 @@ def create_notification_stream(env, topics, connection_channel) published = Time.utc - Time::Span.new(days: time_span[0], hours: time_span[1], minutes: time_span[2], seconds: time_span[3]) video_id = TEST_IDS[rand(TEST_IDS.size)] - video = get_video(video_id) + video = Video.get(video_id) video.published = published response = JSON.parse(video.to_json(locale, nil)) @@ -133,7 +133,7 @@ def create_notification_stream(env, topics, connection_channel) next end - video = get_video(video_id) + video = Video.get(video_id) video.published = Time.unix(published) response = JSON.parse(video.to_json(locale, nil)) diff --git a/src/invidious/routes/api/manifest.cr b/src/invidious/routes/api/manifest.cr index 31147d9c..a443e20d 100644 --- a/src/invidious/routes/api/manifest.cr +++ b/src/invidious/routes/api/manifest.cr @@ -13,7 +13,7 @@ module Invidious::Routes::API::Manifest unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe } begin - video = get_video(id, region: region) + video = Video.get(id, region: region) rescue ex : NotFoundException haltf env, status_code: 404 rescue ex diff --git a/src/invidious/routes/api/v1/authenticated.cr b/src/invidious/routes/api/v1/authenticated.cr index a35d2f2b..05040174 100644 --- a/src/invidious/routes/api/v1/authenticated.cr +++ b/src/invidious/routes/api/v1/authenticated.cr @@ -314,7 +314,7 @@ module Invidious::Routes::API::V1::Authenticated end begin - video = get_video(video_id) + video = Video.get(video_id) rescue ex : NotFoundException return error_json(404, ex) rescue ex diff --git a/src/invidious/routes/api/v1/videos.cr b/src/invidious/routes/api/v1/videos.cr index 7d22522f..06079bba 100644 --- a/src/invidious/routes/api/v1/videos.cr +++ b/src/invidious/routes/api/v1/videos.cr @@ -9,7 +9,7 @@ module Invidious::Routes::API::V1::Videos proxy = {"1", "true"}.any? &.== env.params.query["local"]? begin - video = get_video(id, region: region) + video = Video.get(id, region: region) rescue ex : NotFoundException return error_json(404, ex) rescue ex @@ -40,7 +40,7 @@ module Invidious::Routes::API::V1::Videos # getting video info. begin - video = get_video(id, region: region) + video = Video.get(id, region: region) rescue ex : NotFoundException haltf env, 404 rescue ex @@ -171,7 +171,7 @@ module Invidious::Routes::API::V1::Videos region = find_region(env.params.query["region"]?) begin - video = get_video(id, region: region) + video = Video.get(id, region: region) rescue ex : NotFoundException haltf env, 404 rescue ex diff --git a/src/invidious/routes/embed.cr b/src/invidious/routes/embed.cr index 266f7ba4..2d218f17 100644 --- a/src/invidious/routes/embed.cr +++ b/src/invidious/routes/embed.cr @@ -130,7 +130,7 @@ module Invidious::Routes::Embed subscriptions ||= [] of String begin - video = get_video(id, region: params.region) + video = Video.get(id, region: params.region) rescue ex : NotFoundException return error_template(404, ex) rescue ex diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr index 04bccf81..02d2b37d 100644 --- a/src/invidious/routes/feeds.cr +++ b/src/invidious/routes/feeds.cr @@ -420,7 +420,7 @@ module Invidious::Routes::Feeds updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content) begin - video = get_video(id, force_refresh: true) + video = Video.get(id, force_refresh: true) rescue next # skip this video since it raised an exception (e.g. it is a scheduled live event) end diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr index abd0e945..968724a5 100644 --- a/src/invidious/routes/playlists.cr +++ b/src/invidious/routes/playlists.cr @@ -352,7 +352,7 @@ module Invidious::Routes::Playlists video_id = env.params.query["video_id"] begin - video = get_video(video_id) + video = Video.get(video_id) rescue ex : NotFoundException return error_json(404, ex) rescue ex diff --git a/src/invidious/routes/video_playback.cr b/src/invidious/routes/video_playback.cr index 74ad1d1d..127a80bc 100644 --- a/src/invidious/routes/video_playback.cr +++ b/src/invidious/routes/video_playback.cr @@ -275,7 +275,7 @@ module Invidious::Routes::VideoPlayback end begin - video = get_video(id, region: region) + video = Video.get(id, region: region) rescue ex : NotFoundException return error_template(404, ex) rescue ex diff --git a/src/invidious/routes/watch.cr b/src/invidious/routes/watch.cr index 410e625b..3beb9ec2 100644 --- a/src/invidious/routes/watch.cr +++ b/src/invidious/routes/watch.cr @@ -52,7 +52,7 @@ module Invidious::Routes::Watch env.params.query.delete_all("listen") begin - video = get_video(id, region: params.region) + video = Video.get(id, region: params.region) rescue ex : NotFoundException LOGGER.error("get_video not found: #{id} : #{ex.message}") return error_template(404, ex) diff --git a/src/invidious/user/imports.cr b/src/invidious/user/imports.cr index 108f2ccc..76fb6ba9 100644 --- a/src/invidious/user/imports.cr +++ b/src/invidious/user/imports.cr @@ -59,7 +59,7 @@ struct Invidious::User next if video_id == "Video Id" begin - video = get_video(video_id) + video = Video.get(video_id) rescue ex next end @@ -133,7 +133,7 @@ struct Invidious::User next if !video_id begin - video = get_video(video_id, false) + video = Video.get(video_id) rescue ex next end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index a8f02056..15a007c2 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -5,8 +5,6 @@ enum VideoType end struct Video - include DB::Serializable - # Version of the JSON structure # It prevents us from loading an incompatible version from cache # (either newer or older, if instances with different versions run @@ -18,21 +16,13 @@ struct Video SCHEMA_VERSION = 2 property id : String - - @[DB::Field(converter: Video::JSONConverter)] property info : Hash(String, JSON::Any) - property updated : Time - @[DB::Field(ignore: true)] @captions = [] of Invidious::Videos::Captions::Metadata - @[DB::Field(ignore: true)] property adaptive_fmts : Array(Hash(String, JSON::Any))? - - @[DB::Field(ignore: true)] property fmt_stream : Array(Hash(String, JSON::Any))? - @[DB::Field(ignore: true)] property description : String? module JSONConverter @@ -41,6 +31,49 @@ struct Video end end + # Create new object from cache (JSON) + def initialize(@id, @info) + end + + def self.get(id : String, *, force_refresh = false, region = nil) + key = "video:#{id}" + key += ":#{region}" if !region.nil? + + # Fetch video from cache, unles a force refresh is requested + info = force_refresh ? nil : IV::Cache::INSTANCE.fetch(key) + updated = false + + # Fetch video from youtube, if needed + if info.nil? + video = Video.new(id, fetch_video(id, region)) + updated = true + else + video = Video.new(id, JSON.parse(info).as_h) + + # If video has premiered, live has started or the format + # of the video data has changed, refresh the data. + outdated_data = (video.schema_version != Video::SCHEMA_VERSION) + live_started = (video.live_now && video.published < Time.utc) + + if outdated_data || live_started + video = Video.new(id, fetch_video(id, region)) + updated = true + end + end + + # Store updated entry in cache + # TODO: finer cache control based on video type & publication date + if updated + if video.live_now || video.published < Time.utc + IV::Cache::INSTANCE.store(key, info.to_json, 10.minutes) + else + IV::Cache::INSTANCE.store(key, info.to_json, 2.hours) + end + end + + return video + end + # Methods for API v1 JSON def to_json(locale : String?, json : JSON::Builder) @@ -358,35 +391,6 @@ struct Video getset_bool isUpcoming end -def get_video(id, refresh = true, region = nil, force_refresh = false) - if (video = Invidious::Database::Videos.select(id)) && !region - # If record was last updated over 10 minutes ago, or video has since premiered, - # refresh (expire param in response lasts for 6 hours) - if (refresh && - (Time.utc - video.updated > 10.minutes) || - (video.premiere_timestamp.try &.< Time.utc)) || - force_refresh || - video.schema_version != Video::SCHEMA_VERSION # cache control - begin - video = fetch_video(id, region) - Invidious::Database::Videos.update(video) - rescue ex - Invidious::Database::Videos.delete(id) - raise ex - end - end - else - video = fetch_video(id, region) - Invidious::Database::Videos.insert(video) if !region - end - - return video -rescue DB::Error - # Avoid common `DB::PoolRetryAttemptsExceeded` error and friends - # Note: All DB errors inherit from `DB::Error` - return fetch_video(id, region) -end - def fetch_video(id, region) info = extract_video_info(video_id: id) @@ -415,13 +419,7 @@ def fetch_video(id, region) end end - video = Video.new({ - id: id, - info: info, - updated: Time.utc, - }) - - return video + return info end def process_continuation(query, plid, id) From 47d9f402c4e2b3f4ecf82003e433109878a0d4b0 Mon Sep 17 00:00:00 2001 From: Samantaz Fox Date: Mon, 12 Feb 2024 21:51:25 +0100 Subject: [PATCH 8/8] WIP --- src/invidious/cache.cr | 9 ++++++++- src/invidious/cache/cacheable_item.cr | 9 --------- src/invidious/cache/item_store.cr | 2 +- src/invidious/cache/null_item_store.cr | 2 +- src/invidious/cache/redis_item_store.cr | 3 +-- src/invidious/videos.cr | 13 +++++-------- 6 files changed, 16 insertions(+), 22 deletions(-) delete mode 100644 src/invidious/cache/cacheable_item.cr diff --git a/src/invidious/cache.cr b/src/invidious/cache.cr index e7f2b966..1e64d959 100644 --- a/src/invidious/cache.cr +++ b/src/invidious/cache.cr @@ -3,7 +3,7 @@ require "./cache/*" module Invidious::Cache extend self - INSTANCE = self.init(CONFIG.cache) + private INSTANCE = self.init(CONFIG.cache) def init(cfg : Config::CacheConfig) : ItemStore # Environment variable takes precedence over local config @@ -26,4 +26,11 @@ module Invidious::Cache raise InvalidConfigException.new "Invalid cache url. Only redis:// URL are currently supported." end end + + # Shortcut methods to not have to specify INSTANCE everywhere in the code + {% for method in ["fetch", "store", "delete", "clear"] %} + def {{method.id}}(*args, **kwargs) + INSTANCE.{{method.id}}(*args, **kwargs) + end + {% end %} end diff --git a/src/invidious/cache/cacheable_item.cr b/src/invidious/cache/cacheable_item.cr deleted file mode 100644 index c1295a4a..00000000 --- a/src/invidious/cache/cacheable_item.cr +++ /dev/null @@ -1,9 +0,0 @@ -require "json" - -module Invidious::Cache - # Including this module allows the includer object to be cached. - # The object will automatically inherit from JSON::Serializable. - module CacheableItem - include JSON::Serializable - end -end diff --git a/src/invidious/cache/item_store.cr b/src/invidious/cache/item_store.cr index ebe5b1f9..4780841c 100644 --- a/src/invidious/cache/item_store.cr +++ b/src/invidious/cache/item_store.cr @@ -10,7 +10,7 @@ module Invidious::Cache abstract def fetch(key : String) # Stores a given item into cache - abstract def store(key : String, value : CacheableItem | String, expires : Time::Span) + abstract def store(key : String, value : String, expires : Time::Span) # Prematurely deletes item(s) from the cache abstract def delete(key : String) diff --git a/src/invidious/cache/null_item_store.cr b/src/invidious/cache/null_item_store.cr index 0f599564..786b4109 100644 --- a/src/invidious/cache/null_item_store.cr +++ b/src/invidious/cache/null_item_store.cr @@ -9,7 +9,7 @@ module Invidious::Cache return nil end - def store(key : String, value : CacheableItem | String, expires : Time::Span) + def store(key : String, value : String, expires : Time::Span) end def delete(key : String) diff --git a/src/invidious/cache/redis_item_store.cr b/src/invidious/cache/redis_item_store.cr index bd5550ef..3ae29980 100644 --- a/src/invidious/cache/redis_item_store.cr +++ b/src/invidious/cache/redis_item_store.cr @@ -14,8 +14,7 @@ module Invidious::Cache return @redis.get(key) end - def store(key : String, value : CacheableItem | String, expires : Time::Span) - value = value.to_json if value.is_a?(CacheableItem) + def store(key : String, value : String, expires : Time::Span) @redis.set(key, value, ex: expires.to_i) end diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 15a007c2..097182f8 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -14,6 +14,7 @@ struct Video # the `params` structure in videos/parser.cr!!! # SCHEMA_VERSION = 2 + CACHE_KEY = "video_v#{SCHEMA_VERSION}" property id : String property info : Hash(String, JSON::Any) @@ -36,7 +37,7 @@ struct Video end def self.get(id : String, *, force_refresh = false, region = nil) - key = "video:#{id}" + key = "#{CACHE_KEY}:#{id}" key += ":#{region}" if !region.nil? # Fetch video from cache, unles a force refresh is requested @@ -50,12 +51,8 @@ struct Video else video = Video.new(id, JSON.parse(info).as_h) - # If video has premiered, live has started or the format - # of the video data has changed, refresh the data. - outdated_data = (video.schema_version != Video::SCHEMA_VERSION) - live_started = (video.live_now && video.published < Time.utc) - - if outdated_data || live_started + # If the video has premiered or the live has started, refresh the data. + if (video.live_now && video.published < Time.utc) video = Video.new(id, fetch_video(id, region)) updated = true end @@ -71,7 +68,7 @@ struct Video end end - return video + return Video.new(id, info) end # Methods for API v1 JSON