invidious-mod/src/invidious/videos.cr

1191 行
45 KiB
Crystal
Raw 通常表示 履歴

2018-08-13 10:04:13 +09:00
CAPTION_LANGUAGES = {
2018-08-07 03:23:36 +09:00
"",
"English",
"English (auto-generated)",
"Afrikaans",
"Albanian",
"Amharic",
"Arabic",
"Armenian",
"Azerbaijani",
"Bangla",
"Basque",
"Belarusian",
"Bosnian",
"Bulgarian",
"Burmese",
"Catalan",
"Cebuano",
"Chinese (Simplified)",
"Chinese (Traditional)",
"Corsican",
"Croatian",
"Czech",
"Danish",
"Dutch",
"Esperanto",
"Estonian",
"Filipino",
"Finnish",
"French",
"Galician",
"Georgian",
"German",
"Greek",
"Gujarati",
"Haitian Creole",
"Hausa",
"Hawaiian",
"Hebrew",
"Hindi",
"Hmong",
"Hungarian",
"Icelandic",
"Igbo",
"Indonesian",
"Irish",
"Italian",
"Japanese",
"Javanese",
"Kannada",
"Kazakh",
"Khmer",
"Korean",
"Kurdish",
"Kyrgyz",
"Lao",
"Latin",
"Latvian",
"Lithuanian",
"Luxembourgish",
"Macedonian",
"Malagasy",
"Malay",
"Malayalam",
"Maltese",
"Maori",
"Marathi",
"Mongolian",
"Nepali",
2019-04-20 01:14:11 +09:00
"Norwegian Bokmål",
2018-08-07 03:23:36 +09:00
"Nyanja",
"Pashto",
"Persian",
"Polish",
"Portuguese",
"Punjabi",
"Romanian",
"Russian",
"Samoan",
"Scottish Gaelic",
"Serbian",
"Shona",
"Sindhi",
"Sinhala",
"Slovak",
"Slovenian",
"Somali",
"Southern Sotho",
"Spanish",
2018-08-07 08:25:25 +09:00
"Spanish (Latin America)",
2018-08-07 03:23:36 +09:00
"Sundanese",
"Swahili",
"Swedish",
"Tajik",
"Tamil",
"Telugu",
"Thai",
"Turkish",
"Ukrainian",
"Urdu",
"Uzbek",
"Vietnamese",
"Welsh",
"Western Frisian",
"Xhosa",
"Yiddish",
"Yoruba",
"Zulu",
2018-08-13 10:04:13 +09:00
}
2018-08-07 03:23:36 +09:00
REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"}
2018-08-13 23:17:28 +09:00
2018-08-12 23:34:26 +09:00
# See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L380-#L476
VIDEO_FORMATS = {
"5" => {"ext" => "flv", "width" => 400, "height" => 240, "acodec" => "mp3", "abr" => 64, "vcodec" => "h263"},
"6" => {"ext" => "flv", "width" => 450, "height" => 270, "acodec" => "mp3", "abr" => 64, "vcodec" => "h263"},
"13" => {"ext" => "3gp", "acodec" => "aac", "vcodec" => "mp4v"},
"17" => {"ext" => "3gp", "width" => 176, "height" => 144, "acodec" => "aac", "abr" => 24, "vcodec" => "mp4v"},
"18" => {"ext" => "mp4", "width" => 640, "height" => 360, "acodec" => "aac", "abr" => 96, "vcodec" => "h264"},
"22" => {"ext" => "mp4", "width" => 1280, "height" => 720, "acodec" => "aac", "abr" => 192, "vcodec" => "h264"},
"34" => {"ext" => "flv", "width" => 640, "height" => 360, "acodec" => "aac", "abr" => 128, "vcodec" => "h264"},
"35" => {"ext" => "flv", "width" => 854, "height" => 480, "acodec" => "aac", "abr" => 128, "vcodec" => "h264"},
"36" => {"ext" => "3gp", "width" => 320, "acodec" => "aac", "vcodec" => "mp4v"},
"37" => {"ext" => "mp4", "width" => 1920, "height" => 1080, "acodec" => "aac", "abr" => 192, "vcodec" => "h264"},
"38" => {"ext" => "mp4", "width" => 4096, "height" => 3072, "acodec" => "aac", "abr" => 192, "vcodec" => "h264"},
"43" => {"ext" => "webm", "width" => 640, "height" => 360, "acodec" => "vorbis", "abr" => 128, "vcodec" => "vp8"},
"44" => {"ext" => "webm", "width" => 854, "height" => 480, "acodec" => "vorbis", "abr" => 128, "vcodec" => "vp8"},
"45" => {"ext" => "webm", "width" => 1280, "height" => 720, "acodec" => "vorbis", "abr" => 192, "vcodec" => "vp8"},
"46" => {"ext" => "webm", "width" => 1920, "height" => 1080, "acodec" => "vorbis", "abr" => 192, "vcodec" => "vp8"},
"59" => {"ext" => "mp4", "width" => 854, "height" => 480, "acodec" => "aac", "abr" => 128, "vcodec" => "h264"},
"78" => {"ext" => "mp4", "width" => 854, "height" => 480, "acodec" => "aac", "abr" => 128, "vcodec" => "h264"},
# 3D videos
"82" => {"ext" => "mp4", "height" => 360, "format" => "3D", "acodec" => "aac", "abr" => 128, "vcodec" => "h264"},
"83" => {"ext" => "mp4", "height" => 480, "format" => "3D", "acodec" => "aac", "abr" => 128, "vcodec" => "h264"},
"84" => {"ext" => "mp4", "height" => 720, "format" => "3D", "acodec" => "aac", "abr" => 192, "vcodec" => "h264"},
"85" => {"ext" => "mp4", "height" => 1080, "format" => "3D", "acodec" => "aac", "abr" => 192, "vcodec" => "h264"},
"100" => {"ext" => "webm", "height" => 360, "format" => "3D", "acodec" => "vorbis", "abr" => 128, "vcodec" => "vp8"},
"101" => {"ext" => "webm", "height" => 480, "format" => "3D", "acodec" => "vorbis", "abr" => 192, "vcodec" => "vp8"},
"102" => {"ext" => "webm", "height" => 720, "format" => "3D", "acodec" => "vorbis", "abr" => 192, "vcodec" => "vp8"},
# Apple HTTP Live Streaming
"91" => {"ext" => "mp4", "height" => 144, "format" => "HLS", "acodec" => "aac", "abr" => 48, "vcodec" => "h264"},
"92" => {"ext" => "mp4", "height" => 240, "format" => "HLS", "acodec" => "aac", "abr" => 48, "vcodec" => "h264"},
"93" => {"ext" => "mp4", "height" => 360, "format" => "HLS", "acodec" => "aac", "abr" => 128, "vcodec" => "h264"},
"94" => {"ext" => "mp4", "height" => 480, "format" => "HLS", "acodec" => "aac", "abr" => 128, "vcodec" => "h264"},
"95" => {"ext" => "mp4", "height" => 720, "format" => "HLS", "acodec" => "aac", "abr" => 256, "vcodec" => "h264"},
"96" => {"ext" => "mp4", "height" => 1080, "format" => "HLS", "acodec" => "aac", "abr" => 256, "vcodec" => "h264"},
"132" => {"ext" => "mp4", "height" => 240, "format" => "HLS", "acodec" => "aac", "abr" => 48, "vcodec" => "h264"},
"151" => {"ext" => "mp4", "height" => 72, "format" => "HLS", "acodec" => "aac", "abr" => 24, "vcodec" => "h264"},
# DASH mp4 video
"133" => {"ext" => "mp4", "height" => 240, "format" => "DASH video", "vcodec" => "h264"},
"134" => {"ext" => "mp4", "height" => 360, "format" => "DASH video", "vcodec" => "h264"},
"135" => {"ext" => "mp4", "height" => 480, "format" => "DASH video", "vcodec" => "h264"},
"136" => {"ext" => "mp4", "height" => 720, "format" => "DASH video", "vcodec" => "h264"},
"137" => {"ext" => "mp4", "height" => 1080, "format" => "DASH video", "vcodec" => "h264"},
2019-07-06 03:38:46 +09:00
"138" => {"ext" => "mp4", "format" => "DASH video", "vcodec" => "h264"}, # Height can vary (https://github.com/ytdl-org/youtube-dl/issues/4559)
2018-08-12 23:34:26 +09:00
"160" => {"ext" => "mp4", "height" => 144, "format" => "DASH video", "vcodec" => "h264"},
"212" => {"ext" => "mp4", "height" => 480, "format" => "DASH video", "vcodec" => "h264"},
"264" => {"ext" => "mp4", "height" => 1440, "format" => "DASH video", "vcodec" => "h264"},
"298" => {"ext" => "mp4", "height" => 720, "format" => "DASH video", "vcodec" => "h264", "fps" => 60},
"299" => {"ext" => "mp4", "height" => 1080, "format" => "DASH video", "vcodec" => "h264", "fps" => 60},
"266" => {"ext" => "mp4", "height" => 2160, "format" => "DASH video", "vcodec" => "h264"},
# Dash mp4 audio
"139" => {"ext" => "m4a", "format" => "DASH audio", "acodec" => "aac", "abr" => 48, "container" => "m4a_dash"},
"140" => {"ext" => "m4a", "format" => "DASH audio", "acodec" => "aac", "abr" => 128, "container" => "m4a_dash"},
"141" => {"ext" => "m4a", "format" => "DASH audio", "acodec" => "aac", "abr" => 256, "container" => "m4a_dash"},
"256" => {"ext" => "m4a", "format" => "DASH audio", "acodec" => "aac", "container" => "m4a_dash"},
"258" => {"ext" => "m4a", "format" => "DASH audio", "acodec" => "aac", "container" => "m4a_dash"},
"325" => {"ext" => "m4a", "format" => "DASH audio", "acodec" => "dtse", "container" => "m4a_dash"},
"328" => {"ext" => "m4a", "format" => "DASH audio", "acodec" => "ec-3", "container" => "m4a_dash"},
# Dash webm
"167" => {"ext" => "webm", "height" => 360, "width" => 640, "format" => "DASH video", "container" => "webm", "vcodec" => "vp8"},
"168" => {"ext" => "webm", "height" => 480, "width" => 854, "format" => "DASH video", "container" => "webm", "vcodec" => "vp8"},
"169" => {"ext" => "webm", "height" => 720, "width" => 1280, "format" => "DASH video", "container" => "webm", "vcodec" => "vp8"},
"170" => {"ext" => "webm", "height" => 1080, "width" => 1920, "format" => "DASH video", "container" => "webm", "vcodec" => "vp8"},
"218" => {"ext" => "webm", "height" => 480, "width" => 854, "format" => "DASH video", "container" => "webm", "vcodec" => "vp8"},
"219" => {"ext" => "webm", "height" => 480, "width" => 854, "format" => "DASH video", "container" => "webm", "vcodec" => "vp8"},
"278" => {"ext" => "webm", "height" => 144, "format" => "DASH video", "container" => "webm", "vcodec" => "vp9"},
"242" => {"ext" => "webm", "height" => 240, "format" => "DASH video", "vcodec" => "vp9"},
"243" => {"ext" => "webm", "height" => 360, "format" => "DASH video", "vcodec" => "vp9"},
"244" => {"ext" => "webm", "height" => 480, "format" => "DASH video", "vcodec" => "vp9"},
"245" => {"ext" => "webm", "height" => 480, "format" => "DASH video", "vcodec" => "vp9"},
"246" => {"ext" => "webm", "height" => 480, "format" => "DASH video", "vcodec" => "vp9"},
"247" => {"ext" => "webm", "height" => 720, "format" => "DASH video", "vcodec" => "vp9"},
"248" => {"ext" => "webm", "height" => 1080, "format" => "DASH video", "vcodec" => "vp9"},
"271" => {"ext" => "webm", "height" => 1440, "format" => "DASH video", "vcodec" => "vp9"},
# itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
"272" => {"ext" => "webm", "height" => 2160, "format" => "DASH video", "vcodec" => "vp9"},
"302" => {"ext" => "webm", "height" => 720, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"303" => {"ext" => "webm", "height" => 1080, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"308" => {"ext" => "webm", "height" => 1440, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"313" => {"ext" => "webm", "height" => 2160, "format" => "DASH video", "vcodec" => "vp9"},
"315" => {"ext" => "webm", "height" => 2160, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"330" => {"ext" => "webm", "height" => 144, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"331" => {"ext" => "webm", "height" => 240, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"332" => {"ext" => "webm", "height" => 360, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"333" => {"ext" => "webm", "height" => 480, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"334" => {"ext" => "webm", "height" => 720, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"335" => {"ext" => "webm", "height" => 1080, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"336" => {"ext" => "webm", "height" => 1440, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
"337" => {"ext" => "webm", "height" => 2160, "format" => "DASH video", "vcodec" => "vp9", "fps" => 60},
# Dash webm audio
"171" => {"ext" => "webm", "acodec" => "vorbis", "format" => "DASH audio", "abr" => 128},
"172" => {"ext" => "webm", "acodec" => "vorbis", "format" => "DASH audio", "abr" => 256},
# Dash webm audio with opus inside
"249" => {"ext" => "webm", "format" => "DASH audio", "acodec" => "opus", "abr" => 50},
"250" => {"ext" => "webm", "format" => "DASH audio", "acodec" => "opus", "abr" => 70},
"251" => {"ext" => "webm", "format" => "DASH audio", "acodec" => "opus", "abr" => 160},
2019-07-06 03:38:46 +09:00
# av01 video only formats sometimes served with "unknown" codecs
"394" => {"ext" => "mp4", "height" => 144, "vcodec" => "av01.0.05M.08"},
"395" => {"ext" => "mp4", "height" => 240, "vcodec" => "av01.0.05M.08"},
"396" => {"ext" => "mp4", "height" => 360, "vcodec" => "av01.0.05M.08"},
"397" => {"ext" => "mp4", "height" => 480, "vcodec" => "av01.0.05M.08"},
2018-08-12 23:34:26 +09:00
}
2019-05-01 13:39:04 +09:00
struct VideoPreferences
include JSON::Serializable
property annotations : Bool
property autoplay : Bool
property comments : Array(String)
property continue : Bool
property continue_autoplay : Bool
property controls : Bool
property listen : Bool
property local : Bool
property preferred_captions : Array(String)
property player_style : String
property quality : String
property quality_dash : String
property raw : Bool
property region : String?
property related_videos : Bool
property speed : Float32 | Float64
property video_end : Float64 | Int32
property video_loop : Bool
property extend_desc : Bool
property video_start : Float64 | Int32
property volume : Int32
property vr_mode : Bool
2019-05-01 13:39:04 +09:00
end
2019-03-30 06:30:02 +09:00
struct Video
include DB::Serializable
property id : String
@[DB::Field(converter: Video::JSONConverter)]
property info : Hash(String, JSON::Any)
property updated : Time
@[DB::Field(ignore: true)]
property captions : Array(Caption)?
@[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-10-13 11:31:06 +09:00
def to_json(locale : Hash(String, JSON::Any), json : JSON::Builder)
2019-06-09 03:31:41 +09:00
json.object do
json.field "type", "video"
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
json.field "dislikeCount", self.dislikes
json.field "paid", self.paid
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
2020-06-16 07:33:23 +09:00
json.field "rating", self.average_rating
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
2020-06-16 07:33:23 +09:00
json.field "index", "#{fmt["indexRange"]["start"]}-#{fmt["indexRange"]["end"]}"
json.field "bitrate", fmt["bitrate"].as_i.to_s
json.field "init", "#{fmt["initRange"]["start"]}-#{fmt["initRange"]["end"]}"
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"]
json.field "clen", fmt["contentLength"]
json.field "lmt", fmt["lastModified"]
json.field "projectionType", fmt["projectionType"]
2019-06-09 03:31:41 +09:00
fmt_info = itag_to_metadata?(fmt["itag"])
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
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
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"]
fmt_info = itag_to_metadata?(fmt["itag"])
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
json.field "label", caption.name
2019-06-09 03:31:41 +09:00
json.field "languageCode", caption.languageCode
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"]
2019-08-31 12:57:33 +09:00
json.field "authorUrl", rv["author_url"]?
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
2020-06-16 07:33:23 +09:00
json.field "url", rv["author_thumbnail"]?.try &.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
json.field "viewCountText", rv["short_view_count_text"]?
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
2020-06-16 07:10:30 +09:00
def to_json(locale, json : JSON::Builder | Nil = nil)
2019-06-09 03:31:41 +09:00
if json
2020-06-16 07:10:30 +09:00
to_json(locale, json)
2019-06-09 03:31:41 +09:00
else
JSON.build do |json|
2020-06-16 07:10:30 +09:00
to_json(locale, json)
2019-06-09 03:31:41 +09:00
end
end
end
2020-06-16 07:33:23 +09:00
def title
info["videoDetails"]["title"]?.try &.as_s || ""
2019-06-09 05:08:27 +09:00
end
2020-06-16 07:33:23 +09:00
def ucid
info["videoDetails"]["channelId"]?.try &.as_s || ""
2019-06-09 05:08:27 +09:00
end
2020-06-16 07:33:23 +09:00
def author
info["videoDetails"]["author"]?.try &.as_s || ""
end
2019-03-23 00:32:42 +09:00
2020-06-16 07:33:23 +09:00
def length_seconds : Int32
info["microformat"]?.try &.["playerMicroformatRenderer"]?.try &.["lengthSeconds"]?.try &.as_s.to_i ||
info["videoDetails"]["lengthSeconds"]?.try &.as_s.to_i || 0
2019-03-23 00:32:42 +09:00
end
2020-06-16 07:33:23 +09:00
def views : Int64
info["videoDetails"]["viewCount"]?.try &.as_s.to_i64 || 0_i64
end
2019-03-23 00:32:42 +09:00
2020-06-16 07:33:23 +09:00
def likes : Int64
info["likes"]?.try &.as_i64 || 0_i64
end
2019-03-23 00:32:42 +09:00
2020-06-16 07:33:23 +09:00
def dislikes : Int64
info["dislikes"]?.try &.as_i64 || 0_i64
2019-03-23 00:32:42 +09:00
end
2020-06-16 07:33:23 +09:00
def average_rating : Float64
# (likes / (likes + dislikes) * 4 + 1)
info["videoDetails"]["averageRating"]?.try { |t| t.as_f? || t.as_i64?.try &.to_f64 }.try &.round(4) || 0.0
end
2019-03-23 00:32:42 +09:00
2020-06-16 07:33:23 +09:00
def published : Time
2021-02-21 20:35:21 +09:00
info["microformat"]?.try &.["playerMicroformatRenderer"]?.try &.["publishDate"]?.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)
info["microformat"].as_h["playerMicroformatRenderer"].as_h["publishDate"] = JSON::Any.new(other.to_s("%Y-%m-%d"))
2019-03-23 00:32:42 +09:00
end
2020-06-16 07:33:23 +09:00
def allow_ratings
r = info["videoDetails"]["allowRatings"]?.try &.as_bool
r.nil? ? false : r
end
2020-06-16 07:33:23 +09:00
def live_now
2021-02-25 13:06:50 +09:00
info["microformat"]?.try &.["playerMicroformatRenderer"]?
.try &.["liveBroadcastDetails"]?.try &.["isLiveNow"]?.try &.as_bool || false
end
2020-06-16 07:33:23 +09:00
def is_listed
info["videoDetails"]["isCrawlable"]?.try &.as_bool || false
end
2020-06-16 07:33:23 +09:00
def is_upcoming
info["videoDetails"]["isUpcoming"]?.try &.as_bool || false
end
2020-06-16 07:33:23 +09:00
def premiere_timestamp : Time?
info["microformat"]?.try &.["playerMicroformatRenderer"]?
.try &.["liveBroadcastDetails"]?.try &.["startTimestamp"]?.try { |t| Time.parse_rfc3339(t.as_s) }
end
2018-11-02 22:09:28 +09:00
def keywords
2020-06-16 07:33:23 +09:00
info["videoDetails"]["keywords"]?.try &.as_a.map &.as_s || [] of String
2018-11-02 22:09:28 +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
2020-06-16 07:33:23 +09:00
def allowed_regions
info["microformat"]?.try &.["playerMicroformatRenderer"]?
.try &.["availableCountries"]?.try &.as_a.map &.as_s || [] of String
end
2020-06-16 07:33:23 +09:00
def author_thumbnail : String
info["authorThumbnail"]?.try &.as_s || ""
end
2020-06-16 07:33:23 +09:00
def sub_count_text : String
info["subCountText"]?.try &.as_s || "-"
end
2020-06-16 07:33:23 +09:00
def fmt_stream
return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream
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)
end
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"]}&region=#{self.info["region"]}") if self.info["region"]?
2018-08-05 13:07:38 +09:00
end
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)
end
fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}")
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"]}&region=#{self.info["region"]}") if self.info["region"]?
2018-10-02 09:01:44 +09:00
end
2020-07-05 03:17:46 +09:00
# See https://github.com/TeamNewPipe/NewPipe/issues/2415
# Some streams are segmented by URL `sq/` rather than index, for now we just filter them out
fmt_stream.reject! { |f| !f["indexRange"]? }
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")
end
2019-04-12 07:00:00 +09:00
def storyboards
2020-06-16 07:33:23 +09:00
storyboards = info["storyboards"]?
2019-04-12 07:00:00 +09:00
.try &.as_h
.try &.["playerStoryboardSpecRenderer"]?
2019-10-04 23:23:02 +09:00
.try &.["spec"]?
.try &.as_s.split("|")
2019-04-12 07:00:00 +09:00
if !storyboards
2020-06-16 07:33:23 +09:00
if storyboard = info["storyboards"]?
2019-10-04 23:23:02 +09:00
.try &.as_h
.try &.["playerLiveStoryboardSpecRenderer"]?
.try &.["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
storyboards.each_with_index do |storyboard, i|
width, height, count, storyboard_width, storyboard_height, interval, _, sigh = storyboard.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
2018-10-17 01:15:14 +09:00
def paid
2020-06-16 07:33:23 +09:00
reason = info["playabilityStatus"]?.try &.["reason"]?
paid = reason == "This video requires payment to watch." ? true : false
2020-06-16 07:33:23 +09:00
paid
2018-10-17 01:15:14 +09:00
end
def premium
2020-06-16 07:33:23 +09:00
keywords.includes? "YouTube Red"
end
def captions : Array(Caption)
return @captions.as(Array(Caption)) if @captions
captions = info["captions"]?.try &.["playerCaptionsTracklistRenderer"]?.try &.["captionTracks"]?.try &.as_a.map do |caption|
name = caption["name"]["simpleText"]? || caption["name"]["runs"][0]["text"]
languageCode = caption["languageCode"].to_s
baseUrl = caption["baseUrl"].to_s
caption = Caption.new(name.to_s, languageCode, baseUrl)
caption.name = caption.name.split(" - ")[0]
2020-06-16 07:33:23 +09:00
caption
2019-08-05 10:56:24 +09:00
end
2020-06-16 07:33:23 +09:00
captions ||= [] of Caption
@captions = captions
return @captions.as(Array(Caption))
2018-10-17 01:15:14 +09:00
end
2020-06-16 07:33:23 +09:00
def description
description = info["microformat"]?.try &.["playerMicroformatRenderer"]?
.try &.["description"]?.try &.["simpleText"]?.try &.as_s || ""
end
2018-08-07 08:25:25 +09:00
2020-06-16 07:33:23 +09:00
# TODO
def description=(value : String)
@description = value
end
def description_html
info["descriptionHtml"]?.try &.as_s || "<p></p>"
end
2018-08-05 13:07:38 +09:00
2020-06-16 07:33:23 +09:00
def description_html=(value : String)
info["descriptionHtml"] = JSON::Any.new(value)
2018-08-05 13:07:38 +09:00
end
def short_description
2020-06-18 08:22:28 +09:00
info["shortDescription"]?.try &.as_s? || ""
2020-06-16 07:33:23 +09:00
end
def hls_manifest_url : String?
info["streamingData"]?.try &.["hlsManifestUrl"]?.try &.as_s
end
def dash_manifest_url
info["streamingData"]?.try &.["dashManifestUrl"]?.try &.as_s
end
def genre : String
info["genre"]?.try &.as_s || ""
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
def license : String?
info["license"]?.try &.as_s
end
def is_family_friendly : Bool
info["microformat"]?.try &.["playerMicroformatRenderer"]["isFamilySafe"]?.try &.as_bool || false
end
2018-08-05 13:07:38 +09:00
2021-09-15 19:02:43 +09:00
def is_vr : Bool?
projection_type = info.dig?("streamingData", "adaptiveFormats", 0, "projectionType").try &.as_s
return {"EQUIRECTANGULAR", "MESH"}.includes? projection_type
end
def projection_type : String?
return info.dig?("streamingData", "adaptiveFormats", 0, "projectionType").try &.as_s
end
2020-06-16 07:33:23 +09:00
def wilson_score : Float64
ci_lower_bound(likes, likes + dislikes).round(4)
2018-08-05 13:07:38 +09:00
end
2020-06-16 07:33:23 +09:00
def engagement : Float64
(((likes + dislikes) / views) * 100).round(4)
2020-06-16 07:33:23 +09:00
end
def reason : String?
info["reason"]?.try &.as_s
end
end
2021-07-12 08:17:22 +09:00
struct Caption
property name
property languageCode
property baseUrl
2020-06-16 07:33:23 +09:00
getter name : String
getter languageCode : String
getter baseUrl : String
2018-08-05 05:30:44 +09:00
setter name
2018-08-07 08:25:25 +09:00
def initialize(@name, @languageCode, @baseUrl)
end
2018-08-07 08:25:25 +09:00
end
class VideoRedirect < Exception
2019-09-09 01:08:59 +09:00
property video_id : String
def initialize(@video_id)
end
end
2020-06-16 07:33:23 +09:00
def parse_related(r : JSON::Any) : JSON::Any?
# TODO: r["endScreenPlaylistRenderer"], etc.
return if !r["endScreenVideoRenderer"]?
r = r["endScreenVideoRenderer"].as_h
return if !r["lengthInSeconds"]?
rv = {} of String => JSON::Any
rv["author"] = r["shortBylineText"]["runs"][0]?.try &.["text"] || JSON::Any.new("")
rv["ucid"] = r["shortBylineText"]["runs"][0]?.try &.["navigationEndpoint"]["browseEndpoint"]["browseId"] || JSON::Any.new("")
rv["author_url"] = JSON::Any.new("/channel/#{rv["ucid"]}")
rv["length_seconds"] = JSON::Any.new(r["lengthInSeconds"].as_i.to_s)
rv["title"] = r["title"]["simpleText"]
rv["short_view_count_text"] = JSON::Any.new(r["shortViewCountText"]?.try &.["simpleText"]?.try &.as_s || "")
rv["view_count"] = JSON::Any.new(r["title"]["accessibility"]?.try &.["accessibilityData"]["label"].as_s.match(/(?<views>[1-9](\d+,?)*) views/).try &.["views"].gsub(/\D/, "") || "")
rv["id"] = r["videoId"]
JSON::Any.new(rv)
2018-08-05 05:30:44 +09:00
end
2021-09-15 19:02:43 +09:00
def extract_video_info(video_id : String, proxy_region : String? = nil, context_screen : String? = nil)
2020-06-16 07:33:23 +09:00
params = {} of String => JSON::Any
2021-09-15 19:02:43 +09:00
client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region)
if context_screen == "embed"
client_config.client_type = YoutubeAPI::ClientType::WebScreenEmbed
end
player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config)
if player_response["playabilityStatus"]?.try &.["status"]?.try &.as_s != "OK"
reason = player_response["playabilityStatus"]["errorScreen"]?.try &.["playerErrorMessageRenderer"]?.try &.["subreason"]?.try { |s|
s["simpleText"]?.try &.as_s || s["runs"].as_a.map { |r| r["text"] }.join("")
} || player_response["playabilityStatus"]["reason"].as_s
2020-06-16 07:33:23 +09:00
params["reason"] = JSON::Any.new(reason)
end
2021-09-15 19:02:43 +09:00
params["shortDescription"] = player_response.dig?("videoDetails", "shortDescription") || JSON::Any.new(nil)
# Don't fetch the next endpoint if the video is unavailable.
if !params["reason"]?
next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
player_response = player_response.merge(next_response)
end
2019-04-11 08:02:13 +09:00
2021-09-15 19:02:43 +09:00
# Fetch the video streams using an Android client in order to get the decrypted URLs and
# maybe fix throttling issues (#2194).See for the explanation about the decrypted URLs:
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
if !params["reason"]?
if context_screen == "embed"
client_config.client_type = YoutubeAPI::ClientType::AndroidScreenEmbed
else
client_config.client_type = YoutubeAPI::ClientType::Android
end
stream_data = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config)
params["streamingData"] = stream_data["streamingData"]? || JSON::Any.new("")
end
2019-04-11 08:02:13 +09:00
2020-06-16 07:33:23 +09:00
{"captions", "microformat", "playabilityStatus", "storyboards", "videoDetails"}.each do |f|
params[f] = player_response[f] if player_response[f]?
2019-04-11 08:02:13 +09:00
end
2021-09-15 19:02:43 +09:00
params["relatedVideos"] = (
player_response
.dig?("playerOverlays", "playerOverlayRenderer", "endScreen", "watchNextEndScreenRenderer", "results")
.try &.as_a.compact_map { |r| parse_related r } || \
player_response
.dig?("webWatchNextResponseExtensionData", "relatedVideoArgs")
.try &.as_s.split(",").map { |r|
r = HTTP::Params.parse(r).to_h
JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) }))
}
).try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any)
2019-04-11 08:02:13 +09:00
2021-09-15 19:02:43 +09:00
primary_results = player_response.try &.["contents"]?.try &.["twoColumnWatchNextResults"]?.try &.["results"]?
2020-06-16 07:33:23 +09:00
.try &.["results"]?.try &.["contents"]?
2019-04-11 08:02:13 +09:00
sentiment_bar = primary_results.try &.as_a.select { |object| object["videoPrimaryInfoRenderer"]? }[0]?
.try &.["videoPrimaryInfoRenderer"]?
.try &.["sentimentBar"]?
.try &.["sentimentBarRenderer"]?
.try &.["tooltip"]?
.try &.as_s
2020-06-16 07:33:23 +09:00
likes, dislikes = sentiment_bar.try &.split(" / ", 2).map &.gsub(/\D/, "").to_i64 || {0_i64, 0_i64}
params["likes"] = JSON::Any.new(likes)
params["dislikes"] = JSON::Any.new(dislikes)
2019-04-11 08:02:13 +09:00
2020-06-16 07:33:23 +09:00
params["descriptionHtml"] = JSON::Any.new(primary_results.try &.as_a.select { |object| object["videoSecondaryInfoRenderer"]? }[0]?
.try &.["videoSecondaryInfoRenderer"]?.try &.["description"]?.try &.["runs"]?
.try &.as_a.try { |t| content_to_comment_html(t).gsub("\n", "<br/>") } || "<p></p>")
2019-04-11 08:02:13 +09:00
metadata = primary_results.try &.as_a.select { |object| object["videoSecondaryInfoRenderer"]? }[0]?
.try &.["videoSecondaryInfoRenderer"]?
.try &.["metadataRowContainer"]?
.try &.["metadataRowContainerRenderer"]?
.try &.["rows"]?
.try &.as_a
2020-06-17 07:51:49 +09:00
params["genre"] = params["microformat"]?.try &.["playerMicroformatRenderer"]?.try &.["category"]? || JSON::Any.new("")
params["genreUrl"] = JSON::Any.new(nil)
2019-04-11 08:02:13 +09:00
metadata.try &.each do |row|
title = row["metadataRowRenderer"]?.try &.["title"]?.try &.["simpleText"]?.try &.as_s
contents = row["metadataRowRenderer"]?
.try &.["contents"]?
.try &.as_a[0]?
if title.try &.== "Category"
contents = contents.try &.["runs"]?
.try &.as_a[0]?
2020-06-16 07:33:23 +09:00
params["genre"] = JSON::Any.new(contents.try &.["text"]?.try &.as_s || "")
params["genreUcid"] = JSON::Any.new(contents.try &.["navigationEndpoint"]?.try &.["browseEndpoint"]?
.try &.["browseId"]?.try &.as_s || "")
2019-04-11 08:02:13 +09:00
elsif title.try &.== "License"
contents = contents.try &.["runs"]?
.try &.as_a[0]?
2020-06-16 07:33:23 +09:00
params["license"] = JSON::Any.new(contents.try &.["text"]?.try &.as_s || "")
2019-04-11 08:02:13 +09:00
elsif title.try &.== "Licensed to YouTube by"
2020-06-16 07:33:23 +09:00
params["license"] = JSON::Any.new(contents.try &.["simpleText"]?.try &.as_s || "")
2019-04-11 08:02:13 +09:00
end
end
author_info = primary_results.try &.as_a.select { |object| object["videoSecondaryInfoRenderer"]? }[0]?
2020-06-16 07:33:23 +09:00
.try &.["videoSecondaryInfoRenderer"]?.try &.["owner"]?.try &.["videoOwnerRenderer"]?
2019-04-11 08:02:13 +09:00
2020-06-16 07:33:23 +09:00
params["authorThumbnail"] = JSON::Any.new(author_info.try &.["thumbnail"]?
.try &.["thumbnails"]?.try &.as_a[0]?.try &.["url"]?
.try &.as_s || "")
2019-04-11 08:02:13 +09:00
2020-06-16 07:33:23 +09:00
params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]?
.try { |t| t["simpleText"]? || t["runs"]?.try &.[0]?.try &.["text"]? }.try &.as_s.split(" ", 2)[0] || "-")
2019-04-11 08:02:13 +09:00
2020-06-16 07:33:23 +09:00
params
end
2018-08-05 05:30:44 +09:00
2020-06-16 07:33:23 +09:00
def get_video(id, db, refresh = true, region = nil, force_refresh = false)
if (video = db.query_one?("SELECT * FROM videos WHERE id = $1", id, as: Video)) && !region
# If record was last updated over 10 minutes ago, or video has since premiered,
# refresh (expire param in response lasts for 6 hours)
if (refresh &&
(Time.utc - video.updated > 10.minutes) ||
(video.premiere_timestamp.try &.< Time.utc)) ||
force_refresh
begin
video = fetch_video(id, region)
db.exec("UPDATE videos SET (id, info, updated) = ($1, $2, $3) WHERE id = $1", video.id, video.info.to_json, video.updated)
rescue ex
db.exec("DELETE FROM videos * WHERE id = $1", id)
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)
if !region
db.exec("INSERT INTO videos VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING", video.id, video.info.to_json, video.updated)
2019-02-07 07:12:11 +09:00
end
2018-08-05 05:30:44 +09:00
end
2020-06-16 07:33:23 +09:00
return video
2019-02-07 07:12:11 +09:00
end
2019-06-29 11:17:56 +09:00
def fetch_video(id, region)
2021-09-15 19:02:43 +09:00
info = extract_video_info(video_id: id)
2021-09-15 19:02: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")
bypass_regions = PROXY_LIST.keys & allowed_regions
if !bypass_regions.empty?
region = bypass_regions[rand(bypass_regions.size)]
2021-09-15 19:02: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"]?
end
2018-08-13 23:17:28 +09:00
end
2021-09-15 19:02:43 +09:00
# Try to fetch video info using an embedded client
2018-08-05 05:30:44 +09:00
if info["reason"]?
2021-09-15 19:02:43 +09:00
embed_info = extract_video_info(video_id: id, context_screen: "embed")
info = embed_info if !embed_info["reason"]?
end
2018-09-10 04:47:26 +09:00
raise InfoException.new(info["reason"]?.try &.as_s || "") if !info["videoDetails"]?
2018-08-05 05:30:44 +09:00
video = Video.new({
id: id,
info: info,
updated: Time.utc,
})
2018-08-05 05:30:44 +09:00
return video
end
2020-06-16 07:33:23 +09:00
def itag_to_metadata?(itag : JSON::Any)
return VIDEO_FORMATS[itag.to_s]?
2018-08-05 05:30:44 +09:00
end
2018-08-05 13:07:38 +09:00
2019-08-06 08:49:13 +09:00
def process_continuation(db, query, plid, id)
continuation = nil
if plid
if index = query["index"]?.try &.to_i?
continuation = index
else
continuation = id
end
continuation ||= 0
end
continuation
end
2018-08-05 13:07:38 +09:00
def process_video_params(query, preferences)
2019-05-01 13:39:04 +09:00
annotations = query["iv_load_policy"]?.try &.to_i?
2019-10-05 01:23:28 +09:00
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
2019-05-30 04:24:30 +09:00
comments = query["comments"]?.try &.split(",").map { |a| a.downcase }
2019-10-05 01:23:28 +09:00
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
continue_autoplay = query["continue_autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
listen = query["listen"]?.try { |q| (q == "true" || q == "1").to_unsafe }
local = query["local"]?.try { |q| (q == "true" || q == "1").to_unsafe }
player_style = query["player_style"]?
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
quality = query["quality"]?
quality_dash = query["quality_dash"]?
region = query["region"]?
2019-10-05 01:23:28 +09:00
related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
2019-06-06 01:10:23 +09:00
speed = query["speed"]?.try &.rchop("x").to_f?
2019-10-05 01:23:28 +09:00
video_loop = query["loop"]?.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?
vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe }
2018-08-05 13:07:38 +09:00
if preferences
# region ||= preferences.region
2019-05-01 13:39:04 +09:00
annotations ||= preferences.annotations.to_unsafe
2018-08-05 13:07:38 +09:00
autoplay ||= preferences.autoplay.to_unsafe
2019-05-30 04:24:30 +09:00
comments ||= preferences.comments
2018-11-12 02:45:05 +09:00
continue ||= preferences.continue.to_unsafe
2019-04-19 23:38:27 +09:00
continue_autoplay ||= preferences.continue_autoplay.to_unsafe
2018-10-30 23:41:23 +09:00
listen ||= preferences.listen.to_unsafe
2019-03-13 11:05:49 +09:00
local ||= preferences.local.to_unsafe
player_style ||= preferences.player_style
preferred_captions ||= preferences.captions
quality ||= preferences.quality
quality_dash ||= preferences.quality_dash
2019-03-13 11:05:49 +09:00
related_videos ||= preferences.related_videos.to_unsafe
speed ||= preferences.speed
2018-08-05 13:07:38 +09:00
video_loop ||= preferences.video_loop.to_unsafe
extend_desc ||= preferences.extend_desc.to_unsafe
volume ||= preferences.volume
vr_mode ||= preferences.vr_mode.to_unsafe
2018-08-05 13:07:38 +09:00
end
2019-05-01 13:39:04 +09:00
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
2019-05-30 04:24:30 +09:00
comments ||= CONFIG.default_user_preferences.comments
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
2019-04-19 23:38:27 +09:00
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
listen ||= CONFIG.default_user_preferences.listen.to_unsafe
local ||= CONFIG.default_user_preferences.local.to_unsafe
player_style ||= CONFIG.default_user_preferences.player_style
preferred_captions ||= CONFIG.default_user_preferences.captions
quality ||= CONFIG.default_user_preferences.quality
quality_dash ||= CONFIG.default_user_preferences.quality_dash
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
speed ||= CONFIG.default_user_preferences.speed
video_loop ||= CONFIG.default_user_preferences.video_loop.to_unsafe
extend_desc ||= CONFIG.default_user_preferences.extend_desc.to_unsafe
volume ||= CONFIG.default_user_preferences.volume
vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe
2019-05-01 13:39:04 +09:00
annotations = annotations == 1
autoplay = autoplay == 1
2018-11-12 02:45:05 +09:00
continue = continue == 1
2019-04-19 23:38:27 +09:00
continue_autoplay = continue_autoplay == 1
2018-10-30 23:41:23 +09:00
listen = listen == 1
local = local == 1
related_videos = related_videos == 1
2018-08-05 13:07:38 +09:00
video_loop = video_loop == 1
extend_desc = extend_desc == 1
vr_mode = vr_mode == 1
2018-08-05 13:07:38 +09:00
if CONFIG.disabled?("dash") && quality == "dash"
quality = "high"
end
if CONFIG.disabled?("local") && local
local = false
end
2019-10-05 01:23:28 +09:00
if start = query["t"]? || query["time_continue"]? || query["start"]?
video_start = decode_time(start)
2018-08-05 13:07:38 +09:00
end
video_start ||= 0
if query["end"]?
video_end = decode_time(query["end"])
end
video_end ||= -1
raw = query["raw"]?.try &.to_i?
raw ||= 0
raw = raw == 1
controls = query["controls"]?.try &.to_i?
controls ||= 1
2019-04-05 05:05:54 +09:00
controls = controls >= 1
2018-08-05 13:07:38 +09:00
params = VideoPreferences.new({
annotations: annotations,
autoplay: autoplay,
comments: comments,
continue: continue,
continue_autoplay: continue_autoplay,
controls: controls,
listen: listen,
local: local,
player_style: player_style,
preferred_captions: preferred_captions,
quality: quality,
quality_dash: quality_dash,
raw: raw,
region: region,
related_videos: related_videos,
speed: speed,
video_end: video_end,
video_loop: video_loop,
extend_desc: extend_desc,
video_start: video_start,
volume: volume,
2021-04-12 13:34:56 +09:00
vr_mode: vr_mode,
})
return params
2018-08-05 13:07:38 +09:00
end
2018-08-10 22:50:25 +09:00
2020-06-16 07:10:30 +09:00
def build_thumbnails(id)
2019-03-09 05:42:37 +09:00
return {
{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
json.field "quality", thumbnail[:name]
2019-03-09 05:42:37 +09:00
json.field "url", "#{thumbnail[:host]}/vi/#{id}/#{thumbnail["url"]}.jpg"
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