2021-08-24 01:28:30 +02:00
module Invidious::Routes::API::V1::Videos
2021-08-13 08:31:12 +02:00
def self . videos ( env )
2021-11-08 23:52:55 +01:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2021-07-22 06:34:16 +02:00
env . response . content_type = " application/json "
id = env . params . url [ " id " ]
region = env . params . query [ " region " ]?
2023-01-15 17:04:04 +01:00
proxy = { " 1 " , " true " } . any? & . == env . params . query [ " local " ]?
2021-07-22 06:34:16 +02:00
begin
2021-12-07 02:55:43 +01:00
video = get_video ( id , region : region )
2022-05-27 15:36:13 +02:00
rescue ex : NotFoundException
return error_json ( 404 , ex )
2021-07-22 06:34:16 +02:00
rescue ex
2021-08-13 08:31:12 +02:00
return error_json ( 500 , ex )
2021-07-22 06:34:16 +02:00
end
2023-01-15 17:04:04 +01:00
return JSON . build do | json |
Invidious :: JSONify :: APIv1 . video ( video , json , locale : locale , proxy : proxy )
end
2021-07-22 06:34:16 +02:00
end
2021-08-12 20:46:03 +02:00
def self . captions ( env )
2021-07-22 06:34:16 +02:00
env . response . content_type = " application/json "
id = env . params . url [ " id " ]
2022-02-22 18:11:11 +01:00
region = env . params . query [ " region " ]? || env . params . body [ " region " ]?
if id . nil? || id . size != 11 || ! id . matches? ( / ^[ \ w-]+$ / )
return error_json ( 400 , " Invalid video ID " )
end
2021-07-22 06:34:16 +02:00
# See https://github.com/ytdl-org/youtube-dl/blob/6ab30ff50bf6bd0585927cb73c7421bef184f87a/youtube_dl/extractor/youtube.py#L1354
# It is possible to use `/api/timedtext?type=list&v=#{id}` and
# `/api/timedtext?type=track&v=#{id}&lang=#{lang_code}` directly,
# but this does not provide links for auto-generated captions.
#
# In future this should be investigated as an alternative, since it does not require
# getting video info.
begin
2021-12-07 02:55:43 +01:00
video = get_video ( id , region : region )
2022-05-27 15:36:13 +02:00
rescue ex : NotFoundException
haltf env , 404
2021-07-22 06:34:16 +02:00
rescue ex
2021-08-24 01:28:30 +02:00
haltf env , 500
2021-07-22 06:34:16 +02:00
end
captions = video . captions
label = env . params . query [ " label " ]?
lang = env . params . query [ " lang " ]?
tlang = env . params . query [ " tlang " ]?
if ! label && ! lang
response = JSON . build do | json |
json . object do
json . field " captions " do
json . array do
captions . each do | caption |
json . object do
json . field " label " , caption . name
2021-09-25 04:15:23 +02:00
json . field " languageCode " , caption . language_code
2021-07-22 06:34:16 +02:00
json . field " url " , " /api/v1/captions/ #{ id } ?label= #{ URI . encode_www_form ( caption . name ) } "
end
end
end
end
end
end
return response
end
env . response . content_type = " text/vtt; charset=UTF-8 "
if lang
2022-01-20 16:15:59 +01:00
caption = captions . select ( & . language_code . == lang )
2021-07-22 06:34:16 +02:00
else
2022-01-20 16:15:59 +01:00
caption = captions . select ( & . name . == label )
2021-07-22 06:34:16 +02:00
end
if caption . empty?
2021-08-24 01:28:30 +02:00
haltf env , 404
2021-07-22 06:34:16 +02:00
else
caption = caption [ 0 ]
end
2023-07-23 14:02:02 +02:00
if CONFIG . use_innertube_for_captions
params = Invidious :: Videos :: Transcript . generate_param ( id , caption . language_code , caption . auto_generated )
2023-07-24 01:50:40 +02:00
initial_data = YoutubeAPI . get_transcript ( params )
2021-07-22 06:34:16 +02:00
2023-07-23 14:02:02 +02:00
webvtt = Invidious :: Videos :: Transcript . convert_transcripts_to_vtt ( initial_data , caption . language_code )
else
# Timedtext API handling
url = URI . parse ( " #{ caption . base_url } &tlang= #{ tlang } " ) . request_target
# Auto-generated captions often have cues that aren't aligned properly with the video,
# as well as some other markup that makes it cumbersome, so we try to fix that here
if caption . name . includes? " auto-generated "
caption_xml = YT_POOL . client & . get ( url ) . body
2023-08-25 00:10:50 +02:00
settings_field = {
" Kind " = > " captions " ,
" Language " = > " #{ tlang || caption . language_code } " ,
}
2023-07-23 14:02:02 +02:00
if caption_xml . starts_with? ( " <?xml " )
webvtt = caption . timedtext_to_vtt ( caption_xml , tlang )
else
caption_xml = XML . parse ( caption_xml )
2023-08-25 00:10:50 +02:00
webvtt = WebVTT . build ( settings_field ) do | webvtt |
2023-07-23 14:02:02 +02:00
caption_nodes = caption_xml . xpath_nodes ( " //transcript/text " )
caption_nodes . each_with_index do | node , i |
start_time = node [ " start " ] . to_f . seconds
duration = node [ " dur " ]? . try & . to_f . seconds
duration || = start_time
if caption_nodes . size > i + 1
end_time = caption_nodes [ i + 1 ] [ " start " ] . to_f . seconds
else
end_time = start_time + duration
end
2021-07-22 06:34:16 +02:00
2023-07-23 14:02:02 +02:00
text = HTML . unescape ( node . content )
text = text . gsub ( / <font color=" # [a-fA-F0-9]{6}"> / , " " )
text = text . gsub ( / < \/ font> / , " " )
if md = text . match ( / (?<name>.*) : (?<text>.*) / )
text = " <v #{ md [ " name " ] } > #{ md [ " text " ] } </v> "
end
2021-07-22 06:34:16 +02:00
2023-08-25 00:42:42 +02:00
webvtt . cue ( start_time , end_time , text )
2023-01-03 16:25:05 +01:00
end
end
2021-07-22 06:34:16 +02:00
end
2023-01-03 16:17:47 +01:00
else
2023-10-08 12:40:49 +02:00
webvtt = YT_POOL . client & . get ( " #{ url } &fmt=vtt " ) . body
2023-07-23 14:02:02 +02:00
if webvtt . starts_with? ( " <?xml " )
webvtt = caption . timedtext_to_vtt ( webvtt )
else
2023-10-08 12:40:49 +02:00
# Some captions have "align:[start/end]" and "position:[num]%"
# attributes. Those are causing issues with VideoJS, which is unable
# to properly align the captions on the video, so we remove them.
#
# See: https://github.com/iv-org/invidious/issues/2391
webvtt = webvtt . gsub ( / ([0-9:.]{12} --> [0-9:.]{12}).+ / , " \\ 1 " )
2023-07-23 14:02:02 +02:00
end
2023-01-03 16:25:05 +01:00
end
2021-07-22 06:34:16 +02:00
end
if title = env . params . query [ " title " ]?
# https://blog.fastmail.com/2011/06/24/download-non-english-filenames/
env . response . headers [ " Content-Disposition " ] = " attachment; filename= \" #{ URI . encode_www_form ( title ) } \" ; filename*=UTF-8'' #{ URI . encode_www_form ( title ) } "
end
webvtt
end
2021-08-13 08:31:12 +02:00
# Fetches YouTube storyboards
#
# Which are sprites containing x * y preview
# thumbnails for individual scenes in a video.
# See https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
def self . storyboards ( env )
env . response . content_type = " application/json "
id = env . params . url [ " id " ]
region = env . params . query [ " region " ]?
begin
2021-12-07 02:55:43 +01:00
video = get_video ( id , region : region )
2022-05-27 15:36:13 +02:00
rescue ex : NotFoundException
haltf env , 404
2021-08-13 08:31:12 +02:00
rescue ex
2021-08-24 01:28:30 +02:00
haltf env , 500
2021-08-13 08:31:12 +02:00
end
storyboards = video . storyboards
width = env . params . query [ " width " ]?
height = env . params . query [ " height " ]?
if ! width && ! height
response = JSON . build do | json |
json . object do
json . field " storyboards " do
2022-08-23 19:03:09 +02:00
Invidious :: JSONify :: APIv1 . storyboards ( json , id , storyboards )
2021-08-13 08:31:12 +02:00
end
end
end
return response
end
env . response . content_type = " text/vtt "
2022-01-20 16:15:59 +01:00
storyboard = storyboards . select { | sb | width == " #{ sb [ :width ] } " || height == " #{ sb [ :height ] } " }
2021-08-13 08:31:12 +02:00
if storyboard . empty?
2021-08-24 01:28:30 +02:00
haltf env , 404
2021-08-13 08:31:12 +02:00
else
storyboard = storyboard [ 0 ]
end
2023-08-25 00:10:50 +02:00
WebVTT . build do | vtt |
2021-08-13 08:31:12 +02:00
start_time = 0 . milliseconds
end_time = storyboard [ :interval ] . milliseconds
storyboard [ :storyboard_count ] . times do | i |
url = storyboard [ :url ]
authority = / (i \ d?).ytimg.com / . match ( url ) . not_nil! [ 1 ]?
url = url . gsub ( " $M " , i ) . gsub ( %r( https://i \ d?.ytimg.com/sb/ ) , " " )
url = " #{ HOST_URL } /sb/ #{ authority } / #{ url } "
storyboard [ :storyboard_height ] . times do | j |
storyboard [ :storyboard_width ] . times do | k |
2023-08-25 00:10:50 +02:00
current_cue_url = " #{ url } # xywh= #{ storyboard [ :width ] * k } , #{ storyboard [ :height ] * j } , #{ storyboard [ :width ] - 2 } , #{ storyboard [ :height ] } "
2023-08-25 00:42:42 +02:00
vtt . cue ( start_time , end_time , current_cue_url )
2021-08-13 08:31:12 +02:00
start_time += storyboard [ :interval ] . milliseconds
end_time += storyboard [ :interval ] . milliseconds
end
end
end
end
end
2021-08-12 20:46:03 +02:00
def self . annotations ( env )
2021-07-22 06:34:16 +02:00
env . response . content_type = " text/xml "
id = env . params . url [ " id " ]
source = env . params . query [ " source " ]?
source || = " archive "
if ! id . match ( / [a-zA-Z0-9_-]{11} / )
2021-08-24 01:28:30 +02:00
haltf env , 400
2021-07-22 06:34:16 +02:00
end
annotations = " "
case source
when " archive "
2021-12-06 17:24:49 +01:00
if CONFIG . cache_annotations && ( cached_annotation = Invidious :: Database :: Annotations . select ( id ) )
2021-07-22 06:34:16 +02:00
annotations = cached_annotation . annotations
else
index = CHARS_SAFE . index ( id [ 0 ] ) . not_nil! . to_s . rjust ( 2 , '0' )
# IA doesn't handle leading hyphens,
# so we use https://archive.org/details/youtubeannotations_64
if index == " 62 "
index = " 64 "
id = id . sub ( / ^- / , 'A' )
end
file = URI . encode_www_form ( " #{ id [ 0 , 3 ] } / #{ id } .xml " )
location = make_client ( ARCHIVE_URL , & . get ( " /download/youtubeannotations_ #{ index } / #{ id [ 0 , 2 ] } .tar/ #{ file } " ) )
if ! location . headers [ " Location " ]?
env . response . status_code = location . status_code
end
response = make_client ( URI . parse ( location . headers [ " Location " ] ) , & . get ( location . headers [ " Location " ] ) )
if response . body . empty?
2021-08-24 01:28:30 +02:00
haltf env , 404
2021-07-22 06:34:16 +02:00
end
if response . status_code != 200
2021-08-24 01:28:30 +02:00
haltf env , response . status_code
2021-07-22 06:34:16 +02:00
end
annotations = response . body
2021-12-07 02:55:43 +01:00
cache_annotation ( id , annotations )
2021-07-22 06:34:16 +02:00
end
else # "youtube"
response = YT_POOL . client & . get ( " /annotations_invideo?video_id= #{ id } " )
if response . status_code != 200
2021-08-24 01:28:30 +02:00
haltf env , response . status_code
2021-07-22 06:34:16 +02:00
end
annotations = response . body
end
etag = sha256 ( annotations ) [ 0 , 16 ]
if env . request . headers [ " If-None-Match " ]? . try & . == etag
2021-08-24 01:28:30 +02:00
haltf env , 304
2021-07-22 06:34:16 +02:00
else
env . response . headers [ " ETag " ] = etag
annotations
end
end
2021-08-12 20:46:03 +02:00
def self . comments ( env )
2021-11-08 23:52:55 +01:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2021-08-12 20:46:03 +02:00
region = env . params . query [ " region " ]?
env . response . content_type = " application/json "
id = env . params . url [ " id " ]
source = env . params . query [ " source " ]?
source || = " youtube "
thin_mode = env . params . query [ " thin_mode " ]?
thin_mode = thin_mode == " true "
format = env . params . query [ " format " ]?
format || = " json "
action = env . params . query [ " action " ]?
action || = " action_get_comments "
continuation = env . params . query [ " continuation " ]?
sort_by = env . params . query [ " sort_by " ]? . try & . downcase
if source == " youtube "
sort_by || = " top "
begin
2023-05-06 19:56:30 +02:00
comments = Comments . fetch_youtube ( id , continuation , format , locale , thin_mode , region , sort_by : sort_by )
2022-05-27 15:36:13 +02:00
rescue ex : NotFoundException
return error_json ( 404 , ex )
2021-08-12 20:46:03 +02:00
rescue ex
return error_json ( 500 , ex )
end
return comments
elsif source == " reddit "
sort_by || = " confidence "
begin
2023-05-06 20:02:42 +02:00
comments , reddit_thread = Comments . fetch_reddit ( id , sort_by : sort_by )
2021-08-12 20:46:03 +02:00
rescue ex
comments = nil
reddit_thread = nil
end
if ! reddit_thread || ! comments
2022-01-17 19:11:47 +01:00
return error_json ( 404 , " No reddit threads found " )
2021-08-12 20:46:03 +02:00
end
if format == " json "
reddit_thread = JSON . parse ( reddit_thread . to_json ) . as_h
reddit_thread [ " comments " ] = JSON . parse ( comments . to_json )
return reddit_thread . to_json
else
2023-05-06 20:12:02 +02:00
content_html = Frontend :: Comments . template_reddit ( comments , locale )
2023-05-06 20:20:27 +02:00
content_html = Comments . fill_links ( content_html , " https " , " www.reddit.com " )
content_html = Comments . replace_links ( content_html )
2021-08-12 20:46:03 +02:00
response = {
" title " = > reddit_thread . title ,
" permalink " = > reddit_thread . permalink ,
" contentHtml " = > content_html ,
}
return response . to_json
end
end
end
2023-11-15 05:35:11 +01:00
def self . clips ( env )
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
env . response . content_type = " application/json "
clip_id = env . params . url [ " id " ]
region = env . params . query [ " region " ]?
proxy = { " 1 " , " true " } . any? & . == env . params . query [ " local " ]?
response = YoutubeAPI . resolve_url ( " https://www.youtube.com/clip/ #{ clip_id } " )
return error_json ( 400 , " Invalid clip ID " ) if response [ " error " ]?
video_id = response . dig? ( " endpoint " , " watchEndpoint " , " videoId " ) . try & . as_s
return error_json ( 400 , " Invalid clip ID " ) if video_id . nil?
start_time = nil
end_time = nil
clip_title = nil
if params = response . dig? ( " endpoint " , " watchEndpoint " , " params " ) . try & . as_s
start_time , end_time , clip_title = parse_clip_parameters ( params )
end
begin
video = get_video ( video_id , region : region )
rescue ex : NotFoundException
return error_json ( 404 , ex )
rescue ex
return error_json ( 500 , ex )
end
return JSON . build do | json |
json . object do
json . field " startTime " , start_time
json . field " endTime " , end_time
json . field " clipTitle " , clip_title
json . field " video " do
Invidious :: JSONify :: APIv1 . video ( video , json , locale : locale , proxy : proxy )
end
end
end
end
2021-07-22 06:34:16 +02:00
end