invidious/src/invidious/videos.cr

848 行
30 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",
"Norwegian",
"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
2018-09-25 04:24:33 +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-09-24 09:29:47 +09:00
BYPASS_REGIONS = {
2018-09-26 11:07:18 +09:00
"GB",
2018-09-25 04:24:33 +09:00
"DE",
"FR",
2018-09-24 09:29:47 +09:00
"IN",
2018-09-25 04:24:33 +09:00
"CN",
"RU",
"CA",
"JP",
"IT",
"TH",
"ES",
"AE",
"KR",
"IR",
2018-09-24 09:29:47 +09:00
"BR",
"PK",
2018-09-25 04:24:33 +09:00
"ID",
2018-09-24 09:29:47 +09:00
"BD",
"MX",
"PH",
"EG",
"VN",
"CD",
"TR",
}
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"},
"138" => {"ext" => "mp4", "format" => "DASH video", "vcodec" => "h264"}, # Height can vary (https=>//github.com/rg3/youtube-dl/issues/4559)
"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},
}
2018-08-05 05:30:44 +09:00
class Video
property player_json : JSON::Any?
2018-08-05 05:30:44 +09:00
module HTTPParamConverter
def self.from_rs(rs)
HTTP::Params.parse(rs.read(String))
end
end
2018-11-02 22:09:28 +09:00
def keywords
keywords = self.player_response["videoDetails"]?.try &.["keywords"]?.try &.as_a
2018-11-02 22:26:35 +09:00
keywords ||= [] of String
return keywords
2018-11-02 22:09:28 +09:00
end
2018-08-05 13:07:38 +09:00
def fmt_stream(decrypt_function)
streams = [] of HTTP::Params
2019-02-26 23:12:56 +09:00
if fmt_streams = self.player_response["streamingData"]?.try &.["formats"]?
fmt_streams.as_a.each do |fmt_stream|
if !fmt_stream.as_h?
next
end
fmt = {} of String => String
2019-02-26 23:12:56 +09:00
fmt["lmt"] = fmt_stream["lastModified"]?.try &.as_s || "0"
fmt["projection_type"] = "1"
fmt["type"] = fmt_stream["mimeType"].as_s
fmt["clen"] = fmt_stream["contentLength"]?.try &.as_s || "0"
2019-02-26 23:12:56 +09:00
fmt["bitrate"] = fmt_stream["bitrate"]?.try &.as_i.to_s || "0"
fmt["itag"] = fmt_stream["itag"].as_i.to_s
fmt["url"] = fmt_stream["url"].as_s
fmt["quality"] = fmt_stream["quality"].as_s
if fmt_stream["width"]?
fmt["size"] = "#{fmt_stream["width"]}x#{fmt_stream["height"]}"
fmt["height"] = fmt_stream["height"].as_i.to_s
end
if fmt_stream["fps"]?
fmt["fps"] = fmt_stream["fps"].as_i.to_s
end
if fmt_stream["qualityLabel"]?
fmt["quality_label"] = fmt_stream["qualityLabel"].as_s
end
params = HTTP::Params.new
fmt.each do |key, value|
params[key] = value
end
streams << params
end
streams.sort_by! { |stream| stream["height"].to_i }.reverse!
elsif fmt_stream = self.info["url_encoded_fmt_stream_map"]?
fmt_stream.split(",").each do |string|
if !string.empty?
streams << HTTP::Params.parse(string)
end
2018-08-05 13:07:38 +09:00
end
end
streams.each { |s| s.add("label", "#{s["quality"]} - #{s["type"].split(";")[0].split("/")[1]}") }
streams = streams.uniq { |s| s["label"] }
2018-10-02 09:01:44 +09:00
if self.info["region"]?
streams.each do |fmt|
fmt["url"] += "&region=" + self.info["region"]
end
end
streams.each do |fmt|
2019-03-12 03:14:30 +09:00
fmt["url"] += "&host=" + (URI.parse(fmt["url"]).host || "")
fmt["url"] += decrypt_signature(fmt, decrypt_function)
2018-08-05 13:07:38 +09:00
end
return streams
end
def adaptive_fmts(decrypt_function)
adaptive_fmts = [] of HTTP::Params
2018-09-16 00:25:43 +09:00
2019-02-26 23:12:56 +09:00
if fmts = self.player_response["streamingData"]?.try &.["adaptiveFormats"]?
fmts.as_a.each do |adaptive_fmt|
if !adaptive_fmt.as_h?
next
end
fmt = {} of String => String
if init = adaptive_fmt["initRange"]?
fmt["init"] = "#{init["start"]}-#{init["end"]}"
end
fmt["init"] ||= "0-0"
2019-02-26 23:12:56 +09:00
fmt["lmt"] = adaptive_fmt["lastModified"]?.try &.as_s || "0"
fmt["projection_type"] = "1"
fmt["type"] = adaptive_fmt["mimeType"].as_s
fmt["clen"] = adaptive_fmt["contentLength"]?.try &.as_s || "0"
2019-02-26 23:12:56 +09:00
fmt["bitrate"] = adaptive_fmt["bitrate"]?.try &.as_i.to_s || "0"
fmt["itag"] = adaptive_fmt["itag"].as_i.to_s
fmt["url"] = adaptive_fmt["url"].as_s
if index = adaptive_fmt["indexRange"]?
fmt["index"] = "#{index["start"]}-#{index["end"]}"
end
fmt["index"] ||= "0-0"
if adaptive_fmt["width"]?
fmt["size"] = "#{adaptive_fmt["width"]}x#{adaptive_fmt["height"]}"
end
if adaptive_fmt["fps"]?
fmt["fps"] = adaptive_fmt["fps"].as_i.to_s
end
if adaptive_fmt["qualityLabel"]?
fmt["quality_label"] = adaptive_fmt["qualityLabel"].as_s
end
params = HTTP::Params.new
fmt.each do |key, value|
params[key] = value
end
adaptive_fmts << params
end
elsif fmts = self.info["adaptive_fmts"]?
fmts.split(",") do |string|
adaptive_fmts << HTTP::Params.parse(string)
end
2018-08-05 13:07:38 +09:00
end
2018-10-02 09:01:44 +09:00
if self.info["region"]?
adaptive_fmts.each do |fmt|
fmt["url"] += "&region=" + self.info["region"]
end
end
adaptive_fmts.each do |fmt|
2019-03-12 03:14:30 +09:00
fmt["url"] += "&host=" + (URI.parse(fmt["url"]).host || "")
fmt["url"] += decrypt_signature(fmt, decrypt_function)
2018-08-05 13:07:38 +09:00
end
return adaptive_fmts
end
2018-08-08 01:39:56 +09:00
def video_streams(adaptive_fmts)
video_streams = adaptive_fmts.select { |s| s["type"].starts_with? "video" }
2018-08-08 01:39:56 +09:00
return video_streams
end
2018-08-05 13:07:38 +09:00
def audio_streams(adaptive_fmts)
audio_streams = adaptive_fmts.select { |s| s["type"].starts_with? "audio" }
2018-08-05 13:07:38 +09:00
audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse!
audio_streams.each do |stream|
stream["bitrate"] = (stream["bitrate"].to_f64/1000).to_i.to_s
end
return audio_streams
end
def player_response
if !@player_json
@player_json = JSON.parse(@info["player_response"])
end
2018-08-05 13:07:38 +09:00
return @player_json.not_nil!
end
2018-10-17 01:15:14 +09:00
def paid
reason = self.player_response["playabilityStatus"]?.try &.["reason"]?
if reason == "This video requires payment to watch."
paid = true
else
paid = false
end
return paid
end
def premium
premium = self.player_response.to_s.includes? "Get YouTube without the ads."
return premium
end
def captions
2018-08-07 08:25:25 +09:00
captions = [] of Caption
2018-08-05 13:07:38 +09:00
if player_response["captions"]?
caption_list = player_response["captions"]["playerCaptionsTracklistRenderer"]["captionTracks"]?.try &.as_a
caption_list ||= [] of JSON::Any
2018-08-07 08:25:25 +09:00
caption_list.each do |caption|
caption = Caption.from_json(caption.to_json)
caption.name.simpleText = caption.name.simpleText.split(" - ")[0]
captions << caption
end
2018-08-05 13:07:38 +09:00
end
return captions
end
def short_description
description = self.description.gsub("<br>", " ")
description = description.gsub("<br/>", " ")
description = XML.parse_html(description).content[0..200].gsub('"', "&quot;").gsub("\n", " ").strip(" ")
if description.empty?
description = " "
end
return description
end
def length_seconds
return self.info["length_seconds"].to_i
end
2018-08-05 05:30:44 +09:00
add_mapping({
id: String,
info: {
type: HTTP::Params,
default: HTTP::Params.parse(""),
converter: Video::HTTPParamConverter,
},
updated: Time,
title: String,
views: Int64,
likes: Int32,
dislikes: Int32,
wilson_score: Float64,
published: Time,
description: String,
language: String?,
author: String,
ucid: String,
allowed_regions: Array(String),
is_family_friendly: Bool,
genre: String,
2018-09-10 04:34:16 +09:00
genre_url: String,
license: String,
sub_count_text: String,
2018-10-30 23:04:01 +09:00
author_thumbnail: String,
2018-08-05 05:30:44 +09:00
})
end
2018-08-07 08:25:25 +09:00
class Caption
JSON.mapping(
name: CaptionName,
baseUrl: String,
languageCode: String
)
end
class CaptionName
JSON.mapping(
simpleText: String,
)
end
class VideoRedirect < Exception
end
def get_video(id, db, proxies = {} of String => Array({ip: String, port: Int32}), refresh = true, region = nil)
2018-11-27 11:46:08 +09:00
if db.query_one?("SELECT EXISTS (SELECT true FROM videos WHERE id = $1)", id, as: Bool) && !region
2018-08-05 05:30:44 +09:00
video = db.query_one("SELECT * FROM videos WHERE id = $1", id, as: Video)
2018-09-10 04:41:29 +09:00
# If record was last updated over 10 minutes ago, refresh (expire param in response lasts for 6 hours)
if refresh && Time.now - video.updated > 10.minutes
2018-08-05 05:30:44 +09:00
begin
video = fetch_video(id, proxies, region)
2018-08-05 05:30:44 +09:00
video_array = video.to_a
2018-09-04 23:50:19 +09:00
2018-08-05 05:30:44 +09:00
args = arg_array(video_array[1..-1], 2)
db.exec("UPDATE videos SET (info,updated,title,views,likes,dislikes,wilson_score,\
2018-10-14 10:03:48 +09:00
published,description,language,author,ucid,allowed_regions,is_family_friendly,\
genre,genre_url,license,sub_count_text,author_thumbnail)\
= (#{args}) WHERE id = $1", video_array)
2018-08-05 05:30:44 +09:00
rescue ex
db.exec("DELETE FROM videos * WHERE id = $1", id)
raise ex
end
end
else
video = fetch_video(id, proxies, region)
2018-08-05 05:30:44 +09:00
video_array = video.to_a
2018-09-04 23:50:19 +09:00
2018-08-05 05:30:44 +09:00
args = arg_array(video_array)
2018-11-27 11:46:08 +09:00
if !region
db.exec("INSERT INTO videos VALUES (#{args}) ON CONFLICT (id) DO NOTHING", video_array)
end
2018-08-05 05:30:44 +09:00
end
return video
end
2019-02-07 07:12:11 +09:00
def extract_player_config(body, html)
params = HTTP::Params.new
2018-08-05 05:30:44 +09:00
if md = body.match(/'XSRF_TOKEN': "(?<session_token>[A-Za-z0-9\_\-\=]+)"/)
params["session_token"] = md["session_token"]
end
if md = body.match(/itct=(?<itct>[^"]+)"/)
params["itct"] = md["itct"]
end
2018-08-05 05:30:44 +09:00
if md = body.match(/'COMMENTS_TOKEN': "(?<ctoken>[^"]+)"/)
params["ctoken"] = md["ctoken"]
end
2018-08-05 05:30:44 +09:00
if md = body.match(/'RELATED_PLAYER_ARGS': (?<rvs>{"rvs":"[^"]+"})/)
params["rvs"] = JSON.parse(md["rvs"])["rvs"].as_s
end
html_info = body.match(/ytplayer\.config = (?<info>.*?);ytplayer\.load/).try &.["info"]
2018-08-05 05:30:44 +09:00
if html_info
JSON.parse(html_info)["args"].as_h.each do |key, value|
params[key] = value.to_s
2019-02-07 07:12:11 +09:00
end
else
error_message = html.xpath_node(%q(//h1[@id="unavailable-message"]))
if error_message
params["reason"] = error_message.content.strip
else
params["reason"] = "Could not extract video info."
end
2018-08-05 05:30:44 +09:00
end
2019-02-07 07:12:11 +09:00
return params
end
def fetch_video(id, proxies, region)
client = make_client(YT_URL, proxies, region)
response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
if md = response.headers["location"]?.try &.match(/v=(?<id>[a-zA-Z0-9_-]{11})/)
raise VideoRedirect.new(md["id"])
end
2019-02-07 07:12:11 +09:00
html = XML.parse_html(response.body)
info = extract_player_config(response.body, html)
info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
2018-08-05 05:30:44 +09:00
2019-02-07 07:12:11 +09:00
# Try to use proxies for region-blocked videos
2018-08-13 23:17:28 +09:00
if info["reason"]? && info["reason"].includes? "your country"
2019-02-07 07:12:11 +09:00
bypass_channel = Channel({XML::Node, HTTP::Params} | Nil).new
2018-08-13 23:17:28 +09:00
2018-11-21 01:07:50 +09:00
proxies.each do |proxy_region, list|
2018-08-13 23:17:28 +09:00
spawn do
2018-11-21 01:07:50 +09:00
client = make_client(YT_URL, proxies, proxy_region)
proxy_response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
2018-10-04 00:38:07 +09:00
proxy_html = XML.parse_html(proxy_response.body)
proxy_info = extract_player_config(proxy_response.body, proxy_html)
2019-02-07 07:12:11 +09:00
if !proxy_info["reason"]?
proxy_info["region"] = proxy_region
proxy_info["cookie"] = proxy_response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
2019-02-07 07:12:11 +09:00
bypass_channel.send({proxy_html, proxy_info})
2018-11-21 01:07:50 +09:00
else
2018-10-04 00:38:07 +09:00
bypass_channel.send(nil)
end
2018-08-13 23:17:28 +09:00
end
end
2018-09-26 08:07:00 +09:00
proxies.size.times do
2018-11-21 01:07:50 +09:00
response = bypass_channel.receive
if response
2019-02-07 07:12:11 +09:00
html, info = response
break
2018-08-13 23:17:28 +09:00
end
end
end
2019-02-07 07:12:11 +09:00
# Try to pull streams from embed URL
2018-08-05 05:30:44 +09:00
if info["reason"]?
embed_page = client.get("/embed/#{id}").body
sts = embed_page.match(/"sts"\s*:\s*(?<sts>\d+)/).try &.["sts"]?
sts ||= ""
embed_info = HTTP::Params.parse(client.get("/get_video_info?video_id=#{id}&eurl=https://youtube.googleapis.com/v/#{id}&gl=US&hl=en&disable_polymer=1&sts=#{sts}").body)
2018-11-11 01:50:09 +09:00
2019-02-07 07:12:11 +09:00
if !embed_info["reason"]?
embed_info.each do |key, value|
info[key] = value.to_s
end
2019-02-07 07:12:11 +09:00
else
2018-11-11 01:50:09 +09:00
raise info["reason"]
end
2018-08-05 05:30:44 +09:00
end
2019-01-31 23:48:44 +09:00
if info["errorcode"]?.try &.== "2"
raise "Video unavailable."
end
2018-08-05 05:30:44 +09:00
title = info["title"]
author = info["author"]
ucid = info["ucid"]
2018-11-02 22:09:28 +09:00
views = html.xpath_node(%q(//meta[@itemprop="interactionCount"]))
views = views.try &.["content"].to_i64?
views ||= 0_i64
2018-08-05 05:30:44 +09:00
likes = html.xpath_node(%q(//button[@title="I like this"]/span))
2018-11-02 22:09:28 +09:00
likes = likes.try &.content.delete(",").try &.to_i?
2018-08-05 05:30:44 +09:00
likes ||= 0
dislikes = html.xpath_node(%q(//button[@title="I dislike this"]/span))
2018-11-02 22:09:28 +09:00
dislikes = dislikes.try &.content.delete(",").try &.to_i?
2018-08-05 05:30:44 +09:00
dislikes ||= 0
avg_rating = (likes.to_f/(likes.to_f + dislikes.to_f) * 4 + 1)
avg_rating = avg_rating.nan? ? 0.0 : avg_rating
info["avg_rating"] = "#{avg_rating}"
2019-01-10 23:06:54 +09:00
2018-08-05 05:30:44 +09:00
description = html.xpath_node(%q(//p[@id="eow-description"]))
description = description ? description.to_xml : ""
wilson_score = ci_lower_bound(likes, likes + dislikes)
published = html.xpath_node(%q(//meta[@itemprop="datePublished"])).try &.["content"]
published ||= Time.now.to_s("%Y-%m-%d")
2018-08-05 05:30:44 +09:00
published = Time.parse(published, "%Y-%m-%d", Time::Location.local)
allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",")
allowed_regions ||= [] of String
2019-02-07 07:12:11 +09:00
is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).try &.["content"] == "True"
is_family_friendly ||= true
2018-09-04 23:50:19 +09:00
genre = html.xpath_node(%q(//meta[@itemprop="genre"])).try &.["content"]
genre ||= ""
2019-01-26 02:35:25 +09:00
genre_url = html.xpath_node(%(//ul[contains(@class, "watch-info-tag-list")]/li/a[text()="#{genre}"])).try &.["href"]
# Sometimes YouTube tries to link to invalid/missing channels, so we fix that here
case genre
2019-01-26 02:35:25 +09:00
when "Education"
genre_url = "/channel/UCdxpofrI-dO6oYfsqHDHphw"
when "Gaming"
genre_url = "/channel/UCOpNcN46UbXVtpKMrmU4Abg"
when "Movies"
genre_url = "/channel/UClgRkhTL3_hImCAmdLfDE4g"
2019-01-26 02:35:25 +09:00
when "Nonprofits & Activism"
genre_url = "/channel/UCfFyYRYslvuhwMDnx6KjUvw"
when "Trailers"
genre_url = "/channel/UClgRkhTL3_hImCAmdLfDE4g"
end
genre_url ||= ""
2018-08-05 05:30:44 +09:00
2018-09-10 04:47:26 +09:00
license = html.xpath_node(%q(//h4[contains(text(),"License")]/parent::*/ul/li))
if license
license = license.content
else
license = ""
end
sub_count_text = html.xpath_node(%q(//span[contains(@class, "yt-subscriber-count")]))
if sub_count_text
sub_count_text = sub_count_text["title"]
else
sub_count_text = "0"
2018-09-10 04:47:26 +09:00
end
2018-11-09 09:32:47 +09:00
author_thumbnail = html.xpath_node(%(//span[@class="yt-thumb-clip"]/img))
if author_thumbnail
author_thumbnail = author_thumbnail["data-thumb"]
else
author_thumbnail = ""
end
2018-08-05 05:30:44 +09:00
video = Video.new(id, info, Time.now, title, views, likes, dislikes, wilson_score, published, description,
nil, author, ucid, allowed_regions, is_family_friendly, genre, genre_url, license, sub_count_text, author_thumbnail)
2018-08-05 05:30:44 +09:00
return video
end
2018-08-12 23:24:59 +09:00
def itag_to_metadata?(itag : String)
2018-08-12 23:34:26 +09:00
return VIDEO_FORMATS[itag]?
2018-08-05 05:30:44 +09:00
end
2018-08-05 13:07:38 +09:00
def process_video_params(query, preferences)
autoplay = query["autoplay"]?.try &.to_i?
2018-11-12 02:45:05 +09:00
continue = query["continue"]?.try &.to_i?
2018-10-30 23:41:23 +09:00
listen = query["listen"]? && (query["listen"] == "true" || query["listen"] == "1").to_unsafe
local = query["local"]? && (query["local"] == "true").to_unsafe
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
quality = query["quality"]?
region = query["region"]?
related_videos = query["related_videos"]?
speed = query["speed"]?.try &.to_f?
2018-08-05 13:07:38 +09:00
video_loop = query["loop"]?.try &.to_i?
volume = query["volume"]?.try &.to_i?
2018-08-05 13:07:38 +09:00
if preferences
# region ||= preferences.region
2018-08-05 13:07:38 +09:00
autoplay ||= preferences.autoplay.to_unsafe
2018-11-12 02:45:05 +09:00
continue ||= preferences.continue.to_unsafe
2019-02-05 06:28:51 +09:00
related_videos ||= preferences.related_videos.to_unsafe
2018-10-30 23:41:23 +09:00
listen ||= preferences.listen.to_unsafe
preferred_captions ||= preferences.captions
quality ||= preferences.quality
speed ||= preferences.speed
2018-08-05 13:07:38 +09:00
video_loop ||= preferences.video_loop.to_unsafe
volume ||= preferences.volume
2018-08-05 13:07:38 +09:00
end
autoplay ||= DEFAULT_USER_PREFERENCES.autoplay.to_unsafe
continue ||= DEFAULT_USER_PREFERENCES.continue.to_unsafe
2019-02-05 06:28:51 +09:00
related_videos ||= DEFAULT_USER_PREFERENCES.related_videos.to_unsafe
listen ||= DEFAULT_USER_PREFERENCES.listen.to_unsafe
preferred_captions ||= DEFAULT_USER_PREFERENCES.captions
quality ||= DEFAULT_USER_PREFERENCES.quality
speed ||= DEFAULT_USER_PREFERENCES.speed
video_loop ||= DEFAULT_USER_PREFERENCES.video_loop.to_unsafe
volume ||= DEFAULT_USER_PREFERENCES.volume
autoplay = autoplay == 1
2018-11-12 02:45:05 +09:00
continue = continue == 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
if query["t"]?
video_start = decode_time(query["t"])
end
video_start ||= 0
2018-08-31 10:25:43 +09:00
if query["time_continue"]?
2018-08-31 11:04:41 +09:00
video_start = decode_time(query["time_continue"])
2018-08-06 02:35:33 +09:00
end
video_start ||= 0
if query["start"]?
video_start = decode_time(query["start"])
end
2018-08-05 13:07:38 +09:00
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
controls = controls == 1
params = {
autoplay: autoplay,
2018-11-12 02:45:05 +09:00
continue: continue,
controls: controls,
listen: listen,
local: local,
preferred_captions: preferred_captions,
quality: quality,
raw: raw,
region: region,
2019-02-05 06:28:51 +09:00
related_videos: related_videos,
speed: speed,
video_end: video_end,
video_loop: video_loop,
video_start: video_start,
volume: volume,
}
return params
2018-08-05 13:07:38 +09:00
end
2018-08-10 22:50:25 +09:00
2019-03-09 05:42:37 +09:00
def build_thumbnails(id, config, kemal_config)
return {
{name: "maxres", host: "#{make_host_url(config, kemal_config)}", url: "maxres", height: 720, width: 1280},
{name: "maxresdefault", host: "https://i.ytimg.com", url: "maxresdefault", height: 720, width: 1280},
{name: "sddefault", host: "https://i.ytimg.com", url: "sddefault", height: 480, width: 640},
{name: "high", host: "https://i.ytimg.com", url: "hqdefault", height: 360, width: 480},
{name: "medium", host: "https://i.ytimg.com", url: "mqdefault", height: 180, width: 320},
{name: "default", host: "https://i.ytimg.com", url: "default", height: 90, width: 120},
{name: "start", host: "https://i.ytimg.com", url: "1", height: 90, width: 120},
{name: "middle", host: "https://i.ytimg.com", url: "2", height: 90, width: 120},
{name: "end", host: "https://i.ytimg.com", url: "3", height: 90, width: 120},
}
end
def generate_thumbnails(json, id, config, kemal_config)
2018-08-10 22:50:25 +09:00
json.array do
2019-03-09 05:42:37 +09:00
build_thumbnails(id, config, kemal_config).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