2019-03-29 21:30:02 +00:00
struct PlaylistVideo
2020-06-15 22:10:30 +00:00
def to_xml ( auto_generated , xml : XML :: Builder )
2019-08-05 23:49:13 +00:00
xml . element ( " entry " ) do
xml . element ( " id " ) { xml . text " yt:video: #{ self . id } " }
xml . element ( " yt:videoId " ) { xml . text self . id }
xml . element ( " yt:channelId " ) { xml . text self . ucid }
xml . element ( " title " ) { xml . text self . title }
2020-06-15 22:10:30 +00:00
xml . element ( " link " , rel : " alternate " , href : " #{ HOST_URL } /watch?v= #{ self . id } " )
2019-08-05 23:49:13 +00:00
xml . element ( " author " ) do
if auto_generated
xml . element ( " name " ) { xml . text self . author }
2020-06-15 22:10:30 +00:00
xml . element ( " uri " ) { xml . text " #{ HOST_URL } /channel/ #{ self . ucid } " }
2019-08-05 23:49:13 +00:00
else
xml . element ( " name " ) { xml . text author }
2020-06-15 22:10:30 +00:00
xml . element ( " uri " ) { xml . text " #{ HOST_URL } /channel/ #{ ucid } " }
2019-08-05 23:49:13 +00:00
end
end
xml . element ( " content " , type : " xhtml " ) do
xml . element ( " div " , xmlns : " http://www.w3.org/1999/xhtml " ) do
2020-06-15 22:10:30 +00:00
xml . element ( " a " , href : " #{ HOST_URL } /watch?v= #{ self . id } " ) do
xml . element ( " img " , src : " #{ HOST_URL } /vi/ #{ self . id } /mqdefault.jpg " )
2019-08-05 23:49:13 +00:00
end
end
end
xml . element ( " published " ) { xml . text self . published . to_s ( " %Y-%m-%dT%H:%M:%S%:z " ) }
xml . element ( " media:group " ) do
xml . element ( " media:title " ) { xml . text self . title }
2020-06-15 22:10:30 +00:00
xml . element ( " media:thumbnail " , url : " #{ HOST_URL } /vi/ #{ self . id } /mqdefault.jpg " ,
2019-08-05 23:49:13 +00:00
width : " 320 " , height : " 180 " )
end
end
end
2020-06-15 22:10:30 +00:00
def to_xml ( auto_generated , xml : XML :: Builder? = nil )
2019-08-05 23:49:13 +00:00
if xml
2020-06-15 22:10:30 +00:00
to_xml ( auto_generated , xml )
2019-08-05 23:49:13 +00:00
else
XML . build do | json |
2020-06-15 22:10:30 +00:00
to_xml ( auto_generated , xml )
2019-08-05 23:49:13 +00:00
end
end
end
2020-06-15 22:10:30 +00:00
def to_json ( locale , json : JSON :: Builder , index : Int32 ?)
2019-06-08 18:31:41 +00:00
json . object do
json . field " title " , self . title
json . field " videoId " , self . id
json . field " author " , self . author
json . field " authorId " , self . ucid
json . field " authorUrl " , " /channel/ #{ self . ucid } "
json . field " videoThumbnails " do
2020-06-15 22:10:30 +00:00
generate_thumbnails ( json , self . id )
2019-06-08 18:31:41 +00:00
end
2019-08-05 23:49:13 +00:00
if index
json . field " index " , index
json . field " indexId " , self . index . to_u64 . to_s ( 16 ) . upcase
else
json . field " index " , self . index
end
2019-06-08 18:31:41 +00:00
json . field " lengthSeconds " , self . length_seconds
end
end
2020-06-15 22:10:30 +00:00
def to_json ( locale , json : JSON :: Builder? = nil , index : Int32 ? = nil )
2019-06-08 18:31:41 +00:00
if json
2020-06-15 22:10:30 +00:00
to_json ( locale , json , index : index )
2019-06-08 18:31:41 +00:00
else
JSON . build do | json |
2020-06-15 22:10:30 +00:00
to_json ( locale , json , index : index )
2019-06-08 18:31:41 +00:00
end
end
end
2019-04-03 16:35:58 +00:00
db_mapping ( {
2018-09-29 04:12:35 +00:00
title : String ,
id : String ,
author : String ,
ucid : String ,
length_seconds : Int32 ,
published : Time ,
2019-06-08 01:23:37 +00:00
plid : String ,
2019-08-05 23:49:13 +00:00
index : Int64 ,
2019-03-24 14:10:14 +00:00
live_now : Bool ,
2018-09-29 04:12:35 +00:00
} )
end
2019-03-29 21:30:02 +00:00
struct Playlist
2020-06-15 22:10:30 +00:00
def to_json ( offset , locale , json : JSON :: Builder , continuation : String ? = nil )
2019-08-05 23:49:13 +00:00
json . object do
json . field " type " , " playlist "
json . field " title " , self . title
json . field " playlistId " , self . id
json . field " playlistThumbnail " , self . thumbnail
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
json . field " url " , self . author_thumbnail . not_nil! . gsub ( / = \ d+ / , " =s #{ quality } " )
json . field " width " , quality
json . field " height " , quality
end
end
end
end
2020-06-25 02:18:09 +00:00
json . field " description " , self . description
2019-08-05 23:49:13 +00:00
json . field " descriptionHtml " , self . description_html
json . field " videoCount " , self . video_count
json . field " viewCount " , self . views
json . field " updated " , self . updated . to_unix
json . field " isListed " , self . privacy . public?
json . field " videos " do
json . array do
videos = get_playlist_videos ( PG_DB , self , offset : offset , locale : locale , continuation : continuation )
videos . each_with_index do | video , index |
2020-06-15 22:10:30 +00:00
video . to_json ( locale , json )
2019-08-05 23:49:13 +00:00
end
end
end
end
end
2020-06-15 22:10:30 +00:00
def to_json ( offset , locale , json : JSON :: Builder? = nil , continuation : String ? = nil )
2019-08-05 23:49:13 +00:00
if json
2020-06-15 22:10:30 +00:00
to_json ( offset , locale , json , continuation : continuation )
2019-08-05 23:49:13 +00:00
else
JSON . build do | json |
2020-06-15 22:10:30 +00:00
to_json ( offset , locale , json , continuation : continuation )
2019-08-05 23:49:13 +00:00
end
end
end
2019-04-03 16:35:58 +00:00
db_mapping ( {
2018-09-05 00:27:10 +00:00
title : String ,
id : String ,
author : String ,
2018-09-25 15:28:40 +00:00
author_thumbnail : String ,
2018-09-05 00:27:10 +00:00
ucid : String ,
2020-06-25 02:18:09 +00:00
description : String ,
2018-09-05 00:27:10 +00:00
video_count : Int32 ,
views : Int64 ,
updated : Time ,
2019-08-22 00:08:11 +00:00
thumbnail : String ?,
2018-08-15 15:22:36 +00:00
} )
2019-08-05 23:49:13 +00:00
def privacy
PlaylistPrivacy :: Public
end
2020-06-25 02:18:09 +00:00
def description_html
HTML . escape ( self . description ) . gsub ( " \n " , " <br> " )
end
2018-08-15 15:22:36 +00:00
end
2019-08-05 23:49:13 +00:00
enum PlaylistPrivacy
Public = 0
Unlisted = 1
Private = 2
end
2018-09-22 19:13:10 +00:00
2019-08-05 23:49:13 +00:00
struct InvidiousPlaylist
2020-06-15 22:10:30 +00:00
def to_json ( offset , locale , json : JSON :: Builder , continuation : String ? = nil )
2019-08-05 23:49:13 +00:00
json . object do
json . field " type " , " invidiousPlaylist "
json . field " title " , self . title
json . field " playlistId " , self . id
2018-10-08 02:11:33 +00:00
2019-08-05 23:49:13 +00:00
json . field " author " , self . author
json . field " authorId " , self . ucid
json . field " authorUrl " , nil
json . field " authorThumbnails " , [ ] of String
json . field " description " , html_to_content ( self . description_html )
json . field " descriptionHtml " , self . description_html
json . field " videoCount " , self . video_count
json . field " viewCount " , self . views
json . field " updated " , self . updated . to_unix
json . field " isListed " , self . privacy . public?
json . field " videos " do
json . array do
videos = get_playlist_videos ( PG_DB , self , offset : offset , locale : locale , continuation : continuation )
videos . each_with_index do | video , index |
2020-06-15 22:10:30 +00:00
video . to_json ( locale , json , offset + index )
2019-08-05 23:49:13 +00:00
end
end
end
2018-10-08 02:11:33 +00:00
end
end
2020-06-15 22:10:30 +00:00
def to_json ( offset , locale , json : JSON :: Builder? = nil , continuation : String ? = nil )
2019-08-05 23:49:13 +00:00
if json
2020-06-15 22:10:30 +00:00
to_json ( offset , locale , json , continuation : continuation )
2019-08-05 23:49:13 +00:00
else
JSON . build do | json |
2020-06-15 22:10:30 +00:00
to_json ( offset , locale , json , continuation : continuation )
2019-08-05 23:49:13 +00:00
end
end
end
2018-09-22 19:13:10 +00:00
2019-08-05 23:49:13 +00:00
property thumbnail_id
module PlaylistPrivacyConverter
def self . from_rs ( rs )
return PlaylistPrivacy . parse ( String . new ( rs . read ( Slice ( UInt8 ) ) ) )
2018-09-22 19:13:10 +00:00
end
2019-08-05 23:49:13 +00:00
end
2018-09-22 19:13:10 +00:00
2019-08-05 23:49:13 +00:00
db_mapping ( {
title : String ,
id : String ,
author : String ,
description : { type : String , default : " " } ,
video_count : Int32 ,
created : Time ,
updated : Time ,
privacy : { type : PlaylistPrivacy , default : PlaylistPrivacy :: Private , converter : PlaylistPrivacyConverter } ,
index : Array ( Int64 ) ,
} )
2018-09-22 19:13:10 +00:00
2019-08-05 23:49:13 +00:00
def thumbnail
@thumbnail_id || = PG_DB . query_one? ( " SELECT id FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 1 " , self . id , self . index , as : String ) || " ----------- "
" /vi/ #{ @thumbnail_id } /mqdefault.jpg "
end
2018-12-24 23:47:23 +00:00
2019-08-05 23:49:13 +00:00
def author_thumbnail
nil
2018-08-15 15:22:36 +00:00
end
2019-08-05 23:49:13 +00:00
def ucid
nil
end
def views
0 _i64
end
def description_html
HTML . escape ( self . description ) . gsub ( " \n " , " <br> " )
end
end
def create_playlist ( db , title , privacy , user )
plid = " IVPL #{ Random :: Secure . urlsafe_base64 ( 24 ) [ 0 , 31 ] } "
playlist = InvidiousPlaylist . new (
title : title . byte_slice ( 0 , 150 ) ,
id : plid ,
author : user . email ,
description : " " , # Max 5000 characters
video_count : 0 ,
created : Time . utc ,
updated : Time . utc ,
privacy : privacy ,
index : [ ] of Int64 ,
)
playlist_array = playlist . to_a
args = arg_array ( playlist_array )
db . exec ( " INSERT INTO playlists VALUES ( #{ args } ) " , args : playlist_array )
return playlist
2018-09-22 19:13:10 +00:00
end
2020-05-17 11:28:00 +00:00
def subscribe_playlist ( db , user , playlist )
playlist = InvidiousPlaylist . new (
title : playlist . title . byte_slice ( 0 , 150 ) ,
id : playlist . id ,
author : user . email ,
description : " " , # Max 5000 characters
video_count : playlist . video_count ,
created : Time . utc ,
updated : playlist . updated ,
privacy : PlaylistPrivacy :: Private ,
index : [ ] of Int64 ,
)
playlist_array = playlist . to_a
args = arg_array ( playlist_array )
db . exec ( " INSERT INTO playlists VALUES ( #{ args } ) " , args : playlist_array )
return playlist
end
2018-08-15 15:22:36 +00:00
def produce_playlist_url ( id , index )
if id . starts_with? " UC "
id = " UU " + id . lchop ( " UC " )
end
2019-10-27 17:50:42 +00:00
plid = " VL " + id
data = { " 1:varint " = > index . to_i64 }
. try { | i | Protodec :: Any . cast_json ( i ) }
. try { | i | Protodec :: Any . from_json ( i ) }
. try { | i | Base64 . urlsafe_encode ( i , padding : false ) }
object = {
" 80226972:embedded " = > {
" 2:string " = > plid ,
" 3:base64 " = > {
" 15:string " = > " PT: #{ data } " ,
} ,
} ,
}
continuation = object . try { | i | Protodec :: Any . cast_json ( object ) }
. try { | i | Protodec :: Any . from_json ( i ) }
. try { | i | Base64 . urlsafe_encode ( i ) }
. try { | i | URI . encode_www_form ( i ) }
return " /browse_ajax?continuation= #{ continuation } &gl=US&hl=en "
2018-08-15 15:22:36 +00:00
end
2019-08-05 23:49:13 +00:00
def get_playlist ( db , plid , locale , refresh = true , force_refresh = false )
if plid . starts_with? " IV "
if playlist = db . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
return playlist
else
raise " Playlist does not exist. "
end
else
return fetch_playlist ( plid , locale )
end
end
2018-12-20 21:32:09 +00:00
def fetch_playlist ( plid , locale )
2018-09-22 19:13:10 +00:00
if plid . starts_with? " UC "
plid = " UU #{ plid . lchop ( " UC " ) } "
end
2020-06-25 02:18:09 +00:00
response = YT_POOL . client & . get ( " /playlist?list= #{ plid } &hl=en " )
2018-09-23 17:26:12 +00:00
if response . status_code != 200
2020-06-25 02:18:09 +00:00
if response . headers [ " location " ]? . try & . includes? " /sorry/index "
raise " Could not extract playlist info. Instance is likely blocked. "
else
raise translate ( locale , " Not a playlist. " )
end
2018-09-23 17:26:12 +00:00
end
2020-06-25 02:18:09 +00:00
initial_data = extract_initial_data ( response . body )
playlist_info = initial_data [ " sidebar " ]? . try & . [ " playlistSidebarRenderer " ]? . try & . [ " items " ]? . try & . [ 0 ] [ " playlistSidebarPrimaryInfoRenderer " ]?
2018-08-15 15:22:36 +00:00
2020-06-25 02:18:09 +00:00
raise " Could not extract playlist info " if ! playlist_info
title = playlist_info [ " title " ]? . try & . [ " runs " ] [ 0 ]? . try & . [ " text " ]? . try & . as_s || " "
desc_item = playlist_info [ " description " ]?
description = desc_item . try & . [ " runs " ]? . try & . as_a . map ( & . [ " text " ] . as_s ) . join ( " " ) || desc_item . try & . [ " simpleText " ]? . try & . as_s || " "
2018-08-15 15:22:36 +00:00
2020-06-25 02:18:09 +00:00
thumbnail = playlist_info [ " thumbnailRenderer " ]? . try & . [ " playlistVideoThumbnailRenderer " ]?
. try & . [ " thumbnail " ] [ " thumbnails " ] [ 0 ] [ " url " ]? . try & . as_s
2018-08-15 15:22:36 +00:00
2020-06-25 02:18:09 +00:00
views = 0 _i64
updated = Time . utc
video_count = 0
playlist_info [ " stats " ]? . try & . as_a . each do | stat |
text = stat [ " runs " ]? . try & . as_a . map ( & . [ " text " ] . as_s ) . join ( " " ) || stat [ " simpleText " ]? . try & . as_s
next if ! text
2019-08-22 00:08:11 +00:00
2020-06-25 02:18:09 +00:00
if text . includes? " videos "
video_count = text . gsub ( / \ D / , " " ) . to_i? || 0
elsif text . includes? " views "
views = text . gsub ( / \ D / , " " ) . to_i64? || 0 _i64
else
updated = decode_date ( text . lchop ( " Last updated on " ) . lchop ( " Updated " ) )
end
end
2018-08-15 15:22:36 +00:00
2020-06-25 02:18:09 +00:00
author_info = initial_data [ " sidebar " ]? . try & . [ " playlistSidebarRenderer " ]? . try & . [ " items " ]? . try & . [ 1 ] [ " playlistSidebarSecondaryInfoRenderer " ]?
. try & . [ " videoOwner " ] [ " videoOwnerRenderer " ]?
2019-08-22 00:08:11 +00:00
2020-06-25 02:18:09 +00:00
raise " Could not extract author info " if ! author_info
2019-05-01 13:03:58 +00:00
2020-06-25 02:18:09 +00:00
author_thumbnail = author_info [ " thumbnail " ] [ " thumbnails " ] [ 0 ] [ " url " ]? . try & . as_s || " "
author = author_info [ " title " ] [ " runs " ] [ 0 ] [ " text " ]? . try & . as_s || " "
ucid = author_info [ " title " ] [ " runs " ] [ 0 ] [ " navigationEndpoint " ] [ " browseEndpoint " ] [ " browseId " ]? . try & . as_s || " "
2018-08-15 15:22:36 +00:00
2020-06-25 02:18:09 +00:00
return Playlist . new (
2018-12-15 19:02:53 +00:00
title : title ,
id : plid ,
author : author ,
author_thumbnail : author_thumbnail ,
ucid : ucid ,
2020-06-25 02:18:09 +00:00
description : description ,
2018-12-15 19:02:53 +00:00
video_count : video_count ,
views : views ,
2019-08-22 00:08:11 +00:00
updated : updated ,
2020-06-25 02:18:09 +00:00
thumbnail : thumbnail
2018-08-15 15:22:36 +00:00
)
end
2018-10-08 02:11:33 +00:00
2019-08-05 23:49:13 +00:00
def get_playlist_videos ( db , playlist , offset , locale = nil , continuation = nil )
if playlist . is_a? InvidiousPlaylist
if ! offset
index = PG_DB . query_one? ( " SELECT index FROM playlist_videos WHERE plid = $1 AND id = $2 LIMIT 1 " , playlist . id , continuation , as : Int64 )
offset = playlist . index . index ( index ) || 0
end
db . query_all ( " SELECT * FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 100 OFFSET $3 " , playlist . id , playlist . index , offset , as : PlaylistVideo )
else
fetch_playlist_videos ( playlist . id , playlist . video_count , offset , locale , continuation )
end
end
def fetch_playlist_videos ( plid , video_count , offset = 0 , locale = nil , continuation = nil )
if continuation
2020-06-25 02:18:09 +00:00
response = YT_POOL . client & . get ( " /watch?v= #{ continuation } &list= #{ plid } &gl=US&hl=en " )
initial_data = extract_initial_data ( response . body )
offset = initial_data [ " currentVideoEndpoint " ]? . try & . [ " watchEndpoint " ]? . try & . [ " index " ]? . try & . as_i64 || offset
2019-08-05 23:49:13 +00:00
end
if video_count > 100
url = produce_playlist_url ( plid , offset )
2019-10-25 16:58:16 +00:00
response = YT_POOL . client & . get ( url )
2020-06-25 02:18:09 +00:00
initial_data = JSON . parse ( response . body ) . as_a . find ( & . as_h . [ " response " ]? ) . try & . as_h
2019-08-05 23:49:13 +00:00
elsif offset > 100
return [ ] of PlaylistVideo
else # Extract first page of videos
2020-06-25 02:18:09 +00:00
response = YT_POOL . client & . get ( " /playlist?list= #{ plid } &gl=US&hl=en " )
initial_data = extract_initial_data ( response . body )
2019-08-05 23:49:13 +00:00
end
2020-06-25 02:18:09 +00:00
return [ ] of PlaylistVideo if ! initial_data
videos = extract_playlist_videos ( initial_data )
2019-08-05 23:49:13 +00:00
until videos . empty? || videos [ 0 ] . index == offset
videos . shift
end
return videos
end
2020-06-25 02:18:09 +00:00
def extract_playlist_videos ( initial_data : Hash ( String , JSON :: Any ) )
videos = [ ] of PlaylistVideo
( initial_data [ " contents " ]? . try & . [ " twoColumnBrowseResultsRenderer " ] [ " tabs " ] . as_a . select ( & . [ " tabRenderer " ] [ " selected " ]? . try & . as_bool ) [ 0 ] [ " tabRenderer " ] [ " content " ] [ " sectionListRenderer " ] [ " contents " ] [ 0 ] [ " itemSectionRenderer " ] [ " contents " ] [ 0 ] [ " playlistVideoListRenderer " ] [ " contents " ] . as_a ||
initial_data [ " response " ]? . try & . [ " continuationContents " ] [ " playlistVideoListContinuation " ] [ " contents " ] . as_a ) . try & . each do | item |
if i = item [ " playlistVideoRenderer " ]?
video_id = i [ " navigationEndpoint " ] [ " watchEndpoint " ] [ " videoId " ] . as_s
plid = i [ " navigationEndpoint " ] [ " watchEndpoint " ] [ " playlistId " ] . as_s
index = i [ " navigationEndpoint " ] [ " watchEndpoint " ] [ " index " ] . as_i64
thumbnail = i [ " thumbnail " ] [ " thumbnails " ] [ 0 ] [ " url " ] . as_s
title = i [ " title " ] . try { | t | t [ " simpleText " ]? || t [ " runs " ]? . try & . [ 0 ] [ " text " ]? } . try & . as_s || " "
author = i [ " shortBylineText " ]? . try & . [ " runs " ] [ 0 ] [ " text " ] . as_s || " "
ucid = i [ " shortBylineText " ]? . try & . [ " runs " ] [ 0 ] [ " navigationEndpoint " ] [ " browseEndpoint " ] [ " browseId " ] . as_s || " "
length_seconds = i [ " lengthSeconds " ]? . try & . as_s . to_i
live = false
if ! length_seconds
live = true
length_seconds = 0
end
videos << PlaylistVideo . new (
title : title ,
id : video_id ,
author : author ,
ucid : ucid ,
length_seconds : length_seconds ,
published : Time . utc ,
plid : plid ,
live_now : live ,
index : index - 1
)
end
end
return videos
end
2018-10-08 02:11:33 +00:00
def template_playlist ( playlist )
html = <<-END_HTML
< h3 >
< a href = " /playlist?list= #{ playlist [ " playlistId " ] } " >
#{playlist["title"]}
< / a>
< / h3>
< div class = " pure-menu pure-menu-scrollable playlist-restricted " >
< ol class = " pure-menu-list " >
END_HTML
playlist [ " videos " ] . as_a . each do | video |
html += <<-END_HTML
< li class = " pure-menu-item " >
< a href = " /watch?v= #{ video [ " videoId " ] } &list= #{ playlist [ " playlistId " ] } " >
2019-03-03 16:03:24 +00:00
< div class = " thumbnail " >
< img class = " thumbnail " src = " /vi/ #{ video [ " videoId " ] } /mqdefault.jpg " >
< p class = " length " > #{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
< / div>
2018-10-08 02:11:33 +00:00
< p style = " width:100% " > #{video["title"]}</p>
< p >
2019-05-02 01:03:39 +00:00
< b style = " width:100% " > #{video["author"]}</b>
2018-10-08 02:11:33 +00:00
< / p>
< / a>
< / li>
END_HTML
end
html += <<-END_HTML
< / ol>
< / div>
< hr >
END_HTML
html
end