Add a function to build youtube search filters
(it aims at replacing produce_search_params)
This commit is contained in:
parent
80417281c4
commit
c01a29fe76
3 changed files with 152 additions and 14 deletions
|
@ -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")
|
||||
|
|
92
spec/invidious/search/yt_filters_spec.cr
Normal file
92
spec/invidious/search/yt_filters_spec.cr
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue