2022-07-25 06:30:58 +09:00
|
|
|
enum VideoType
|
|
|
|
Video
|
|
|
|
Livestream
|
|
|
|
Scheduled
|
|
|
|
end
|
|
|
|
|
2019-03-30 06:30:02 +09:00
|
|
|
struct Video
|
2020-07-26 23:58:50 +09:00
|
|
|
include DB::Serializable
|
|
|
|
|
|
|
|
property id : String
|
|
|
|
|
|
|
|
@[DB::Field(converter: Video::JSONConverter)]
|
|
|
|
property info : Hash(String, JSON::Any)
|
|
|
|
property updated : Time
|
|
|
|
|
|
|
|
@[DB::Field(ignore: true)]
|
2022-05-24 05:37:58 +09:00
|
|
|
@captions = [] of Invidious::Videos::Caption
|
2020-07-26 23:58:50 +09:00
|
|
|
|
|
|
|
@[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?
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
module JSONConverter
|
2018-08-05 05:30:44 +09:00
|
|
|
def self.from_rs(rs)
|
2020-06-16 07:33:23 +09:00
|
|
|
JSON.parse(rs.read(String)).as_h
|
2018-08-05 05:30:44 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-11-09 07:52:55 +09:00
|
|
|
def to_json(locale : String?, json : JSON::Builder)
|
2019-06-09 03:31:41 +09:00
|
|
|
json.object do
|
2022-07-25 06:30:58 +09:00
|
|
|
json.field "type", self.video_type
|
2019-06-09 03:31:41 +09:00
|
|
|
|
|
|
|
json.field "title", self.title
|
|
|
|
json.field "videoId", self.id
|
2020-06-16 07:10:30 +09:00
|
|
|
|
|
|
|
json.field "error", info["reason"] if info["reason"]?
|
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "videoThumbnails" do
|
2020-06-16 07:10:30 +09:00
|
|
|
generate_thumbnails(json, self.id)
|
2019-06-09 03:31:41 +09:00
|
|
|
end
|
|
|
|
json.field "storyboards" do
|
2020-06-16 07:10:30 +09:00
|
|
|
generate_storyboards(json, self.id, self.storyboards)
|
2019-06-09 03:31:41 +09:00
|
|
|
end
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
json.field "description", self.description
|
2019-06-09 05:08:27 +09:00
|
|
|
json.field "descriptionHtml", self.description_html
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "published", self.published.to_unix
|
|
|
|
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
|
|
|
json.field "keywords", self.keywords
|
|
|
|
|
|
|
|
json.field "viewCount", self.views
|
|
|
|
json.field "likeCount", self.likes
|
2022-06-21 08:01:25 +09:00
|
|
|
json.field "dislikeCount", 0_i64
|
2019-06-09 03:31:41 +09:00
|
|
|
|
2021-08-15 17:38:30 +09:00
|
|
|
json.field "paid", self.paid
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "premium", self.premium
|
|
|
|
json.field "isFamilyFriendly", self.is_family_friendly
|
|
|
|
json.field "allowedRegions", self.allowed_regions
|
|
|
|
json.field "genre", self.genre
|
|
|
|
json.field "genreUrl", self.genre_url
|
|
|
|
|
|
|
|
json.field "author", self.author
|
|
|
|
json.field "authorId", self.ucid
|
|
|
|
json.field "authorUrl", "/channel/#{self.ucid}"
|
|
|
|
|
|
|
|
json.field "authorThumbnails" do
|
|
|
|
json.array do
|
|
|
|
qualities = {32, 48, 76, 100, 176, 512}
|
|
|
|
|
|
|
|
qualities.each do |quality|
|
|
|
|
json.object do
|
2019-08-01 09:16:09 +09:00
|
|
|
json.field "url", self.author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "width", quality
|
|
|
|
json.field "height", quality
|
2019-04-11 07:58:42 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2019-06-09 03:31:41 +09:00
|
|
|
end
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "subCountText", self.sub_count_text
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2019-07-30 09:41:45 +09:00
|
|
|
json.field "lengthSeconds", self.length_seconds
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "allowRatings", self.allow_ratings
|
2022-06-21 08:01:25 +09:00
|
|
|
json.field "rating", 0_i64
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "isListed", self.is_listed
|
|
|
|
json.field "liveNow", self.live_now
|
|
|
|
json.field "isUpcoming", self.is_upcoming
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
if self.premiere_timestamp
|
2020-06-16 07:33:23 +09:00
|
|
|
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
|
2019-06-09 03:31:41 +09:00
|
|
|
end
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2020-06-16 07:10:30 +09:00
|
|
|
if hlsvp = self.hls_manifest_url
|
|
|
|
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", HOST_URL)
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "hlsUrl", hlsvp
|
|
|
|
end
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2020-06-16 07:10:30 +09:00
|
|
|
json.field "dashUrl", "#{HOST_URL}/api/manifest/dash/id/#{id}"
|
2019-06-09 03:31:41 +09:00
|
|
|
|
|
|
|
json.field "adaptiveFormats" do
|
|
|
|
json.array do
|
2020-06-16 07:33:23 +09:00
|
|
|
self.adaptive_fmts.each do |fmt|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.object do
|
2022-04-28 04:44:31 +09:00
|
|
|
# Only available on regular videos, not livestreams/OTF streams
|
2022-04-27 07:20:48 +09:00
|
|
|
if init_range = fmt["initRange"]?
|
|
|
|
json.field "init", "#{init_range["start"]}-#{init_range["end"]}"
|
|
|
|
end
|
|
|
|
if index_range = fmt["indexRange"]?
|
|
|
|
json.field "index", "#{index_range["start"]}-#{index_range["end"]}"
|
|
|
|
end
|
|
|
|
|
|
|
|
# Not available on MPEG-4 Timed Text (`text/mp4`) streams (livestreams only)
|
|
|
|
json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]?
|
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "url", fmt["url"]
|
2020-06-16 07:33:23 +09:00
|
|
|
json.field "itag", fmt["itag"].as_i.to_s
|
|
|
|
json.field "type", fmt["mimeType"]
|
2022-04-27 07:20:48 +09:00
|
|
|
json.field "clen", fmt["contentLength"]? || "-1"
|
2020-06-16 07:33:23 +09:00
|
|
|
json.field "lmt", fmt["lastModified"]
|
|
|
|
json.field "projectionType", fmt["projectionType"]
|
2019-06-09 03:31:41 +09:00
|
|
|
|
2022-05-24 04:54:48 +09:00
|
|
|
if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"])
|
2020-06-16 07:33:23 +09:00
|
|
|
fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "fps", fps
|
|
|
|
json.field "container", fmt_info["ext"]
|
|
|
|
json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"]
|
|
|
|
|
|
|
|
if fmt_info["height"]?
|
|
|
|
json.field "resolution", "#{fmt_info["height"]}p"
|
|
|
|
|
|
|
|
quality_label = "#{fmt_info["height"]}p"
|
|
|
|
if fps > 30
|
|
|
|
quality_label += "60"
|
|
|
|
end
|
|
|
|
json.field "qualityLabel", quality_label
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
if fmt_info["width"]?
|
|
|
|
json.field "size", "#{fmt_info["width"]}x#{fmt_info["height"]}"
|
2019-04-11 07:58:42 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2022-04-27 07:21:23 +09:00
|
|
|
|
2022-05-02 00:00:56 +09:00
|
|
|
# Livestream chunk infos
|
|
|
|
json.field "targetDurationSec", fmt["targetDurationSec"].as_i if fmt.has_key?("targetDurationSec")
|
|
|
|
json.field "maxDvrDurationSec", fmt["maxDvrDurationSec"].as_i if fmt.has_key?("maxDvrDurationSec")
|
|
|
|
|
2022-04-27 07:21:23 +09:00
|
|
|
# Audio-related data
|
|
|
|
json.field "audioQuality", fmt["audioQuality"] if fmt.has_key?("audioQuality")
|
|
|
|
json.field "audioSampleRate", fmt["audioSampleRate"].as_s.to_i if fmt.has_key?("audioSampleRate")
|
|
|
|
json.field "audioChannels", fmt["audioChannels"] if fmt.has_key?("audioChannels")
|
|
|
|
|
|
|
|
# Extra misc stuff
|
|
|
|
json.field "colorInfo", fmt["colorInfo"] if fmt.has_key?("colorInfo")
|
|
|
|
json.field "captionTrack", fmt["captionTrack"] if fmt.has_key?("captionTrack")
|
2019-04-11 07:58:42 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2019-06-09 03:31:41 +09:00
|
|
|
end
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "formatStreams" do
|
|
|
|
json.array do
|
2020-06-16 07:33:23 +09:00
|
|
|
self.fmt_stream.each do |fmt|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.object do
|
|
|
|
json.field "url", fmt["url"]
|
2020-06-16 07:33:23 +09:00
|
|
|
json.field "itag", fmt["itag"].as_i.to_s
|
|
|
|
json.field "type", fmt["mimeType"]
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "quality", fmt["quality"]
|
|
|
|
|
2022-05-24 04:54:48 +09:00
|
|
|
fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"])
|
2019-06-09 03:31:41 +09:00
|
|
|
if fmt_info
|
2020-06-16 07:33:23 +09:00
|
|
|
fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "fps", fps
|
|
|
|
json.field "container", fmt_info["ext"]
|
|
|
|
json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"]
|
|
|
|
|
|
|
|
if fmt_info["height"]?
|
|
|
|
json.field "resolution", "#{fmt_info["height"]}p"
|
|
|
|
|
|
|
|
quality_label = "#{fmt_info["height"]}p"
|
|
|
|
if fps > 30
|
|
|
|
quality_label += "60"
|
|
|
|
end
|
|
|
|
json.field "qualityLabel", quality_label
|
|
|
|
|
|
|
|
if fmt_info["width"]?
|
|
|
|
json.field "size", "#{fmt_info["width"]}x#{fmt_info["height"]}"
|
2019-04-11 07:58:42 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2019-06-09 03:31:41 +09:00
|
|
|
end
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "captions" do
|
|
|
|
json.array do
|
|
|
|
self.captions.each do |caption|
|
|
|
|
json.object do
|
2021-06-27 23:18:16 +09:00
|
|
|
json.field "label", caption.name
|
2021-09-25 11:15:23 +09:00
|
|
|
json.field "language_code", caption.language_code
|
2021-06-27 23:18:16 +09:00
|
|
|
json.field "url", "/api/v1/captions/#{id}?label=#{URI.encode_www_form(caption.name)}"
|
2019-04-11 07:58:42 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2019-06-09 03:31:41 +09:00
|
|
|
end
|
2019-04-11 07:58:42 +09:00
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "recommendedVideos" do
|
|
|
|
json.array do
|
2020-06-16 07:33:23 +09:00
|
|
|
self.related_videos.each do |rv|
|
2019-06-09 03:31:41 +09:00
|
|
|
if rv["id"]?
|
|
|
|
json.object do
|
|
|
|
json.field "videoId", rv["id"]
|
|
|
|
json.field "title", rv["title"]
|
|
|
|
json.field "videoThumbnails" do
|
2020-06-16 07:10:30 +09:00
|
|
|
generate_thumbnails(json, rv["id"])
|
2019-04-11 07:58:42 +09:00
|
|
|
end
|
2019-08-27 22:00:04 +09:00
|
|
|
|
2019-06-09 03:31:41 +09:00
|
|
|
json.field "author", rv["author"]
|
2022-02-03 09:44:11 +09:00
|
|
|
json.field "authorUrl", "/channel/#{rv["ucid"]?}"
|
2019-08-31 12:57:33 +09:00
|
|
|
json.field "authorId", rv["ucid"]?
|
2019-08-27 22:00:04 +09:00
|
|
|
if rv["author_thumbnail"]?
|
|
|
|
json.field "authorThumbnails" do
|
|
|
|
json.array do
|
|
|
|
qualities = {32, 48, 76, 100, 176, 512}
|
|
|
|
|
|
|
|
qualities.each do |quality|
|
|
|
|
json.object do
|
2022-02-03 11:55:43 +09:00
|
|
|
json.field "url", rv["author_thumbnail"].gsub(/s\d+-/, "s#{quality}-")
|
2019-08-27 22:00:04 +09:00
|
|
|
json.field "width", quality
|
|
|
|
json.field "height", quality
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
|
2022-02-03 09:44:11 +09:00
|
|
|
json.field "viewCountText", rv["short_view_count"]?
|
2020-06-16 07:33:23 +09:00
|
|
|
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
|
2019-04-11 07:58:42 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-10-29 21:53:06 +09:00
|
|
|
# TODO: remove the locale and follow the crystal convention
|
2021-11-09 07:52:55 +09:00
|
|
|
def to_json(locale : String?, _json : Nil)
|
2021-10-29 21:53:06 +09:00
|
|
|
JSON.build { |json| to_json(locale, json) }
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_json(json : JSON::Builder | Nil = nil)
|
|
|
|
to_json(nil, json)
|
2019-06-09 03:31:41 +09:00
|
|
|
end
|
|
|
|
|
2022-07-25 06:30:58 +09:00
|
|
|
def video_type : VideoType
|
|
|
|
video_type = info["videoType"]?.try &.as_s || "video"
|
|
|
|
return VideoType.parse?(video_type) || VideoType::Video
|
2019-03-23 00:32:42 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def published : Time
|
2022-07-25 06:30:58 +09:00
|
|
|
return info["published"]?
|
2022-01-21 06:22:48 +09:00
|
|
|
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
|
2020-06-16 07:33:23 +09:00
|
|
|
end
|
2019-03-23 00:32:42 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def published=(other : Time)
|
2022-07-25 06:30:58 +09:00
|
|
|
info["published"] = JSON::Any.new(other.to_s("%Y-%m-%d"))
|
2020-06-16 07:33:23 +09:00
|
|
|
end
|
2019-03-23 01:06:58 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def live_now
|
2022-07-25 06:30:58 +09:00
|
|
|
return (self.video_type == VideoType::Livestream)
|
2020-06-16 07:33:23 +09:00
|
|
|
end
|
2019-03-23 02:24:47 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def premiere_timestamp : Time?
|
2022-01-21 06:22:48 +09:00
|
|
|
info
|
|
|
|
.dig?("microformat", "playerMicroformatRenderer", "liveBroadcastDetails", "startTimestamp")
|
|
|
|
.try { |t| Time.parse_rfc3339(t.as_s) }
|
2019-03-23 01:06:58 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def related_videos
|
|
|
|
info["relatedVideos"]?.try &.as_a.map { |h| h.as_h.transform_values &.as_s } || [] of Hash(String, String)
|
|
|
|
end
|
2019-02-26 08:28:35 +09:00
|
|
|
|
2022-07-25 06:30:58 +09:00
|
|
|
# Methods for parsing streaming data
|
2019-02-26 08:28:35 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def fmt_stream
|
|
|
|
return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream
|
2020-07-26 23:58:50 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
fmt_stream = info["streamingData"]?.try &.["formats"]?.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
|
|
|
|
fmt_stream.each do |fmt|
|
|
|
|
if s = (fmt["cipher"]? || fmt["signatureCipher"]?).try { |h| HTTP::Params.parse(h.as_s) }
|
|
|
|
s.each do |k, v|
|
|
|
|
fmt[k] = JSON::Any.new(v)
|
2019-02-26 08:28:35 +09:00
|
|
|
end
|
2020-09-28 02:19:44 +09:00
|
|
|
fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}")
|
2018-08-05 13:07:38 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}")
|
|
|
|
fmt["url"] = JSON::Any.new("#{fmt["url"]}®ion=#{self.info["region"]}") if self.info["region"]?
|
2018-08-05 13:07:38 +09:00
|
|
|
end
|
2022-04-27 07:20:48 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
|
|
|
@fmt_stream = fmt_stream
|
|
|
|
return @fmt_stream.as(Array(Hash(String, JSON::Any)))
|
2018-08-05 13:07:38 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def adaptive_fmts
|
|
|
|
return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) if @adaptive_fmts
|
|
|
|
fmt_stream = info["streamingData"]?.try &.["adaptiveFormats"]?.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
|
|
|
|
fmt_stream.each do |fmt|
|
|
|
|
if s = (fmt["cipher"]? || fmt["signatureCipher"]?).try { |h| HTTP::Params.parse(h.as_s) }
|
|
|
|
s.each do |k, v|
|
|
|
|
fmt[k] = JSON::Any.new(v)
|
2018-09-13 12:31:47 +09:00
|
|
|
end
|
2020-09-28 02:19:44 +09:00
|
|
|
fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}")
|
2018-09-13 12:31:47 +09:00
|
|
|
end
|
2018-08-05 13:07:38 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}")
|
|
|
|
fmt["url"] = JSON::Any.new("#{fmt["url"]}®ion=#{self.info["region"]}") if self.info["region"]?
|
2018-10-02 09:01:44 +09:00
|
|
|
end
|
2022-04-27 07:20:48 +09:00
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
|
|
|
@adaptive_fmts = fmt_stream
|
|
|
|
return @adaptive_fmts.as(Array(Hash(String, JSON::Any)))
|
2018-08-05 13:07:38 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def video_streams
|
|
|
|
adaptive_fmts.select &.["mimeType"]?.try &.as_s.starts_with?("video")
|
2018-08-05 13:07:38 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def audio_streams
|
|
|
|
adaptive_fmts.select &.["mimeType"]?.try &.as_s.starts_with?("audio")
|
2018-08-19 01:47:16 +09:00
|
|
|
end
|
|
|
|
|
2022-07-25 06:30:58 +09:00
|
|
|
# Misc. methods
|
|
|
|
|
2019-04-12 07:00:00 +09:00
|
|
|
def storyboards
|
2022-01-21 06:22:48 +09:00
|
|
|
storyboards = info.dig?("storyboards", "playerStoryboardSpecRenderer", "spec")
|
|
|
|
.try &.as_s.split("|")
|
2019-04-12 07:00:00 +09:00
|
|
|
|
|
|
|
if !storyboards
|
2022-01-21 06:22:48 +09:00
|
|
|
if storyboard = info.dig?("storyboards", "playerLiveStoryboardSpecRenderer", "spec").try &.as_s
|
2019-04-12 07:00:00 +09:00
|
|
|
return [{
|
2019-04-19 06:23:50 +09:00
|
|
|
url: storyboard.split("#")[0],
|
|
|
|
width: 106,
|
|
|
|
height: 60,
|
|
|
|
count: -1,
|
|
|
|
interval: 5000,
|
|
|
|
storyboard_width: 3,
|
|
|
|
storyboard_height: 3,
|
|
|
|
storyboard_count: -1,
|
|
|
|
}]
|
2019-04-12 07:00:00 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
items = [] of NamedTuple(
|
|
|
|
url: String,
|
|
|
|
width: Int32,
|
|
|
|
height: Int32,
|
|
|
|
count: Int32,
|
|
|
|
interval: Int32,
|
|
|
|
storyboard_width: Int32,
|
|
|
|
storyboard_height: Int32,
|
|
|
|
storyboard_count: Int32)
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
return items if !storyboards
|
2019-04-12 07:00:00 +09:00
|
|
|
|
2019-05-27 08:55:22 +09:00
|
|
|
url = URI.parse(storyboards.shift)
|
|
|
|
params = HTTP::Params.parse(url.query || "")
|
2019-04-12 07:00:00 +09:00
|
|
|
|
2022-01-21 01:17:22 +09:00
|
|
|
storyboards.each_with_index do |sb, i|
|
|
|
|
width, height, count, storyboard_width, storyboard_height, interval, _, sigh = sb.split("#")
|
2019-05-27 08:55:22 +09:00
|
|
|
params["sigh"] = sigh
|
|
|
|
url.query = params.to_s
|
2019-04-12 07:00:00 +09:00
|
|
|
|
|
|
|
width = width.to_i
|
|
|
|
height = height.to_i
|
|
|
|
count = count.to_i
|
|
|
|
interval = interval.to_i
|
|
|
|
storyboard_width = storyboard_width.to_i
|
|
|
|
storyboard_height = storyboard_height.to_i
|
2019-10-04 23:23:02 +09:00
|
|
|
storyboard_count = (count / (storyboard_width * storyboard_height)).ceil.to_i
|
2019-04-12 07:00:00 +09:00
|
|
|
|
|
|
|
items << {
|
2019-05-27 08:55:22 +09:00
|
|
|
url: url.to_s.sub("$L", i).sub("$N", "M$M"),
|
2019-04-12 07:00:00 +09:00
|
|
|
width: width,
|
|
|
|
height: height,
|
|
|
|
count: count,
|
|
|
|
interval: interval,
|
|
|
|
storyboard_width: storyboard_width,
|
|
|
|
storyboard_height: storyboard_height,
|
2019-10-04 23:23:02 +09:00
|
|
|
storyboard_count: storyboard_count,
|
2019-04-12 07:00:00 +09:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
items
|
|
|
|
end
|
|
|
|
|
2021-08-15 17:38:30 +09:00
|
|
|
def paid
|
2022-07-25 06:30:58 +09:00
|
|
|
return (self.reason || "").includes? "requires payment"
|
2021-08-15 17:38:30 +09:00
|
|
|
end
|
|
|
|
|
2018-10-17 01:15:14 +09:00
|
|
|
def premium
|
2020-06-16 07:33:23 +09:00
|
|
|
keywords.includes? "YouTube Red"
|
|
|
|
end
|
|
|
|
|
2022-05-24 05:37:58 +09:00
|
|
|
def captions : Array(Invidious::Videos::Caption)
|
|
|
|
if @captions.empty? && @info.has_key?("captions")
|
|
|
|
@captions = Invidious::Videos::Caption.from_yt_json(info["captions"])
|
2019-08-05 10:56:24 +09:00
|
|
|
end
|
2022-05-24 05:37:58 +09:00
|
|
|
|
|
|
|
return @captions
|
2018-10-17 01:15:14 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def hls_manifest_url : String?
|
2022-01-21 06:22:48 +09:00
|
|
|
info.dig?("streamingData", "hlsManifestUrl").try &.as_s
|
2020-06-16 07:33:23 +09:00
|
|
|
end
|
|
|
|
|
|
|
|
def dash_manifest_url
|
2022-01-21 06:22:48 +09:00
|
|
|
info.dig?("streamingData", "dashManifestUrl").try &.as_s
|
2020-06-16 07:33:23 +09:00
|
|
|
end
|
|
|
|
|
2020-06-17 07:51:49 +09:00
|
|
|
def genre_url : String?
|
|
|
|
info["genreUcid"]? ? "/channel/#{info["genreUcid"]}" : nil
|
2020-06-16 07:33:23 +09:00
|
|
|
end
|
|
|
|
|
2021-08-13 04:26:50 +09:00
|
|
|
def is_vr : Bool?
|
2022-07-25 06:30:58 +09:00
|
|
|
return {"EQUIRECTANGULAR", "MESH"}.includes? self.projection_type
|
2021-09-10 16:42:39 +09:00
|
|
|
end
|
|
|
|
|
|
|
|
def projection_type : String?
|
|
|
|
return info.dig?("streamingData", "adaptiveFormats", 0, "projectionType").try &.as_s
|
2021-04-11 22:09:10 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
def reason : String?
|
|
|
|
info["reason"]?.try &.as_s
|
|
|
|
end
|
2022-07-25 06:30:58 +09:00
|
|
|
|
|
|
|
# Macros defining getters/setters for various types of data
|
|
|
|
|
|
|
|
private macro getset_string(name)
|
|
|
|
# Return {{name.stringify}} from `info`
|
|
|
|
def {{name.id.underscore}} : String
|
|
|
|
return info[{{name.stringify}}]?.try &.as_s || ""
|
|
|
|
end
|
|
|
|
|
|
|
|
# Update {{name.stringify}} into `info`
|
|
|
|
def {{name.id.underscore}}=(value : String)
|
|
|
|
info[{{name.stringify}}] = JSON::Any.new(value)
|
|
|
|
end
|
|
|
|
|
|
|
|
{% if flag?(:debug_macros) %} {{debug}} {% end %}
|
|
|
|
end
|
|
|
|
|
|
|
|
private macro getset_string_array(name)
|
|
|
|
# Return {{name.stringify}} from `info`
|
|
|
|
def {{name.id.underscore}} : Array(String)
|
|
|
|
return info[{{name.stringify}}]?.try &.as_a.map &.as_s || [] of String
|
|
|
|
end
|
|
|
|
|
|
|
|
# Update {{name.stringify}} into `info`
|
|
|
|
def {{name.id.underscore}}=(value : Array(String))
|
|
|
|
info[{{name.stringify}}] = JSON::Any.new(value)
|
|
|
|
end
|
|
|
|
|
|
|
|
{% if flag?(:debug_macros) %} {{debug}} {% end %}
|
|
|
|
end
|
|
|
|
|
|
|
|
{% for op, type in {i32: Int32, i64: Int64} %}
|
|
|
|
private macro getset_{{op}}(name)
|
|
|
|
def \{{name.id.underscore}} : {{type}}
|
|
|
|
return info[\{{name.stringify}}]?.try &.as_i.to_{{op}} || 0_{{op}}
|
|
|
|
end
|
|
|
|
|
|
|
|
def \{{name.id.underscore}}=(value : Int)
|
|
|
|
info[\{{name.stringify}}] = JSON::Any.new(value.to_i64)
|
|
|
|
end
|
|
|
|
|
|
|
|
\{% if flag?(:debug_macros) %} \{{debug}} \{% end %}
|
|
|
|
end
|
|
|
|
{% end %}
|
|
|
|
|
|
|
|
private macro getset_bool(name)
|
|
|
|
# Return {{name.stringify}} from `info`
|
|
|
|
def {{name.id.underscore}} : Bool
|
|
|
|
return info[{{name.stringify}}]?.try &.as_bool || false
|
|
|
|
end
|
|
|
|
|
|
|
|
# Update {{name.stringify}} into `info`
|
|
|
|
def {{name.id.underscore}}=(value : Bool)
|
|
|
|
info[{{name.stringify}}] = JSON::Any.new(value)
|
|
|
|
end
|
|
|
|
|
|
|
|
{% if flag?(:debug_macros) %} {{debug}} {% end %}
|
|
|
|
end
|
|
|
|
|
|
|
|
# Method definitions, using the macros above
|
|
|
|
|
|
|
|
getset_string author
|
|
|
|
getset_string authorThumbnail
|
|
|
|
getset_string description
|
|
|
|
getset_string descriptionHtml
|
|
|
|
getset_string genre
|
|
|
|
getset_string genreUcid
|
|
|
|
getset_string license
|
|
|
|
getset_string shortDescription
|
|
|
|
getset_string subCountText
|
|
|
|
getset_string title
|
|
|
|
getset_string ucid
|
|
|
|
|
|
|
|
getset_string_array allowedRegions
|
|
|
|
getset_string_array keywords
|
|
|
|
|
|
|
|
getset_i32 lengthSeconds
|
|
|
|
getset_i64 likes
|
|
|
|
getset_i64 views
|
|
|
|
|
|
|
|
getset_bool allowRatings
|
|
|
|
getset_bool authorVerified
|
|
|
|
getset_bool isFamilyFriendly
|
|
|
|
getset_bool isListed
|
|
|
|
getset_bool isUpcoming
|
2020-07-26 23:58:50 +09:00
|
|
|
end
|
2018-10-21 10:37:55 +09:00
|
|
|
|
2018-10-07 12:22:22 +09:00
|
|
|
class VideoRedirect < Exception
|
2019-09-09 01:08:59 +09:00
|
|
|
property video_id : String
|
|
|
|
|
|
|
|
def initialize(@video_id)
|
|
|
|
end
|
2018-10-07 12:22:22 +09:00
|
|
|
end
|
|
|
|
|
2021-12-07 10:55:43 +09:00
|
|
|
def get_video(id, refresh = true, region = nil, force_refresh = false)
|
2021-11-27 03:36:31 +09:00
|
|
|
if (video = Invidious::Database::Videos.select(id)) && !region
|
2020-06-16 07:33:23 +09:00
|
|
|
# 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
|
|
|
|
begin
|
|
|
|
video = fetch_video(id, region)
|
2021-11-27 03:36:31 +09:00
|
|
|
Invidious::Database::Videos.update(video)
|
2020-06-16 07:33:23 +09:00
|
|
|
rescue ex
|
2021-11-27 03:36:31 +09:00
|
|
|
Invidious::Database::Videos.delete(id)
|
2020-06-16 07:33:23 +09:00
|
|
|
raise ex
|
|
|
|
end
|
2019-02-07 07:12:11 +09:00
|
|
|
end
|
|
|
|
else
|
2020-06-16 07:33:23 +09:00
|
|
|
video = fetch_video(id, region)
|
2021-11-27 03:36:31 +09:00
|
|
|
Invidious::Database::Videos.insert(video) if !region
|
2018-08-05 05:30:44 +09:00
|
|
|
end
|
|
|
|
|
2020-06-16 07:33:23 +09:00
|
|
|
return video
|
2022-04-09 05:52:34 +09:00
|
|
|
rescue DB::Error
|
|
|
|
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
|
|
|
|
# Note: All DB errors inherit from `DB::Error`
|
|
|
|
return fetch_video(id, region)
|
2019-02-07 07:12:11 +09:00
|
|
|
end
|
|
|
|
|
2019-06-29 11:17:56 +09:00
|
|
|
def fetch_video(id, region)
|
2021-08-14 05:29:43 +09:00
|
|
|
info = extract_video_info(video_id: id)
|
2018-10-07 12:22:22 +09:00
|
|
|
|
2021-08-14 05:29:43 +09:00
|
|
|
allowed_regions = info
|
|
|
|
.dig?("microformat", "playerMicroformatRenderer", "availableCountries")
|
|
|
|
.try &.as_a.map &.as_s || [] of String
|
2019-08-14 05:21:00 +09:00
|
|
|
|
|
|
|
# Check for region-blocks
|
2020-06-16 07:33:23 +09:00
|
|
|
if info["reason"]?.try &.as_s.includes?("your country")
|
2019-08-19 23:00:37 +09:00
|
|
|
bypass_regions = PROXY_LIST.keys & allowed_regions
|
|
|
|
if !bypass_regions.empty?
|
|
|
|
region = bypass_regions[rand(bypass_regions.size)]
|
2021-08-14 05:29:43 +09:00
|
|
|
region_info = extract_video_info(video_id: id, proxy_region: region)
|
2020-06-16 07:33:23 +09:00
|
|
|
region_info["region"] = JSON::Any.new(region) if region
|
|
|
|
info = region_info if !region_info["reason"]?
|
2019-08-19 23:00:37 +09:00
|
|
|
end
|
2018-08-13 23:17:28 +09:00
|
|
|
end
|
|
|
|
|
2021-08-17 02:41:16 +09:00
|
|
|
# Try to fetch video info using an embedded client
|
2018-08-05 05:30:44 +09:00
|
|
|
if info["reason"]?
|
2021-08-17 02:41:16 +09:00
|
|
|
embed_info = extract_video_info(video_id: id, context_screen: "embed")
|
|
|
|
info = embed_info if !embed_info["reason"]?
|
2018-09-24 02:13:08 +09:00
|
|
|
end
|
2018-09-10 04:47:26 +09:00
|
|
|
|
2022-02-11 14:43:14 +09:00
|
|
|
if reason = info["reason"]?
|
2022-05-27 22:36:13 +09:00
|
|
|
if reason == "Video unavailable"
|
|
|
|
raise NotFoundException.new(reason.as_s || "")
|
|
|
|
else
|
|
|
|
raise InfoException.new(reason.as_s || "")
|
|
|
|
end
|
2022-02-11 14:43:14 +09:00
|
|
|
end
|
2018-08-05 05:30:44 +09:00
|
|
|
|
2020-07-26 23:58:50 +09:00
|
|
|
video = Video.new({
|
|
|
|
id: id,
|
|
|
|
info: info,
|
|
|
|
updated: Time.utc,
|
|
|
|
})
|
|
|
|
|
2018-08-05 05:30:44 +09:00
|
|
|
return video
|
|
|
|
end
|
|
|
|
|
2021-12-07 10:55:43 +09:00
|
|
|
def process_continuation(query, plid, id)
|
2019-08-06 08:49:13 +09:00
|
|
|
continuation = nil
|
|
|
|
if plid
|
|
|
|
if index = query["index"]?.try &.to_i?
|
|
|
|
continuation = index
|
|
|
|
else
|
|
|
|
continuation = id
|
|
|
|
end
|
|
|
|
continuation ||= 0
|
|
|
|
end
|
|
|
|
|
|
|
|
continuation
|
|
|
|
end
|
|
|
|
|
2020-06-16 07:10:30 +09:00
|
|
|
def build_thumbnails(id)
|
2019-03-09 05:42:37 +09:00
|
|
|
return {
|
2021-04-01 09:23:59 +09:00
|
|
|
{host: HOST_URL, height: 720, width: 1280, name: "maxres", url: "maxres"},
|
|
|
|
{host: HOST_URL, height: 720, width: 1280, name: "maxresdefault", url: "maxresdefault"},
|
|
|
|
{host: HOST_URL, height: 480, width: 640, name: "sddefault", url: "sddefault"},
|
|
|
|
{host: HOST_URL, height: 360, width: 480, name: "high", url: "hqdefault"},
|
|
|
|
{host: HOST_URL, height: 180, width: 320, name: "medium", url: "mqdefault"},
|
|
|
|
{host: HOST_URL, height: 90, width: 120, name: "default", url: "default"},
|
|
|
|
{host: HOST_URL, height: 90, width: 120, name: "start", url: "1"},
|
|
|
|
{host: HOST_URL, height: 90, width: 120, name: "middle", url: "2"},
|
|
|
|
{host: HOST_URL, height: 90, width: 120, name: "end", url: "3"},
|
2019-03-09 05:42:37 +09:00
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2020-06-16 07:10:30 +09:00
|
|
|
def generate_thumbnails(json, id)
|
2018-08-10 22:50:25 +09:00
|
|
|
json.array do
|
2020-06-16 07:10:30 +09:00
|
|
|
build_thumbnails(id).each do |thumbnail|
|
2018-08-10 22:50:25 +09:00
|
|
|
json.object do
|
2018-08-12 23:46:47 +09:00
|
|
|
json.field "quality", thumbnail[:name]
|
2019-03-09 05:42:37 +09:00
|
|
|
json.field "url", "#{thumbnail[:host]}/vi/#{id}/#{thumbnail["url"]}.jpg"
|
2018-08-12 23:46:47 +09:00
|
|
|
json.field "width", thumbnail[:width]
|
|
|
|
json.field "height", thumbnail[:height]
|
2018-08-10 22:50:25 +09:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2019-04-12 07:00:00 +09:00
|
|
|
|
2020-06-16 07:10:30 +09:00
|
|
|
def generate_storyboards(json, id, storyboards)
|
2019-04-12 07:00:00 +09:00
|
|
|
json.array do
|
|
|
|
storyboards.each do |storyboard|
|
|
|
|
json.object do
|
2019-05-03 04:20:19 +09:00
|
|
|
json.field "url", "/api/v1/storyboards/#{id}?width=#{storyboard[:width]}&height=#{storyboard[:height]}"
|
|
|
|
json.field "templateUrl", storyboard[:url]
|
2019-04-12 07:00:00 +09:00
|
|
|
json.field "width", storyboard[:width]
|
|
|
|
json.field "height", storyboard[:height]
|
|
|
|
json.field "count", storyboard[:count]
|
|
|
|
json.field "interval", storyboard[:interval]
|
|
|
|
json.field "storyboardWidth", storyboard[:storyboard_width]
|
|
|
|
json.field "storyboardHeight", storyboard[:storyboard_height]
|
|
|
|
json.field "storyboardCount", storyboard[:storyboard_count]
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|