Encapsulate videos parser and clip functions inside it's own Invidious::Videos::Parser and Invidious::Videos::Clip module (#5745)

Part of https://github.com/iv-org/invidious/issues/5744
This commit is contained in:
Fijxu
2026-05-28 13:11:41 -04:00
committed by GitHub
parent 8b183caa2a
commit 4ae227ce91
9 changed files with 603 additions and 591 deletions

View File

@@ -7,7 +7,7 @@ Spectator.describe "parse_video_info" do
_next = load_mock("video/regular_mrbeast.next") _next = load_mock("video/regular_mrbeast.next")
raw_data = _player.merge!(_next) raw_data = _player.merge!(_next)
info = parse_video_info("2isYuQZMbdU", raw_data) info = Invidious::Videos::Parser.parse_video_info("2isYuQZMbdU", raw_data)
# Some basic verifications # Some basic verifications
expect(typeof(info)).to eq(Hash(String, JSON::Any)) expect(typeof(info)).to eq(Hash(String, JSON::Any))
@@ -88,7 +88,7 @@ Spectator.describe "parse_video_info" do
_next = load_mock("video/regular_no-description.next") _next = load_mock("video/regular_no-description.next")
raw_data = _player.merge!(_next) raw_data = _player.merge!(_next)
info = parse_video_info("iuevw6218F0", raw_data) info = Invidious::Videos::Parser.parse_video_info("iuevw6218F0", raw_data)
# Some basic verifications # Some basic verifications
expect(typeof(info)).to eq(Hash(String, JSON::Any)) expect(typeof(info)).to eq(Hash(String, JSON::Any))

View File

@@ -7,7 +7,7 @@ Spectator.describe "parse_video_info" do
_next = load_mock("video/scheduled_live_PBD-Podcast.next") _next = load_mock("video/scheduled_live_PBD-Podcast.next")
raw_data = _player.merge!(_next) raw_data = _player.merge!(_next)
info = parse_video_info("N-yVic7BbY0", raw_data) info = Invidious::Videos::Parser.parse_video_info("N-yVic7BbY0", raw_data)
# Some basic verifications # Some basic verifications
expect(typeof(info)).to eq(Hash(String, JSON::Any)) expect(typeof(info)).to eq(Hash(String, JSON::Any))

View File

@@ -410,7 +410,7 @@ module Invidious::Routes::API::V1::Videos
clip_title = nil clip_title = nil
if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s
start_time, end_time, clip_title = parse_clip_parameters(params) start_time, end_time, clip_title = Invidious::Videos::Clip.parse_clip_parameters(params)
end end
begin begin

View File

@@ -122,7 +122,7 @@ module Invidious::Routes::Embed
else nil # Continue else nil # Continue
end end
params = process_video_params(env.params.query, preferences) params = Invidious::Videos.process_video_params(env.params.query, preferences)
user = env.get?("user").try &.as(User) user = env.get?("user").try &.as(User)
if user if user

View File

@@ -47,7 +47,7 @@ module Invidious::Routes::Watch
end end
subscriptions ||= [] of String subscriptions ||= [] of String
params = process_video_params(env.params.query, preferences) params = Invidious::Videos.process_video_params(env.params.query, preferences)
env.params.query.delete_all("listen") env.params.query.delete_all("listen")
begin begin
@@ -273,7 +273,7 @@ module Invidious::Routes::Watch
if video_id = response.dig?("endpoint", "watchEndpoint", "videoId") if video_id = response.dig?("endpoint", "watchEndpoint", "videoId")
if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s
start_time, end_time, _ = parse_clip_parameters(params) start_time, end_time, _ = Invidious::Videos::Clip.parse_clip_parameters(params)
env.params.query["start"] = start_time.to_s if start_time != nil env.params.query["start"] = start_time.to_s if start_time != nil
env.params.query["end"] = end_time.to_s if end_time != nil env.params.query["end"] = end_time.to_s if end_time != nil
end end

View File

@@ -324,7 +324,7 @@ rescue DB::Error
end end
def fetch_video(id, region) def fetch_video(id, region)
info = extract_video_info(video_id: id) info = Invidious::Videos::Parser.extract_video_info(video_id: id)
if info.nil? if info.nil?
raise InfoException.new("Invidious companion is not available. \ raise InfoException.new("Invidious companion is not available. \

View File

@@ -1,22 +1,26 @@
require "json" require "json"
# returns start_time, end_time and clip_title module Invidious::Videos::Clip
def parse_clip_parameters(params) : {Float64?, Float64?, String?} extend self
decoded_protobuf = params.try { |i| URI.decode_www_form(i) }
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
start_time = decoded_protobuf # returns start_time, end_time and clip_title
.try(&.["50:0:embedded"]["2:1:varint"].as_i64) def parse_clip_parameters(params) : {Float64?, Float64?, String?}
.try { |i| i/1000 } decoded_protobuf = params.try { |i| URI.decode_www_form(i) }
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
end_time = decoded_protobuf start_time = decoded_protobuf
.try(&.["50:0:embedded"]["3:2:varint"].as_i64) .try(&.["50:0:embedded"]["2:1:varint"].as_i64)
.try { |i| i/1000 } .try { |i| i/1000 }
clip_title = decoded_protobuf end_time = decoded_protobuf
.try(&.["50:0:embedded"]["4:3:string"].as_s) .try(&.["50:0:embedded"]["3:2:varint"].as_i64)
.try { |i| i/1000 }
return start_time, end_time, clip_title clip_title = decoded_protobuf
.try(&.["50:0:embedded"]["4:3:string"].as_s)
return start_time, end_time, clip_title
end
end end

View File

@@ -1,465 +1,469 @@
require "json" require "json"
# Use to parse both "compactVideoRenderer" and "endScreenVideoRenderer". module Invidious::Videos::Parser
# The former is preferred as it has more videos in it. The second has extend self
# the same 11 first entries as the compact rendered.
#
# TODO: "compactRadioRenderer" (Mix) and
# TODO: Use a proper struct/class instead of a hacky JSON object
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
return nil if !related["videoId"]?
# The compact renderer has video length in seconds, where the end # Use to parse both "compactVideoRenderer" and "endScreenVideoRenderer".
# screen rendered has a full text version ("42:40") # The former is preferred as it has more videos in it. The second has
length = related["lengthInSeconds"]?.try &.as_i.to_s # the same 11 first entries as the compact rendered.
length ||= related.dig?("lengthText", "simpleText").try do |box| #
decode_length_seconds(box.as_s).to_s # TODO: "compactRadioRenderer" (Mix) and
# TODO: Use a proper struct/class instead of a hacky JSON object
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
return nil if !related["videoId"]?
# The compact renderer has video length in seconds, where the end
# screen rendered has a full text version ("42:40")
length = related["lengthInSeconds"]?.try &.as_i.to_s
length ||= related.dig?("lengthText", "simpleText").try do |box|
decode_length_seconds(box.as_s).to_s
end
# Both have "short", so the "long" option shouldn't be required
channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
.try &.dig?("runs", 0)
author = channel_info.try &.dig?("text")
author_verified = has_verified_badge?(related["ownerBadges"]?).to_s
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
short_view_count = related.try do |r|
HelperExtractors.get_short_view_count(r).to_s
end
LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
if published_time_text = related["publishedTimeText"]?
decoded_time = decode_date(published_time_text["simpleText"].to_s)
published = decoded_time.to_rfc3339.to_s
else
published = nil
end
# TODO: when refactoring video types, make a struct for related videos
# or reuse an existing type, if that fits.
return {
"id" => related["videoId"],
"title" => related["title"]["simpleText"],
"author" => author || JSON::Any.new(""),
"ucid" => JSON::Any.new(ucid || ""),
"length_seconds" => JSON::Any.new(length || "0"),
"short_view_count" => JSON::Any.new(short_view_count || "0"),
"author_verified" => JSON::Any.new(author_verified),
"published" => JSON::Any.new(published || ""),
}
end end
# Both have "short", so the "long" option shouldn't be required def extract_video_info(video_id : String)
channel_info = (related["shortBylineText"]? || related["longBylineText"]?) # Fetch data from the player endpoint
.try &.dig?("runs", 0) player_response = YoutubeAPI.player(video_id: video_id)
author = channel_info.try &.dig?("text") if player_response.nil?
author_verified = has_verified_badge?(related["ownerBadges"]?).to_s return nil
end
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) } playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
short_view_count = related.try do |r| if playability_status != "OK"
HelperExtractors.get_short_view_count(r).to_s subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason")
end reason = subreason.try &.[]?("simpleText").try &.as_s
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
reason ||= player_response.dig("playabilityStatus", "reason").as_s
LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container") # Stop here if video is not a scheduled livestream or
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) ||
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
return {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
"reason" => JSON::Any.new(reason),
}
end
elsif video_id != player_response.dig?("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
# Line to be reverted if one day we solve the video not available issue.
if published_time_text = related["publishedTimeText"]? # Although technically not a call to /videoplayback the fact that YouTube is returning the
decoded_time = decode_date(published_time_text["simpleText"].to_s) # wrong video means that we should count it as a failure.
published = decoded_time.to_rfc3339.to_s Helpers.get_playback_statistic["totalRequests"] += 1
else
published = nil
end
# TODO: when refactoring video types, make a struct for related videos
# or reuse an existing type, if that fits.
return {
"id" => related["videoId"],
"title" => related["title"]["simpleText"],
"author" => author || JSON::Any.new(""),
"ucid" => JSON::Any.new(ucid || ""),
"length_seconds" => JSON::Any.new(length || "0"),
"short_view_count" => JSON::Any.new(short_view_count || "0"),
"author_verified" => JSON::Any.new(author_verified),
"published" => JSON::Any.new(published || ""),
}
end
def extract_video_info(video_id : String)
# Fetch data from the player endpoint
player_response = YoutubeAPI.player(video_id: video_id)
if player_response.nil?
return nil
end
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
if playability_status != "OK"
subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason")
reason = subreason.try &.[]?("simpleText").try &.as_s
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
reason ||= player_response.dig("playabilityStatus", "reason").as_s
# Stop here if video is not a scheduled livestream or
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) ||
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
return { return {
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), "version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
"reason" => JSON::Any.new(reason), "reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
} }
else
reason = nil
end end
elsif video_id != player_response.dig?("videoDetails", "videoId")
# YouTube may return a different video player response than expected.
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
# Line to be reverted if one day we solve the video not available issue.
# Although technically not a call to /videoplayback the fact that YouTube is returning the # Don't fetch the next endpoint if the video is unavailable.
# wrong video means that we should count it as a failure. if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
Helpers.get_playback_statistic["totalRequests"] += 1 next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
# Remove the microformat returned by the /next endpoint on some videos
# to prevent player_response microformat from being overwritten.
next_response.delete("microformat")
player_response = player_response.merge(next_response)
end
return { params = self.parse_video_info(video_id, player_response)
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64), params["reason"] = JSON::Any.new(reason) if reason
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
}
else
reason = nil
end
# Don't fetch the next endpoint if the video is unavailable. {"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) params[f] = player_response[f] if player_response[f]?
next_response = YoutubeAPI.next({"videoId": video_id, "params": ""}) end
# Remove the microformat returned by the /next endpoint on some videos
# to prevent player_response microformat from being overwritten.
next_response.delete("microformat")
player_response = player_response.merge(next_response)
end
params = parse_video_info(video_id, player_response) # Convert URLs, if those are present
params["reason"] = JSON::Any.new(reason) if reason if streaming_data = player_response["streamingData"]?
%w[formats adaptiveFormats].each do |key|
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f| streaming_data.as_h[key]?.try &.as_a.each do |format|
params[f] = player_response[f] if player_response[f]? format = format.as_h
end if format["url"]?.nil?
format["url"] = format["signatureCipher"]
# Convert URLs, if those are present end
if streaming_data = player_response["streamingData"]? format["url"] = JSON::Any.new(convert_url(format))
%w[formats adaptiveFormats].each do |key|
streaming_data.as_h[key]?.try &.as_a.each do |format|
format = format.as_h
if format["url"]?.nil?
format["url"] = format["signatureCipher"]
end end
format["url"] = JSON::Any.new(convert_url(format))
end end
params["streamingData"] = streaming_data
end end
params["streamingData"] = streaming_data # Data structure version, for cache control
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
return params
end end
# Data structure version, for cache control def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64) LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
response = YoutubeAPI.player(video_id: id)
return params playability_status = response["playabilityStatus"]["status"]
end LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)? if id != response.dig?("videoDetails", "videoId")
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.") # YouTube may return a different video player response than expected.
response = YoutubeAPI.player(video_id: id) # See: https://github.com/TeamNewPipe/NewPipe/issues/8713
raise InfoException.new(
playability_status = response["playabilityStatus"]["status"] "The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)"
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.") )
elsif playability_status == "OK"
if id != response.dig?("videoDetails", "videoId") return response
# YouTube may return a different video player response than expected. else
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713 return nil
raise InfoException.new(
"The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)"
)
elsif playability_status == "OK"
return response
else
return nil
end
end
def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any)) : Hash(String, JSON::Any)
# Top level elements
main_results = player_response.dig?("contents", "twoColumnWatchNextResults")
raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results
# Primary results are not available on Music videos
# See: https://github.com/iv-org/invidious/pull/3238#issuecomment-1207193725
if primary_results = main_results.dig?("results", "results", "contents")
video_primary_renderer = primary_results
.as_a.find(&.["videoPrimaryInfoRenderer"]?)
.try &.["videoPrimaryInfoRenderer"]
video_secondary_renderer = primary_results
.as_a.find(&.["videoSecondaryInfoRenderer"]?)
.try &.["videoSecondaryInfoRenderer"]
raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
end
video_details = player_response.dig?("videoDetails")
if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
microformat = {} of String => JSON::Any
end
raise BrokenTubeException.new("videoDetails") if !video_details
# Basic video infos
title = video_details["title"]?.try &.as_s
# We have to try to extract viewCount from videoPrimaryInfoRenderer first,
# then from videoDetails, as the latter is "0" for livestreams (we want
# to get the amount of viewers watching).
views_txt = extract_text(
video_primary_renderer
.try &.dig?("viewCount", "videoViewCountRenderer", "viewCount")
)
views_txt ||= video_details["viewCount"]?.try &.as_s || ""
views = views_txt.gsub(/\D/, "").to_i64?
length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"])
.try &.as_s.to_i64
published = microformat["publishDate"]?
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?(
"playabilityStatus", "liveStreamability",
"liveStreamabilityRenderer", "offlineSlate",
"liveStreamOfflineSlateRenderer", "scheduledStartTime"
)
.try &.as_s.to_i64
.try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool
live_now ||= video_details.dig?("isLive").try &.as_bool || false
post_live_dvr = video_details.dig?("isPostLiveDvr")
.try &.as_bool || false
# Extra video infos
allowed_regions = microformat["availableCountries"]?
.try &.as_a.map &.as_s || [] of String
allow_ratings = video_details["allowRatings"]?.try &.as_bool
family_friendly = microformat["isFamilySafe"]?.try &.as_bool
is_listed = video_details["isCrawlable"]?.try &.as_bool
is_upcoming = video_details["isUpcoming"]?.try &.as_bool
keywords = video_details["keywords"]?
.try &.as_a.map &.as_s || [] of String
# Related videos
LOGGER.debug("extract_video_info: parsing related videos...")
related = [] of JSON::Any
# Parse "compactVideoRenderer" items (under secondary results)
secondary_results = main_results
.dig?("secondaryResults", "secondaryResults", "results")
secondary_results.try &.as_a.each do |element|
if item = element["compactVideoRenderer"]?
related_video = parse_related_video(item)
related << JSON::Any.new(related_video) if related_video
end end
end end
# If nothing was found previously, fall back to end screen renderer def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any)) : Hash(String, JSON::Any)
if related.empty? # Top level elements
# Container for "endScreenVideoRenderer" items
player_overlays = player_response.dig?(
"playerOverlays", "playerOverlayRenderer",
"endScreen", "watchNextEndScreenRenderer", "results"
)
player_overlays.try &.as_a.each do |element| main_results = player_response.dig?("contents", "twoColumnWatchNextResults")
if item = element["endScreenVideoRenderer"]?
related_video = parse_related_video(item) raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results
# Primary results are not available on Music videos
# See: https://github.com/iv-org/invidious/pull/3238#issuecomment-1207193725
if primary_results = main_results.dig?("results", "results", "contents")
video_primary_renderer = primary_results
.as_a.find(&.["videoPrimaryInfoRenderer"]?)
.try &.["videoPrimaryInfoRenderer"]
video_secondary_renderer = primary_results
.as_a.find(&.["videoSecondaryInfoRenderer"]?)
.try &.["videoSecondaryInfoRenderer"]
raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
end
video_details = player_response.dig?("videoDetails")
if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
microformat = {} of String => JSON::Any
end
raise BrokenTubeException.new("videoDetails") if !video_details
# Basic video infos
title = video_details["title"]?.try &.as_s
# We have to try to extract viewCount from videoPrimaryInfoRenderer first,
# then from videoDetails, as the latter is "0" for livestreams (we want
# to get the amount of viewers watching).
views_txt = extract_text(
video_primary_renderer
.try &.dig?("viewCount", "videoViewCountRenderer", "viewCount")
)
views_txt ||= video_details["viewCount"]?.try &.as_s || ""
views = views_txt.gsub(/\D/, "").to_i64?
length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"])
.try &.as_s.to_i64
published = microformat["publishDate"]?
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?(
"playabilityStatus", "liveStreamability",
"liveStreamabilityRenderer", "offlineSlate",
"liveStreamOfflineSlateRenderer", "scheduledStartTime"
)
.try &.as_s.to_i64
.try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool
live_now ||= video_details.dig?("isLive").try &.as_bool || false
post_live_dvr = video_details.dig?("isPostLiveDvr")
.try &.as_bool || false
# Extra video infos
allowed_regions = microformat["availableCountries"]?
.try &.as_a.map &.as_s || [] of String
allow_ratings = video_details["allowRatings"]?.try &.as_bool
family_friendly = microformat["isFamilySafe"]?.try &.as_bool
is_listed = video_details["isCrawlable"]?.try &.as_bool
is_upcoming = video_details["isUpcoming"]?.try &.as_bool
keywords = video_details["keywords"]?
.try &.as_a.map &.as_s || [] of String
# Related videos
LOGGER.debug("extract_video_info: parsing related videos...")
related = [] of JSON::Any
# Parse "compactVideoRenderer" items (under secondary results)
secondary_results = main_results
.dig?("secondaryResults", "secondaryResults", "results")
secondary_results.try &.as_a.each do |element|
if item = element["compactVideoRenderer"]?
related_video = self.parse_related_video(item)
related << JSON::Any.new(related_video) if related_video related << JSON::Any.new(related_video) if related_video
end end
end end
end
# Likes # If nothing was found previously, fall back to end screen renderer
if related.empty?
toplevel_buttons = video_primary_renderer # Container for "endScreenVideoRenderer" items
.try &.dig?("videoActions", "menuRenderer", "topLevelButtons") player_overlays = player_response.dig?(
"playerOverlays", "playerOverlayRenderer",
if toplevel_buttons "endScreen", "watchNextEndScreenRenderer", "results"
# New Format as of december 2023
likes_button = toplevel_buttons.dig?(0,
"segmentedLikeDislikeButtonViewModel",
"likeButtonViewModel",
"likeButtonViewModel",
"toggleButtonViewModel",
"toggleButtonViewModel",
"defaultButtonViewModel",
"buttonViewModel"
)
likes_button ||= toplevel_buttons.try &.as_a
.find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE")
.try &.["toggleButtonRenderer"]
# New format as of september 2022
likes_button ||= toplevel_buttons.try &.as_a
.find(&.["segmentedLikeDislikeButtonRenderer"]?)
.try &.dig?(
"segmentedLikeDislikeButtonRenderer",
"likeButton", "toggleButtonRenderer"
) )
if likes_button player_overlays.try &.as_a.each do |element|
likes_txt = likes_button.dig?("accessibilityText") if item = element["endScreenVideoRenderer"]?
# Note: The like count from `toggledText` is off by one, as it would related_video = self.parse_related_video(item)
# represent the new like count in the event where the user clicks on "like". related << JSON::Any.new(related_video) if related_video
likes_txt ||= (likes_button["defaultText"]? || likes_button["toggledText"]?) end
.try &.dig?("accessibility", "accessibilityData", "label")
likes = likes_txt.as_s.gsub(/\D/, "").to_i64? if likes_txt
LOGGER.trace("extract_video_info: Found \"likes\" button. Button text is \"#{likes_txt}\"")
LOGGER.debug("extract_video_info: Likes count is #{likes}") if likes
end
end
# Description
description = microformat.dig?("description", "simpleText").try &.as_s || ""
short_description = player_response.dig?("videoDetails", "shortDescription")
# description_html = video_secondary_renderer.try &.dig?("description", "runs")
# .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id)
# Video metadata
metadata = video_secondary_renderer
.try &.dig?("metadataRowContainer", "metadataRowContainerRenderer", "rows")
.try &.as_a
genre = microformat["category"]?
genre_ucid = nil
license = nil
metadata.try &.each do |row|
metadata_title = extract_text(row.dig?("metadataRowRenderer", "title"))
contents = row.dig?("metadataRowRenderer", "contents", 0)
if metadata_title == "Category"
contents = contents.try &.dig?("runs", 0)
genre = contents.try &.["text"]?
genre_ucid = contents.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId")
elsif metadata_title == "License"
license = contents.try &.dig?("runs", 0, "text")
elsif metadata_title == "Licensed to YouTube by"
license = contents.try &.["simpleText"]?
end
end
# Music section
music_list = [] of VideoMusic
music_desclist = player_response.dig?(
"engagementPanels", 1, "engagementPanelSectionListRenderer",
"content", "structuredDescriptionContentRenderer", "items", 2,
"videoDescriptionMusicSectionRenderer", "carouselLockups"
)
music_desclist.try &.as_a.each do |music_desc|
artist = nil
album = nil
music_license = nil
# Used when the video has multiple songs
if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title")
# "simpleText" for plain text / "runs" when song has a link
song = song_title["simpleText"]? || song_title.dig?("runs", 0, "text")
# some videos can have empty tracks. See: https://www.youtube.com/watch?v=eBGIQ7ZuuiU
next if !song
end
music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc|
desc_title = extract_text(desc.dig?("infoRowRenderer", "title"))
if desc_title == "ARTIST"
artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "SONG"
song = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "ALBUM"
album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "LICENSES"
music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata"))
end end
end end
music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s)
end
# Author infos # Likes
author = video_details["author"]?.try &.as_s toplevel_buttons = video_primary_renderer
ucid = video_details["channelId"]?.try &.as_s .try &.dig?("videoActions", "menuRenderer", "topLevelButtons")
if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer") if toplevel_buttons
author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url") # New Format as of december 2023
author_verified = has_verified_badge?(author_info["badges"]?) likes_button = toplevel_buttons.dig?(0,
"segmentedLikeDislikeButtonViewModel",
"likeButtonViewModel",
"likeButtonViewModel",
"toggleButtonViewModel",
"toggleButtonViewModel",
"defaultButtonViewModel",
"buttonViewModel"
)
subs_text = author_info["subscriberCountText"]? likes_button ||= toplevel_buttons.try &.as_a
.try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") } .find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE")
.try &.as_s.split(" ", 2)[0] .try &.["toggleButtonRenderer"]
end
# Return data # New format as of september 2022
likes_button ||= toplevel_buttons.try &.as_a
.find(&.["segmentedLikeDislikeButtonRenderer"]?)
.try &.dig?(
"segmentedLikeDislikeButtonRenderer",
"likeButton", "toggleButtonRenderer"
)
if live_now if likes_button
video_type = VideoType::Livestream likes_txt = likes_button.dig?("accessibilityText")
elsif !premiere_timestamp.nil? # Note: The like count from `toggledText` is off by one, as it would
video_type = VideoType::Scheduled # represent the new like count in the event where the user clicks on "like".
published = premiere_timestamp || Time.utc likes_txt ||= (likes_button["defaultText"]? || likes_button["toggledText"]?)
else .try &.dig?("accessibility", "accessibilityData", "label")
video_type = VideoType::Video likes = likes_txt.as_s.gsub(/\D/, "").to_i64? if likes_txt
end
LOGGER.trace("extract_video_info: Found \"likes\" button. Button text is \"#{likes_txt}\"")
LOGGER.debug("extract_video_info: Likes count is #{likes}") if likes
end
end
params = {
"videoType" => JSON::Any.new(video_type.to_s),
# Basic video infos
"title" => JSON::Any.new(title || ""),
"views" => JSON::Any.new(views || 0_i64),
"likes" => JSON::Any.new(likes || 0_i64),
"lengthSeconds" => JSON::Any.new(length_txt || 0_i64),
"published" => JSON::Any.new(published.to_rfc3339),
# Extra video infos
"allowedRegions" => JSON::Any.new(allowed_regions.map { |v| JSON::Any.new(v) }),
"allowRatings" => JSON::Any.new(allow_ratings || false),
"isFamilyFriendly" => JSON::Any.new(family_friendly || false),
"isListed" => JSON::Any.new(is_listed || false),
"isUpcoming" => JSON::Any.new(is_upcoming || false),
"keywords" => JSON::Any.new(keywords.map { |v| JSON::Any.new(v) }),
"isPostLiveDvr" => JSON::Any.new(post_live_dvr),
# Related videos
"relatedVideos" => JSON::Any.new(related),
# Description # Description
"description" => JSON::Any.new(description || ""),
"descriptionHtml" => JSON::Any.new(description_html || "<p></p>"), description = microformat.dig?("description", "simpleText").try &.as_s || ""
"shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), short_description = player_response.dig?("videoDetails", "shortDescription")
# description_html = video_secondary_renderer.try &.dig?("description", "runs")
# .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id)
# Video metadata # Video metadata
"genre" => JSON::Any.new(genre.try &.as_s || ""),
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?), metadata = video_secondary_renderer
"license" => JSON::Any.new(license.try &.as_s || ""), .try &.dig?("metadataRowContainer", "metadataRowContainerRenderer", "rows")
.try &.as_a
genre = microformat["category"]?
genre_ucid = nil
license = nil
metadata.try &.each do |row|
metadata_title = extract_text(row.dig?("metadataRowRenderer", "title"))
contents = row.dig?("metadataRowRenderer", "contents", 0)
if metadata_title == "Category"
contents = contents.try &.dig?("runs", 0)
genre = contents.try &.["text"]?
genre_ucid = contents.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId")
elsif metadata_title == "License"
license = contents.try &.dig?("runs", 0, "text")
elsif metadata_title == "Licensed to YouTube by"
license = contents.try &.["simpleText"]?
end
end
# Music section # Music section
"music" => JSON.parse(music_list.to_json),
music_list = [] of VideoMusic
music_desclist = player_response.dig?(
"engagementPanels", 1, "engagementPanelSectionListRenderer",
"content", "structuredDescriptionContentRenderer", "items", 2,
"videoDescriptionMusicSectionRenderer", "carouselLockups"
)
music_desclist.try &.as_a.each do |music_desc|
artist = nil
album = nil
music_license = nil
# Used when the video has multiple songs
if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title")
# "simpleText" for plain text / "runs" when song has a link
song = song_title["simpleText"]? || song_title.dig?("runs", 0, "text")
# some videos can have empty tracks. See: https://www.youtube.com/watch?v=eBGIQ7ZuuiU
next if !song
end
music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc|
desc_title = extract_text(desc.dig?("infoRowRenderer", "title"))
if desc_title == "ARTIST"
artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "SONG"
song = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "ALBUM"
album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
elsif desc_title == "LICENSES"
music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata"))
end
end
music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s)
end
# Author infos # Author infos
"author" => JSON::Any.new(author || ""),
"ucid" => JSON::Any.new(ucid || ""),
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
"authorVerified" => JSON::Any.new(author_verified || false),
"subCountText" => JSON::Any.new(subs_text || "-"),
}
return params author = video_details["author"]?.try &.as_s
end ucid = video_details["channelId"]?.try &.as_s
private def convert_url(fmt) if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) } author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url")
url = URI.parse(cfr["url"]) author_verified = has_verified_badge?(author_info["badges"]?)
params = url.query_params
LOGGER.debug("convert_url: Decoding '#{cfr}'") subs_text = author_info["subscriberCountText"]?
else .try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
url = URI.parse(fmt["url"].as_s) .try &.as_s.split(" ", 2)[0]
params = url.query_params end
# Return data
if live_now
video_type = VideoType::Livestream
elsif !premiere_timestamp.nil?
video_type = VideoType::Scheduled
published = premiere_timestamp || Time.utc
else
video_type = VideoType::Video
end
params = {
"videoType" => JSON::Any.new(video_type.to_s),
# Basic video infos
"title" => JSON::Any.new(title || ""),
"views" => JSON::Any.new(views || 0_i64),
"likes" => JSON::Any.new(likes || 0_i64),
"lengthSeconds" => JSON::Any.new(length_txt || 0_i64),
"published" => JSON::Any.new(published.to_rfc3339),
# Extra video infos
"allowedRegions" => JSON::Any.new(allowed_regions.map { |v| JSON::Any.new(v) }),
"allowRatings" => JSON::Any.new(allow_ratings || false),
"isFamilyFriendly" => JSON::Any.new(family_friendly || false),
"isListed" => JSON::Any.new(is_listed || false),
"isUpcoming" => JSON::Any.new(is_upcoming || false),
"keywords" => JSON::Any.new(keywords.map { |v| JSON::Any.new(v) }),
"isPostLiveDvr" => JSON::Any.new(post_live_dvr),
# Related videos
"relatedVideos" => JSON::Any.new(related),
# Description
"description" => JSON::Any.new(description || ""),
"descriptionHtml" => JSON::Any.new(description_html || "<p></p>"),
"shortDescription" => JSON::Any.new(short_description.try &.as_s || nil),
# Video metadata
"genre" => JSON::Any.new(genre.try &.as_s || ""),
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?),
"license" => JSON::Any.new(license.try &.as_s || ""),
# Music section
"music" => JSON.parse(music_list.to_json),
# Author infos
"author" => JSON::Any.new(author || ""),
"ucid" => JSON::Any.new(ucid || ""),
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
"authorVerified" => JSON::Any.new(author_verified || false),
"subCountText" => JSON::Any.new(subs_text || "-"),
}
return params
end end
url.query_params = params private def convert_url(fmt)
LOGGER.trace("convert_url: new url is '#{url}'") if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
url = URI.parse(cfr["url"])
params = url.query_params
return url.to_s LOGGER.debug("convert_url: Decoding '#{cfr}'")
rescue ex else
LOGGER.debug("convert_url: Error when parsing video URL") url = URI.parse(fmt["url"].as_s)
LOGGER.trace(ex.inspect_with_backtrace) params = url.query_params
return "" end
url.query_params = params
LOGGER.trace("convert_url: new url is '#{url}'")
return url.to_s
rescue ex
LOGGER.debug("convert_url: Error when parsing video URL")
LOGGER.trace(ex.inspect_with_backtrace)
return ""
end
end end

View File

@@ -1,162 +1,166 @@
struct VideoPreferences module Invidious::Videos
include JSON::Serializable extend self
property annotations : Bool struct VideoPreferences
property preload : Bool include JSON::Serializable
property autoplay : Bool
property comments : Array(String) property annotations : Bool
property continue : Bool property preload : Bool
property continue_autoplay : Bool property autoplay : Bool
property controls : Bool property comments : Array(String)
property listen : Bool property continue : Bool
property local : Bool property continue_autoplay : Bool
property preferred_captions : Array(String) property controls : Bool
property player_style : String property listen : Bool
property quality : String property local : Bool
property quality_dash : String property preferred_captions : Array(String)
property raw : Bool property player_style : String
property region : String? property quality : String
property related_videos : Bool property quality_dash : String
property speed : Float32 | Float64 property raw : Bool
property video_end : Float64 | Int32 property region : String?
property video_loop : Bool property related_videos : Bool
property extend_desc : Bool property speed : Float32 | Float64
property video_start : Float64 | Int32 property video_end : Float64 | Int32
property volume : Int32 property video_loop : Bool
property vr_mode : Bool property extend_desc : Bool
property save_player_pos : Bool property video_start : Float64 | Int32
end property volume : Int32
property vr_mode : Bool
def process_video_params(query, preferences) property save_player_pos : Bool
annotations = query["iv_load_policy"]?.try &.to_i? end
preload = query["preload"]?.try { |q| (q == "true" || q == "1").to_unsafe }
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe } def process_video_params(query, preferences)
comments = query["comments"]?.try &.split(",").map(&.downcase) annotations = query["iv_load_policy"]?.try &.to_i?
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe } preload = query["preload"]?.try { |q| (q == "true" || q == "1").to_unsafe }
continue_autoplay = query["continue_autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe } autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
listen = query["listen"]?.try { |q| (q == "true" || q == "1").to_unsafe } comments = query["comments"]?.try &.split(",").map(&.downcase)
local = query["local"]?.try { |q| (q == "true" || q == "1").to_unsafe } continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
player_style = query["player_style"]? continue_autoplay = query["continue_autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
preferred_captions = query["subtitles"]?.try &.split(",").map(&.downcase) listen = query["listen"]?.try { |q| (q == "true" || q == "1").to_unsafe }
quality = query["quality"]? local = query["local"]?.try { |q| (q == "true" || q == "1").to_unsafe }
quality_dash = query["quality_dash"]? player_style = query["player_style"]?
region = query["region"]? preferred_captions = query["subtitles"]?.try &.split(",").map(&.downcase)
related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe } quality = query["quality"]?
speed = query["speed"]?.try &.rchop("x").to_f? quality_dash = query["quality_dash"]?
video_loop = query["loop"]?.try { |q| (q == "true" || q == "1").to_unsafe } region = query["region"]?
extend_desc = query["extend_desc"]?.try { |q| (q == "true" || q == "1").to_unsafe } related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
volume = query["volume"]?.try &.to_i? speed = query["speed"]?.try &.rchop("x").to_f?
vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe } video_loop = query["loop"]?.try { |q| (q == "true" || q == "1").to_unsafe }
save_player_pos = query["save_player_pos"]?.try { |q| (q == "true" || q == "1").to_unsafe } extend_desc = query["extend_desc"]?.try { |q| (q == "true" || q == "1").to_unsafe }
volume = query["volume"]?.try &.to_i?
if preferences vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe }
# region ||= preferences.region save_player_pos = query["save_player_pos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
annotations ||= preferences.annotations.to_unsafe
preload ||= preferences.preload.to_unsafe if preferences
autoplay ||= preferences.autoplay.to_unsafe # region ||= preferences.region
comments ||= preferences.comments annotations ||= preferences.annotations.to_unsafe
continue ||= preferences.continue.to_unsafe preload ||= preferences.preload.to_unsafe
continue_autoplay ||= preferences.continue_autoplay.to_unsafe autoplay ||= preferences.autoplay.to_unsafe
listen ||= preferences.listen.to_unsafe comments ||= preferences.comments
local ||= preferences.local.to_unsafe continue ||= preferences.continue.to_unsafe
player_style ||= preferences.player_style continue_autoplay ||= preferences.continue_autoplay.to_unsafe
preferred_captions ||= preferences.captions listen ||= preferences.listen.to_unsafe
quality ||= preferences.quality local ||= preferences.local.to_unsafe
quality_dash ||= preferences.quality_dash player_style ||= preferences.player_style
related_videos ||= preferences.related_videos.to_unsafe preferred_captions ||= preferences.captions
speed ||= preferences.speed quality ||= preferences.quality
video_loop ||= preferences.video_loop.to_unsafe quality_dash ||= preferences.quality_dash
extend_desc ||= preferences.extend_desc.to_unsafe related_videos ||= preferences.related_videos.to_unsafe
volume ||= preferences.volume speed ||= preferences.speed
vr_mode ||= preferences.vr_mode.to_unsafe video_loop ||= preferences.video_loop.to_unsafe
save_player_pos ||= preferences.save_player_pos.to_unsafe extend_desc ||= preferences.extend_desc.to_unsafe
end volume ||= preferences.volume
vr_mode ||= preferences.vr_mode.to_unsafe
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe save_player_pos ||= preferences.save_player_pos.to_unsafe
preload ||= CONFIG.default_user_preferences.preload.to_unsafe end
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
comments ||= CONFIG.default_user_preferences.comments annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
continue ||= CONFIG.default_user_preferences.continue.to_unsafe preload ||= CONFIG.default_user_preferences.preload.to_unsafe
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
listen ||= CONFIG.default_user_preferences.listen.to_unsafe comments ||= CONFIG.default_user_preferences.comments
local ||= CONFIG.default_user_preferences.local.to_unsafe continue ||= CONFIG.default_user_preferences.continue.to_unsafe
player_style ||= CONFIG.default_user_preferences.player_style continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
preferred_captions ||= CONFIG.default_user_preferences.captions listen ||= CONFIG.default_user_preferences.listen.to_unsafe
quality ||= CONFIG.default_user_preferences.quality local ||= CONFIG.default_user_preferences.local.to_unsafe
quality_dash ||= CONFIG.default_user_preferences.quality_dash player_style ||= CONFIG.default_user_preferences.player_style
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe preferred_captions ||= CONFIG.default_user_preferences.captions
speed ||= CONFIG.default_user_preferences.speed quality ||= CONFIG.default_user_preferences.quality
video_loop ||= CONFIG.default_user_preferences.video_loop.to_unsafe quality_dash ||= CONFIG.default_user_preferences.quality_dash
extend_desc ||= CONFIG.default_user_preferences.extend_desc.to_unsafe related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
volume ||= CONFIG.default_user_preferences.volume speed ||= CONFIG.default_user_preferences.speed
vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe video_loop ||= CONFIG.default_user_preferences.video_loop.to_unsafe
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe extend_desc ||= CONFIG.default_user_preferences.extend_desc.to_unsafe
volume ||= CONFIG.default_user_preferences.volume
annotations = annotations == 1 vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe
preload = preload == 1 save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe
autoplay = autoplay == 1
continue = continue == 1 annotations = annotations == 1
continue_autoplay = continue_autoplay == 1 preload = preload == 1
listen = listen == 1 autoplay = autoplay == 1
local = local == 1 continue = continue == 1
related_videos = related_videos == 1 continue_autoplay = continue_autoplay == 1
video_loop = video_loop == 1 listen = listen == 1
extend_desc = extend_desc == 1 local = local == 1
vr_mode = vr_mode == 1 related_videos = related_videos == 1
save_player_pos = save_player_pos == 1 video_loop = video_loop == 1
extend_desc = extend_desc == 1
if CONFIG.disabled?("dash") && quality == "dash" vr_mode = vr_mode == 1
quality = "high" save_player_pos = save_player_pos == 1
end
if CONFIG.disabled?("dash") && quality == "dash"
if CONFIG.disabled?("local") && local quality = "high"
local = false end
end
if CONFIG.disabled?("local") && local
if start = query["t"]? || query["time_continue"]? || query["start"]? local = false
video_start = decode_time(start) end
end
video_start ||= 0 if start = query["t"]? || query["time_continue"]? || query["start"]?
video_start = decode_time(start)
if query["end"]? end
video_end = decode_time(query["end"]) video_start ||= 0
end
video_end ||= -1 if query["end"]?
video_end = decode_time(query["end"])
raw = query["raw"]?.try &.to_i? end
raw ||= 0 video_end ||= -1
raw = raw == 1
raw = query["raw"]?.try &.to_i?
controls = query["controls"]?.try &.to_i? raw ||= 0
controls ||= 1 raw = raw == 1
controls = controls >= 1
controls = query["controls"]?.try &.to_i?
params = VideoPreferences.new({ controls ||= 1
annotations: annotations, controls = controls >= 1
preload: preload,
autoplay: autoplay, params = VideoPreferences.new({
comments: comments, annotations: annotations,
continue: continue, preload: preload,
continue_autoplay: continue_autoplay, autoplay: autoplay,
controls: controls, comments: comments,
listen: listen, continue: continue,
local: local, continue_autoplay: continue_autoplay,
player_style: player_style, controls: controls,
preferred_captions: preferred_captions, listen: listen,
quality: quality, local: local,
quality_dash: quality_dash, player_style: player_style,
raw: raw, preferred_captions: preferred_captions,
region: region, quality: quality,
related_videos: related_videos, quality_dash: quality_dash,
speed: speed, raw: raw,
video_end: video_end, region: region,
video_loop: video_loop, related_videos: related_videos,
extend_desc: extend_desc, speed: speed,
video_start: video_start, video_end: video_end,
volume: volume, video_loop: video_loop,
vr_mode: vr_mode, extend_desc: extend_desc,
save_player_pos: save_player_pos, video_start: video_start,
}) volume: volume,
vr_mode: vr_mode,
return params save_player_pos: save_player_pos,
})
return params
end
end end