2019-04-03 16:35:58 +00:00
require " ./macros "
2019-04-10 21:23:37 +00:00
struct Nonce
db_mapping ( {
nonce : String ,
expire : Time ,
} )
end
struct SessionId
db_mapping ( {
id : String ,
email : String ,
issued : String ,
} )
end
2019-04-15 16:13:09 +00:00
struct Annotation
db_mapping ( {
id : String ,
annotations : String ,
} )
end
2019-04-03 16:35:58 +00:00
struct ConfigPreferences
module StringToArray
2019-08-15 16:29:55 +00:00
def self . to_json ( value : Array ( String ) , json : JSON :: Builder )
json . array do
value . each do | element |
json . string element
end
end
end
def self . from_json ( value : JSON :: PullParser ) : Array ( String )
begin
result = [ ] of String
value . read_array do
result << HTML . escape ( value . read_string [ 0 , 100 ] )
end
rescue ex
result = [ HTML . escape ( value . read_string [ 0 , 100 ] ) , " " ]
end
result
end
2019-04-03 16:35:58 +00:00
def self . to_yaml ( value : Array ( String ) , yaml : YAML :: Nodes :: Builder )
yaml . sequence do
value . each do | element |
yaml . scalar element
end
end
end
def self . from_yaml ( ctx : YAML :: ParseContext , node : YAML :: Nodes :: Node ) : Array ( String )
begin
unless node . is_a? ( YAML :: Nodes :: Sequence )
node . raise " Expected sequence, not #{ node . class } "
end
result = [ ] of String
2019-04-04 00:04:33 +00:00
node . nodes . each do | item |
unless item . is_a? ( YAML :: Nodes :: Scalar )
node . raise " Expected scalar, not #{ item . class } "
2019-04-03 16:35:58 +00:00
end
2019-08-15 16:29:55 +00:00
result << HTML . escape ( item . value [ 0 , 100 ] )
2019-04-03 16:35:58 +00:00
end
rescue ex
if node . is_a? ( YAML :: Nodes :: Scalar )
2019-08-15 16:29:55 +00:00
result = [ HTML . escape ( node . value [ 0 , 100 ] ) , " " ]
2019-04-03 16:35:58 +00:00
else
result = [ " " , " " ]
end
end
result
end
end
2019-08-15 16:29:55 +00:00
module BoolToString
def self . to_json ( value : String , json : JSON :: Builder )
json . string value
end
def self . from_json ( value : JSON :: PullParser ) : String
begin
result = value . read_string
if result . empty?
CONFIG . default_user_preferences . dark_mode
else
result
end
rescue ex
2019-10-21 00:42:18 +00:00
if value . read_bool
2019-08-15 16:29:55 +00:00
" dark "
else
" light "
end
end
end
def self . to_yaml ( value : String , yaml : YAML :: Nodes :: Builder )
yaml . scalar value
end
def self . from_yaml ( ctx : YAML :: ParseContext , node : YAML :: Nodes :: Node ) : String
unless node . is_a? ( YAML :: Nodes :: Scalar )
2019-10-21 00:42:18 +00:00
node . raise " Expected scalar, not #{ node . class } "
2019-08-15 16:29:55 +00:00
end
case node . value
when " true "
" dark "
when " false "
" light "
when " "
CONFIG . default_user_preferences . dark_mode
else
node . value
end
end
end
2019-04-03 16:35:58 +00:00
yaml_mapping ( {
2019-05-01 04:39:04 +00:00
annotations : { type : Bool , default : false } ,
annotations_subscribed : { type : Bool , default : false } ,
autoplay : { type : Bool , default : false } ,
captions : { type : Array ( String ) , default : [ " " , " " , " " ] , converter : StringToArray } ,
comments : { type : Array ( String ) , default : [ " youtube " , " " ] , converter : StringToArray } ,
continue : { type : Bool , default : false } ,
continue_autoplay : { type : Bool , default : true } ,
2019-11-04 22:08:13 +00:00
dark_mode : { type : String , default : " " , converter : BoolToString } ,
2019-05-01 04:39:04 +00:00
latest_only : { type : Bool , default : false } ,
listen : { type : Bool , default : false } ,
local : { type : Bool , default : false } ,
locale : { type : String , default : " en-US " } ,
max_results : { type : Int32 , default : 40 } ,
notifications_only : { type : Bool , default : false } ,
2019-08-09 19:09:24 +00:00
player_style : { type : String , default : " invidious " } ,
2019-05-01 04:39:04 +00:00
quality : { type : String , default : " hd720 " } ,
2019-10-21 00:42:18 +00:00
default_home : { type : String , default : " Popular " } ,
feed_menu : { type : Array ( String ) , default : [ " Popular " , " Trending " , " Subscriptions " , " Playlists " ] } ,
2019-05-01 04:39:04 +00:00
related_videos : { type : Bool , default : true } ,
sort : { type : String , default : " published " } ,
speed : { type : Float32 , default : 1.0_f32 } ,
thin_mode : { type : Bool , default : false } ,
unseen_only : { type : Bool , default : false } ,
video_loop : { type : Bool , default : false } ,
volume : { type : Int32 , default : 100 } ,
2019-04-03 16:35:58 +00:00
} )
end
2019-03-29 21:30:02 +00:00
struct Config
2019-04-03 16:35:58 +00:00
module ConfigPreferencesConverter
2019-07-18 23:51:10 +00:00
def self . to_yaml ( value : Preferences , yaml : YAML :: Nodes :: Builder )
value . to_yaml ( yaml )
end
2019-04-03 16:35:58 +00:00
def self . from_yaml ( ctx : YAML :: ParseContext , node : YAML :: Nodes :: Node ) : Preferences
Preferences . new ( * ConfigPreferences . new ( ctx , node ) . to_tuple )
end
2019-07-18 23:51:10 +00:00
end
2019-04-03 16:35:58 +00:00
2019-07-18 23:51:10 +00:00
module FamilyConverter
def self . to_yaml ( value : Socket :: Family , yaml : YAML :: Nodes :: Builder )
case value
when Socket :: Family :: UNSPEC
yaml . scalar nil
when Socket :: Family :: INET
yaml . scalar " ipv4 "
when Socket :: Family :: INET6
yaml . scalar " ipv6 "
2020-04-09 17:18:09 +00:00
when Socket :: Family :: UNIX
raise " Invalid socket family #{ value } "
2019-07-18 23:51:10 +00:00
end
end
def self . from_yaml ( ctx : YAML :: ParseContext , node : YAML :: Nodes :: Node ) : Socket :: Family
if node . is_a? ( YAML :: Nodes :: Scalar )
case node . value . downcase
when " ipv4 "
Socket :: Family :: INET
when " ipv6 "
Socket :: Family :: INET6
else
Socket :: Family :: UNSPEC
end
else
node . raise " Expected scalar, not #{ node . class } "
end
2019-04-03 16:35:58 +00:00
end
end
2019-11-09 19:18:19 +00:00
module StringToCookies
def self . to_yaml ( value : HTTP :: Cookies , yaml : YAML :: Nodes :: Builder )
( value . map { | c | " #{ c . name } = #{ c . value } " } ) . join ( " ; " ) . to_yaml ( yaml )
end
def self . from_yaml ( ctx : YAML :: ParseContext , node : YAML :: Nodes :: Node ) : HTTP :: Cookies
unless node . is_a? ( YAML :: Nodes :: Scalar )
node . raise " Expected scalar, not #{ node . class } "
end
cookies = HTTP :: Cookies . new
node . value . split ( " ; " ) . each do | cookie |
next if cookie . strip . empty?
name , value = cookie . split ( " = " , 2 )
cookies << HTTP :: Cookie . new ( name . strip , value . strip )
end
cookies
end
end
2019-07-07 14:07:53 +00:00
def disabled? ( option )
case disabled = CONFIG . disable_proxy
when Bool
return disabled
when Array
if disabled . includes? option
return true
else
return false
end
2020-04-09 17:18:09 +00:00
else
return false
2019-07-07 14:07:53 +00:00
end
end
2018-08-04 20:30:44 +00:00
YAML . mapping ( {
2019-05-21 14:00:35 +00:00
channel_threads : Int32 , # Number of threads to use for crawling videos from channels (for updating subscriptions)
feed_threads : Int32 , # Number of threads to use for updating feeds
db : DBConfig , # Database configuration
2019-04-04 12:49:53 +00:00
full_refresh : Bool , # Used for crawling channels: threads should check all videos uploaded by a channel
https_only : Bool ?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
hmac_key : String ?, # HMAC signing key for CSRF tokens and verifying pubsub subscriptions
domain : String ?, # Domain to be used for links to resources on the site where an absolute URL is required
use_pubsub_feeds : { type : Bool | Int32 , default : false } , # Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
2019-04-03 16:35:58 +00:00
captcha_enabled : { type : Bool , default : true } ,
login_enabled : { type : Bool , default : true } ,
registration_enabled : { type : Bool , default : true } ,
statistics_enabled : { type : Bool , default : false } ,
admins : { type : Array ( String ) , default : [ ] of String } ,
external_port : { type : Int32 ?, default : nil } ,
default_user_preferences : { type : Preferences ,
default : Preferences . new ( * ConfigPreferences . from_yaml ( " " ) . to_tuple ) ,
converter : ConfigPreferencesConverter ,
} ,
2019-07-18 23:51:10 +00:00
dmca_content : { type : Array ( String ) , default : [ ] of String } , # For compliance with DMCA, disables download widget using list of video IDs
check_tables : { type : Bool , default : false } , # Check table integrity, automatically try to add any missing columns, create tables, etc.
cache_annotations : { type : Bool , default : false } , # Cache annotations requested from IA, will not cache empty annotations or annotations that only contain cards
banner : { type : String ?, default : nil } , # Optional banner to be displayed along top of page for announcements, etc.
hsts : { type : Bool ?, default : true } , # Enables 'Strict-Transport-Security'. Ensure that `domain` and all subdomains are served securely
disable_proxy : { type : Bool ? | Array ( String ) ?, default : false } , # Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
force_resolve : { type : Socket :: Family , default : Socket :: Family :: UNSPEC , converter : FamilyConverter } , # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
2019-09-23 17:05:29 +00:00
port : { type : Int32 , default : 3000 } , # Port to listen for connections (overrided by command line argument)
host_binding : { type : String , default : " 0.0.0.0 " } , # Host to bind (overrided by command line argument)
2019-10-27 18:18:07 +00:00
pool_size : { type : Int32 , default : 100 } , # Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
admin_email : { type : String , default : " omarroth@protonmail.com " } , # Email for bug reports
2019-11-09 19:18:19 +00:00
cookies : { type : HTTP :: Cookies , default : HTTP :: Cookies . new , converter : StringToCookies } , # Saved cookies in "name1=value1; name2=value2..." format
captcha_key : { type : String ?, default : nil } , # Key for Anti-Captcha
2018-08-04 20:30:44 +00:00
} )
end
2019-05-21 14:00:35 +00:00
struct DBConfig
yaml_mapping ( {
user : String ,
password : String ,
host : String ,
port : Int32 ,
dbname : String ,
} )
end
2019-06-09 18:48:31 +00:00
def login_req ( f_req )
2018-08-04 20:30:44 +00:00
data = {
2019-06-09 18:48:31 +00:00
# Unfortunately there's not much information available on `bgRequest`; part of Google's BotGuard
2019-07-12 17:04:39 +00:00
# Generally this is much longer (>1250 characters), see also
# https://github.com/ytdl-org/youtube-dl/commit/baf67a604d912722b0fe03a40e9dc5349a2208cb .
2019-06-09 18:48:31 +00:00
# For now this can be empty.
2019-07-12 17:04:39 +00:00
" bgRequest " = > % | [ " identifier " , " " ] | ,
" pstMsg " = > " 1 " ,
" checkConnection " = > " youtube " ,
" checkedDomains " = > " youtube " ,
" hl " = > " en " ,
" deviceinfo " = > % | [ null , null , null , [ ] , null , " US " , null , null , [ ] , " GlifWebSignIn " , null , [ null , null , [ ] ] ] | ,
" f.req " = > f_req ,
2018-08-04 20:30:44 +00:00
" flowName " = > " GlifWebSignIn " ,
" flowEntry " = > " ServiceLogin " ,
2019-07-12 17:04:39 +00:00
# "cookiesDisabled" => "false",
# "gmscoreversion" => "undefined",
# "continue" => "https://accounts.google.com/ManageAccount",
# "azt" => "",
# "bgHash" => "",
2019-06-09 18:48:31 +00:00
}
2018-08-04 20:30:44 +00:00
return HTTP :: Params . encode ( data )
end
2019-06-08 20:08:27 +00:00
def html_to_content ( description_html : String )
description = description_html . gsub ( / (<br>)|(<br \/ >) / , {
" <br> " : " \n " ,
" <br/> " : " \n " ,
} )
if ! description . empty?
description = XML . parse_html ( description ) . content . strip ( " \n " )
2018-08-10 13:38:31 +00:00
end
2019-06-08 20:08:27 +00:00
return description
2018-08-10 13:38:31 +00:00
end
2018-08-10 14:44:19 +00:00
2020-06-15 22:33:23 +00:00
def extract_videos ( initial_data : Hash ( String , JSON :: Any ) )
extract_items ( initial_data ) . select ( & . is_a? ( SearchVideo ) ) . map ( & . as ( SearchVideo ) )
2018-09-20 14:36:09 +00:00
end
2020-06-15 22:33:23 +00:00
def extract_items ( initial_data : Hash ( String , JSON :: Any ) )
items = [ ] of SearchItem
initial_data . try { | t |
t [ " contents " ]? || t [ " response " ]?
} . try { | t |
t [ " twoColumnBrowseResultsRenderer " ]? . try & . [ " tabs " ] . as_a [ 0 ]? . try & . [ " tabRenderer " ] [ " content " ] ||
t [ " twoColumnSearchResultsRenderer " ]? . try & . [ " primaryContents " ] ||
t [ " continuationContents " ]?
} . try { | t | t [ " sectionListRenderer " ]? || t [ " sectionListContinuation " ]? }
. try & . [ " contents " ]
. as_a . each { | c |
c . try & . [ " itemSectionRenderer " ] [ " contents " ] . as_a
. try { | t | t [ 0 ]? . try & . [ " shelfRenderer " ]? . try & . [ " content " ] [ " expandedShelfContentsRenderer " ]? . try & . [ " items " ] . as_a || t }
. each { | item |
if i = item [ " videoRenderer " ]?
video_id = i [ " videoId " ] . as_s
title = i [ " title " ] . try { | t | t [ " simpleText " ]? . try & . as_s || t [ " runs " ]? . try & . as_a . map ( & . [ " text " ] . as_s ) . join ( " " ) } || " "
author_info = i [ " ownerText " ]? . try & . [ " runs " ] . as_a [ 0 ]?
author = author_info . try & . [ " text " ] . as_s || " "
author_id = author_info . try & . [ " navigationEndpoint " ]? . try & . [ " browseEndpoint " ] [ " browseId " ] . as_s || " "
published = i [ " publishedTimeText " ]? . try & . [ " simpleText " ]? . try { | t | decode_date ( t . as_s ) } || Time . local
view_count = i [ " viewCountText " ]? . try & . [ " simpleText " ]? . try & . as_s . gsub ( / \ D+ / , " " ) . to_i64? || 0 _i64
description_html = i [ " descriptionSnippet " ]? . try { | t | parse_content ( t ) } || " "
length_seconds = i [ " lengthText " ]? . try & . [ " simpleText " ]? . try & . as_s . try { | t | decode_length_seconds ( t ) } || 0
live_now = false
paid = false
premium = false
premiere_timestamp = i [ " upcomingEventData " ]? . try & . [ " startTime " ]? . try { | t | Time . unix ( t . as_s . to_i64 ) }
i [ " badges " ]? . try & . as_a . each do | badge |
b = badge [ " metadataBadgeRenderer " ]
case b [ " label " ] . as_s
when " LIVE NOW "
live_now = true
when " New " , " 4K " , " CC "
# TODO
when " Premium "
paid = true
# TODO: Potentially available as i["topStandaloneBadge"]["metadataBadgeRenderer"]
premium = true
else nil # Ignore
end
end
items << SearchVideo . new (
title : title ,
id : video_id ,
author : author ,
ucid : author_id ,
published : published ,
views : view_count ,
description_html : description_html ,
length_seconds : length_seconds ,
live_now : live_now ,
paid : paid ,
premium : premium ,
premiere_timestamp : premiere_timestamp
)
elsif i = item [ " channelRenderer " ]?
author = i [ " title " ] [ " simpleText " ]? . try & . as_s || " "
author_id = i [ " channelId " ]? . try & . as_s || " "
author_thumbnail = i [ " thumbnail " ] [ " thumbnails " ]? . try & . as_a [ 0 ]? . try { | u | " https: #{ u [ " url " ] } " } || " "
subscriber_count = i [ " subscriberCountText " ]? . try & . [ " simpleText " ]? . try & . as_s . try { | s | short_text_to_number ( s . split ( " " ) [ 0 ] ) } || 0
auto_generated = false
auto_generated = true if ! i [ " videoCountText " ]?
video_count = i [ " videoCountText " ]? . try & . [ " runs " ] . as_a [ 0 ]? . try & . [ " text " ] . as_s . gsub ( / \ D / , " " ) . to_i || 0
description_html = i [ " descriptionSnippet " ]? . try { | t | parse_content ( t ) } || " "
items << SearchChannel . new (
author : author ,
ucid : author_id ,
author_thumbnail : author_thumbnail ,
subscriber_count : subscriber_count ,
video_count : video_count ,
description_html : description_html ,
auto_generated : auto_generated ,
)
elsif i = item [ " playlistRenderer " ]?
title = i [ " title " ] [ " simpleText " ]? . try & . as_s || " "
plid = i [ " playlistId " ]? . try & . as_s || " "
video_count = i [ " videoCount " ]? . try & . as_s . to_i || 0
playlist_thumbnail = i [ " thumbnails " ] . as_a [ 0 ]? . try & . [ " thumbnails " ]? . try & . as_a [ 0 ]? . try & . [ " url " ] . as_s || " "
author_info = i [ " shortBylineText " ] [ " runs " ] . as_a [ 0 ]?
author = author_info . try & . [ " text " ] . as_s || " "
author_id = author_info . try & . [ " navigationEndpoint " ]? . try & . [ " browseEndpoint " ] [ " browseId " ] . as_s || " "
videos = i [ " videos " ]? . try & . as_a . map do | v |
v = v [ " childVideoRenderer " ]
v_title = v [ " title " ] [ " simpleText " ]? . try & . as_s || " "
v_id = v [ " videoId " ]? . try & . as_s || " "
v_length_seconds = v [ " lengthText " ]? . try & . [ " simpleText " ]? . try { | t | decode_length_seconds ( t . as_s ) } || 0
SearchPlaylistVideo . new (
title : v_title ,
id : v_id ,
length_seconds : v_length_seconds
)
end || [ ] of SearchPlaylistVideo
# TODO: i["publishedTimeText"]?
items << SearchPlaylist . new (
title : title ,
id : plid ,
author : author ,
ucid : author_id ,
video_count : video_count ,
videos : videos ,
thumbnail : playlist_thumbnail
)
elsif i = item [ " radioRenderer " ]? # Mix
# TODO
elsif i = item [ " showRenderer " ]? # Show
# TODO
elsif i = item [ " shelfRenderer " ]?
elsif i = item [ " horizontalCardListRenderer " ]?
elsif i = item [ " searchPyvRenderer " ]? # Ad
end
}
}
items
end
def extract_videos_html ( nodeset , ucid = nil , author_name = nil )
extract_items_html ( nodeset , ucid , author_name ) . select ( & . is_a? ( SearchVideo ) ) . map ( & . as ( SearchVideo ) )
end
def extract_items_html ( nodeset , ucid = nil , author_name = nil )
# TODO: Make this a 'CommonItem', so it makes more sense to be used here
2018-09-20 14:36:09 +00:00
items = [ ] of SearchItem
2018-08-10 14:44:19 +00:00
nodeset . each do | node |
2019-02-15 23:28:54 +00:00
anchor = node . xpath_node ( % q ( . / / h3 [ contains ( @class , " yt-lockup-title " ) ] / a ) )
2018-08-10 14:44:19 +00:00
if ! anchor
next
end
2019-02-15 23:28:54 +00:00
title = anchor . content . strip
id = anchor [ " href " ]
2018-08-10 14:44:19 +00:00
if anchor [ " href " ] . starts_with? " https://www.googleadservices.com "
next
end
2019-08-22 00:08:11 +00:00
author_id = node . xpath_node ( % q ( . / / div [ contains ( @class , " yt-lockup-byline " ) ] / a ) ) . try & . [ " href " ] . split ( " / " ) [ - 1 ] || ucid || " "
author = node . xpath_node ( % q ( . / / div [ contains ( @class , " yt-lockup-byline " ) ] / a ) ) . try & . content . strip || author_name || " "
2019-06-08 20:08:27 +00:00
description_html = node . xpath_node ( % q ( . / / div [ contains ( @class , " yt-lockup-description " ) ] ) ) . try & . to_s || " "
2018-09-19 20:24:19 +00:00
2018-09-23 17:13:08 +00:00
tile = node . xpath_node ( % q ( . / / div [ contains ( @class , " yt-lockup-tile " ) ] ) )
if ! tile
next
end
case tile [ " class " ]
2018-09-20 14:36:09 +00:00
when . includes? " yt-lockup-playlist "
plid = HTTP :: Params . parse ( URI . parse ( id ) . query . not_nil! ) [ " list " ]
2018-08-21 00:25:12 +00:00
2018-09-22 16:14:57 +00:00
anchor = node . xpath_node ( % q ( . / / div [ contains ( @class , " yt-lockup-meta " ) ] / a ) )
2018-09-29 04:12:35 +00:00
2018-09-22 16:14:57 +00:00
if ! anchor
anchor = node . xpath_node ( % q ( . / / ul [ @class = " yt-lockup-meta-info " ] / li / a ) )
end
2018-09-29 04:12:35 +00:00
2019-08-16 20:46:37 +00:00
video_count = node . xpath_node ( % q ( . / / span [ @class = " formatted-video-count-label " ] / b ) ) ||
node . xpath_node ( % q ( . / / span [ @class = " formatted-video-count-label " ] ) )
2018-09-29 04:12:35 +00:00
if video_count
video_count = video_count . content
if video_count == " 50+ "
author = " YouTube "
author_id = " UC-9-kyTW8ZkZNDHQJ6FgpwQ "
end
2019-04-12 21:37:35 +00:00
video_count = video_count . gsub ( / \ D / , " " ) . to_i?
2018-09-20 14:36:09 +00:00
end
video_count || = 0
videos = [ ] of SearchPlaylistVideo
2018-09-22 15:49:42 +00:00
node . xpath_nodes ( % q ( . / / * [ contains ( @class , " yt-lockup-playlist-items " ) ] / li ) ) . each do | video |
2018-09-20 14:36:09 +00:00
anchor = video . xpath_node ( % q ( . / / a ) )
if anchor
2018-09-22 15:49:42 +00:00
video_title = anchor . content . strip
2018-09-20 14:36:09 +00:00
id = HTTP :: Params . parse ( URI . parse ( anchor [ " href " ] ) . query . not_nil! ) [ " v " ]
end
video_title || = " "
id || = " "
2018-09-19 20:24:19 +00:00
2018-09-20 14:36:09 +00:00
anchor = video . xpath_node ( % q ( . / / span / span ) )
if anchor
length_seconds = decode_length_seconds ( anchor . content )
end
length_seconds || = 0
2018-08-10 14:44:19 +00:00
2018-09-20 14:36:09 +00:00
videos << SearchPlaylistVideo . new (
video_title ,
id ,
length_seconds
)
end
2018-08-10 14:44:19 +00:00
2019-08-17 01:06:21 +00:00
playlist_thumbnail = node . xpath_node ( % q ( . / / span / img ) ) . try & . [ " data-thumb " ]?
playlist_thumbnail || = node . xpath_node ( % q ( . / / span / img ) ) . try & . [ " src " ]
2019-03-17 14:00:00 +00:00
2018-09-20 14:36:09 +00:00
items << SearchPlaylist . new (
2019-08-22 00:08:11 +00:00
title : title ,
id : plid ,
author : author ,
ucid : author_id ,
video_count : video_count ,
videos : videos ,
thumbnail : playlist_thumbnail
2018-09-20 14:36:09 +00:00
)
when . includes? " yt-lockup-channel "
2018-09-22 15:49:42 +00:00
author = title . strip
2018-11-28 16:20:52 +00:00
ucid = node . xpath_node ( % q ( . / / button [ contains ( @class , " yt-uix-subscription-button " ) ] ) ) . try & . [ " data-channel-external-id " ]?
ucid || = id . split ( " / " ) [ - 1 ]
2018-09-20 14:36:09 +00:00
author_thumbnail = node . xpath_node ( % q ( . / / div / span / img ) ) . try & . [ " data-thumb " ]?
author_thumbnail || = node . xpath_node ( % q ( . / / div / span / img ) ) . try & . [ " src " ]
2019-04-02 13:51:28 +00:00
if author_thumbnail
author_thumbnail = URI . parse ( author_thumbnail )
author_thumbnail . scheme = " https "
author_thumbnail = author_thumbnail . to_s
end
2018-09-20 14:36:09 +00:00
author_thumbnail || = " "
2019-09-13 01:09:23 +00:00
subscriber_count = node . xpath_node ( % q ( . / / span [ contains ( @class , " subscriber-count " ) ] ) )
. try & . [ " title " ] . try { | text | short_text_to_number ( text ) } || 0
2018-09-20 14:36:09 +00:00
2019-04-12 21:37:35 +00:00
video_count = node . xpath_node ( % q ( . / / ul [ @class = " yt-lockup-meta-info " ] / li ) ) . try & . content . split ( " " ) [ 0 ] . gsub ( / \ D / , " " ) . to_i?
2018-09-20 14:36:09 +00:00
items << SearchChannel . new (
2018-12-15 19:02:53 +00:00
author : author ,
ucid : ucid ,
author_thumbnail : author_thumbnail ,
subscriber_count : subscriber_count ,
2019-09-12 17:11:21 +00:00
video_count : video_count || 0 ,
description_html : description_html ,
auto_generated : video_count ? false : true ,
2018-09-20 14:36:09 +00:00
)
2018-08-10 14:44:19 +00:00
else
2018-09-20 14:36:09 +00:00
id = id . lchop ( " /watch?v= " )
2018-08-10 14:44:19 +00:00
2019-08-21 23:23:20 +00:00
metadata = node . xpath_node ( % q ( . / / div [ contains ( @class , " yt-lockup-meta " ) ] / ul ) )
2018-09-20 14:36:09 +00:00
2019-08-21 23:23:20 +00:00
published = metadata . try & . xpath_node ( % q ( . / / li [ contains ( text ( ) , " ago " ) ] ) ) . try { | node | decode_date ( node . content . sub ( / ^[a-zA-Z]+ / , " " ) ) }
published || = metadata . try & . xpath_node ( % q ( . / / span [ @data - timestamp ] ) ) . try { | node | Time . unix ( node [ " data-timestamp " ] . to_i64 ) }
2019-06-08 00:56:41 +00:00
published || = Time . utc
2018-09-20 14:36:09 +00:00
2019-08-21 23:23:20 +00:00
view_count = metadata . try & . xpath_node ( % q ( . / / li [ contains ( text ( ) , " views " ) ] ) ) . try & . content . gsub ( / \ D / , " " ) . to_i64?
2018-09-20 14:36:09 +00:00
view_count || = 0 _i64
2019-08-21 23:23:20 +00:00
length_seconds = node . xpath_node ( % q ( . / / span [ @class = " video-time " ] ) ) . try { | node | decode_length_seconds ( node . content ) }
length_seconds || = - 1
2018-09-20 14:36:09 +00:00
2019-08-21 23:23:20 +00:00
live_now = node . xpath_node ( % q ( . / / span [ contains ( @class , " yt-badge-live " ) ] ) ) ? true : false
premium = node . xpath_node ( % q ( . / / span [ text ( ) = " Premium " ] ) ) ? true : false
2018-10-16 16:15:14 +00:00
2019-01-03 02:09:00 +00:00
if ! premium || node . xpath_node ( % q ( . / / span [ contains ( text ( ) , " Free episode " ) ] ) )
2018-10-16 16:15:14 +00:00
paid = false
2019-01-03 02:09:00 +00:00
else
paid = true
2018-10-16 16:15:14 +00:00
end
2020-06-15 22:33:23 +00:00
premiere_timestamp = node . xpath_node ( % q ( . / / ul [ @class = " yt-lockup-meta-info " ] / li / span [ @class = " localized-date " ] ) ) . try & . [ " data-timestamp " ]? . try & . to_i64?
2019-03-22 17:24:47 +00:00
if premiere_timestamp
premiere_timestamp = Time . unix ( premiere_timestamp )
end
2018-09-20 14:36:09 +00:00
items << SearchVideo . new (
2018-12-15 19:02:53 +00:00
title : title ,
id : id ,
author : author ,
ucid : author_id ,
published : published ,
views : view_count ,
description_html : description_html ,
length_seconds : length_seconds ,
live_now : live_now ,
paid : paid ,
2019-03-22 17:24:47 +00:00
premium : premium ,
premiere_timestamp : premiere_timestamp
2018-09-20 14:36:09 +00:00
)
end
2018-08-10 14:44:19 +00:00
end
2018-09-20 14:36:09 +00:00
return items
2018-08-10 14:44:19 +00:00
end
2019-02-15 23:28:54 +00:00
def extract_shelf_items ( nodeset , ucid = nil , author_name = nil )
items = [ ] of SearchPlaylist
nodeset . each do | shelf |
shelf_anchor = shelf . xpath_node ( % q ( . / / h2 [ contains ( @class , " branded-page-module-title " ) ] ) )
2019-08-21 23:23:20 +00:00
next if ! shelf_anchor
2019-02-15 23:28:54 +00:00
2019-08-21 23:23:20 +00:00
title = shelf_anchor . xpath_node ( % q ( . / / span [ contains ( @class , " branded-page-module-title-text " ) ] ) ) . try & . content . strip
2019-02-15 23:28:54 +00:00
title || = " "
id = shelf_anchor . xpath_node ( % q ( . / / a ) ) . try & . [ " href " ]
2019-08-21 23:23:20 +00:00
next if ! id
2019-02-15 23:28:54 +00:00
2019-08-21 23:23:20 +00:00
shelf_is_playlist = false
2019-02-15 23:28:54 +00:00
videos = [ ] of SearchPlaylistVideo
2019-08-21 23:23:20 +00:00
shelf . xpath_nodes ( % q ( . / / ul [ contains ( @class , " yt-uix-shelfslider-list " ) or contains ( @class , " expanded-shelf-content-list " ) ] / li ) ) . each do | child_node |
2019-02-15 23:28:54 +00:00
type = child_node . xpath_node ( % q ( . / div ) )
2020-04-09 17:18:09 +00:00
next if ! type
2019-02-15 23:28:54 +00:00
case type [ " class " ]
when . includes? " yt-lockup-video "
2019-08-21 23:23:20 +00:00
shelf_is_playlist = true
2019-02-15 23:28:54 +00:00
anchor = child_node . xpath_node ( % q ( . / / h3 [ contains ( @class , " yt-lockup-title " ) ] / a ) )
if anchor
video_title = anchor . content . strip
video_id = HTTP :: Params . parse ( URI . parse ( anchor [ " href " ] ) . query . not_nil! ) [ " v " ]
end
video_title || = " "
video_id || = " "
anchor = child_node . xpath_node ( % q ( . / / span [ @class = " video-time " ] ) )
if anchor
length_seconds = decode_length_seconds ( anchor . content )
end
length_seconds || = 0
videos << SearchPlaylistVideo . new (
video_title ,
video_id ,
length_seconds
)
when . includes? " yt-lockup-playlist "
anchor = child_node . xpath_node ( % q ( . / / h3 [ contains ( @class , " yt-lockup-title " ) ] / a ) )
if anchor
playlist_title = anchor . content . strip
params = HTTP :: Params . parse ( URI . parse ( anchor [ " href " ] ) . query . not_nil! )
plid = params [ " list " ]
end
playlist_title || = " "
plid || = " "
2019-03-17 14:00:00 +00:00
playlist_thumbnail = child_node . xpath_node ( % q ( . / / span / img ) ) . try & . [ " data-thumb " ]?
playlist_thumbnail || = child_node . xpath_node ( % q ( . / / span / img ) ) . try & . [ " src " ]
2019-08-16 20:46:37 +00:00
video_count = child_node . xpath_node ( % q ( . / / span [ @class = " formatted-video-count-label " ] / b ) ) ||
child_node . xpath_node ( % q ( . / / span [ @class = " formatted-video-count-label " ] ) )
if video_count
video_count = video_count . content . gsub ( / \ D / , " " ) . to_i?
2019-03-17 23:31:11 +00:00
end
video_count || = 50
2019-08-21 23:23:20 +00:00
videos = [ ] of SearchPlaylistVideo
child_node . xpath_nodes ( % q ( . / / * [ contains ( @class , " yt-lockup-playlist-items " ) ] / li ) ) . each do | video |
anchor = video . xpath_node ( % q ( . / / a ) )
if anchor
video_title = anchor . content . strip
id = HTTP :: Params . parse ( URI . parse ( anchor [ " href " ] ) . query . not_nil! ) [ " v " ]
end
video_title || = " "
id || = " "
anchor = video . xpath_node ( % q ( . / / span / span ) )
if anchor
length_seconds = decode_length_seconds ( anchor . content )
end
length_seconds || = 0
videos << SearchPlaylistVideo . new (
video_title ,
id ,
length_seconds
)
end
2019-02-15 23:28:54 +00:00
items << SearchPlaylist . new (
2019-08-22 00:08:11 +00:00
title : playlist_title ,
id : plid ,
author : author_name ,
ucid : ucid ,
video_count : video_count ,
videos : videos ,
thumbnail : playlist_thumbnail
2019-02-15 23:28:54 +00:00
)
2020-04-09 17:18:09 +00:00
else
next # Skip
2019-02-15 23:28:54 +00:00
end
end
2019-08-21 23:23:20 +00:00
if shelf_is_playlist
2019-02-15 23:28:54 +00:00
plid = HTTP :: Params . parse ( URI . parse ( id ) . query . not_nil! ) [ " list " ]
items << SearchPlaylist . new (
2019-08-22 00:08:11 +00:00
title : title ,
id : plid ,
author : author_name ,
ucid : ucid ,
video_count : videos . size ,
videos : videos ,
thumbnail : " https://i.ytimg.com/vi/ #{ videos [ 0 ] . id } /mqdefault.jpg "
2019-02-15 23:28:54 +00:00
)
end
end
return items
end
2019-04-10 21:23:37 +00:00
2019-08-05 23:49:13 +00:00
def check_enum ( db , logger , enum_name , struct_type = nil )
2020-06-15 22:57:20 +00:00
return # TODO
2019-08-05 23:49:13 +00:00
if ! db . query_one? ( " SELECT true FROM pg_type WHERE typname = $1 " , enum_name , as : Bool )
logger . puts ( " CREATE TYPE #{ enum_name } " )
db . using_connection do | conn |
conn . as ( PG :: Connection ) . exec_all ( File . read ( " config/sql/ #{ enum_name } .sql " ) )
end
end
end
def check_table ( db , logger , table_name , struct_type = nil )
2019-04-10 21:23:37 +00:00
# Create table if it doesn't exist
2019-04-10 22:16:18 +00:00
begin
db . exec ( " SELECT * FROM #{ table_name } LIMIT 0 " )
rescue ex
2019-06-08 01:07:55 +00:00
logger . puts ( " CREATE TABLE #{ table_name } " )
2019-04-10 22:09:36 +00:00
2019-04-10 21:23:37 +00:00
db . using_connection do | conn |
conn . as ( PG :: Connection ) . exec_all ( File . read ( " config/sql/ #{ table_name } .sql " ) )
end
end
2020-06-15 22:57:20 +00:00
return if ! struct_type
2019-04-10 21:23:37 +00:00
struct_array = struct_type . to_type_tuple
column_array = get_column_array ( db , table_name )
column_types = File . read ( " config/sql/ #{ table_name } .sql " ) . match ( / CREATE TABLE public \ . #{ table_name } \ n \ ((?<types>[ \ d \ D]*?) \ ); / )
2020-06-15 22:57:20 +00:00
. try & . [ " types " ] . split ( " , " ) . map { | line | line . strip } . reject & . starts_with? ( " CONSTRAINT " )
2019-04-10 21:23:37 +00:00
2020-06-15 22:57:20 +00:00
return if ! column_types
2019-04-10 21:23:37 +00:00
struct_array . each_with_index do | name , i |
if name != column_array [ i ]?
if ! column_array [ i ]?
new_column = column_types . select { | line | line . starts_with? name } [ 0 ]
2019-06-08 01:07:55 +00:00
logger . puts ( " ALTER TABLE #{ table_name } ADD COLUMN #{ new_column } " )
2019-04-10 22:09:36 +00:00
db . exec ( " ALTER TABLE #{ table_name } ADD COLUMN #{ new_column } " )
2019-04-10 21:23:37 +00:00
next
end
# Column doesn't exist
if ! column_array . includes? name
new_column = column_types . select { | line | line . starts_with? name } [ 0 ]
db . exec ( " ALTER TABLE #{ table_name } ADD COLUMN #{ new_column } " )
end
# Column exists but in the wrong position, rotate
if struct_array . includes? column_array [ i ]
until name == column_array [ i ]
new_column = column_types . select { | line | line . starts_with? column_array [ i ] } [ 0 ]? . try & . gsub ( " #{ column_array [ i ] } " , " #{ column_array [ i ] } _new " )
# There's a column we didn't expect
if ! new_column
2019-06-08 01:07:55 +00:00
logger . puts ( " ALTER TABLE #{ table_name } DROP COLUMN #{ column_array [ i ] } " )
2019-04-10 22:09:36 +00:00
db . exec ( " ALTER TABLE #{ table_name } DROP COLUMN #{ column_array [ i ] } CASCADE " )
2019-04-10 21:23:37 +00:00
column_array = get_column_array ( db , table_name )
next
end
2019-06-08 01:07:55 +00:00
logger . puts ( " ALTER TABLE #{ table_name } ADD COLUMN #{ new_column } " )
2019-04-10 22:09:36 +00:00
db . exec ( " ALTER TABLE #{ table_name } ADD COLUMN #{ new_column } " )
2019-06-08 01:07:55 +00:00
logger . puts ( " UPDATE #{ table_name } SET #{ column_array [ i ] } _new= #{ column_array [ i ] } " )
2019-04-10 22:09:36 +00:00
db . exec ( " UPDATE #{ table_name } SET #{ column_array [ i ] } _new= #{ column_array [ i ] } " )
2019-06-08 01:07:55 +00:00
logger . puts ( " ALTER TABLE #{ table_name } DROP COLUMN #{ column_array [ i ] } CASCADE " )
2019-04-10 22:09:36 +00:00
db . exec ( " ALTER TABLE #{ table_name } DROP COLUMN #{ column_array [ i ] } CASCADE " )
2019-06-08 01:07:55 +00:00
logger . puts ( " ALTER TABLE #{ table_name } RENAME COLUMN #{ column_array [ i ] } _new TO #{ column_array [ i ] } " )
2019-04-10 22:09:36 +00:00
db . exec ( " ALTER TABLE #{ table_name } RENAME COLUMN #{ column_array [ i ] } _new TO #{ column_array [ i ] } " )
2019-04-10 21:23:37 +00:00
column_array = get_column_array ( db , table_name )
end
else
2019-06-08 01:07:55 +00:00
logger . puts ( " ALTER TABLE #{ table_name } DROP COLUMN #{ column_array [ i ] } CASCADE " )
2019-04-10 22:09:36 +00:00
db . exec ( " ALTER TABLE #{ table_name } DROP COLUMN #{ column_array [ i ] } CASCADE " )
2019-04-10 21:23:37 +00:00
end
end
end
2020-06-15 22:57:20 +00:00
return if column_array . size <= struct_array . size
2020-06-15 22:33:23 +00:00
column_array . each do | column |
if ! struct_array . includes? column
logger . puts ( " ALTER TABLE #{ table_name } DROP COLUMN #{ column } CASCADE " )
db . exec ( " ALTER TABLE #{ table_name } DROP COLUMN #{ column } CASCADE " )
end
end
2019-04-10 21:23:37 +00:00
end
class PG:: ResultSet
def field ( index = @column_index )
@fields . not_nil! [ index ]
end
end
def get_column_array ( db , table_name )
column_array = [ ] of String
db . query ( " SELECT * FROM #{ table_name } LIMIT 0 " ) do | rs |
rs . column_count . times do | i |
column = rs . as ( PG :: ResultSet ) . field ( i )
column_array << column . name
end
end
return column_array
end
2019-04-15 16:13:09 +00:00
def cache_annotation ( db , id , annotations )
if ! CONFIG . cache_annotations
return
end
body = XML . parse ( annotations )
nodeset = body . xpath_nodes ( % q ( / document / annotations / annotation ) )
2020-04-07 18:34:40 +00:00
return if nodeset == 0
2019-04-15 16:13:09 +00:00
has_legacy_annotations = false
nodeset . each do | node |
if ! { " branding " , " card " , " drawer " } . includes? node [ " type " ]?
has_legacy_annotations = true
break
end
end
2020-06-15 22:10:30 +00:00
db . exec ( " INSERT INTO annotations VALUES ($1, $2) ON CONFLICT DO NOTHING " , id , annotations ) if has_legacy_annotations
2019-04-15 16:13:09 +00:00
end
2019-05-19 00:14:58 +00:00
2020-06-15 22:10:30 +00:00
def create_notification_stream ( env , topics , connection_channel )
2019-06-03 18:36:49 +00:00
connection = Channel ( PQ :: Notification ) . new ( 8 )
2019-06-03 18:12:06 +00:00
connection_channel . send ( { true , connection } )
2019-05-21 14:01:17 +00:00
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
since = env . params . query [ " since " ]? . try & . to_i?
2019-06-02 12:41:53 +00:00
id = 0
2019-05-21 14:01:17 +00:00
2019-06-02 12:41:53 +00:00
if topics . includes? " debug "
2019-05-21 14:01:17 +00:00
spawn do
2019-06-03 18:12:06 +00:00
begin
loop do
time_span = [ 0 , 0 , 0 , 0 ]
time_span [ rand ( 4 ) ] = rand ( 30 ) + 5
2020-04-09 17:18:09 +00:00
published = Time . utc - Time :: Span . new ( days : time_span [ 0 ] , hours : time_span [ 1 ] , minutes : time_span [ 2 ] , seconds : time_span [ 3 ] )
2019-06-03 18:12:06 +00:00
video_id = TEST_IDS [ rand ( TEST_IDS . size ) ]
2019-06-29 02:17:56 +00:00
video = get_video ( video_id , PG_DB )
2019-06-03 18:12:06 +00:00
video . published = published
2020-06-15 22:10:30 +00:00
response = JSON . parse ( video . to_json ( locale ) )
2019-06-03 18:12:06 +00:00
if fields_text = env . params . query [ " fields " ]?
begin
JSONFilter . filter ( response , fields_text )
rescue ex
env . response . status_code = 400
response = { " error " = > ex . message }
end
2019-05-21 14:01:17 +00:00
end
2019-06-03 18:12:06 +00:00
env . response . puts " id: #{ id } "
env . response . puts " data: #{ response . to_json } "
env . response . puts
env . response . flush
2019-05-21 14:01:17 +00:00
2019-06-03 18:12:06 +00:00
id += 1
2019-06-02 12:41:53 +00:00
2019-06-03 18:12:06 +00:00
sleep 1 . minute
2019-06-16 00:18:36 +00:00
Fiber . yield
2019-06-03 18:12:06 +00:00
end
rescue ex
2019-06-02 12:41:53 +00:00
end
end
end
spawn do
2019-06-03 18:12:06 +00:00
begin
if since
topics . try & . each do | topic |
case topic
when . match ( / UC[A-Za-z0-9_-]{22} / )
PG_DB . query_all ( " SELECT * FROM channel_videos WHERE ucid = $1 AND published > $2 ORDER BY published DESC LIMIT 15 " ,
topic , Time . unix ( since . not_nil! ) , as : ChannelVideo ) . each do | video |
2020-06-15 22:10:30 +00:00
response = JSON . parse ( video . to_json ( locale ) )
2019-06-03 18:12:06 +00:00
if fields_text = env . params . query [ " fields " ]?
begin
JSONFilter . filter ( response , fields_text )
rescue ex
env . response . status_code = 400
response = { " error " = > ex . message }
end
2019-06-02 12:41:53 +00:00
end
2019-06-03 18:12:06 +00:00
env . response . puts " id: #{ id } "
env . response . puts " data: #{ response . to_json } "
env . response . puts
env . response . flush
2019-06-02 12:41:53 +00:00
2019-06-03 18:12:06 +00:00
id += 1
end
else
# TODO
2019-06-02 12:41:53 +00:00
end
2019-05-21 14:01:17 +00:00
end
end
end
2019-06-02 12:41:53 +00:00
end
2019-05-21 14:01:17 +00:00
2019-06-02 12:41:53 +00:00
spawn do
2019-06-03 18:12:06 +00:00
begin
loop do
event = connection . receive
notification = JSON . parse ( event . payload )
topic = notification [ " topic " ] . as_s
video_id = notification [ " videoId " ] . as_s
published = notification [ " published " ] . as_i64
2019-06-03 18:36:49 +00:00
if ! topics . try & . includes? topic
next
end
2019-06-29 02:17:56 +00:00
video = get_video ( video_id , PG_DB )
2019-06-03 18:12:06 +00:00
video . published = Time . unix ( published )
2020-06-15 22:10:30 +00:00
response = JSON . parse ( video . to_json ( locale ) )
2019-06-03 18:12:06 +00:00
if fields_text = env . params . query [ " fields " ]?
begin
JSONFilter . filter ( response , fields_text )
rescue ex
env . response . status_code = 400
response = { " error " = > ex . message }
end
2019-06-02 12:41:53 +00:00
end
2019-06-03 18:36:49 +00:00
env . response . puts " id: #{ id } "
env . response . puts " data: #{ response . to_json } "
env . response . puts
env . response . flush
2019-06-02 12:41:53 +00:00
2019-06-03 18:36:49 +00:00
id += 1
2019-06-02 12:41:53 +00:00
end
2019-06-03 18:12:06 +00:00
rescue ex
ensure
connection_channel . send ( { false , connection } )
2019-05-21 14:01:17 +00:00
end
2019-06-02 12:41:53 +00:00
end
2019-06-03 18:12:06 +00:00
begin
# Send heartbeat
loop do
2019-06-08 00:56:41 +00:00
env . response . puts " :keepalive #{ Time . utc . to_unix } "
2019-06-03 18:12:06 +00:00
env . response . puts
env . response . flush
sleep ( 20 + rand ( 11 ) ) . seconds
end
rescue ex
ensure
connection_channel . send ( { false , connection } )
2019-05-21 14:01:17 +00:00
end
end
2019-07-11 12:27:42 +00:00
2020-06-15 22:33:23 +00:00
def extract_initial_data ( body ) : Hash ( String , JSON :: Any )
initial_data = body . match ( / window \ ["ytInitialData" \ ] \ s*= \ s*(?<info>.*?);+ \ n / ) . try & . [ " info " ] || " {} "
2019-07-11 12:27:42 +00:00
if initial_data . starts_with? ( " JSON.parse( \" " )
2020-06-15 22:33:23 +00:00
return JSON . parse ( JSON . parse ( %( {"initial_data":" #{ initial_data [ 12 .. - 3 ] } "} ) ) [ " initial_data " ] . as_s ) . as_h
2019-07-11 12:27:42 +00:00
else
2020-06-15 22:33:23 +00:00
return JSON . parse ( initial_data ) . as_h
2019-07-11 12:27:42 +00:00
end
end
2019-07-18 23:51:10 +00:00
def proxy_file ( response , env )
if response . headers . includes_word? ( " Content-Encoding " , " gzip " )
2020-06-15 22:57:20 +00:00
Compress :: Gzip :: Writer . open ( env . response ) do | deflate |
IO . copy response . body_io , deflate
2019-07-18 23:51:10 +00:00
end
elsif response . headers . includes_word? ( " Content-Encoding " , " deflate " )
2020-06-15 22:57:20 +00:00
Compress :: Deflate :: Writer . open ( env . response ) do | deflate |
IO . copy response . body_io , deflate
2019-07-18 23:51:10 +00:00
end
else
2020-06-15 22:57:20 +00:00
IO . copy response . body_io , env . response
end
end
# See https://github.com/kemalcr/kemal/pull/576
class HTTP::Server::Response:: Output
def close
return if closed?
unless response . wrote_headers?
response . content_length = @out_count
end
ensure_headers_written
super
if @chunked
@io << " 0 \ r \n \ r \n "
@io . flush
end
2019-07-18 23:51:10 +00:00
end
end
class HTTP::Client:: Response
def pipe ( io )
HTTP . serialize_body ( io , headers , @body , @body_io , @version )
end
end
# Supports serialize_body without first writing headers
module HTTP
def self . serialize_body ( io , headers , body , body_io , version )
if body
io << body
elsif body_io
content_length = content_length ( headers )
if content_length
copied = IO . copy ( body_io , io )
if copied != content_length
raise ArgumentError . new ( " Content-Length header is #{ content_length } but body had #{ copied } bytes " )
end
elsif Client :: Response . supports_chunked? ( version )
headers [ " Transfer-Encoding " ] = " chunked "
serialize_chunked_body ( io , body_io )
else
io << body
end
end
end
end
class HTTP:: Client
property family : Socket :: Family = Socket :: Family :: UNSPEC
private def socket
socket = @socket
return socket if socket
hostname = @host . starts_with? ( '[' ) && @host . ends_with? ( ']' ) ? @host [ 1 .. - 2 ] : @host
socket = TCPSocket . new hostname , @port , @dns_timeout , @connect_timeout , @family
socket . read_timeout = @read_timeout if @read_timeout
socket . sync = false
{% if ! flag? ( :without_openssl ) %}
if tls = @tls
socket = OpenSSL :: SSL :: Socket :: Client . new ( socket , context : tls , sync_close : true , hostname : @host )
end
{% end %}
@socket = socket
end
end
class TCPSocket
def initialize ( host , port , dns_timeout = nil , connect_timeout = nil , family = Socket :: Family :: UNSPEC )
Addrinfo . tcp ( host , port , timeout : dns_timeout , family : family ) do | addrinfo |
super ( addrinfo . family , addrinfo . type , addrinfo . protocol )
connect ( addrinfo , timeout : connect_timeout ) do | error |
close
error
end
end
end
end