summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--spec/invidious/helpers_spec.cr14
-rw-r--r--spec/invidious/search/yt_filters_spec.cr92
-rw-r--r--src/invidious/search/filters.cr60
3 files changed, 152 insertions, 14 deletions
diff --git a/spec/invidious/helpers_spec.cr b/spec/invidious/helpers_spec.cr
index b2436989..5ecebef3 100644
--- a/spec/invidious/helpers_spec.cr
+++ b/spec/invidious/helpers_spec.cr
@@ -29,20 +29,6 @@ Spectator.describe "Helper" do
end
end
- describe "#produce_search_params" do
- it "correctly produces token for searching with specified filters" do
- expect(produce_search_params).to eq("CAASAhABSAA%3D")
-
- expect(produce_search_params(sort: "upload_date", content_type: "video")).to eq("CAISAhABSAA%3D")
-
- expect(produce_search_params(content_type: "playlist")).to eq("CAASAhADSAA%3D")
-
- expect(produce_search_params(sort: "date", content_type: "video", features: ["hd", "cc", "purchased", "hdr"])).to eq("CAISCxABIAEwAUgByAEBSAA%3D")
-
- expect(produce_search_params(content_type: "channel")).to eq("CAASAhACSAA%3D")
- end
- end
-
describe "#produce_comment_continuation" do
it "correctly produces a continuation token for comments" do
expect(produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA")).to eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")
diff --git a/spec/invidious/search/yt_filters_spec.cr b/spec/invidious/search/yt_filters_spec.cr
new file mode 100644
index 00000000..27357058
--- /dev/null
+++ b/spec/invidious/search/yt_filters_spec.cr
@@ -0,0 +1,92 @@
+require "../../../src/invidious/search/filters"
+
+require "http/params"
+require "spectator"
+
+Spectator.configure do |config|
+ config.fail_blank
+ config.randomize
+end
+
+# Encoded filter values are extracted from the search
+# page of Youtube with any browser devtools HTML inspector.
+
+DATE_FILTERS = {
+ Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D",
+ Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D",
+ Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D",
+ Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D",
+ Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D",
+}
+
+TYPE_FILTERS = {
+ Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D",
+ Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D",
+ Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D",
+ Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D",
+}
+
+DURATION_FILTERS = {
+ Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D",
+ Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D",
+ Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D",
+}
+
+FEATURE_FILTERS = {
+ Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D",
+ Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D",
+ Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D",
+ Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D",
+ Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D",
+ Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D",
+ Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D",
+ Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D",
+ Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D",
+ Invidious::Search::Filters::Features::Location => "EgO4AQE%3D",
+ Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D",
+}
+
+SORT_FILTERS = {
+ Invidious::Search::Filters::Sort::Relevance => "",
+ Invidious::Search::Filters::Sort::Date => "CAI%3D",
+ Invidious::Search::Filters::Sort::Views => "CAM%3D",
+ Invidious::Search::Filters::Sort::Rating => "CAE%3D",
+}
+
+Spectator.describe Invidious::Search::Filters do
+ # -------------------
+ # Encode YT params
+ # -------------------
+
+ describe "#to_yt_params" do
+ sample DATE_FILTERS do |value, result|
+ it "Encodes upload date filter '#{value}'" do
+ expect(described_class.new(date: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample TYPE_FILTERS do |value, result|
+ it "Encodes content type filter '#{value}'" do
+ expect(described_class.new(type: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample DURATION_FILTERS do |value, result|
+ it "Encodes duration filter '#{value}'" do
+ expect(described_class.new(duration: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample FEATURE_FILTERS do |value, result|
+ it "Encodes feature filter '#{value}'" do
+ expect(described_class.new(features: value).to_yt_params).to eq(result)
+ end
+ end
+
+ sample SORT_FILTERS do |value, result|
+ it "Encodes sort filter '#{value}'" do
+ expect(described_class.new(sort: value).to_yt_params).to eq(result)
+ end
+ end
+ end
+end
diff --git a/src/invidious/search/filters.cr b/src/invidious/search/filters.cr
index 75ac287a..f1fd2695 100644
--- a/src/invidious/search/filters.cr
+++ b/src/invidious/search/filters.cr
@@ -1,3 +1,6 @@
+require "protodec/utils"
+require "http/params"
+
module Invidious::Search
struct Filters
# Values correspond to { "2:embedded": { "1:varint": <X> }}
@@ -74,6 +77,63 @@ module Invidious::Search
@features : Features = Features::None,
@sort : Sort = Sort::Relevance
)
+ # -------------------
+ # Youtube params
+ # -------------------
+
+ # Produce the youtube search parameters for the
+ # innertube API (base64-encoded protobuf object).
+ def to_yt_params(page : Int = 1) : String
+ # Initialize the embedded protobuf object
+ embedded = {} of String => Int64
+
+ # Add these field only if associated parameter is selected
+ embedded["1:varint"] = @date.to_i64 if !@date.none?
+ embedded["2:varint"] = @type.to_i64 if !@type.all?
+ embedded["3:varint"] = @duration.to_i64 if !@duration.none?
+
+ if !@features.none?
+ # All features have a value of "1" when enabled, and
+ # the field is omitted when the feature is no selected.
+ embedded["4:varint"] = 1_i64 if @features.includes?(Features::HD)
+ embedded["5:varint"] = 1_i64 if @features.includes?(Features::Subtitles)
+ embedded["6:varint"] = 1_i64 if @features.includes?(Features::CCommons)
+ embedded["7:varint"] = 1_i64 if @features.includes?(Features::ThreeD)
+ embedded["8:varint"] = 1_i64 if @features.includes?(Features::Live)
+ embedded["9:varint"] = 1_i64 if @features.includes?(Features::Purchased)
+ embedded["14:varint"] = 1_i64 if @features.includes?(Features::FourK)
+ embedded["15:varint"] = 1_i64 if @features.includes?(Features::ThreeSixty)
+ embedded["23:varint"] = 1_i64 if @features.includes?(Features::Location)
+ embedded["25:varint"] = 1_i64 if @features.includes?(Features::HDR)
+ embedded["26:varint"] = 1_i64 if @features.includes?(Features::VR180)
+ end
+
+ # Initialize an empty protobuf object
+ object = {} of String => (Int64 | String | Hash(String, Int64))
+
+ # As usual, everything can be omitted if it has no value
+ object["2:embedded"] = embedded if !embedded.empty?
+
+ # Default sort is "relevance", so when this option is selected,
+ # the associated field can be omitted.
+ if !@sort.relevance?
+ object["1:varint"] = @sort.to_i64
+ end
+
+ # Add page number (if provided)
+ if page > 1
+ object["9:varint"] = ((page - 1) * 20).to_i64
+ end
+
+ # If the object is empty, return an empty string,
+ # otherwise encode to protobuf then to base64
+ return "" if object.empty?
+
+ return object
+ .try { |i| Protodec::Any.cast_json(i) }
+ .try { |i| Protodec::Any.from_json(i) }
+ .try { |i| Base64.urlsafe_encode(i) }
+ .try { |i| URI.encode_www_form(i) }
end
end
end