2018-09-04 19:52:10 +05:30
# "Invidious" (which is an alternative front-end to YouTube)
2019-03-15 22:14:53 +05:30
# Copyright (C) 2019 Omar Roth
2018-01-28 23:02:40 +05:30
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2018-11-23 00:56:08 +05:30
require " digest/md5 "
2019-01-24 01:45:19 +05:30
require " file_utils "
2017-11-23 13:18:55 +05:30
require " kemal "
2018-07-19 00:56:02 +05:30
require " openssl/hmac "
2018-02-04 03:43:14 +05:30
require " option_parser "
2017-11-24 09:36:43 +05:30
require " pg "
2018-11-22 04:42:13 +05:30
require " sqlite3 "
2018-01-17 01:32:35 +05:30
require " xml "
2018-03-10 00:12:23 +05:30
require " yaml "
2018-07-30 23:04:57 +05:30
require " zip "
2019-10-27 23:20:42 +05:30
require " protodec/utils "
2018-08-05 02:00:44 +05:30
require " ./invidious/helpers/* "
2018-07-06 18:29:56 +05:30
require " ./invidious/* "
2017-11-30 03:03:46 +05:30
2020-02-04 20:20:28 +05:30
ENV_CONFIG_NAME = " INVIDIOUS_CONFIG "
CONFIG_STR = ENV . has_key? ( ENV_CONFIG_NAME ) ? ENV . fetch ( ENV_CONFIG_NAME ) : File . read ( " config/config.yml " )
CONFIG = Config . from_yaml ( CONFIG_STR )
HMAC_KEY = CONFIG . hmac_key || Random :: Secure . hex ( 32 )
2018-03-10 00:12:23 +05:30
PG_URL = URI . new (
scheme : " postgres " ,
2019-05-21 19:30:35 +05:30
user : CONFIG . db . user ,
password : CONFIG . db . password ,
host : CONFIG . db . host ,
port : CONFIG . db . port ,
path : CONFIG . db . dbname ,
2018-03-10 00:12:23 +05:30
)
2019-06-23 19:09:14 +05:30
PG_DB = DB . open PG_URL
ARCHIVE_URL = URI . parse ( " https://archive.org " )
LOGIN_URL = URI . parse ( " https://accounts.google.com " )
PUBSUB_URL = URI . parse ( " https://pubsubhubbub.appspot.com " )
REDDIT_URL = URI . parse ( " https://www.reddit.com " )
2020-03-10 20:42:11 +05:30
TEXTCAPTCHA_URL = URI . parse ( " https://textcaptcha.com " )
2019-06-23 19:09:14 +05:30
YT_URL = URI . parse ( " https://www.youtube.com " )
2019-06-07 23:09:12 +05:30
CHARS_SAFE = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ "
TEST_IDS = { " AgbeGFYluEA " , " BaW_jenozKc " , " a9LDPn-MO4I " , " ddFvjfvPnqk " , " iqKdEhx-dD4 " }
2019-06-09 02:34:55 +05:30
MAX_ITEMS_PER_PAGE = 1500
2018-03-05 09:55:03 +05:30
2019-11-25 00:11:47 +05:30
REQUEST_HEADERS_WHITELIST = { " accept " , " accept-encoding " , " cache-control " , " content-length " , " if-none-match " , " range " }
RESPONSE_HEADERS_BLACKLIST = { " access-control-allow-origin " , " alt-svc " , " server " }
2019-07-05 02:00:00 +05:30
HTTP_CHUNK_SIZE = 10485760 # ~10MB
2019-06-23 19:09:14 +05:30
2020-02-16 00:22:28 +05:30
CURRENT_BRANCH = {{ " #{ ` git branch | sed -n '/* /s///p' ` . strip } " }}
2019-06-23 19:09:14 +05:30
CURRENT_COMMIT = {{ " #{ ` git rev-list HEAD --max-count=1 --abbrev-commit ` . strip } " }}
CURRENT_VERSION = {{ " #{ ` git describe --tags --abbrev=0 ` . strip } " }}
2019-05-09 22:22:37 +05:30
# This is used to determine the `?v=` on the end of file URLs (for cache busting). We
# only need to expire modified assets, so we can use this to find the last commit that changes
# any assets
ASSET_COMMIT = {{ " #{ ` git rev-list HEAD --max-count=1 --abbrev-commit -- assets ` . strip } " }}
2019-04-06 18:58:53 +05:30
SOFTWARE = {
" name " = > " invidious " ,
" version " = > " #{ CURRENT_VERSION } - #{ CURRENT_COMMIT } " ,
" branch " = > " #{ CURRENT_BRANCH } " ,
}
2018-12-21 03:02:09 +05:30
LOCALES = {
" ar " = > load_locale ( " ar " ) ,
" de " = > load_locale ( " de " ) ,
2019-05-20 23:36:54 +05:30
" el " = > load_locale ( " el " ) ,
2018-12-21 03:02:09 +05:30
" en-US " = > load_locale ( " en-US " ) ,
2019-04-19 21:50:18 +05:30
" eo " = > load_locale ( " eo " ) ,
2019-04-06 03:54:06 +05:30
" es " = > load_locale ( " es " ) ,
2019-03-02 06:54:53 +05:30
" eu " = > load_locale ( " eu " ) ,
2019-01-22 02:34:09 +05:30
" fr " = > load_locale ( " fr " ) ,
2020-04-21 03:10:03 +05:30
" hu " = > load_locale ( " hu-HU " ) ,
2019-07-13 07:37:40 +05:30
" is " = > load_locale ( " is " ) ,
2019-02-20 05:16:31 +05:30
" it " = > load_locale ( " it " ) ,
2019-10-26 15:04:25 +05:30
" ja " = > load_locale ( " ja " ) ,
2019-12-06 01:56:35 +05:30
" nb-NO " = > load_locale ( " nb-NO " ) ,
2018-12-21 03:02:09 +05:30
" nl " = > load_locale ( " nl " ) ,
" pl " = > load_locale ( " pl " ) ,
2020-04-05 02:27:29 +05:30
" pt-BR " = > load_locale ( " pt-BR " ) ,
2020-04-21 03:10:03 +05:30
" pt-PT " = > load_locale ( " pt-PT " ) ,
2019-12-04 06:11:58 +05:30
" ro " = > load_locale ( " ro " ) ,
2018-12-21 03:02:09 +05:30
" ru " = > load_locale ( " ru " ) ,
2020-04-05 02:27:29 +05:30
" sv " = > load_locale ( " sv-SE " ) ,
2019-09-23 22:19:07 +05:30
" tr " = > load_locale ( " tr " ) ,
2019-04-19 21:50:18 +05:30
" uk " = > load_locale ( " uk " ) ,
2019-07-05 09:41:04 +05:30
" zh-CN " = > load_locale ( " zh-CN " ) ,
2019-10-09 19:53:26 +05:30
" zh-TW " = > load_locale ( " zh-TW " ) ,
2018-12-21 03:02:09 +05:30
}
2020-03-07 00:23:35 +05:30
YT_POOL = QUICPool . new ( YT_URL , capacity : CONFIG . pool_size , timeout : 0.1 )
2019-10-25 22:28:16 +05:30
2019-04-06 18:58:53 +05:30
config = CONFIG
logger = Invidious :: LogHandler . new
Kemal . config . extra_options do | parser |
parser . banner = " Usage: invidious [arguments] "
parser . on ( " -c THREADS " , " --channel-threads=THREADS " , " Number of threads for refreshing channels (default: #{ config . channel_threads } ) " ) do | number |
begin
config . channel_threads = number . to_i
rescue ex
puts " THREADS must be integer "
exit
end
end
parser . on ( " -f THREADS " , " --feed-threads=THREADS " , " Number of threads for refreshing feeds (default: #{ config . feed_threads } ) " ) do | number |
begin
config . feed_threads = number . to_i
rescue ex
puts " THREADS must be integer "
exit
end
end
parser . on ( " -o OUTPUT " , " --output=OUTPUT " , " Redirect output (default: STDOUT) " ) do | output |
FileUtils . mkdir_p ( File . dirname ( output ) )
logger = Invidious :: LogHandler . new ( File . open ( output , mode : " a " ) )
end
parser . on ( " -v " , " --version " , " Print version " ) do | output |
puts SOFTWARE . to_pretty_json
exit
end
end
Kemal :: CLI . new ARGV
2019-04-15 21:43:09 +05:30
# Check table integrity
2019-04-11 22:43:25 +05:30
if CONFIG . check_tables
2019-08-06 05:19:13 +05:30
check_enum ( PG_DB , logger , " privacy " , PlaylistPrivacy )
check_table ( PG_DB , logger , " channels " , InvidiousChannel )
check_table ( PG_DB , logger , " channel_videos " , ChannelVideo )
check_table ( PG_DB , logger , " playlists " , InvidiousPlaylist )
check_table ( PG_DB , logger , " playlist_videos " , PlaylistVideo )
check_table ( PG_DB , logger , " nonces " , Nonce )
check_table ( PG_DB , logger , " session_ids " , SessionId )
check_table ( PG_DB , logger , " users " , User )
check_table ( PG_DB , logger , " videos " , Video )
2019-04-15 21:43:09 +05:30
if CONFIG . cache_annotations
2019-08-06 05:19:13 +05:30
check_table ( PG_DB , logger , " annotations " , Annotation )
2019-04-15 21:43:09 +05:30
end
2019-04-11 22:43:25 +05:30
end
2018-03-26 08:48:29 +05:30
2019-04-11 02:53:37 +05:30
# Start jobs
2019-05-15 22:56:29 +05:30
2019-05-28 01:18:57 +05:30
refresh_channels ( PG_DB , logger , config )
refresh_feeds ( PG_DB , logger , config )
2019-03-04 06:48:23 +05:30
subscribe_to_feeds ( PG_DB , logger , HMAC_KEY , config )
2019-03-02 06:55:16 +05:30
statistics = {
" error " = > " Statistics are not availabile. " ,
}
if config . statistics_enabled
spawn do
2019-08-27 18:38:26 +05:30
statistics = {
" version " = > " 2.0 " ,
" software " = > SOFTWARE ,
" openRegistrations " = > config . registration_enabled ,
" usage " = > {
" users " = > {
" total " = > PG_DB . query_one ( " SELECT count(*) FROM users " , as : Int64 ) ,
" activeHalfyear " = > PG_DB . query_one ( " SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months' " , as : Int64 ) ,
" activeMonth " = > PG_DB . query_one ( " SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month' " , as : Int64 ) ,
2019-03-02 06:55:16 +05:30
} ,
2019-08-27 18:38:26 +05:30
} ,
" metadata " = > {
" updatedAt " = > Time . utc . to_unix ,
" lastChannelRefreshedAt " = > PG_DB . query_one? ( " SELECT updated FROM channels ORDER BY updated DESC LIMIT 1 " , as : Time ) . try & . to_unix || 0 _i64 ,
} ,
}
2019-03-02 06:55:16 +05:30
2019-08-27 18:38:26 +05:30
loop do
2019-03-02 06:55:16 +05:30
sleep 1 . minute
2019-06-16 05:48:36 +05:30
Fiber . yield
2019-08-27 18:38:26 +05:30
statistics [ " usage " ] . as ( Hash ) [ " users " ] . as ( Hash ) [ " total " ] = PG_DB . query_one ( " SELECT count(*) FROM users " , as : Int64 )
statistics [ " usage " ] . as ( Hash ) [ " users " ] . as ( Hash ) [ " activeHalfyear " ] = PG_DB . query_one ( " SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months' " , as : Int64 )
statistics [ " usage " ] . as ( Hash ) [ " users " ] . as ( Hash ) [ " activeMonth " ] = PG_DB . query_one ( " SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month' " , as : Int64 )
statistics [ " metadata " ] . as ( Hash ( String , Int64 ) ) [ " updatedAt " ] = Time . utc . to_unix
statistics [ " metadata " ] . as ( Hash ( String , Int64 ) ) [ " lastChannelRefreshedAt " ] = PG_DB . query_one? ( " SELECT updated FROM channels ORDER BY updated DESC LIMIT 1 " , as : Time ) . try & . to_unix || 0 _i64
2019-03-02 06:55:16 +05:30
end
end
end
2019-03-02 04:17:06 +05:30
2018-02-08 09:34:47 +05:30
top_videos = [ ] of Video
2019-03-02 03:36:45 +05:30
if config . top_enabled
spawn do
pull_top_videos ( config , PG_DB ) do | videos |
top_videos = videos
end
2018-02-08 09:34:47 +05:30
end
end
2018-11-09 07:38:03 +05:30
popular_videos = [ ] of ChannelVideo
spawn do
pull_popular_videos ( PG_DB ) do | videos |
popular_videos = videos
end
end
2020-01-25 03:32:28 +05:30
decrypt_function = [ ] of { SigProc , Int32 }
2018-07-17 21:23:17 +05:30
spawn do
2018-08-05 02:00:44 +05:30
update_decrypt_function do | function |
decrypt_function = function
2018-07-17 21:23:17 +05:30
end
end
2019-11-10 00:48:19 +05:30
if CONFIG . captcha_key
spawn do
bypass_captcha ( CONFIG . captcha_key , logger ) do | cookies |
cookies . each do | cookie |
config . cookies << cookie
end
# Persist cookies between runs
2019-11-10 20:32:02 +05:30
CONFIG . cookies = config . cookies
2019-11-10 00:48:19 +05:30
File . write ( " config/config.yml " , config . to_yaml )
end
end
end
2019-06-04 00:06:49 +05:30
connection_channel = Channel ( { Bool , Channel ( PQ :: Notification ) } ) . new ( 32 )
2019-06-03 23:42:06 +05:30
spawn do
connections = [ ] of Channel ( PQ :: Notification )
PG . connect_listen ( PG_URL , " notifications " ) { | event | connections . each { | connection | connection . send ( event ) } }
loop do
action , connection = connection_channel . receive
case action
when true
connections << connection
when false
connections . delete ( connection )
end
2019-06-02 18:11:53 +05:30
end
end
2018-03-25 09:26:41 +05:30
before_all do | env |
2020-03-16 03:16:08 +05:30
begin
preferences = Preferences . from_json ( env . request . cookies [ " PREFS " ]? . try & . value || " {} " )
rescue
preferences = Preferences . from_json ( " {} " )
end
2019-05-11 03:18:38 +05:30
env . response . headers [ " X-XSS-Protection " ] = " 1; mode=block "
2018-09-06 08:21:40 +05:30
env . response . headers [ " X-Content-Type-Options " ] = " nosniff "
2020-03-16 03:16:08 +05:30
extra_media_csp = " "
if CONFIG . disabled? ( " local " ) || ! preferences . local
extra_media_csp += " https://*.googlevideo.com:443 "
end
# TODO: Remove style-src's 'unsafe-inline', requires to remove all inline styles (<style> [..] </style>, style=" [..] ")
2020-03-20 00:07:22 +05:30
env . response . headers [ " Content-Security-Policy " ] = " default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; manifest-src 'self'; media-src 'self' blob: #{ extra_media_csp } "
2019-04-08 00:31:08 +05:30
env . response . headers [ " Referrer-Policy " ] = " same-origin "
2019-05-14 18:51:01 +05:30
if ( Kemal . config . ssl || config . https_only ) && config . hsts
2019-05-01 07:23:56 +05:30
env . response . headers [ " Strict-Transport-Security " ] = " max-age=31536000; includeSubDomains; preload "
2019-04-08 00:31:08 +05:30
end
2019-03-29 00:13:40 +05:30
2019-11-20 22:33:52 +05:30
next if {
" /sb/ " ,
" /vi/ " ,
" /s_p/ " ,
" /yts/ " ,
" /ggpht/ " ,
" /api/manifest/ " ,
" /videoplayback " ,
" /latest_version " ,
} . any? { | r | env . request . resource . starts_with? r }
2018-07-16 21:54:24 +05:30
if env . request . cookies . has_key? " SID "
2018-04-01 05:39:27 +05:30
sid = env . request . cookies [ " SID " ] . value
2018-07-06 05:13:26 +05:30
2019-04-19 02:53:50 +05:30
if sid . starts_with? " v1: "
raise " Cannot use token as SID "
end
2018-07-19 00:56:02 +05:30
# Invidious users only have SID
if ! env . request . cookies . has_key? " SSID "
2019-04-16 09:53:40 +05:30
if email = PG_DB . query_one? ( " SELECT email FROM session_ids WHERE id = $1 " , sid , as : String )
2019-02-12 08:22:47 +05:30
user = PG_DB . query_one ( " SELECT * FROM users WHERE email = $1 " , email , as : User )
2019-08-06 05:19:13 +05:30
csrf_token = generate_response ( sid , {
" :authorize_token " ,
" :playlist_ajax " ,
" :signout " ,
" :subscription_ajax " ,
" :token_ajax " ,
" :watch_ajax " ,
} , HMAC_KEY , PG_DB , 1 . week )
2018-11-09 05:12:25 +05:30
2019-03-11 23:14:25 +05:30
preferences = user . preferences
2018-08-15 23:10:42 +05:30
env . set " sid " , sid
2019-04-19 02:53:50 +05:30
env . set " csrf_token " , csrf_token
2019-04-16 09:53:40 +05:30
env . set " user " , user
2018-07-19 00:56:02 +05:30
end
else
2019-04-16 09:53:40 +05:30
headers = HTTP :: Headers . new
headers [ " Cookie " ] = env . request . headers [ " Cookie " ]
2018-07-19 00:56:02 +05:30
begin
2019-02-11 00:03:29 +05:30
user , sid = get_user ( sid , headers , PG_DB , false )
2019-08-06 05:19:13 +05:30
csrf_token = generate_response ( sid , {
" :authorize_token " ,
" :playlist_ajax " ,
" :signout " ,
" :subscription_ajax " ,
" :token_ajax " ,
" :watch_ajax " ,
} , HMAC_KEY , PG_DB , 1 . week )
2018-11-16 07:53:17 +05:30
2019-03-11 23:14:25 +05:30
preferences = user . preferences
2018-08-15 23:10:42 +05:30
env . set " sid " , sid
2019-04-19 02:53:50 +05:30
env . set " csrf_token " , csrf_token
2019-04-16 09:53:40 +05:30
env . set " user " , user
2018-07-19 00:56:02 +05:30
rescue ex
end
2018-07-16 23:20:41 +05:30
end
2018-04-14 08:02:14 +05:30
end
2018-08-17 20:49:20 +05:30
2019-08-15 21:59:55 +05:30
dark_mode = convert_theme ( env . params . query [ " dark_mode " ]? ) || preferences . dark_mode . to_s
2019-03-11 23:14:25 +05:30
thin_mode = env . params . query [ " thin_mode " ]? || preferences . thin_mode . to_s
thin_mode = thin_mode == " true "
locale = env . params . query [ " hl " ]? || preferences . locale
preferences . dark_mode = dark_mode
preferences . thin_mode = thin_mode
preferences . locale = locale
env . set " preferences " , preferences
2018-12-21 03:02:09 +05:30
2018-08-17 20:49:20 +05:30
current_page = env . request . path
if env . request . query
query = HTTP :: Params . parse ( env . request . query . not_nil! )
if query [ " referer " ]?
query [ " referer " ] = get_referer ( env , " / " )
end
current_page += " ? #{ query } "
end
2019-09-24 23:01:33 +05:30
env . set " current_page " , URI . encode_www_form ( current_page )
2018-03-22 23:14:36 +05:30
end
2018-02-08 09:34:47 +05:30
get " / " do | env |
2019-10-21 06:12:18 +05:30
preferences = env . get ( " preferences " ) . as ( Preferences )
locale = LOCALES [ preferences . locale ]?
2018-07-29 09:01:02 +05:30
user = env . get? " user "
2018-12-21 03:02:09 +05:30
2019-10-21 06:12:18 +05:30
case preferences . default_home
when " "
templated " empty "
2019-03-02 03:36:45 +05:30
when " Popular "
templated " popular "
when " Top "
2019-10-21 06:12:18 +05:30
if config . top_enabled
templated " top "
else
templated " empty "
end
2019-03-02 03:36:45 +05:30
when " Trending "
env . redirect " /feed/trending "
when " Subscriptions "
if user
env . redirect " /feed/subscriptions "
else
templated " popular "
end
2019-10-21 06:12:18 +05:30
when " Playlists "
if user
env . redirect " /view_all_playlists "
else
templated " popular "
end
2020-04-09 22:48:09 +05:30
else
templated " empty "
2019-03-02 03:36:45 +05:30
end
2017-12-31 02:58:41 +05:30
end
2019-03-13 07:21:23 +05:30
get " /privacy " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
templated " privacy "
end
2018-11-10 22:38:03 +05:30
get " /licenses " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-11-10 22:38:03 +05:30
rendered " licenses "
end
2018-08-05 02:00:44 +05:30
# Videos
2017-12-31 02:58:41 +05:30
get " /watch " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-02-07 05:25:22 +05:30
region = env . params . query [ " region " ]?
2018-12-21 03:02:09 +05:30
2018-08-07 07:22:37 +05:30
if env . params . query . to_s . includes? ( " %20 " ) || env . params . query . to_s . includes? ( " + " )
url = " /watch? " + env . params . query . to_s . gsub ( " %20 " , " " ) . delete ( " + " )
next env . redirect url
end
2018-11-06 21:25:52 +05:30
if env . params . query [ " v " ]?
2018-07-29 07:10:59 +05:30
id = env . params . query [ " v " ]
2018-08-05 03:49:42 +05:30
2018-11-06 21:25:52 +05:30
if env . params . query [ " v " ] . empty?
error_message = " Invalid parameters. "
2019-06-18 00:36:02 +05:30
env . response . status_code = 400
2018-11-06 21:25:52 +05:30
next templated " error "
end
2018-08-05 03:49:42 +05:30
if id . size > 11
2018-08-07 05:58:16 +05:30
url = " /watch?v= #{ id [ 0 , 11 ] } "
env . params . query . delete_all ( " v " )
if env . params . query . size > 0
url += " & #{ env . params . query } "
end
next env . redirect url
2018-08-05 03:49:42 +05:30
end
2018-07-29 07:10:59 +05:30
else
next env . redirect " / "
end
2020-02-29 00:40:01 +05:30
plid = env . params . query [ " list " ]? . try & . gsub ( / [^a-zA-Z0-9_-] / , " " )
2019-08-06 05:19:13 +05:30
continuation = process_continuation ( PG_DB , env . params . query , plid , id )
2018-11-01 03:17:53 +05:30
nojs = env . params . query [ " nojs " ]?
nojs || = " 0 "
nojs = nojs == " 1 "
2018-10-08 07:41:33 +05:30
2019-03-27 22:01:05 +05:30
preferences = env . get ( " preferences " ) . as ( Preferences )
2018-07-29 07:10:59 +05:30
2019-05-01 10:09:04 +05:30
user = env . get? ( " user " ) . try & . as ( User )
if user
2018-08-05 09:37:38 +05:30
subscriptions = user . subscriptions
2018-11-20 09:36:59 +05:30
watched = user . watched
2019-06-01 20:21:31 +05:30
notifications = user . notifications
2018-07-06 05:13:26 +05:30
end
subscriptions || = [ ] of String
2018-08-26 06:35:51 +05:30
params = process_video_params ( env . params . query , preferences )
2018-10-30 20:11:23 +05:30
env . params . query . delete_all ( " listen " )
2018-01-07 08:09:24 +05:30
begin
2019-06-29 07:47:56 +05:30
video = get_video ( id , PG_DB , region : params . region )
2018-10-07 08:52:22 +05:30
rescue ex : VideoRedirect
2019-09-08 21:38:59 +05:30
next env . redirect env . request . resource . gsub ( id , ex . video_id )
2018-01-07 08:09:24 +05:30
rescue ex
error_message = ex . message
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2019-06-08 06:37:55 +05:30
logger . puts ( " #{ id } : #{ ex . message } " )
2018-01-07 08:09:24 +05:30
next templated " error "
end
2017-12-31 02:58:41 +05:30
2019-05-01 18:08:42 +05:30
if preferences . annotations_subscribed &&
subscriptions . includes? ( video . ucid ) &&
( env . params . query [ " iv_load_policy " ]? || " 1 " ) == " 1 "
2019-05-01 10:09:04 +05:30
params . annotations = true
end
2019-05-01 18:08:42 +05:30
env . params . query . delete_all ( " iv_load_policy " )
2019-05-01 10:09:04 +05:30
2018-11-20 09:36:59 +05:30
if watched && ! watched . includes? id
2020-02-28 22:16:24 +05:30
PG_DB . exec ( " UPDATE users SET watched = array_append(watched, $1) WHERE email = $2 " , id , user . as ( User ) . email )
2018-11-20 09:36:59 +05:30
end
2019-06-01 20:21:31 +05:30
if notifications && notifications . includes? id
PG_DB . exec ( " UPDATE users SET notifications = array_remove(notifications, $1) WHERE email = $2 " , id , user . as ( User ) . email )
env . get ( " user " ) . as ( User ) . notifications . delete ( id )
notifications . delete ( id )
end
2018-11-01 03:17:53 +05:30
if nojs
if preferences
source = preferences . comments [ 0 ]
if source . empty?
source = preferences . comments [ 1 ]
end
if source == " youtube "
begin
2019-06-29 07:47:56 +05:30
comment_html = JSON . parse ( fetch_youtube_comments ( id , PG_DB , nil , " html " , locale , preferences . thin_mode , region ) ) [ " contentHtml " ]
2018-11-01 03:17:53 +05:30
rescue ex
if preferences . comments [ 1 ] == " reddit "
comments , reddit_thread = fetch_reddit_comments ( id )
2018-12-21 03:02:09 +05:30
comment_html = template_reddit_comments ( comments , locale )
2018-11-01 03:17:53 +05:30
comment_html = fill_links ( comment_html , " https " , " www.reddit.com " )
comment_html = replace_links ( comment_html )
end
end
elsif source == " reddit "
begin
comments , reddit_thread = fetch_reddit_comments ( id )
2018-12-21 03:02:09 +05:30
comment_html = template_reddit_comments ( comments , locale )
2018-11-01 03:17:53 +05:30
comment_html = fill_links ( comment_html , " https " , " www.reddit.com " )
comment_html = replace_links ( comment_html )
rescue ex
if preferences . comments [ 1 ] == " youtube "
2019-06-29 07:47:56 +05:30
comment_html = JSON . parse ( fetch_youtube_comments ( id , PG_DB , nil , " html " , locale , preferences . thin_mode , region ) ) [ " contentHtml " ]
2018-11-01 03:17:53 +05:30
end
end
end
else
2019-06-29 07:47:56 +05:30
comment_html = JSON . parse ( fetch_youtube_comments ( id , PG_DB , nil , " html " , locale , preferences . thin_mode , region ) ) [ " contentHtml " ]
2018-11-01 03:17:53 +05:30
end
comment_html || = " "
end
2018-08-05 09:37:38 +05:30
fmt_stream = video . fmt_stream ( decrypt_function )
adaptive_fmts = video . adaptive_fmts ( decrypt_function )
2019-03-11 22:13:48 +05:30
2019-05-01 10:09:04 +05:30
if params . local
2019-03-11 23:25:05 +05:30
fmt_stream . each { | fmt | fmt [ " url " ] = URI . parse ( fmt [ " url " ] ) . full_path }
adaptive_fmts . each { | fmt | fmt [ " url " ] = URI . parse ( fmt [ " url " ] ) . full_path }
2019-03-11 22:13:48 +05:30
end
2018-08-07 22:09:56 +05:30
video_streams = video . video_streams ( adaptive_fmts )
2018-08-05 09:37:38 +05:30
audio_streams = video . audio_streams ( adaptive_fmts )
2018-01-21 22:37:32 +05:30
2019-05-04 21:17:54 +05:30
# Older videos may not have audio sources available.
# We redirect here so they're not unplayable
2019-10-20 22:18:11 +05:30
if audio_streams . empty? && ! video . live_now
2019-08-09 08:39:34 +05:30
if params . quality == " dash "
env . params . query . delete_all ( " quality " )
env . params . query [ " quality " ] = " medium "
next env . redirect " /watch? #{ env . params . query } "
elsif params . listen
env . params . query . delete_all ( " listen " )
env . params . query [ " listen " ] = " 0 "
next env . redirect " /watch? #{ env . params . query } "
end
2019-05-04 21:17:54 +05:30
end
2018-08-05 09:37:38 +05:30
captions = video . captions
2018-08-06 23:53:36 +05:30
2018-08-26 06:35:51 +05:30
preferred_captions = captions . select { | caption |
2019-05-01 10:09:04 +05:30
params . preferred_captions . includes? ( caption . name . simpleText ) ||
params . preferred_captions . includes? ( caption . languageCode . split ( " - " ) [ 0 ] )
2018-08-26 06:35:51 +05:30
}
preferred_captions . sort_by! { | caption |
2019-05-01 10:09:04 +05:30
( params . preferred_captions . index ( caption . name . simpleText ) ||
params . preferred_captions . index ( caption . languageCode . split ( " - " ) [ 0 ] ) ) . not_nil!
2018-08-26 06:35:51 +05:30
}
captions = captions - preferred_captions
2018-08-11 21:22:13 +05:30
aspect_ratio = " 16:9 "
2018-05-30 05:10:36 +05:30
2019-06-09 01:38:27 +05:30
video . description_html = fill_links ( video . description_html , " https " , " www.youtube.com " )
video . description_html = replace_links ( video . description_html )
2018-03-14 05:07:56 +05:30
2019-03-06 00:26:59 +05:30
host_url = make_host_url ( config , Kemal . config )
2018-07-22 21:39:43 +05:30
2019-01-12 23:30:44 +05:30
if video . player_response [ " streamingData " ]? . try & . [ " hlsManifestUrl " ]?
hlsvp = video . player_response [ " streamingData " ] [ " hlsManifestUrl " ] . as_s
2018-08-05 09:37:38 +05:30
hlsvp = hlsvp . gsub ( " https://manifest.googlevideo.com " , host_url )
2018-07-28 04:55:58 +05:30
end
2018-09-15 07:54:28 +05:30
thumbnail = " /vi/ #{ video . id } /maxres.jpg "
2018-08-05 09:37:38 +05:30
2019-05-01 10:09:04 +05:30
if params . raw
2019-06-02 02:56:18 +05:30
if params . listen
url = audio_streams [ 0 ] [ " url " ]
audio_streams . each do | fmt |
if fmt [ " bitrate " ] == params . quality . rchop ( " k " )
url = fmt [ " url " ]
end
end
else
url = fmt_stream [ 0 ] [ " url " ]
2018-08-06 00:33:13 +05:30
2019-06-02 02:56:18 +05:30
fmt_stream . each do | fmt |
if fmt [ " label " ] . split ( " - " ) [ 0 ] == params . quality
url = fmt [ " url " ]
end
2018-08-06 00:33:13 +05:30
end
end
next env . redirect url
end
2018-01-20 06:01:47 +05:30
rvs = [ ] of Hash ( String , String )
2018-08-13 21:20:09 +05:30
video . info [ " rvs " ]? . try & . split ( " , " ) . each do | rv |
rvs << HTTP :: Params . parse ( rv ) . to_h
2018-01-20 06:01:47 +05:30
end
2018-01-21 22:37:32 +05:30
2018-01-07 08:09:24 +05:30
rating = video . info [ " avg_rating " ] . to_f64
2019-07-31 20:18:45 +05:30
if video . views > 0
engagement = ( ( video . dislikes . to_f + video . likes . to_f ) / video . views * 100 )
else
engagement = 0
end
2018-02-12 09:36:29 +05:30
2018-08-18 22:17:16 +05:30
playability_status = video . player_response [ " playabilityStatus " ]?
2019-06-08 20:48:45 +05:30
if playability_status && playability_status [ " status " ] == " LIVE_STREAM_OFFLINE " && ! video . premiere_timestamp
2018-08-18 22:17:16 +05:30
reason = playability_status [ " reason " ]? . try & . as_s
end
reason || = " "
2017-11-23 13:18:55 +05:30
templated " watch "
end
2019-07-25 21:04:01 +05:30
get " /embed/ " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2020-02-29 00:40:01 +05:30
if plid = env . params . query [ " list " ]? . try & . gsub ( / [^a-zA-Z0-9_-] / , " " )
2019-07-25 21:04:01 +05:30
begin
2019-08-06 05:19:13 +05:30
playlist = get_playlist ( PG_DB , plid , locale : locale )
offset = env . params . query [ " index " ]? . try & . to_i? || 0
videos = get_playlist_videos ( PG_DB , playlist , offset : offset , locale : locale )
2019-07-25 21:04:01 +05:30
rescue ex
error_message = ex . message
env . response . status_code = 500
next templated " error "
end
url = " /embed/ #{ videos [ 0 ] . id } ? #{ env . params . query } "
if env . params . query . size > 0
url += " ? #{ env . params . query } "
end
else
url = " / "
end
env . redirect url
end
2018-08-05 02:00:44 +05:30
get " /embed/:id " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-08-07 07:22:37 +05:30
id = env . params . url [ " id " ]
2019-08-06 05:19:13 +05:30
2020-02-29 00:40:01 +05:30
plid = env . params . query [ " list " ]? . try & . gsub ( / [^a-zA-Z0-9_-] / , " " )
2019-08-06 05:19:13 +05:30
continuation = process_continuation ( PG_DB , env . params . query , plid , id )
2019-04-14 00:56:32 +05:30
if md = env . params . query [ " playlist " ]?
. try & . match ( / [a-zA-Z0-9_-]{11}(,[a-zA-Z0-9_-]{11})* / )
video_series = md [ 0 ] . split ( " , " )
env . params . query . delete ( " playlist " )
end
2018-08-05 03:49:42 +05:30
2019-03-27 22:01:05 +05:30
preferences = env . get ( " preferences " ) . as ( Preferences )
2019-03-26 03:03:46 +05:30
2018-08-07 07:22:37 +05:30
if id . includes? ( " %20 " ) || id . includes? ( " + " ) || env . params . query . to_s . includes? ( " %20 " ) || env . params . query . to_s . includes? ( " + " )
id = env . params . url [ " id " ] . gsub ( " %20 " , " " ) . delete ( " + " )
2018-08-07 05:58:16 +05:30
2018-08-07 07:22:37 +05:30
url = " /embed/ #{ id } "
2018-08-07 05:58:16 +05:30
2018-08-07 07:22:37 +05:30
if env . params . query . size > 0
url += " ? #{ env . params . query . to_s . gsub ( " %20 " , " " ) . delete ( " + " ) } "
2018-08-05 03:49:42 +05:30
end
2018-08-07 07:22:37 +05:30
next env . redirect url
end
2019-04-14 00:56:32 +05:30
# YouTube embed supports `videoseries` with either `list=PLID`
# or `playlist=VIDEO_ID,VIDEO_ID`
2019-07-25 05:37:48 +05:30
case id
when " videoseries "
2019-04-14 00:56:32 +05:30
url = " "
if plid
begin
2019-08-06 05:19:13 +05:30
playlist = get_playlist ( PG_DB , plid , locale : locale )
offset = env . params . query [ " index " ]? . try & . to_i? || 0
videos = get_playlist_videos ( PG_DB , playlist , offset : offset , locale : locale )
2019-04-14 00:56:32 +05:30
rescue ex
error_message = ex . message
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2019-04-14 00:56:32 +05:30
next templated " error "
end
url = " /embed/ #{ videos [ 0 ] . id } "
elsif video_series
url = " /embed/ #{ video_series . shift } "
env . params . query [ " playlist " ] = video_series . join ( " , " )
else
next env . redirect " / "
end
if env . params . query . size > 0
url += " ? #{ env . params . query } "
end
next env . redirect url
2019-07-25 05:37:48 +05:30
when " live_stream "
2019-10-25 22:28:16 +05:30
response = YT_POOL . client & . get ( " /embed/live_stream?channel= #{ env . params . query [ " channel " ]? || " " } " )
2019-07-25 05:37:48 +05:30
video_id = response . body . match ( / "video_id":"(?<video_id>[a-zA-Z0-9_-]{11})" / ) . try & . [ " video_id " ]
env . params . query . delete_all ( " channel " )
if ! video_id || video_id == " live_stream "
error_message = " Video is unavailable. "
next templated " error "
end
url = " /embed/ #{ video_id } "
if env . params . query . size > 0
url += " ? #{ env . params . query } "
end
next env . redirect url
when id . size > 11
2018-08-07 07:22:37 +05:30
url = " /embed/ #{ id [ 0 , 11 ] } "
if env . params . query . size > 0
url += " ? #{ env . params . query } "
end
next env . redirect url
2020-04-09 22:48:09 +05:30
else nil # Continue
2018-08-05 02:00:44 +05:30
end
2019-03-26 03:03:46 +05:30
params = process_video_params ( env . params . query , preferences )
2018-07-22 21:39:43 +05:30
2019-05-01 10:09:04 +05:30
user = env . get? ( " user " ) . try & . as ( User )
if user
subscriptions = user . subscriptions
watched = user . watched
2019-06-01 20:21:31 +05:30
notifications = user . notifications
2019-05-01 10:09:04 +05:30
end
subscriptions || = [ ] of String
2018-07-22 21:39:43 +05:30
begin
2019-06-29 07:47:56 +05:30
video = get_video ( id , PG_DB , region : params . region )
2018-10-07 08:52:22 +05:30
rescue ex : VideoRedirect
2019-09-08 21:38:59 +05:30
next env . redirect env . request . resource . gsub ( id , ex . video_id )
2018-07-22 21:39:43 +05:30
rescue ex
2018-08-05 02:00:44 +05:30
error_message = ex . message
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2018-08-05 02:00:44 +05:30
next templated " error "
2018-07-22 21:39:43 +05:30
end
2019-05-01 18:08:42 +05:30
if preferences . annotations_subscribed &&
subscriptions . includes? ( video . ucid ) &&
( env . params . query [ " iv_load_policy " ]? || " 1 " ) == " 1 "
2019-05-01 10:09:04 +05:30
params . annotations = true
end
2019-07-06 03:25:06 +05:30
# if watched && !watched.includes? id
2020-02-28 22:16:24 +05:30
# PG_DB.exec("UPDATE users SET watched = array_append(watched, $1) WHERE email = $2", id, user.as(User).email)
2019-07-06 03:25:06 +05:30
# end
2019-05-01 10:09:04 +05:30
2019-06-01 20:21:31 +05:30
if notifications && notifications . includes? id
PG_DB . exec ( " UPDATE users SET notifications = array_remove(notifications, $1) WHERE email = $2 " , id , user . as ( User ) . email )
env . get ( " user " ) . as ( User ) . notifications . delete ( id )
notifications . delete ( id )
end
2018-08-05 09:37:38 +05:30
fmt_stream = video . fmt_stream ( decrypt_function )
adaptive_fmts = video . adaptive_fmts ( decrypt_function )
2019-03-11 22:13:48 +05:30
2019-05-01 10:09:04 +05:30
if params . local
2019-03-11 23:25:05 +05:30
fmt_stream . each { | fmt | fmt [ " url " ] = URI . parse ( fmt [ " url " ] ) . full_path }
adaptive_fmts . each { | fmt | fmt [ " url " ] = URI . parse ( fmt [ " url " ] ) . full_path }
2019-03-11 22:13:48 +05:30
end
2018-08-07 22:09:56 +05:30
video_streams = video . video_streams ( adaptive_fmts )
2018-08-05 09:37:38 +05:30
audio_streams = video . audio_streams ( adaptive_fmts )
2018-07-22 21:39:43 +05:30
2019-10-20 22:18:11 +05:30
if audio_streams . empty? && ! video . live_now
2019-08-09 08:39:34 +05:30
if params . quality == " dash "
env . params . query . delete_all ( " quality " )
2019-10-20 22:18:11 +05:30
next env . redirect " /embed/ #{ id } ? #{ env . params . query } "
2019-08-09 08:39:34 +05:30
elsif params . listen
env . params . query . delete_all ( " listen " )
env . params . query [ " listen " ] = " 0 "
2019-10-20 22:18:11 +05:30
next env . redirect " /embed/ #{ id } ? #{ env . params . query } "
2019-08-09 08:39:34 +05:30
end
end
2018-08-05 09:37:38 +05:30
captions = video . captions
2018-07-22 21:39:43 +05:30
2018-08-26 06:35:51 +05:30
preferred_captions = captions . select { | caption |
2019-05-01 10:09:04 +05:30
params . preferred_captions . includes? ( caption . name . simpleText ) ||
params . preferred_captions . includes? ( caption . languageCode . split ( " - " ) [ 0 ] )
2018-08-26 06:35:51 +05:30
}
preferred_captions . sort_by! { | caption |
2019-05-01 10:09:04 +05:30
( params . preferred_captions . index ( caption . name . simpleText ) ||
params . preferred_captions . index ( caption . languageCode . split ( " - " ) [ 0 ] ) ) . not_nil!
2018-08-26 06:35:51 +05:30
}
captions = captions - preferred_captions
aspect_ratio = nil
2019-06-09 01:38:27 +05:30
video . description_html = fill_links ( video . description_html , " https " , " www.youtube.com " )
video . description_html = replace_links ( video . description_html )
2018-07-22 21:39:43 +05:30
2019-03-06 00:26:59 +05:30
host_url = make_host_url ( config , Kemal . config )
2018-07-22 21:39:43 +05:30
2019-01-12 23:30:44 +05:30
if video . player_response [ " streamingData " ]? . try & . [ " hlsManifestUrl " ]?
hlsvp = video . player_response [ " streamingData " ] [ " hlsManifestUrl " ] . as_s
2018-08-05 09:37:38 +05:30
hlsvp = hlsvp . gsub ( " https://manifest.googlevideo.com " , host_url )
2018-08-05 02:00:44 +05:30
end
2018-07-22 21:39:43 +05:30
2018-09-15 07:54:28 +05:30
thumbnail = " /vi/ #{ video . id } /maxres.jpg "
2018-07-22 21:39:43 +05:30
2019-05-01 10:09:04 +05:30
if params . raw
2018-08-05 02:00:44 +05:30
url = fmt_stream [ 0 ] [ " url " ]
2018-07-22 21:39:43 +05:30
2018-08-05 02:00:44 +05:30
fmt_stream . each do | fmt |
2019-05-01 10:09:04 +05:30
if fmt [ " label " ] . split ( " - " ) [ 0 ] == params . quality
2018-08-05 02:00:44 +05:30
url = fmt [ " url " ]
end
2018-07-22 21:39:43 +05:30
end
2018-08-05 02:00:44 +05:30
next env . redirect url
end
2018-07-22 21:39:43 +05:30
2018-08-05 02:00:44 +05:30
rendered " embed "
end
2018-07-21 01:06:23 +05:30
2019-08-06 05:19:13 +05:30
# Playlists
2019-10-21 06:12:18 +05:30
get " /feed/playlists " do | env |
env . redirect " /view_all_playlists "
end
2019-08-06 05:19:13 +05:30
get " /view_all_playlists " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
user = user . as ( User )
2020-05-17 16:58:00 +05:30
items_created = PG_DB . query_all ( " SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created " , user . email , as : InvidiousPlaylist )
items_created . map! do | item |
item . author = " "
item
end
items_saved = PG_DB . query_all ( " SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created " , user . email , as : InvidiousPlaylist )
items_saved . map! do | item |
2019-08-06 05:19:13 +05:30
item . author = " "
item
end
templated " view_all_playlists "
end
get " /create_playlist " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
user = user . as ( User )
sid = sid . as ( String )
csrf_token = generate_response ( sid , { " :create_playlist " } , HMAC_KEY , PG_DB )
templated " create_playlist "
end
post " /create_playlist " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
error_message = ex . message
env . response . status_code = 400
next templated " error "
end
title = env . params . body [ " title " ]? . try & . as ( String )
if ! title || title . empty?
error_message = " Title cannot be empty. "
next templated " error "
end
privacy = PlaylistPrivacy . parse? ( env . params . body [ " privacy " ]? . try & . as ( String ) || " " )
if ! privacy
error_message = " Invalid privacy setting. "
next templated " error "
end
if PG_DB . query_one ( " SELECT count(*) FROM playlists WHERE author = $1 " , user . email , as : Int64 ) >= 100
error_message = " User cannot have more than 100 playlists. "
next templated " error "
end
playlist = create_playlist ( PG_DB , title , privacy , user )
env . redirect " /playlist?list= #{ playlist . id } "
end
2020-05-17 16:58:00 +05:30
get " /subscribe_playlist " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
user = user . as ( User )
playlist_id = env . params . query [ " list " ]
playlist = get_playlist ( PG_DB , playlist_id , locale )
subscribe_playlist ( PG_DB , user , playlist )
env . redirect " /playlist?list= #{ playlist . id } "
end
2019-08-06 05:19:13 +05:30
get " /delete_playlist " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
user = user . as ( User )
sid = sid . as ( String )
plid = env . params . query [ " list " ]?
playlist = PG_DB . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
if ! playlist || playlist . author != user . email
next env . redirect referer
end
csrf_token = generate_response ( sid , { " :delete_playlist " } , HMAC_KEY , PG_DB )
templated " delete_playlist "
end
post " /delete_playlist " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
plid = env . params . query [ " list " ]?
if ! plid
next env . redirect referer
end
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
error_message = ex . message
env . response . status_code = 400
next templated " error "
end
playlist = PG_DB . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
if ! playlist || playlist . author != user . email
next env . redirect referer
end
PG_DB . exec ( " DELETE FROM playlist_videos * WHERE plid = $1 " , plid )
PG_DB . exec ( " DELETE FROM playlists * WHERE id = $1 " , plid )
env . redirect " /view_all_playlists "
end
get " /edit_playlist " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
user = user . as ( User )
sid = sid . as ( String )
plid = env . params . query [ " list " ]?
if ! plid || ! plid . starts_with? ( " IV " )
next env . redirect referer
end
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
begin
playlist = PG_DB . query_one ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
if ! playlist || playlist . author != user . email
next env . redirect referer
end
rescue ex
next env . redirect referer
end
begin
videos = get_playlist_videos ( PG_DB , playlist , offset : ( page - 1 ) * 100 , locale : locale )
rescue ex
videos = [ ] of PlaylistVideo
end
csrf_token = generate_response ( sid , { " :edit_playlist " } , HMAC_KEY , PG_DB )
templated " edit_playlist "
end
post " /edit_playlist " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
plid = env . params . query [ " list " ]?
if ! plid
next env . redirect referer
end
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
error_message = ex . message
env . response . status_code = 400
next templated " error "
end
playlist = PG_DB . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
if ! playlist || playlist . author != user . email
next env . redirect referer
end
title = env . params . body [ " title " ]? . try & . delete ( " <> " ) || " "
privacy = PlaylistPrivacy . parse ( env . params . body [ " privacy " ]? || " Public " )
description = env . params . body [ " description " ]? . try & . delete ( " \ r " ) || " "
if title != playlist . title ||
privacy != playlist . privacy ||
description != playlist . description
updated = Time . utc
else
updated = playlist . updated
end
PG_DB . exec ( " UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5 " , title , privacy , description , updated , plid )
env . redirect " /playlist?list= #{ plid } "
end
get " /add_playlist_items " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
if ! user
next env . redirect " / "
end
user = user . as ( User )
sid = sid . as ( String )
plid = env . params . query [ " list " ]?
if ! plid || ! plid . starts_with? ( " IV " )
next env . redirect referer
end
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
begin
playlist = PG_DB . query_one ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
if ! playlist || playlist . author != user . email
next env . redirect referer
end
rescue ex
next env . redirect referer
end
query = env . params . query [ " q " ]?
if query
begin
search_query , count , items = process_search_query ( query , page , user , region : nil )
videos = items . select { | item | item . is_a? SearchVideo } . map { | item | item . as ( SearchVideo ) }
rescue ex
videos = [ ] of SearchVideo
count = 0
end
else
videos = [ ] of SearchVideo
count = 0
end
env . set " add_playlist_items " , plid
templated " add_playlist_items "
end
post " /playlist_ajax " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env , " / " )
redirect = env . params . query [ " redirect " ]?
redirect || = " true "
redirect = redirect == " true "
if ! user
if redirect
next env . redirect referer
else
error_message = { " error " = > " No such user " } . to_json
env . response . status_code = 403
next error_message
end
end
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
if redirect
error_message = ex . message
env . response . status_code = 400
next templated " error "
else
error_message = { " error " = > ex . message } . to_json
env . response . status_code = 400
next error_message
end
end
if env . params . query [ " action_create_playlist " ]?
action = " action_create_playlist "
elsif env . params . query [ " action_delete_playlist " ]?
action = " action_delete_playlist "
elsif env . params . query [ " action_edit_playlist " ]?
action = " action_edit_playlist "
elsif env . params . query [ " action_add_video " ]?
action = " action_add_video "
video_id = env . params . query [ " video_id " ]
elsif env . params . query [ " action_remove_video " ]?
action = " action_remove_video "
elsif env . params . query [ " action_move_video_before " ]?
action = " action_move_video_before "
else
next env . redirect referer
end
begin
playlist_id = env . params . query [ " playlist_id " ]
playlist = get_playlist ( PG_DB , playlist_id , locale ) . as ( InvidiousPlaylist )
raise " Invalid user " if playlist . author != user . email
rescue ex
if redirect
error_message = ex . message
env . response . status_code = 400
next templated " error "
else
error_message = { " error " = > ex . message } . to_json
env . response . status_code = 400
next error_message
end
end
if ! user . password
# TODO: Playlist stub, sync with YouTube for Google accounts
# playlist_ajax(playlist_id, action, env.request.headers)
end
email = user . email
case action
when " action_edit_playlist "
# TODO: Playlist stub
when " action_add_video "
if playlist . index . size >= 500
env . response . status_code = 400
if redirect
error_message = " Playlist cannot have more than 500 videos "
next templated " error "
else
error_message = { " error " = > " Playlist cannot have more than 500 videos " } . to_json
next error_message
end
end
video_id = env . params . query [ " video_id " ]
begin
video = get_video ( video_id , PG_DB )
rescue ex
env . response . status_code = 500
if redirect
error_message = ex . message
next templated " error "
else
error_message = { " error " = > ex . message } . to_json
next error_message
end
end
playlist_video = PlaylistVideo . new (
title : video . title ,
id : video . id ,
author : video . author ,
ucid : video . ucid ,
length_seconds : video . length_seconds ,
published : video . published ,
plid : playlist_id ,
live_now : video . live_now ,
index : Random :: Secure . rand ( 0 _i64 .. Int64 :: MAX )
)
video_array = playlist_video . to_a
args = arg_array ( video_array )
PG_DB . exec ( " INSERT INTO playlist_videos VALUES ( #{ args } ) " , args : video_array )
2020-02-28 22:16:24 +05:30
PG_DB . exec ( " UPDATE playlists SET index = array_append(index, $1), video_count = cardinality(index), updated = $2 WHERE id = $3 " , playlist_video . index , Time . utc , playlist_id )
2019-08-06 05:19:13 +05:30
when " action_remove_video "
index = env . params . query [ " set_video_id " ]
PG_DB . exec ( " DELETE FROM playlist_videos * WHERE index = $1 " , index )
2020-02-28 22:16:24 +05:30
PG_DB . exec ( " UPDATE playlists SET index = array_remove(index, $1), video_count = cardinality(index), updated = $2 WHERE id = $3 " , index , Time . utc , playlist_id )
2019-08-06 05:19:13 +05:30
when " action_move_video_before "
# TODO: Playlist stub
2020-04-09 22:48:09 +05:30
else
error_message = { " error " = > " Unsupported action #{ action } " } . to_json
env . response . status_code = 400
next error_message
2019-08-06 05:19:13 +05:30
end
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
end
2018-08-15 20:52:36 +05:30
get " /playlist " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2019-08-06 05:19:13 +05:30
user = env . get? ( " user " ) . try & . as ( User )
referer = get_referer ( env )
2020-02-29 00:40:01 +05:30
plid = env . params . query [ " list " ]? . try & . gsub ( / [^a-zA-Z0-9_-] / , " " )
2018-08-15 20:52:36 +05:30
if ! plid
next env . redirect " / "
end
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2018-10-07 08:48:50 +05:30
if plid . starts_with? " RD "
next env . redirect " /mix?list= #{ plid } "
end
2018-09-18 06:37:32 +05:30
begin
2019-08-06 05:19:13 +05:30
playlist = get_playlist ( PG_DB , plid , locale )
2018-09-18 06:37:32 +05:30
rescue ex
error_message = ex . message
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2018-09-18 06:37:32 +05:30
next templated " error "
2018-08-15 20:52:36 +05:30
end
2019-08-06 05:19:13 +05:30
if playlist . privacy == PlaylistPrivacy :: Private && playlist . author != user . try & . email
error_message = " This playlist is private. "
env . response . status_code = 403
next templated " error "
end
2018-09-28 20:24:45 +05:30
begin
2019-08-06 05:19:13 +05:30
videos = get_playlist_videos ( PG_DB , playlist , offset : ( page - 1 ) * 100 , locale : locale )
2018-09-28 20:24:45 +05:30
rescue ex
videos = [ ] of PlaylistVideo
end
2019-08-06 05:19:13 +05:30
if playlist . author == user . try & . email
env . set " remove_playlist_items " , plid
end
2018-08-15 20:52:36 +05:30
templated " playlist "
end
2018-09-29 09:42:35 +05:30
get " /mix " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-29 09:42:35 +05:30
rdid = env . params . query [ " list " ]?
if ! rdid
next env . redirect " / "
end
continuation = env . params . query [ " continuation " ]?
continuation || = rdid . lchop ( " RD " )
begin
2018-12-21 03:02:09 +05:30
mix = fetch_mix ( rdid , continuation , locale : locale )
2018-09-29 09:42:35 +05:30
rescue ex
error_message = ex . message
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2018-09-29 09:42:35 +05:30
next templated " error "
end
templated " mix "
end
2018-08-05 02:00:44 +05:30
# Search
2018-07-21 01:06:23 +05:30
2018-11-22 07:30:17 +05:30
get " /opensearch.xml " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-11-22 07:30:17 +05:30
env . response . content_type = " application/opensearchdescription+xml "
2019-03-06 00:26:59 +05:30
host = make_host_url ( config , Kemal . config )
2019-01-19 20:40:52 +05:30
2018-11-22 07:30:17 +05:30
XML . build ( indent : " " , encoding : " UTF-8 " ) do | xml |
xml . element ( " OpenSearchDescription " , xmlns : " http://a9.com/-/spec/opensearch/1.1/ " ) do
xml . element ( " ShortName " ) { xml . text " Invidious " }
xml . element ( " LongName " ) { xml . text " Invidious Search " }
xml . element ( " Description " ) { xml . text " Search for videos, channels, and playlists on Invidious " }
xml . element ( " InputEncoding " ) { xml . text " UTF-8 " }
2019-01-19 20:40:52 +05:30
xml . element ( " Image " , width : 48 , height : 48 , type : " image/x-icon " ) { xml . text " #{ host } /favicon.ico " }
xml . element ( " Url " , type : " text/html " , method : " get " , template : " #{ host } /search?q={searchTerms} " )
2018-11-22 07:30:17 +05:30
end
end
end
2018-08-05 02:00:44 +05:30
get " /results " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-06 05:05:52 +05:30
query = env . params . query [ " search_query " ]?
query || = env . params . query [ " q " ]?
query || = " "
2018-08-05 09:37:38 +05:30
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2018-08-06 05:05:52 +05:30
if query
2019-09-24 23:01:33 +05:30
env . redirect " /search?q= #{ URI . encode_www_form ( query ) } &page= #{ page } "
2018-08-05 02:00:44 +05:30
else
env . redirect " / "
end
end
2018-07-22 07:26:11 +05:30
2018-08-05 02:00:44 +05:30
get " /search " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-02-07 05:51:40 +05:30
region = env . params . query [ " region " ]?
2018-12-21 03:02:09 +05:30
2018-08-06 05:05:52 +05:30
query = env . params . query [ " search_query " ]?
query || = env . params . query [ " q " ]?
2018-08-05 09:37:38 +05:30
query || = " "
2018-07-22 07:26:11 +05:30
2019-01-03 07:44:31 +05:30
if query . empty?
next env . redirect " / "
end
2018-08-05 02:00:44 +05:30
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2018-07-22 07:26:11 +05:30
2018-09-17 08:44:51 +05:30
user = env . get? " user "
2018-09-18 03:08:18 +05:30
2019-08-06 05:19:13 +05:30
begin
search_query , count , videos = process_search_query ( query , page , user , region : nil )
rescue ex
error_message = ex . message
env . response . status_code = 500
next templated " error "
2018-09-14 04:17:31 +05:30
end
2018-07-22 07:26:11 +05:30
2019-07-27 19:21:10 +05:30
env . set " search " , query
2018-08-05 02:00:44 +05:30
templated " search "
end
2018-07-22 07:26:11 +05:30
2018-08-05 02:00:44 +05:30
# Users
2018-07-22 07:26:11 +05:30
2018-08-05 02:00:44 +05:30
get " /login " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
user = env . get? " user "
if user
next env . redirect " /feed/subscriptions "
end
2018-07-22 07:26:11 +05:30
2019-03-02 03:36:45 +05:30
if ! config . login_enabled
error_message = " Login has been disabled by administrator. "
2019-06-18 00:36:02 +05:30
env . response . status_code = 400
2019-03-02 03:36:45 +05:30
next templated " error "
end
2018-08-09 06:56:02 +05:30
referer = get_referer ( env , " /feed/subscriptions " )
2018-07-22 07:26:11 +05:30
2019-03-20 02:43:23 +05:30
email = nil
password = nil
captcha = nil
2018-08-05 02:00:44 +05:30
account_type = env . params . query [ " type " ]?
account_type || = " invidious "
2018-07-30 07:45:18 +05:30
2018-11-23 00:56:08 +05:30
captcha_type = env . params . query [ " captcha " ]?
captcha_type || = " image "
2018-08-05 02:00:44 +05:30
tfa = env . params . query [ " tfa " ]?
2019-10-27 09:49:05 +05:30
prompt = nil
2018-07-22 07:26:11 +05:30
2018-08-05 02:00:44 +05:30
templated " login "
end
2018-07-22 07:26:11 +05:30
2018-08-05 02:00:44 +05:30
post " /login " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-17 20:49:20 +05:30
referer = get_referer ( env , " /feed/subscriptions " )
2018-07-22 07:26:11 +05:30
2019-03-02 03:36:45 +05:30
if ! config . login_enabled
error_message = " Login has been disabled by administrator. "
2019-06-18 00:36:02 +05:30
env . response . status_code = 403
2019-03-02 03:36:45 +05:30
next templated " error "
end
2019-06-07 21:58:58 +05:30
# https://stackoverflow.com/a/574698
email = env . params . body [ " email " ]? . try & . downcase . byte_slice ( 0 , 254 )
2018-08-05 02:00:44 +05:30
password = env . params . body [ " password " ]?
2018-07-22 07:26:11 +05:30
2018-08-05 02:00:44 +05:30
account_type = env . params . query [ " type " ]?
2019-03-20 02:43:23 +05:30
account_type || = " invidious "
2018-07-22 07:26:11 +05:30
2019-03-20 02:43:23 +05:30
case account_type
when " google "
2018-08-05 02:00:44 +05:30
tfa_code = env . params . body [ " tfa " ]? . try & . lchop ( " G- " )
2019-06-10 00:18:31 +05:30
traceback = IO :: Memory . new
2018-07-22 07:26:11 +05:30
2019-04-16 09:53:40 +05:30
# See https://github.com/ytdl-org/youtube-dl/blob/2019.04.07/youtube_dl/extractor/youtube.py#L82
2019-11-19 03:58:32 +05:30
# TODO: Convert to QUIC
2018-08-05 02:00:44 +05:30
begin
2019-11-25 00:11:47 +05:30
client = QUIC :: Client . new ( LOGIN_URL )
2018-08-05 02:00:44 +05:30
headers = HTTP :: Headers . new
2018-07-22 07:26:11 +05:30
2019-07-12 22:34:39 +05:30
login_page = client . get ( " /ServiceLogin " )
2018-08-05 02:00:44 +05:30
headers = login_page . cookies . add_request_headers ( headers )
2018-07-22 07:26:11 +05:30
2018-11-17 23:47:40 +05:30
lookup_req = {
email , nil , [ ] of String , nil , " US " , nil , nil , 2 , false , true ,
{ nil , nil ,
2019-07-12 22:34:39 +05:30
{ 2 , 1 , nil , 1 ,
" https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn " ,
nil , [ ] of String , 4 } ,
2018-11-17 23:47:40 +05:30
1 ,
2019-07-12 22:34:39 +05:30
{ nil , nil , [ ] of String } ,
2018-11-17 23:47:40 +05:30
nil , nil , nil , true ,
2019-06-10 00:18:31 +05:30
} ,
email ,
2018-11-17 23:47:40 +05:30
} . to_json
2018-07-21 01:06:23 +05:30
2019-06-10 00:18:31 +05:30
traceback << " Getting lookup... "
2018-07-21 01:06:23 +05:30
2019-07-12 22:34:39 +05:30
headers [ " Content-Type " ] = " application/x-www-form-urlencoded;charset=utf-8 "
headers [ " Google-Accounts-XSRF " ] = " 1 "
2019-06-10 00:18:31 +05:30
response = client . post ( " /_/signin/sl/lookup " , headers , login_req ( lookup_req ) )
lookup_results = JSON . parse ( response . body [ 5 .. - 1 ] )
traceback << " done, returned #{ response . status_code } .<br/> "
2018-07-21 01:06:23 +05:30
2018-08-05 02:00:44 +05:30
user_hash = lookup_results [ 0 ] [ 2 ]
2018-07-24 01:39:11 +05:30
2019-10-27 09:49:05 +05:30
if token = env . params . body [ " token " ]?
answer = env . params . body [ " answer " ]?
captcha = { token , answer }
else
captcha = nil
end
2018-11-17 23:47:40 +05:30
challenge_req = {
user_hash , nil , 1 , nil ,
2018-11-20 21:37:50 +05:30
{ 1 , nil , nil , nil ,
2019-10-27 09:49:05 +05:30
{ password , captcha , true } ,
2018-11-20 21:37:50 +05:30
} ,
2018-11-17 23:47:40 +05:30
{ nil , nil ,
2019-07-12 22:34:39 +05:30
{ 2 , 1 , nil , 1 ,
" https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn " ,
nil , [ ] of String , 4 } ,
2018-11-17 23:47:40 +05:30
1 ,
2019-07-12 22:34:39 +05:30
{ nil , nil , [ ] of String } ,
2019-06-10 00:18:31 +05:30
nil , nil , nil , true ,
} ,
2018-11-17 23:47:40 +05:30
} . to_json
2018-07-24 01:39:11 +05:30
2019-06-10 00:18:31 +05:30
traceback << " Getting challenge... "
2018-07-24 01:39:11 +05:30
2019-06-10 00:18:31 +05:30
response = client . post ( " /_/signin/sl/challenge " , headers , login_req ( challenge_req ) )
headers = response . cookies . add_request_headers ( headers )
challenge_results = JSON . parse ( response . body [ 5 .. - 1 ] )
traceback << " done, returned #{ response . status_code } .<br/> "
2018-07-24 01:39:11 +05:30
2019-09-24 23:01:33 +05:30
headers [ " Cookie " ] = URI . decode_www_form ( headers [ " Cookie " ] )
2018-07-24 01:39:11 +05:30
2019-06-15 18:52:23 +05:30
if challenge_results [ 0 ] [ 3 ]? . try & . == 7
error_message = translate ( locale , " Account has temporarily been disabled " )
2019-06-18 00:36:02 +05:30
env . response . status_code = 423
2019-06-15 18:52:23 +05:30
next templated " error "
end
2019-10-27 09:49:05 +05:30
if token = challenge_results [ 0 ] [ - 1 ]? . try & . [ - 1 ]? . try & . as_h? . try & . [ " 5001 " ]? . try & . [ - 1 ] . as_a? . try & . [ - 1 ] . as_s
account_type = " google "
captcha_type = " image "
prompt = nil
tfa = tfa_code
captcha = { tokens : [ token ] , question : " " }
next templated " login "
2019-09-18 02:33:07 +05:30
end
2018-08-05 02:00:44 +05:30
if challenge_results [ 0 ] [ - 1 ]? . try & . [ 5 ] == " INCORRECT_ANSWER_ENTERED "
2018-12-21 03:02:09 +05:30
error_message = translate ( locale , " Incorrect password " )
2019-06-18 00:36:02 +05:30
env . response . status_code = 401
2018-08-05 02:00:44 +05:30
next templated " error "
2018-07-24 01:39:11 +05:30
end
2018-07-27 08:12:12 +05:30
2019-07-22 23:58:36 +05:30
prompt_type = challenge_results [ 0 ] [ - 1 ]? . try & . [ 0 ] . as_a? . try & . [ 0 ] [ 2 ]?
if { " TWO_STEP_VERIFICATION " , " LOGIN_CHALLENGE " } . includes? prompt_type
traceback << " Handling prompt #{ prompt_type } .<br/> "
case prompt_type
when " TWO_STEP_VERIFICATION "
prompt_type = 2
2020-04-09 22:48:09 +05:30
else # "LOGIN_CHALLENGE"
2019-07-22 23:58:36 +05:30
prompt_type = 4
end
2019-06-10 00:18:31 +05:30
2018-08-05 02:00:44 +05:30
# Prefer Authenticator app and SMS over unsupported protocols
2019-09-18 02:33:07 +05:30
if ! { 6 , 9 , 12 , 15 } . includes? ( challenge_results [ 0 ] [ - 1 ] [ 0 ] [ 0 ] [ 8 ] . as_i ) && prompt_type == 2
2019-07-22 23:58:36 +05:30
tfa = challenge_results [ 0 ] [ - 1 ] [ 0 ] . as_a . select { | auth_type | { 6 , 9 , 12 , 15 } . includes? auth_type [ 8 ] } [ 0 ]
2019-06-10 00:18:31 +05:30
traceback << " Selecting challenge #{ tfa [ 8 ] } ... "
2019-07-22 23:58:36 +05:30
select_challenge = { prompt_type , nil , nil , nil , { tfa [ 8 ] } } . to_json
2018-07-27 08:12:12 +05:30
2018-08-05 02:00:44 +05:30
tl = challenge_results [ 1 ] [ 2 ]
2018-07-27 08:12:12 +05:30
2019-06-10 00:18:31 +05:30
tfa = client . post ( " /_/signin/selectchallenge?TL= #{ tl } " , headers , login_req ( select_challenge ) ) . body
2018-08-05 02:00:44 +05:30
tfa = tfa [ 5 .. - 1 ]
tfa = JSON . parse ( tfa ) [ 0 ] [ - 1 ]
2019-06-10 00:18:31 +05:30
traceback << " done.<br/> "
2018-07-29 08:26:30 +05:30
else
2019-06-10 00:18:31 +05:30
traceback << " Using challenge #{ challenge_results [ 0 ] [ - 1 ] [ 0 ] [ 0 ] [ 8 ] } .<br/> "
2018-08-05 02:00:44 +05:30
tfa = challenge_results [ 0 ] [ - 1 ] [ 0 ] [ 0 ]
2018-07-27 20:19:34 +05:30
end
2018-07-28 18:21:37 +05:30
2019-07-22 23:58:36 +05:30
if tfa [ 5 ] == " QUOTA_EXCEEDED "
error_message = translate ( locale , " Quota exceeded, try again in a few hours " )
env . response . status_code = 423
next templated " error "
end
2018-07-28 18:21:37 +05:30
2019-07-22 23:58:36 +05:30
if ! tfa_code
account_type = " google "
captcha_type = " image "
case tfa [ 8 ]
when 6 , 9
prompt = " Google verification code "
when 12
prompt = " Login verification, recovery email: #{ tfa [ - 1 ] [ tfa [ - 1 ] . as_h . keys [ 0 ] ] [ 0 ] } "
when 15
prompt = " Login verification, security question: #{ tfa [ - 1 ] [ tfa [ - 1 ] . as_h . keys [ 0 ] ] [ 0 ] } "
2018-08-05 02:00:44 +05:30
else
2019-07-22 23:58:36 +05:30
prompt = " Google verification code "
2018-08-05 02:00:44 +05:30
end
2018-07-28 18:21:37 +05:30
2019-10-27 09:49:05 +05:30
tfa = nil
2019-07-22 23:58:36 +05:30
captcha = nil
next templated " login "
end
2018-07-28 18:21:37 +05:30
2019-07-22 23:58:36 +05:30
tl = challenge_results [ 1 ] [ 2 ]
request_type = tfa [ 8 ]
case request_type
when 6 # Authenticator app
tfa_req = {
user_hash , nil , 2 , nil ,
{ 6 , nil , nil , nil , nil ,
{ tfa_code , false } ,
} ,
} . to_json
when 9 # Voice or text message
tfa_req = {
user_hash , nil , 2 , nil ,
{ 9 , nil , nil , nil , nil , nil , nil , nil ,
{ nil , tfa_code , false , 2 } ,
} ,
} . to_json
when 12 # Recovery email
tfa_req = {
user_hash , nil , 4 , nil ,
{ 12 , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil ,
{ tfa_code } ,
} ,
} . to_json
when 15 # Security question
tfa_req = {
user_hash , nil , 5 , nil ,
{ 15 , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil , nil ,
{ tfa_code } ,
} ,
} . to_json
else
error_message = translate ( locale , " Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on. " )
env . response . status_code = 500
next templated " error "
end
2018-07-28 18:21:37 +05:30
2019-07-22 23:58:36 +05:30
traceback << " Submitting challenge... "
2019-06-10 00:18:31 +05:30
2019-07-22 23:58:36 +05:30
response = client . post ( " /_/signin/challenge?hl=en&TL= #{ tl } " , headers , login_req ( tfa_req ) )
headers = response . cookies . add_request_headers ( headers )
challenge_results = JSON . parse ( response . body [ 5 .. - 1 ] )
if ( challenge_results [ 0 ] [ - 1 ]? . try & . [ 5 ] == " INCORRECT_ANSWER_ENTERED " ) ||
( challenge_results [ 0 ] [ - 1 ]? . try & . [ 5 ] == " INVALID_INPUT " )
error_message = translate ( locale , " Invalid TFA code " )
env . response . status_code = 401
next templated " error "
2018-07-28 18:21:37 +05:30
end
2019-07-22 23:58:36 +05:30
traceback << " done.<br/> "
2018-07-28 18:21:37 +05:30
end
2019-06-10 00:18:31 +05:30
traceback << " Logging in... "
2019-11-25 00:11:47 +05:30
location = URI . parse ( challenge_results [ 0 ] [ - 1 ] [ 2 ] . to_s )
2019-06-15 18:52:23 +05:30
cookies = HTTP :: Cookies . from_headers ( headers )
2018-08-01 21:14:02 +05:30
2019-11-25 00:11:47 +05:30
headers . delete ( " Content-Type " )
headers . delete ( " Google-Accounts-XSRF " )
2019-06-10 00:18:31 +05:30
loop do
2019-11-25 00:11:47 +05:30
if ! location || location . path == " /ManageAccount "
2019-06-10 00:18:31 +05:30
break
end
2019-09-18 02:33:07 +05:30
# Occasionally there will be a second page after login confirming
# the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently don't handle.
2019-11-25 00:11:47 +05:30
if location . path . starts_with? " /b/0/SmsAuthInterstitial "
2019-09-18 02:33:07 +05:30
traceback << " Unhandled dialog /b/0/SmsAuthInterstitial. "
end
2019-06-15 18:52:23 +05:30
2019-11-25 00:11:47 +05:30
login = client . get ( location . full_path , headers )
2018-08-01 21:14:02 +05:30
2019-11-25 00:11:47 +05:30
headers = login . cookies . add_request_headers ( headers )
location = login . headers [ " Location " ]? . try { | u | URI . parse ( u ) }
2019-06-10 00:18:31 +05:30
end
2018-07-30 07:31:28 +05:30
2019-11-25 00:11:47 +05:30
cookies = HTTP :: Cookies . from_headers ( headers )
2019-06-10 00:18:31 +05:30
sid = cookies [ " SID " ]? . try & . value
if ! sid
raise " Couldn't get SID. "
end
2018-07-28 18:21:37 +05:30
2019-02-11 00:03:29 +05:30
user , sid = get_user ( sid , headers , PG_DB )
2018-07-28 18:21:37 +05:30
2018-08-05 02:00:44 +05:30
# We are now logged in
2019-06-10 00:18:31 +05:30
traceback << " done.<br/> "
2018-08-01 10:26:17 +05:30
2018-08-05 02:00:44 +05:30
host = URI . parse ( env . request . headers [ " Host " ] ) . host
2018-07-28 18:21:37 +05:30
2019-03-02 03:36:45 +05:30
if Kemal . config . ssl || config . https_only
2019-02-24 21:19:48 +05:30
secure = true
else
secure = false
end
2019-03-20 20:18:37 +05:30
cookies . each do | cookie |
2019-03-02 03:36:45 +05:30
if Kemal . config . ssl || config . https_only
2019-02-24 21:19:48 +05:30
cookie . secure = secure
2018-07-29 08:26:30 +05:30
else
2019-02-24 21:19:48 +05:30
cookie . secure = secure
2018-07-29 08:26:30 +05:30
end
2018-07-28 18:21:37 +05:30
2019-03-20 20:18:37 +05:30
if cookie . extension
2019-03-24 00:35:13 +05:30
cookie . extension = cookie . extension . not_nil! . gsub ( " .youtube.com " , host )
cookie . extension = cookie . extension . not_nil! . gsub ( " Secure; " , " " )
2019-03-20 20:18:37 +05:30
end
2019-03-20 02:43:23 +05:30
env . response . cookies << cookie
2018-08-05 02:00:44 +05:30
end
2018-08-01 10:26:17 +05:30
2019-02-24 21:19:48 +05:30
if env . request . cookies [ " PREFS " ]?
preferences = env . get ( " preferences " ) . as ( Preferences )
2019-04-08 20:16:58 +05:30
PG_DB . exec ( " UPDATE users SET preferences = $1 WHERE email = $2 " , preferences . to_json , user . email )
2019-02-24 21:19:48 +05:30
2019-03-17 23:10:24 +05:30
cookie = env . request . cookies [ " PREFS " ]
2019-06-08 06:53:37 +05:30
cookie . expires = Time . utc ( 1990 , 1 , 1 )
2019-03-17 23:10:24 +05:30
env . response . cookies << cookie
2019-02-24 21:19:48 +05:30
end
2018-08-05 02:00:44 +05:30
env . redirect referer
rescue ex
2019-06-10 00:18:31 +05:30
traceback . rewind
# error_message = translate(locale, "Login failed. This may be because two-factor authentication is not turned on for your account.")
error_message = %( #{ ex . message } <br/>Traceback:<br/><div style="padding-left:2em" id="traceback"> #{ traceback . gets_to_end } </div> )
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2018-08-05 02:00:44 +05:30
next templated " error "
2018-08-01 10:26:17 +05:30
end
2019-03-20 02:43:23 +05:30
when " invidious "
2018-08-05 02:00:44 +05:30
if ! email
2018-12-21 03:02:09 +05:30
error_message = translate ( locale , " User ID is a required field " )
2019-06-18 00:36:02 +05:30
env . response . status_code = 401
2018-08-05 02:00:44 +05:30
next templated " error "
end
2018-08-01 10:31:01 +05:30
2018-08-05 02:00:44 +05:30
if ! password
2018-12-21 03:02:09 +05:30
error_message = translate ( locale , " Password is a required field " )
2019-06-18 00:36:02 +05:30
env . response . status_code = 401
2018-08-05 02:00:44 +05:30
next templated " error "
end
2018-08-01 10:31:01 +05:30
2019-04-19 05:47:50 +05:30
user = PG_DB . query_one? ( " SELECT * FROM users WHERE email = $1 " , email , as : User )
2018-08-01 10:31:01 +05:30
2019-03-20 02:43:23 +05:30
if user
2018-08-05 02:00:44 +05:30
if ! user . password
2019-04-19 21:44:11 +05:30
error_message = translate ( locale , " Please sign in using 'Log in with Google' " )
2019-06-18 00:36:02 +05:30
env . response . status_code = 400
2018-08-05 02:00:44 +05:30
next templated " error "
end
2018-07-28 18:21:37 +05:30
2019-06-08 06:53:37 +05:30
if Crypto :: Bcrypt :: Password . new ( user . password . not_nil! ) . verify ( password . byte_slice ( 0 , 55 ) )
2018-08-15 23:10:42 +05:30
sid = Base64 . urlsafe_encode ( Random :: Secure . random_bytes ( 32 ) )
2019-06-08 06:26:41 +05:30
PG_DB . exec ( " INSERT INTO session_ids VALUES ($1, $2, $3) " , sid , email , Time . utc )
2018-08-01 10:31:01 +05:30
2019-03-02 03:36:45 +05:30
if Kemal . config . ssl || config . https_only
2018-08-05 02:00:44 +05:30
secure = true
2018-08-01 10:31:01 +05:30
else
2018-08-05 02:00:44 +05:30
secure = false
2018-08-01 10:31:01 +05:30
end
2019-03-02 03:36:45 +05:30
if config . domain
2019-06-08 06:26:41 +05:30
env . response . cookies [ " SID " ] = HTTP :: Cookie . new ( name : " SID " , domain : " #{ config . domain } " , value : sid , expires : Time . utc + 2 . years ,
2018-11-16 04:11:43 +05:30
secure : secure , http_only : true )
else
2019-06-08 06:26:41 +05:30
env . response . cookies [ " SID " ] = HTTP :: Cookie . new ( name : " SID " , value : sid , expires : Time . utc + 2 . years ,
2018-11-16 04:11:43 +05:30
secure : secure , http_only : true )
end
2018-08-05 02:00:44 +05:30
else
2019-04-19 21:44:11 +05:30
error_message = translate ( locale , " Wrong username or password " )
2019-06-18 00:36:02 +05:30
env . response . status_code = 401
2018-08-05 02:00:44 +05:30
next templated " error "
end
2019-02-24 21:19:48 +05:30
# Since this user has already registered, we don't want to overwrite their preferences
if env . request . cookies [ " PREFS " ]?
2019-03-17 23:10:24 +05:30
cookie = env . request . cookies [ " PREFS " ]
2019-06-08 06:53:37 +05:30
cookie . expires = Time . utc ( 1990 , 1 , 1 )
2019-03-17 23:10:24 +05:30
env . response . cookies << cookie
2019-02-24 21:19:48 +05:30
end
2019-03-20 02:43:23 +05:30
else
2019-03-02 03:36:45 +05:30
if ! config . registration_enabled
error_message = " Registration has been disabled by administrator. "
2019-06-18 00:36:02 +05:30
env . response . status_code = 400
2019-03-02 03:36:45 +05:30
next templated " error "
end
2019-05-27 19:36:32 +05:30
if password . empty?
error_message = translate ( locale , " Password cannot be empty " )
2019-06-18 00:36:02 +05:30
env . response . status_code = 401
2019-05-27 19:36:32 +05:30
next templated " error "
end
# See https://security.stackexchange.com/a/39851
if password . bytesize > 55
error_message = translate ( locale , " Password should not be longer than 55 characters " )
2019-06-18 00:36:02 +05:30
env . response . status_code = 400
2019-05-27 19:36:32 +05:30
next templated " error "
end
password = password . byte_slice ( 0 , 55 )
2019-03-20 02:43:23 +05:30
if config . captcha_enabled
captcha_type = env . params . body [ " captcha_type " ]?
answer = env . params . body [ " answer " ]?
change_type = env . params . body [ " change_type " ]?
if ! captcha_type || change_type
if change_type
captcha_type = change_type
end
captcha_type || = " image "
account_type = " invidious "
tfa = false
2019-07-22 23:58:36 +05:30
prompt = " "
2019-03-20 02:43:23 +05:30
if captcha_type == " image "
captcha = generate_captcha ( HMAC_KEY , PG_DB )
else
captcha = generate_text_captcha ( HMAC_KEY , PG_DB )
end
next templated " login "
end
2019-04-16 09:53:40 +05:30
tokens = env . params . body . select { | k , v | k . match ( / ^token \ [ \ d+ \ ]$ / ) } . map { | k , v | v }
2019-03-20 02:43:23 +05:30
answer || = " "
captcha_type || = " image "
case captcha_type
when " image "
answer = answer . lstrip ( '0' )
answer = OpenSSL :: HMAC . hexdigest ( :sha256 , HMAC_KEY , answer )
begin
2019-04-19 02:53:50 +05:30
validate_request ( tokens [ 0 ] , answer , env . request , HMAC_KEY , PG_DB , locale )
2019-03-20 02:43:23 +05:30
rescue ex
error_message = ex . message
2019-04-19 02:53:50 +05:30
env . response . status_code = 400
2019-03-20 02:43:23 +05:30
next templated " error "
end
2020-04-09 22:48:09 +05:30
else # "text"
2019-03-20 02:43:23 +05:30
answer = Digest :: MD5 . hexdigest ( answer . downcase . strip )
found_valid_captcha = false
2019-04-19 21:44:11 +05:30
error_message = translate ( locale , " Erroneous CAPTCHA " )
2019-04-16 09:53:40 +05:30
tokens . each_with_index do | token , i |
2019-03-20 02:43:23 +05:30
begin
2019-04-19 02:53:50 +05:30
validate_request ( token , answer , env . request , HMAC_KEY , PG_DB , locale )
2019-03-20 02:43:23 +05:30
found_valid_captcha = true
rescue ex
error_message = ex . message
end
end
if ! found_valid_captcha
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2019-03-20 02:43:23 +05:30
next templated " error "
end
end
end
2018-08-15 23:10:42 +05:30
sid = Base64 . urlsafe_encode ( Random :: Secure . random_bytes ( 32 ) )
2019-02-11 00:03:29 +05:30
user , sid = create_user ( sid , email , password )
2018-08-05 02:00:44 +05:30
user_array = user . to_a
2018-08-01 10:31:01 +05:30
2019-02-12 08:17:26 +05:30
user_array [ 4 ] = user_array [ 4 ] . to_json
2018-08-05 02:00:44 +05:30
args = arg_array ( user_array )
2018-07-28 18:21:37 +05:30
2019-09-24 23:07:06 +05:30
PG_DB . exec ( " INSERT INTO users VALUES ( #{ args } ) " , args : user_array )
2019-06-08 06:26:41 +05:30
PG_DB . exec ( " INSERT INTO session_ids VALUES ($1, $2, $3) " , sid , email , Time . utc )
2018-07-28 18:21:37 +05:30
2019-04-11 06:26:38 +05:30
view_name = " subscriptions_ #{ sha256 ( user . email ) } "
2019-07-08 00:30:42 +05:30
PG_DB . exec ( " CREATE MATERIALIZED VIEW #{ view_name } AS #{ MATERIALIZED_VIEW_SQL . call ( user . email ) } " )
2018-10-09 19:10:29 +05:30
2019-03-02 03:36:45 +05:30
if Kemal . config . ssl || config . https_only
2018-08-05 02:00:44 +05:30
secure = true
else
secure = false
2018-07-28 18:21:37 +05:30
end
2018-08-05 02:00:44 +05:30
2019-03-02 03:36:45 +05:30
if config . domain
2019-06-08 06:26:41 +05:30
env . response . cookies [ " SID " ] = HTTP :: Cookie . new ( name : " SID " , domain : " #{ config . domain } " , value : sid , expires : Time . utc + 2 . years ,
2018-11-16 04:11:43 +05:30
secure : secure , http_only : true )
else
2019-06-08 06:26:41 +05:30
env . response . cookies [ " SID " ] = HTTP :: Cookie . new ( name : " SID " , value : sid , expires : Time . utc + 2 . years ,
2018-11-16 04:11:43 +05:30
secure : secure , http_only : true )
end
2019-02-24 21:19:48 +05:30
if env . request . cookies [ " PREFS " ]?
2019-04-08 20:16:58 +05:30
preferences = env . get ( " preferences " ) . as ( Preferences )
PG_DB . exec ( " UPDATE users SET preferences = $1 WHERE email = $2 " , preferences . to_json , user . email )
2019-02-24 21:19:48 +05:30
2019-03-17 23:10:24 +05:30
cookie = env . request . cookies [ " PREFS " ]
2019-06-08 06:53:37 +05:30
cookie . expires = Time . utc ( 1990 , 1 , 1 )
2019-03-17 23:10:24 +05:30
env . response . cookies << cookie
2019-02-24 21:19:48 +05:30
end
2018-07-28 18:21:37 +05:30
end
2019-03-20 02:43:23 +05:30
env . redirect referer
else
2018-08-05 02:00:44 +05:30
env . redirect referer
end
2018-07-28 18:21:37 +05:30
end
2019-04-16 09:53:40 +05:30
post " /signout " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-11-09 05:12:25 +05:30
user = env . get? " user "
2019-04-16 09:53:40 +05:30
sid = env . get? " sid "
2018-08-09 06:56:02 +05:30
referer = get_referer ( env )
2018-07-14 19:06:31 +05:30
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
end
2018-11-09 05:12:25 +05:30
2019-07-13 07:30:50 +05:30
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2018-07-14 19:06:31 +05:30
2019-07-13 07:30:50 +05:30
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
error_message = ex . message
env . response . status_code = 400
next templated " error "
end
2018-11-09 05:12:25 +05:30
2019-07-13 07:30:50 +05:30
PG_DB . exec ( " DELETE FROM session_ids * WHERE id = $1 " , sid )
env . request . cookies . each do | cookie |
cookie . expires = Time . utc ( 1990 , 1 , 1 )
env . response . cookies << cookie
2018-11-09 05:12:25 +05:30
end
2018-11-08 11:42:14 +05:30
env . redirect referer
2018-08-05 02:00:44 +05:30
end
2018-07-14 19:06:31 +05:30
2018-08-05 02:00:44 +05:30
get " /preferences " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-08-09 06:56:02 +05:30
referer = get_referer ( env )
2018-07-19 21:34:29 +05:30
2019-03-11 23:14:25 +05:30
preferences = env . get ( " preferences " ) . as ( Preferences )
2019-02-24 21:19:48 +05:30
2019-03-11 23:14:25 +05:30
templated " preferences "
2018-08-05 02:00:44 +05:30
end
2018-07-19 21:34:29 +05:30
2018-08-05 02:00:44 +05:30
post " /preferences " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-08-09 06:56:02 +05:30
referer = get_referer ( env )
2018-07-16 18:38:18 +05:30
2019-02-24 21:19:48 +05:30
video_loop = env . params . body [ " video_loop " ]? . try & . as ( String )
video_loop || = " off "
video_loop = video_loop == " on "
2019-05-01 10:09:04 +05:30
annotations = env . params . body [ " annotations " ]? . try & . as ( String )
annotations || = " off "
annotations = annotations == " on "
annotations_subscribed = env . params . body [ " annotations_subscribed " ]? . try & . as ( String )
annotations_subscribed || = " off "
annotations_subscribed = annotations_subscribed == " on "
2019-02-24 21:19:48 +05:30
autoplay = env . params . body [ " autoplay " ]? . try & . as ( String )
autoplay || = " off "
autoplay = autoplay == " on "
continue = env . params . body [ " continue " ]? . try & . as ( String )
continue || = " off "
continue = continue == " on "
2019-04-19 20:08:27 +05:30
continue_autoplay = env . params . body [ " continue_autoplay " ]? . try & . as ( String )
continue_autoplay || = " off "
continue_autoplay = continue_autoplay == " on "
2019-02-24 21:19:48 +05:30
listen = env . params . body [ " listen " ]? . try & . as ( String )
listen || = " off "
listen = listen == " on "
2019-03-13 07:35:49 +05:30
local = env . params . body [ " local " ]? . try & . as ( String )
local || = " off "
local = local == " on "
2019-06-09 02:34:55 +05:30
speed = env . params . body [ " speed " ]? . try & . as ( String ) . to_f32?
2019-04-03 22:05:58 +05:30
speed || = CONFIG . default_user_preferences . speed
2019-02-24 21:19:48 +05:30
2019-08-09 05:30:22 +05:30
player_style = env . params . body [ " player_style " ]? . try & . as ( String )
player_style || = CONFIG . default_user_preferences . player_style
2019-02-24 21:19:48 +05:30
quality = env . params . body [ " quality " ]? . try & . as ( String )
2019-04-03 22:05:58 +05:30
quality || = CONFIG . default_user_preferences . quality
2019-02-24 21:19:48 +05:30
volume = env . params . body [ " volume " ]? . try & . as ( String ) . to_i?
2019-04-03 22:05:58 +05:30
volume || = CONFIG . default_user_preferences . volume
2019-02-24 21:19:48 +05:30
2019-03-02 03:36:45 +05:30
comments = [ ] of String
2 . times do | i |
2019-04-03 22:05:58 +05:30
comments << ( env . params . body [ " comments[ #{ i } ] " ]? . try & . as ( String ) || CONFIG . default_user_preferences . comments [ i ] )
2019-03-02 03:36:45 +05:30
end
2019-02-24 21:19:48 +05:30
2019-03-02 03:36:45 +05:30
captions = [ ] of String
3 . times do | i |
2019-04-03 22:05:58 +05:30
captions << ( env . params . body [ " captions[ #{ i } ] " ]? . try & . as ( String ) || CONFIG . default_user_preferences . captions [ i ] )
2019-03-02 03:36:45 +05:30
end
2019-02-24 21:19:48 +05:30
related_videos = env . params . body [ " related_videos " ]? . try & . as ( String )
related_videos || = " off "
related_videos = related_videos == " on "
2019-10-21 06:12:18 +05:30
default_home = env . params . body [ " default_home " ]? . try & . as ( String ) || CONFIG . default_user_preferences . default_home
feed_menu = [ ] of String
5 . times do | index |
option = env . params . body [ " feed_menu[ #{ index } ] " ]? . try & . as ( String ) || " "
if ! option . empty?
feed_menu << option
end
end
2019-02-24 21:19:48 +05:30
locale = env . params . body [ " locale " ]? . try & . as ( String )
2019-04-03 22:05:58 +05:30
locale || = CONFIG . default_user_preferences . locale
2019-02-24 21:19:48 +05:30
dark_mode = env . params . body [ " dark_mode " ]? . try & . as ( String )
2019-08-15 21:59:55 +05:30
dark_mode || = CONFIG . default_user_preferences . dark_mode
2019-02-24 21:19:48 +05:30
thin_mode = env . params . body [ " thin_mode " ]? . try & . as ( String )
thin_mode || = " off "
thin_mode = thin_mode == " on "
max_results = env . params . body [ " max_results " ]? . try & . as ( String ) . to_i?
2019-04-03 22:05:58 +05:30
max_results || = CONFIG . default_user_preferences . max_results
2019-02-24 21:19:48 +05:30
sort = env . params . body [ " sort " ]? . try & . as ( String )
2019-04-03 22:05:58 +05:30
sort || = CONFIG . default_user_preferences . sort
2019-02-24 21:19:48 +05:30
latest_only = env . params . body [ " latest_only " ]? . try & . as ( String )
latest_only || = " off "
latest_only = latest_only == " on "
unseen_only = env . params . body [ " unseen_only " ]? . try & . as ( String )
unseen_only || = " off "
unseen_only = unseen_only == " on "
notifications_only = env . params . body [ " notifications_only " ]? . try & . as ( String )
notifications_only || = " off "
notifications_only = notifications_only == " on "
2019-08-15 21:59:55 +05:30
# Convert to JSON and back again to take advantage of converters used for compatability
2019-06-09 02:34:55 +05:30
preferences = Preferences . from_json ( {
annotations : annotations ,
annotations_subscribed : annotations_subscribed ,
autoplay : autoplay ,
captions : captions ,
comments : comments ,
continue : continue ,
continue_autoplay : continue_autoplay ,
dark_mode : dark_mode ,
latest_only : latest_only ,
listen : listen ,
local : local ,
locale : locale ,
max_results : max_results ,
notifications_only : notifications_only ,
2019-08-09 05:30:22 +05:30
player_style : player_style ,
2019-06-09 02:34:55 +05:30
quality : quality ,
2019-10-21 06:12:18 +05:30
default_home : default_home ,
feed_menu : feed_menu ,
2019-06-09 02:34:55 +05:30
related_videos : related_videos ,
sort : sort ,
speed : speed ,
thin_mode : thin_mode ,
unseen_only : unseen_only ,
video_loop : video_loop ,
volume : volume ,
} . to_json ) . to_json
2019-02-24 21:19:48 +05:30
if user = env . get? " user "
2018-08-05 02:00:44 +05:30
user = user . as ( User )
PG_DB . exec ( " UPDATE users SET preferences = $1 WHERE email = $2 " , preferences , user . email )
2019-03-02 03:36:45 +05:30
if config . admins . includes? user . email
2019-10-21 06:31:27 +05:30
config . default_user_preferences . default_home = env . params . body [ " admin_default_home " ]? . try & . as ( String ) || config . default_user_preferences . default_home
2019-03-02 03:36:45 +05:30
2019-10-21 06:12:18 +05:30
admin_feed_menu = [ ] of String
5 . times do | index |
option = env . params . body [ " admin_feed_menu[ #{ index } ] " ]? . try & . as ( String ) || " "
2019-03-02 03:36:45 +05:30
if ! option . empty?
2019-10-21 06:12:18 +05:30
admin_feed_menu << option
2019-03-02 03:36:45 +05:30
end
end
2019-10-21 06:28:50 +05:30
config . default_user_preferences . feed_menu = admin_feed_menu
2019-03-02 03:36:45 +05:30
top_enabled = env . params . body [ " top_enabled " ]? . try & . as ( String )
top_enabled || = " off "
config . top_enabled = top_enabled == " on "
captcha_enabled = env . params . body [ " captcha_enabled " ]? . try & . as ( String )
captcha_enabled || = " off "
config . captcha_enabled = captcha_enabled == " on "
login_enabled = env . params . body [ " login_enabled " ]? . try & . as ( String )
login_enabled || = " off "
config . login_enabled = login_enabled == " on "
registration_enabled = env . params . body [ " registration_enabled " ]? . try & . as ( String )
registration_enabled || = " off "
config . registration_enabled = registration_enabled == " on "
2019-03-02 06:55:16 +05:30
statistics_enabled = env . params . body [ " statistics_enabled " ]? . try & . as ( String )
statistics_enabled || = " off "
config . statistics_enabled = statistics_enabled == " on "
2019-10-21 06:28:50 +05:30
CONFIG . default_user_preferences = config . default_user_preferences
2019-03-02 03:36:45 +05:30
File . write ( " config/config.yml " , config . to_yaml )
end
2019-02-24 21:19:48 +05:30
else
2019-03-27 21:42:39 +05:30
if Kemal . config . ssl || config . https_only
secure = true
else
secure = false
end
if config . domain
2019-06-08 06:26:41 +05:30
env . response . cookies [ " PREFS " ] = HTTP :: Cookie . new ( name : " PREFS " , domain : " #{ config . domain } " , value : preferences , expires : Time . utc + 2 . years ,
2019-03-27 21:42:39 +05:30
secure : secure , http_only : true )
else
2019-06-08 06:26:41 +05:30
env . response . cookies [ " PREFS " ] = HTTP :: Cookie . new ( name : " PREFS " , value : preferences , expires : Time . utc + 2 . years ,
2019-03-27 21:42:39 +05:30
secure : secure , http_only : true )
end
2018-08-01 00:10:26 +05:30
end
2018-08-05 02:00:44 +05:30
env . redirect referer
end
2018-08-01 00:10:26 +05:30
2018-08-07 22:19:14 +05:30
get " /toggle_theme " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-06-05 06:28:56 +05:30
referer = get_referer ( env , unroll : false )
2018-08-07 22:19:14 +05:30
2019-05-05 18:04:27 +05:30
redirect = env . params . query [ " redirect " ]?
redirect || = " true "
redirect = redirect == " true "
2019-02-24 21:19:48 +05:30
if user = env . get? " user "
2018-08-07 22:19:14 +05:30
user = user . as ( User )
preferences = user . preferences
2019-08-15 21:59:55 +05:30
case preferences . dark_mode
when " dark "
preferences . dark_mode = " light "
else
preferences . dark_mode = " dark "
end
preferences = preferences . to_json
PG_DB . exec ( " UPDATE users SET preferences = $1 WHERE email = $2 " , preferences , user . email )
2019-02-24 21:19:48 +05:30
else
2019-03-11 23:14:25 +05:30
preferences = env . get ( " preferences " ) . as ( Preferences )
2019-08-15 21:59:55 +05:30
case preferences . dark_mode
when " dark "
preferences . dark_mode = " light "
else
preferences . dark_mode = " dark "
end
2019-03-27 21:42:39 +05:30
preferences = preferences . to_json
2019-02-24 21:19:48 +05:30
2019-03-27 21:42:39 +05:30
if Kemal . config . ssl || config . https_only
secure = true
else
secure = false
end
if config . domain
2019-06-08 06:26:41 +05:30
env . response . cookies [ " PREFS " ] = HTTP :: Cookie . new ( name : " PREFS " , domain : " #{ config . domain } " , value : preferences , expires : Time . utc + 2 . years ,
2019-03-27 21:42:39 +05:30
secure : secure , http_only : true )
else
2019-06-08 06:26:41 +05:30
env . response . cookies [ " PREFS " ] = HTTP :: Cookie . new ( name : " PREFS " , value : preferences , expires : Time . utc + 2 . years ,
2019-03-27 21:42:39 +05:30
secure : secure , http_only : true )
end
2018-08-07 22:19:14 +05:30
end
2019-05-05 18:04:27 +05:30
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
2018-08-07 22:19:14 +05:30
end
2019-04-16 09:53:40 +05:30
post " /watch_ajax " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-11-20 09:36:59 +05:30
user = env . get? " user "
2019-04-16 09:53:40 +05:30
sid = env . get? " sid "
2018-11-20 09:36:59 +05:30
referer = get_referer ( env , " /feed/subscriptions " )
redirect = env . params . query [ " redirect " ]?
2019-04-16 09:53:40 +05:30
redirect || = " true "
2018-11-20 09:36:59 +05:30
redirect = redirect == " true "
2019-04-16 09:53:40 +05:30
if ! user
2019-04-19 02:53:50 +05:30
if redirect
next env . redirect referer
else
error_message = { " error " = > " No such user " } . to_json
env . response . status_code = 403
next error_message
end
2018-11-20 09:36:59 +05:30
end
2018-12-21 03:02:09 +05:30
2019-04-16 09:53:40 +05:30
user = user . as ( User )
sid = sid . as ( String )
2019-04-19 02:53:50 +05:30
token = env . params . body [ " csrf_token " ]?
2018-11-20 09:36:59 +05:30
id = env . params . query [ " id " ]?
if ! id
2019-03-23 20:54:30 +05:30
env . response . status_code = 400
next
2018-11-20 09:36:59 +05:30
end
2019-04-16 09:53:40 +05:30
begin
2019-04-19 02:53:50 +05:30
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
2019-04-16 09:53:40 +05:30
rescue ex
2019-08-06 05:19:13 +05:30
env . response . status_code = 400
2019-04-16 09:53:40 +05:30
if redirect
error_message = ex . message
next templated " error "
else
error_message = { " error " = > ex . message } . to_json
next error_message
end
end
if env . params . query [ " action_mark_watched " ]?
action = " action_mark_watched "
elsif env . params . query [ " action_mark_unwatched " ]?
action = " action_mark_unwatched "
else
next env . redirect referer
end
case action
when " action_mark_watched "
if ! user . watched . includes? id
2020-02-28 22:16:24 +05:30
PG_DB . exec ( " UPDATE users SET watched = array_append(watched, $1) WHERE email = $2 " , id , user . email )
2019-04-16 09:53:40 +05:30
end
when " action_mark_unwatched "
2018-11-22 04:42:13 +05:30
PG_DB . exec ( " UPDATE users SET watched = array_remove(watched, $1) WHERE email = $2 " , id , user . email )
2020-04-09 22:48:09 +05:30
else
error_message = { " error " = > " Unsupported action #{ action } " } . to_json
env . response . status_code = 400
next error_message
2018-11-20 09:36:59 +05:30
end
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
end
2018-08-05 09:37:38 +05:30
# /modify_notifications
# will "ding" all subscriptions.
2018-08-05 02:00:44 +05:30
# /modify_notifications?receive_all_updates=false&receive_no_updates=false
# will "unding" all subscriptions.
get " /modify_notifications " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
user = env . get? " user "
2019-04-07 23:29:12 +05:30
sid = env . get? " sid "
referer = get_referer ( env , " / " )
2018-07-14 19:06:31 +05:30
2019-04-07 23:29:12 +05:30
redirect = env . params . query [ " redirect " ]?
redirect || = " false "
redirect = redirect == " true "
2018-07-30 07:35:40 +05:30
2019-04-19 02:53:50 +05:30
if ! user
if redirect
next env . redirect referer
else
error_message = { " error " = > " No such user " } . to_json
env . response . status_code = 403
next error_message
end
2019-04-07 23:29:12 +05:30
end
user = user . as ( User )
if ! user . password
2018-08-05 02:00:44 +05:30
channel_req = { } of String = > String
2018-02-27 06:29:02 +05:30
2018-08-05 02:00:44 +05:30
channel_req [ " receive_all_updates " ] = env . params . query [ " receive_all_updates " ]? || " true "
channel_req [ " receive_no_updates " ] = env . params . query [ " receive_no_updates " ]? || " "
channel_req [ " receive_post_updates " ] = env . params . query [ " receive_post_updates " ]? || " true "
2018-01-07 23:12:24 +05:30
2018-08-05 02:00:44 +05:30
channel_req . reject! { | k , v | v != " true " && v != " false " }
2018-01-07 08:09:24 +05:30
2018-08-05 02:00:44 +05:30
headers = HTTP :: Headers . new
headers [ " Cookie " ] = env . request . headers [ " Cookie " ]
2017-12-31 02:51:43 +05:30
2019-10-25 22:28:16 +05:30
html = YT_POOL . client & . get ( " /subscription_manager?disable_polymer=1 " , headers )
2019-04-07 23:29:12 +05:30
cookies = HTTP :: Cookies . from_headers ( headers )
html . cookies . each do | cookie |
if { " VISITOR_INFO1_LIVE " , " YSC " , " SIDCC " } . includes? cookie . name
if cookies [ cookie . name ]?
cookies [ cookie . name ] = cookie
else
cookies << cookie
end
end
end
headers = cookies . add_request_headers ( headers )
match = html . body . match ( / 'XSRF_TOKEN': "(?<session_token>[A-Za-z0-9 \ _ \ - \ =]+)" / )
2018-08-05 02:00:44 +05:30
if match
session_token = match [ " session_token " ]
else
next env . redirect referer
end
2018-07-19 00:56:02 +05:30
2019-04-07 23:29:12 +05:30
headers [ " content-type " ] = " application/x-www-form-urlencoded "
2018-08-05 02:00:44 +05:30
channel_req [ " session_token " ] = session_token
2018-04-08 08:06:09 +05:30
2019-04-07 23:29:12 +05:30
subs = XML . parse_html ( html . body )
2018-08-05 02:00:44 +05:30
subs . xpath_nodes ( % q ( / / a [ @class = " subscription-title yt-uix-sessionlink " ] / @href ) ) . each do | channel |
channel_id = channel . content . lstrip ( " /channel/ " ) . not_nil!
channel_req [ " channel_id " ] = channel_id
2019-10-25 22:28:16 +05:30
YT_POOL . client & . post ( " /subscription_ajax?action_update_subscription_preferences=1 " , headers , form : channel_req )
2018-08-05 02:00:44 +05:30
end
2018-07-19 00:56:02 +05:30
end
2019-04-07 23:29:12 +05:30
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
end
2019-04-16 09:53:40 +05:30
post " /subscription_ajax " do | env |
2019-04-07 23:29:12 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env , " / " )
redirect = env . params . query [ " redirect " ]?
2019-04-16 09:53:40 +05:30
redirect || = " true "
2019-04-07 23:29:12 +05:30
redirect = redirect == " true "
2019-04-16 09:53:40 +05:30
if ! user
2019-04-19 02:53:50 +05:30
if redirect
next env . redirect referer
else
error_message = { " error " = > " No such user " } . to_json
env . response . status_code = 403
next error_message
end
2019-04-07 23:29:12 +05:30
end
user = user . as ( User )
2019-04-16 09:53:40 +05:30
sid = sid . as ( String )
2019-04-19 02:53:50 +05:30
token = env . params . body [ " csrf_token " ]?
2019-04-16 09:53:40 +05:30
begin
2019-04-19 02:53:50 +05:30
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
2019-04-16 09:53:40 +05:30
rescue ex
if redirect
error_message = ex . message
2019-04-19 02:53:50 +05:30
env . response . status_code = 400
2019-04-16 09:53:40 +05:30
next templated " error "
else
error_message = { " error " = > ex . message } . to_json
2019-04-19 02:53:50 +05:30
env . response . status_code = 400
2019-04-16 09:53:40 +05:30
next error_message
end
end
2019-04-07 23:29:12 +05:30
2019-06-08 06:26:41 +05:30
if env . params . query [ " action_create_subscription_to_channel " ]? . try & . to_i? . try & . == 1
2019-04-07 23:29:12 +05:30
action = " action_create_subscription_to_channel "
2019-06-08 06:26:41 +05:30
elsif env . params . query [ " action_remove_subscriptions " ]? . try & . to_i? . try & . == 1
2019-04-07 23:29:12 +05:30
action = " action_remove_subscriptions "
else
next env . redirect referer
end
channel_id = env . params . query [ " c " ]?
channel_id || = " "
if ! user . password
2019-04-15 04:38:00 +05:30
# Sync subscriptions with YouTube
2019-05-15 22:56:29 +05:30
subscribe_ajax ( channel_id , action , env . request . headers )
2019-04-07 23:29:12 +05:30
end
2019-05-15 22:56:29 +05:30
email = user . email
2019-04-07 23:29:12 +05:30
case action
2019-06-08 06:26:41 +05:30
when " action_create_subscription_to_channel "
2019-04-07 23:29:12 +05:30
if ! user . subscriptions . includes? channel_id
get_channel ( channel_id , PG_DB , false , false )
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions, $1) WHERE email = $2 " , channel_id , email )
2019-04-07 23:29:12 +05:30
end
2019-06-08 06:26:41 +05:30
when " action_remove_subscriptions "
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2 " , channel_id , email )
2020-04-09 22:48:09 +05:30
else
error_message = { " error " = > " Unsupported action #{ action } " } . to_json
env . response . status_code = 400
next error_message
2019-04-07 23:29:12 +05:30
end
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
2018-08-05 02:00:44 +05:30
end
2018-04-29 20:10:33 +05:30
2018-08-05 02:00:44 +05:30
get " /subscription_manager " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
user = env . get? " user "
2019-02-11 00:03:29 +05:30
sid = env . get? " sid "
2019-04-19 02:53:50 +05:30
referer = get_referer ( env )
2018-08-09 06:56:02 +05:30
2019-04-19 02:53:50 +05:30
if ! user
2018-08-09 06:56:02 +05:30
next env . redirect referer
2018-04-28 19:57:05 +05:30
end
2018-08-05 02:00:44 +05:30
user = user . as ( User )
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
if ! user . password
# Refresh account
headers = HTTP :: Headers . new
headers [ " Cookie " ] = env . request . headers [ " Cookie " ]
2018-04-08 08:06:09 +05:30
2019-02-11 00:03:29 +05:30
user , sid = get_user ( sid , headers , PG_DB )
2018-08-05 02:00:44 +05:30
end
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
action_takeout = env . params . query [ " action_takeout " ]? . try & . to_i?
action_takeout || = 0
action_takeout = action_takeout == 1
2018-07-19 00:56:02 +05:30
2018-08-05 02:00:44 +05:30
format = env . params . query [ " format " ]?
format || = " rss "
2018-07-19 00:56:02 +05:30
2019-04-22 21:10:29 +05:30
if user . subscriptions . empty?
values = " '{}' "
else
values = " VALUES #{ user . subscriptions . map { | id | %( ( ' #{ id } ' ) ) } . join ( " , " ) } "
end
subscriptions = PG_DB . query_all ( " SELECT * FROM channels WHERE id = ANY( #{ values } ) " , as : InvidiousChannel )
2018-08-05 02:00:44 +05:30
subscriptions . sort_by! { | channel | channel . author . downcase }
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
if action_takeout
2019-03-06 00:26:59 +05:30
host_url = make_host_url ( config , Kemal . config )
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
if format == " json "
env . response . content_type = " application/json "
env . response . headers [ " content-disposition " ] = " attachment "
next {
" subscriptions " = > user . subscriptions ,
" watch_history " = > user . watched ,
" preferences " = > user . preferences ,
} . to_json
else
env . response . content_type = " application/xml "
env . response . headers [ " content-disposition " ] = " attachment "
export = XML . build do | xml |
xml . element ( " opml " , version : " 1.1 " ) do
xml . element ( " body " ) do
if format == " newpipe "
title = " YouTube Subscriptions "
else
title = " Invidious Subscriptions "
end
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
xml . element ( " outline " , text : title , title : title ) do
subscriptions . each do | channel |
if format == " newpipe "
xmlUrl = " https://www.youtube.com/feeds/videos.xml?channel_id= #{ channel . id } "
else
2018-08-05 09:37:38 +05:30
xmlUrl = " #{ host_url } /feed/channel/ #{ channel . id } "
2018-08-05 02:00:44 +05:30
end
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
xml . element ( " outline " , text : channel . author , title : channel . author ,
" type " : " rss " , xmlUrl : xmlUrl )
end
end
end
2018-07-19 00:56:02 +05:30
end
2018-03-16 22:10:29 +05:30
end
2018-08-05 02:00:44 +05:30
next export . gsub ( %( <?xml version="1.0"?> \n ) , " " )
end
end
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
templated " subscription_manager "
end
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
get " /data_control " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
user = env . get? " user "
2018-08-09 06:56:02 +05:30
referer = get_referer ( env )
2018-03-16 22:10:29 +05:30
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
2018-08-05 02:00:44 +05:30
end
2019-07-13 07:30:50 +05:30
user = user . as ( User )
templated " data_control "
2018-08-05 02:00:44 +05:30
end
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
post " /data_control " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
user = env . get? " user "
2018-08-09 06:56:02 +05:30
referer = get_referer ( env )
2018-03-16 22:10:29 +05:30
2018-08-05 02:00:44 +05:30
if user
user = user . as ( User )
2018-04-29 20:10:33 +05:30
2019-04-25 06:48:35 +05:30
spawn do
# Since import can take a while, if we're not done after 20 seconds
# push out content to prevent timeout.
# Interesting to note is that Chrome will try to render before the content has finished loading,
# which is why we include a loading icon. Firefox and its derivatives will not see this page,
# instead redirecting immediately once the connection has closed.
# https://stackoverflow.com/q/2091239 is helpful but not directly applicable here.
sleep 20 . seconds
env . response . puts %( <meta http-equiv="refresh" content="0; url= #{ referer } "> )
2019-05-09 22:22:37 +05:30
env . response . puts %( <link rel="stylesheet" href="/css/ionicons.min.css?v= #{ ASSET_COMMIT } "> )
env . response . puts %( <link rel="stylesheet" href="/css/default.css?v= #{ ASSET_COMMIT } "> )
2019-08-15 21:59:55 +05:30
if env . get ( " preferences " ) . as ( Preferences ) . dark_mode == " dark "
2019-05-09 22:22:37 +05:30
env . response . puts %( <link rel="stylesheet" href="/css/darktheme.css?v= #{ ASSET_COMMIT } "> )
2019-04-25 06:48:35 +05:30
else
2019-05-09 22:22:37 +05:30
env . response . puts %( <link rel="stylesheet" href="/css/lighttheme.css?v= #{ ASSET_COMMIT } "> )
2019-04-25 06:48:35 +05:30
end
env . response . puts %( <h3><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3> )
env . response . flush
loop do
2019-06-08 06:26:41 +05:30
env . response . puts %( <!-- keepalive #{ Time . utc . to_unix } --> )
2019-04-25 06:48:35 +05:30
env . response . flush
sleep ( 20 + rand ( 11 ) ) . seconds
end
end
2018-08-05 02:00:44 +05:30
HTTP :: FormData . parse ( env . request ) do | part |
body = part . body . gets_to_end
if body . empty?
next
2018-07-19 00:56:02 +05:30
end
2018-04-18 04:24:33 +05:30
2020-04-09 22:48:09 +05:30
# TODO: Unify into single import based on content-type
2018-08-05 02:00:44 +05:30
case part . name
when " import_invidious "
body = JSON . parse ( body )
2018-07-26 20:50:15 +05:30
2018-11-10 04:55:24 +05:30
if body [ " subscriptions " ]?
user . subscriptions += body [ " subscriptions " ] . as_a . map { | a | a . as_s }
user . subscriptions . uniq!
2019-03-06 21:24:56 +05:30
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-11-10 04:55:24 +05:30
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2 " , user . subscriptions , user . email )
2018-08-05 02:00:44 +05:30
end
2018-07-26 20:50:15 +05:30
2018-11-09 04:13:28 +05:30
if body [ " watch_history " ]?
2018-11-10 04:55:24 +05:30
user . watched += body [ " watch_history " ] . as_a . map { | a | a . as_s }
user . watched . uniq!
PG_DB . exec ( " UPDATE users SET watched = $1 WHERE email = $2 " , user . watched , user . email )
2018-07-26 20:50:15 +05:30
end
2018-04-29 20:10:33 +05:30
2018-11-09 04:05:26 +05:30
if body [ " preferences " ]?
2019-05-31 05:01:22 +05:30
user . preferences = Preferences . from_json ( body [ " preferences " ] . to_json , user . preferences )
2018-11-10 04:55:24 +05:30
PG_DB . exec ( " UPDATE users SET preferences = $1 WHERE email = $2 " , user . preferences . to_json , user . email )
2018-11-09 04:05:26 +05:30
end
2018-08-05 02:00:44 +05:30
when " import_youtube "
subscriptions = XML . parse ( body )
2018-11-10 04:55:24 +05:30
user . subscriptions += subscriptions . xpath_nodes ( % q ( / / outline [ @type = " rss " ] ) ) . map do | channel |
channel [ " xmlUrl " ] . match ( / UC[a-zA-Z0-9_-]{22} / ) . not_nil! [ 0 ]
end
user . subscriptions . uniq!
2019-01-03 06:58:01 +05:30
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-10-07 04:49:47 +05:30
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2 " , user . subscriptions , user . email )
2018-11-10 04:55:24 +05:30
when " import_freetube "
user . subscriptions += body . scan ( / "channelId":"(?<channel_id>[a-zA-Z0-9_-]{24})" / ) . map do | md |
md [ " channel_id " ]
end
user . subscriptions . uniq!
2019-01-03 06:58:01 +05:30
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-11-10 04:55:24 +05:30
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2 " , user . subscriptions , user . email )
2018-08-05 02:00:44 +05:30
when " import_newpipe_subscriptions "
body = JSON . parse ( body )
2019-04-23 02:09:57 +05:30
user . subscriptions += body [ " subscriptions " ] . as_a . compact_map do | channel |
if match = channel [ " url " ] . as_s . match ( / \/ channel \/ (?<channel>UC[a-zA-Z0-9_-]{22}) / )
next match [ " channel " ]
elsif match = channel [ " url " ] . as_s . match ( / \/ user \/ (?<user>.+) / )
2019-10-25 22:28:16 +05:30
response = YT_POOL . client & . get ( " /user/ #{ match [ " user " ] } ?disable_polymer=1&hl=en&gl=US " )
2020-01-14 18:51:17 +05:30
html = XML . parse_html ( response . body )
ucid = html . xpath_node ( % q ( / / link [ @rel = " canonical " ] ) ) . try & . [ " href " ] . split ( " / " ) [ - 1 ]
next ucid if ucid
2019-04-23 02:09:57 +05:30
end
nil
2018-11-10 04:55:24 +05:30
end
user . subscriptions . uniq!
2019-01-03 06:58:01 +05:30
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-11-10 04:55:24 +05:30
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2 " , user . subscriptions , user . email )
2018-08-05 02:00:44 +05:30
when " import_newpipe "
2018-11-10 04:55:24 +05:30
Zip :: Reader . open ( IO :: Memory . new ( body ) ) do | file |
2018-08-05 02:00:44 +05:30
file . each_entry do | entry |
if entry . filename == " newpipe.db "
2018-11-22 04:42:13 +05:30
tempfile = File . tempfile ( " .db " )
File . write ( tempfile . path , entry . io . gets_to_end )
db = DB . open ( " sqlite3:// " + tempfile . path )
2018-04-29 20:10:33 +05:30
2018-11-22 04:42:13 +05:30
user . watched += db . query_all ( " SELECT url FROM streams " , as : String ) . map { | url | url . lchop ( " https://www.youtube.com/watch?v= " ) }
2018-11-10 04:55:24 +05:30
user . watched . uniq!
2018-07-19 00:56:02 +05:30
2018-11-10 04:55:24 +05:30
PG_DB . exec ( " UPDATE users SET watched = $1 WHERE email = $2 " , user . watched , user . email )
2018-10-07 04:49:47 +05:30
2018-11-22 04:42:13 +05:30
user . subscriptions += db . query_all ( " SELECT url FROM subscriptions " , as : String ) . map { | url | url . lchop ( " https://www.youtube.com/channel/ " ) }
2018-11-10 04:55:24 +05:30
user . subscriptions . uniq!
2019-01-03 06:58:01 +05:30
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-11-10 04:55:24 +05:30
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = $1 WHERE email = $2 " , user . subscriptions , user . email )
2018-11-22 04:42:13 +05:30
db . close
tempfile . delete
2018-08-05 02:00:44 +05:30
end
2018-07-19 00:56:02 +05:30
end
2018-07-08 19:27:06 +05:30
end
2020-04-09 22:48:09 +05:30
else nil # Ignore
2018-07-19 00:56:02 +05:30
end
2018-08-05 02:00:44 +05:30
end
end
2018-07-19 00:56:02 +05:30
2018-08-05 02:00:44 +05:30
env . redirect referer
end
2018-07-19 00:56:02 +05:30
2019-04-22 20:48:17 +05:30
get " /change_password " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
2019-04-22 20:48:17 +05:30
end
2019-07-13 07:30:50 +05:30
user = user . as ( User )
sid = sid . as ( String )
csrf_token = generate_response ( sid , { " :change_password " } , HMAC_KEY , PG_DB )
templated " change_password "
2019-04-22 20:48:17 +05:30
end
post " /change_password " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
end
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
# We don't store passwords for Google accounts
if ! user . password
error_message = " Cannot change password for Google accounts "
env . response . status_code = 400
next templated " error "
end
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
error_message = ex . message
env . response . status_code = 400
next templated " error "
end
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
password = env . params . body [ " password " ]?
if ! password
error_message = translate ( locale , " Password is a required field " )
env . response . status_code = 401
next templated " error "
end
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
new_passwords = env . params . body . select { | k , v | k . match ( / ^new_password \ [ \ d+ \ ]$ / ) } . map { | k , v | v }
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
if new_passwords . size <= 1 || new_passwords . uniq . size != 1
error_message = translate ( locale , " New passwords must match " )
env . response . status_code = 400
next templated " error "
end
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
new_password = new_passwords . uniq [ 0 ]
if new_password . empty?
error_message = translate ( locale , " Password cannot be empty " )
env . response . status_code = 401
next templated " error "
end
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
if new_password . bytesize > 55
error_message = translate ( locale , " Password should not be longer than 55 characters " )
env . response . status_code = 400
next templated " error "
end
2019-04-22 20:48:17 +05:30
2019-07-13 07:30:50 +05:30
if ! Crypto :: Bcrypt :: Password . new ( user . password . not_nil! ) . verify ( password . byte_slice ( 0 , 55 ) )
error_message = translate ( locale , " Incorrect password " )
env . response . status_code = 401
next templated " error "
2019-04-22 20:48:17 +05:30
end
2019-07-13 07:30:50 +05:30
new_password = Crypto :: Bcrypt :: Password . create ( new_password , cost : 10 )
PG_DB . exec ( " UPDATE users SET password = $1 WHERE email = $2 " , new_password . to_s , user . email )
2019-04-22 20:48:17 +05:30
env . redirect referer
end
2018-11-08 11:42:14 +05:30
get " /delete_account " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-11-08 11:42:14 +05:30
user = env . get? " user "
2019-04-16 09:53:40 +05:30
sid = env . get? " sid "
2018-11-08 11:42:14 +05:30
referer = get_referer ( env )
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
2018-11-08 11:42:14 +05:30
end
2019-07-13 07:30:50 +05:30
user = user . as ( User )
sid = sid . as ( String )
csrf_token = generate_response ( sid , { " :delete_account " } , HMAC_KEY , PG_DB )
templated " delete_account "
2018-11-08 11:42:14 +05:30
end
post " /delete_account " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-11-08 11:42:14 +05:30
user = env . get? " user "
2019-04-16 09:53:40 +05:30
sid = env . get? " sid "
2018-11-08 11:42:14 +05:30
referer = get_referer ( env )
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
end
2018-11-08 11:42:14 +05:30
2019-07-13 07:30:50 +05:30
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2018-11-08 11:42:14 +05:30
2019-07-13 07:30:50 +05:30
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
error_message = ex . message
env . response . status_code = 400
next templated " error "
end
2018-11-08 11:42:14 +05:30
2019-07-13 07:30:50 +05:30
view_name = " subscriptions_ #{ sha256 ( user . email ) } "
PG_DB . exec ( " DELETE FROM users * WHERE email = $1 " , user . email )
PG_DB . exec ( " DELETE FROM session_ids * WHERE email = $1 " , user . email )
PG_DB . exec ( " DROP MATERIALIZED VIEW #{ view_name } " )
env . request . cookies . each do | cookie |
cookie . expires = Time . utc ( 1990 , 1 , 1 )
env . response . cookies << cookie
2018-11-08 11:42:14 +05:30
end
env . redirect referer
end
2018-08-05 02:00:44 +05:30
get " /clear_watch_history " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
user = env . get? " user "
2019-04-16 09:53:40 +05:30
sid = env . get? " sid "
2018-11-08 11:42:14 +05:30
referer = get_referer ( env )
2018-08-09 06:56:02 +05:30
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
2018-11-08 11:42:14 +05:30
end
2019-07-13 07:30:50 +05:30
user = user . as ( User )
sid = sid . as ( String )
csrf_token = generate_response ( sid , { " :clear_watch_history " } , HMAC_KEY , PG_DB )
templated " clear_watch_history "
2018-11-08 11:42:14 +05:30
end
post " /clear_watch_history " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-11-08 11:42:14 +05:30
user = env . get? " user "
2019-04-16 09:53:40 +05:30
sid = env . get? " sid "
2018-08-09 06:56:02 +05:30
referer = get_referer ( env )
2018-03-16 22:10:29 +05:30
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
end
2018-11-08 11:42:14 +05:30
2019-07-13 07:30:50 +05:30
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2018-11-08 11:42:14 +05:30
2019-07-13 07:30:50 +05:30
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
error_message = ex . message
env . response . status_code = 400
next templated " error "
2018-08-05 02:00:44 +05:30
end
2019-07-13 07:30:50 +05:30
PG_DB . exec ( " UPDATE users SET watched = '{}' WHERE email = $1 " , user . email )
2018-08-05 02:00:44 +05:30
env . redirect referer
end
2019-05-15 22:56:29 +05:30
get " /authorize_token " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
end
2019-05-15 22:56:29 +05:30
2019-07-13 07:30:50 +05:30
user = user . as ( User )
sid = sid . as ( String )
csrf_token = generate_response ( sid , { " :authorize_token " } , HMAC_KEY , PG_DB )
2019-05-15 22:56:29 +05:30
2019-07-13 07:30:50 +05:30
scopes = env . params . query [ " scopes " ]? . try & . split ( " , " )
scopes || = [ ] of String
2019-05-15 22:56:29 +05:30
2019-07-13 07:30:50 +05:30
callback_url = env . params . query [ " callback_url " ]?
if callback_url
callback_url = URI . parse ( callback_url )
2019-05-15 22:56:29 +05:30
end
2019-07-13 07:30:50 +05:30
expire = env . params . query [ " expire " ]? . try & . to_i?
templated " authorize_token "
2019-05-15 22:56:29 +05:30
end
2019-04-19 02:53:50 +05:30
post " /authorize_token " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
2019-07-13 07:30:50 +05:30
if ! user
next env . redirect referer
end
2019-04-19 02:53:50 +05:30
2019-07-13 07:30:50 +05:30
user = env . get ( " user " ) . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2019-04-19 02:53:50 +05:30
2019-07-13 07:30:50 +05:30
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
error_message = ex . message
env . response . status_code = 400
next templated " error "
end
2019-04-19 02:53:50 +05:30
2019-07-13 07:30:50 +05:30
scopes = env . params . body . select { | k , v | k . match ( / ^scopes \ [ \ d+ \ ]$ / ) } . map { | k , v | v }
callback_url = env . params . body [ " callbackUrl " ]?
expire = env . params . body [ " expire " ]? . try & . to_i?
2019-04-19 02:53:50 +05:30
2019-07-13 07:30:50 +05:30
access_token = generate_token ( user . email , scopes , expire , HMAC_KEY , PG_DB )
2019-04-19 02:53:50 +05:30
2019-07-13 07:30:50 +05:30
if callback_url
2019-09-24 23:01:33 +05:30
access_token = URI . encode_www_form ( access_token )
2019-07-13 07:30:50 +05:30
url = URI . parse ( callback_url )
2019-04-19 02:53:50 +05:30
2019-07-13 07:30:50 +05:30
if url . query
query = HTTP :: Params . parse ( url . query . not_nil! )
2019-04-19 02:53:50 +05:30
else
2019-07-13 07:30:50 +05:30
query = HTTP :: Params . new
2019-04-19 02:53:50 +05:30
end
2019-07-13 07:30:50 +05:30
query [ " token " ] = access_token
url . query = query . to_s
env . redirect url . to_s
else
csrf_token = " "
env . set " access_token " , access_token
templated " authorize_token "
2019-04-19 02:53:50 +05:30
end
end
get " /token_manager " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env , " /subscription_manager " )
if ! user
next env . redirect referer
end
user = user . as ( User )
tokens = PG_DB . query_all ( " SELECT id, issued FROM session_ids WHERE email = $1 ORDER BY issued DESC " , user . email , as : { session : String , issued : Time } )
templated " token_manager "
end
post " /token_ajax " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
redirect = env . params . query [ " redirect " ]?
redirect || = " true "
redirect = redirect == " true "
if ! user
if redirect
next env . redirect referer
else
error_message = { " error " = > " No such user " } . to_json
env . response . status_code = 403
next error_message
end
end
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
if redirect
error_message = ex . message
2019-06-18 00:36:02 +05:30
env . response . status_code = 400
2019-04-19 02:53:50 +05:30
next templated " error "
else
error_message = { " error " = > ex . message } . to_json
env . response . status_code = 400
next error_message
end
end
if env . params . query [ " action_revoke_token " ]?
action = " action_revoke_token "
else
next env . redirect referer
end
session = env . params . query [ " session " ]?
session || = " "
case action
when . starts_with? " action_revoke_token "
PG_DB . exec ( " DELETE FROM session_ids * WHERE id = $1 AND email = $2 " , session , user . email )
2020-04-09 22:48:09 +05:30
else
error_message = { " error " = > " Unsupported action #{ action } " } . to_json
env . response . status_code = 400
next error_message
2019-04-19 02:53:50 +05:30
end
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
end
2018-08-05 02:00:44 +05:30
# Feeds
2018-11-26 22:20:34 +05:30
get " /feed/top " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2019-03-02 03:36:45 +05:30
if config . top_enabled
templated " top "
else
env . redirect " / "
end
2018-11-26 22:20:34 +05:30
end
get " /feed/popular " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-11-26 22:20:34 +05:30
templated " popular "
end
2018-11-20 22:48:12 +05:30
get " /feed/trending " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-11-20 22:48:12 +05:30
trending_type = env . params . query [ " type " ]?
2018-12-21 04:18:45 +05:30
trending_type || = " Default "
2018-11-20 22:48:12 +05:30
region = env . params . query [ " region " ]?
2018-12-21 04:18:45 +05:30
region || = " US "
2018-11-20 22:48:12 +05:30
begin
2019-06-29 07:47:56 +05:30
trending , plid = fetch_trending ( trending_type , region , locale )
2018-11-20 22:48:12 +05:30
rescue ex
error_message = " #{ ex . message } "
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2018-11-20 22:48:12 +05:30
next templated " error "
end
templated " trending "
end
2018-08-05 02:00:44 +05:30
get " /feed/subscriptions " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
user = env . get? " user "
2019-02-11 00:03:29 +05:30
sid = env . get? " sid "
2018-08-09 06:56:02 +05:30
referer = get_referer ( env )
2018-08-05 02:00:44 +05:30
2019-06-07 23:09:12 +05:30
if ! user
next env . redirect referer
end
2019-06-07 23:12:07 +05:30
user = user . as ( User )
sid = sid . as ( String )
token = user . token
2018-11-22 00:36:29 +05:30
2019-06-07 23:09:12 +05:30
if user . preferences . unseen_only
2019-06-07 23:12:07 +05:30
env . set " show_watched " , true
end
2018-08-05 02:00:44 +05:30
2019-06-07 23:12:07 +05:30
# Refresh account
headers = HTTP :: Headers . new
headers [ " Cookie " ] = env . request . headers [ " Cookie " ]
2018-08-05 02:00:44 +05:30
2019-06-07 23:12:07 +05:30
if ! user . password
user , sid = get_user ( sid , headers , PG_DB )
end
2018-03-26 08:51:24 +05:30
2019-06-09 02:34:55 +05:30
max_results = env . params . query [ " max_results " ]? . try & . to_i? . try & . clamp ( 0 , MAX_ITEMS_PER_PAGE )
max_results || = user . preferences . max_results
max_results || = CONFIG . default_user_preferences . max_results
2018-08-05 02:00:44 +05:30
2019-06-07 23:12:07 +05:30
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2018-08-05 02:00:44 +05:30
2019-06-07 23:09:12 +05:30
videos , notifications = get_subscription_feed ( PG_DB , user , max_results , page )
2019-02-18 22:59:57 +05:30
2019-06-07 23:12:07 +05:30
# "updated" here is used for delivering new notifications, so if
# we know a user has looked at their feed e.g. in the past 10 minutes,
# they've already seen a video posted 20 minutes ago, and don't need
# to be notified.
2019-06-08 06:26:41 +05:30
PG_DB . exec ( " UPDATE users SET notifications = $1, updated = $2 WHERE email = $3 " , [ ] of String , Time . utc ,
2019-06-07 23:12:07 +05:30
user . email )
user . notifications = [ ] of String
env . set " user " , user
2018-08-05 02:00:44 +05:30
2019-06-07 23:12:07 +05:30
templated " subscriptions "
end
2018-03-16 22:10:29 +05:30
2018-11-20 09:36:59 +05:30
get " /feed/history " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-11-20 09:36:59 +05:30
user = env . get? " user "
referer = get_referer ( env )
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2019-06-07 23:09:12 +05:30
if ! user
next env . redirect referer
end
2019-06-07 23:12:07 +05:30
user = user . as ( User )
2018-11-10 08:07:46 +05:30
2019-06-09 02:34:55 +05:30
max_results = env . params . query [ " max_results " ]? . try & . to_i? . try & . clamp ( 0 , MAX_ITEMS_PER_PAGE )
max_results || = user . preferences . max_results
max_results || = CONFIG . default_user_preferences . max_results
if user . watched [ ( page - 1 ) * max_results ]?
watched = user . watched . reverse [ ( page - 1 ) * max_results , max_results ]
2018-11-20 09:36:59 +05:30
end
2019-06-09 02:34:55 +05:30
watched || = [ ] of String
2018-11-10 04:55:24 +05:30
2019-06-07 23:12:07 +05:30
templated " history "
end
2018-08-05 02:00:44 +05:30
get " /feed/channel/:ucid " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2019-06-08 08:09:32 +05:30
env . response . content_type = " application/atom+xml "
2019-03-06 00:26:59 +05:30
2018-08-05 02:00:44 +05:30
ucid = env . params . url [ " ucid " ]
2018-04-08 08:06:09 +05:30
2019-09-07 21:15:37 +05:30
params = HTTP :: Params . parse ( env . params . query [ " params " ]? || " " )
2018-09-21 20:10:04 +05:30
begin
2019-06-29 07:18:24 +05:30
channel = get_about_info ( ucid , locale )
2019-09-08 21:38:59 +05:30
rescue ex : ChannelRedirect
next env . redirect env . request . resource . gsub ( ucid , ex . channel_id )
2018-09-21 20:10:04 +05:30
rescue ex
2018-10-24 07:28:07 +05:30
error_message = ex . message
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2018-09-05 07:34:40 +05:30
end
2020-04-10 22:19:51 +05:30
response = YT_POOL . client & . get ( " /feeds/videos.xml?channel_id= #{ channel . ucid } " )
rss = XML . parse_html ( response . body )
2019-02-19 03:36:00 +05:30
2020-04-08 00:04:40 +05:30
videos = rss . xpath_nodes ( " //feed/entry " ) . map do | entry |
2019-02-19 03:36:00 +05:30
video_id = entry . xpath_node ( " videoid " ) . not_nil! . content
title = entry . xpath_node ( " title " ) . not_nil! . content
2019-03-08 08:43:54 +05:30
published = Time . parse_rfc3339 ( entry . xpath_node ( " published " ) . not_nil! . content )
updated = Time . parse_rfc3339 ( entry . xpath_node ( " updated " ) . not_nil! . content )
2019-02-19 03:36:00 +05:30
author = entry . xpath_node ( " author/name " ) . not_nil! . content
ucid = entry . xpath_node ( " channelid " ) . not_nil! . content
2019-06-09 01:38:27 +05:30
description_html = entry . xpath_node ( " group/description " ) . not_nil! . to_s
2019-02-19 03:36:00 +05:30
views = entry . xpath_node ( " group/community/statistics " ) . not_nil! . [ " views " ] . to_i64
2020-04-08 00:04:40 +05:30
SearchVideo . new (
2019-02-19 03:36:00 +05:30
title : title ,
id : video_id ,
author : author ,
ucid : ucid ,
published : published ,
views : views ,
2019-06-09 01:38:27 +05:30
description_html : description_html ,
2019-02-19 03:36:00 +05:30
length_seconds : 0 ,
live_now : false ,
paid : false ,
2019-03-22 22:54:47 +05:30
premium : false ,
premiere_timestamp : nil
2019-02-19 03:36:00 +05:30
)
end
2018-07-16 21:54:24 +05:30
2019-03-06 00:26:59 +05:30
host_url = make_host_url ( config , Kemal . config )
2018-07-16 21:54:24 +05:30
2019-06-07 23:09:12 +05:30
XML . build ( indent : " " , encoding : " UTF-8 " ) do | xml |
2018-08-05 02:00:44 +05:30
xml . element ( " feed " , " xmlns:yt " : " http://www.youtube.com/xml/schemas/2015 " ,
2018-12-23 23:37:04 +05:30
" xmlns:media " : " http://search.yahoo.com/mrss/ " , xmlns : " http://www.w3.org/2005/Atom " ,
" xml:lang " : " en-US " ) do
2019-06-07 23:09:12 +05:30
xml . element ( " link " , rel : " self " , href : " #{ host_url } #{ env . request . resource } " )
2019-06-29 07:18:24 +05:30
xml . element ( " id " ) { xml . text " yt:channel: #{ channel . ucid } " }
xml . element ( " yt:channelId " ) { xml . text channel . ucid }
xml . element ( " title " ) { xml . text channel . author }
xml . element ( " link " , rel : " alternate " , href : " #{ host_url } /channel/ #{ channel . ucid } " )
2018-07-28 20:19:58 +05:30
2018-08-05 02:00:44 +05:30
xml . element ( " author " ) do
2019-06-29 07:18:24 +05:30
xml . element ( " name " ) { xml . text channel . author }
xml . element ( " uri " ) { xml . text " #{ host_url } /channel/ #{ channel . ucid } " }
2018-08-05 02:00:44 +05:30
end
2018-07-29 09:01:02 +05:30
2018-09-05 07:34:40 +05:30
videos . each do | video |
2019-09-07 21:15:37 +05:30
video . to_xml ( host_url , channel . auto_generated , params , xml )
2019-06-07 23:12:07 +05:30
end
end
end
end
2018-07-16 21:54:24 +05:30
2018-08-05 02:00:44 +05:30
get " /feed/private " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2019-06-08 08:09:32 +05:30
env . response . content_type = " application/atom+xml "
2019-03-06 00:26:59 +05:30
2018-08-05 02:00:44 +05:30
token = env . params . query [ " token " ]?
2018-07-16 21:54:24 +05:30
2018-08-05 02:00:44 +05:30
if ! token
2019-03-23 20:54:30 +05:30
env . response . status_code = 403
next
2018-08-05 02:00:44 +05:30
end
2018-03-25 09:08:35 +05:30
2018-08-05 02:00:44 +05:30
user = PG_DB . query_one? ( " SELECT * FROM users WHERE token = $1 " , token . strip , as : User )
if ! user
2019-03-23 20:54:30 +05:30
env . response . status_code = 403
next
2018-08-05 02:00:44 +05:30
end
2018-07-17 18:49:45 +05:30
2019-06-09 02:34:55 +05:30
max_results = env . params . query [ " max_results " ]? . try & . to_i? . try & . clamp ( 0 , MAX_ITEMS_PER_PAGE )
max_results || = user . preferences . max_results
max_results || = CONFIG . default_user_preferences . max_results
2018-07-17 18:49:45 +05:30
2018-08-05 02:00:44 +05:30
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2018-03-25 09:08:35 +05:30
2019-09-07 21:15:37 +05:30
params = HTTP :: Params . parse ( env . params . query [ " params " ]? || " " )
2019-06-07 23:09:12 +05:30
videos , notifications = get_subscription_feed ( PG_DB , user , max_results , page )
2019-03-06 00:26:59 +05:30
host_url = make_host_url ( config , Kemal . config )
2018-07-31 21:14:07 +05:30
2019-06-07 23:09:12 +05:30
XML . build ( indent : " " , encoding : " UTF-8 " ) do | xml |
2018-12-23 23:37:04 +05:30
xml . element ( " feed " , " xmlns:yt " : " http://www.youtube.com/xml/schemas/2015 " ,
" xmlns:media " : " http://search.yahoo.com/mrss/ " , xmlns : " http://www.w3.org/2005/Atom " ,
2018-08-05 02:00:44 +05:30
" xml:lang " : " en-US " ) do
2018-08-05 09:37:38 +05:30
xml . element ( " link " , " type " : " text/html " , rel : " alternate " , href : " #{ host_url } /feed/subscriptions " )
2019-06-07 23:09:12 +05:30
xml . element ( " link " , " type " : " application/atom+xml " , rel : " self " ,
href : " #{ host_url } #{ env . request . resource } " )
2018-12-21 03:02:09 +05:30
xml . element ( " title " ) { xml . text translate ( locale , " Invidious Private Feed for `x` " , user . email ) }
2018-07-18 19:45:58 +05:30
2019-06-08 07:57:37 +05:30
( notifications + videos ) . each do | video |
2019-09-07 21:15:37 +05:30
video . to_xml ( locale , host_url , params , xml )
2018-08-05 02:00:44 +05:30
end
2018-04-18 02:57:55 +05:30
end
2018-08-05 02:00:44 +05:30
end
end
2018-03-25 09:08:35 +05:30
2018-09-18 04:43:24 +05:30
get " /feed/playlist/:plid " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2019-06-08 08:09:32 +05:30
env . response . content_type = " application/atom+xml "
2019-03-06 00:26:59 +05:30
2018-09-18 04:43:24 +05:30
plid = env . params . url [ " plid " ]
2019-09-07 21:15:37 +05:30
params = HTTP :: Params . parse ( env . params . query [ " params " ]? || " " )
2019-03-06 00:26:59 +05:30
host_url = make_host_url ( config , Kemal . config )
2018-09-18 04:43:24 +05:30
path = env . request . path
2019-08-06 05:19:13 +05:30
if plid . starts_with? " IV "
if playlist = PG_DB . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
videos = get_playlist_videos ( PG_DB , playlist , offset : 0 , locale : locale )
next XML . build ( indent : " " , encoding : " UTF-8 " ) do | xml |
xml . element ( " feed " , " xmlns:yt " : " http://www.youtube.com/xml/schemas/2015 " ,
" xmlns:media " : " http://search.yahoo.com/mrss/ " , xmlns : " http://www.w3.org/2005/Atom " ,
" xml:lang " : " en-US " ) do
xml . element ( " link " , rel : " self " , href : " #{ host_url } #{ env . request . resource } " )
xml . element ( " id " ) { xml . text " iv:playlist: #{ plid } " }
xml . element ( " iv:playlistId " ) { xml . text plid }
xml . element ( " title " ) { xml . text playlist . title }
xml . element ( " link " , rel : " alternate " , href : " #{ host_url } /playlist?list= #{ plid } " )
xml . element ( " author " ) do
xml . element ( " name " ) { xml . text playlist . author }
end
videos . each do | video |
video . to_xml ( host_url , false , xml )
end
end
end
else
env . response . status_code = 404
next
end
end
2019-10-25 22:28:16 +05:30
response = YT_POOL . client & . get ( " /feeds/videos.xml?playlist_id= #{ plid } " )
2018-09-18 04:43:24 +05:30
document = XML . parse ( response . body )
document . xpath_nodes ( % q ( / / * [ @href ] | / / * [ @url ] ) ) . each do | node |
node . attributes . each do | attribute |
case attribute . name
2019-09-07 21:15:37 +05:30
when " url " , " href "
full_path = URI . parse ( node [ attribute . name ] ) . full_path
query_string_opt = full_path . starts_with? ( " /watch?v= " ) ? " & #{ params } " : " "
node [ attribute . name ] = " #{ host_url } #{ full_path } #{ query_string_opt } "
2020-04-09 22:48:09 +05:30
else nil # Skip
2018-09-18 04:43:24 +05:30
end
end
end
document = document . to_xml ( options : XML :: SaveOptions :: NO_DECL )
document . scan ( / <uri>(?<url>[^<]+)< \/ uri> / ) . each do | match |
content = " #{ host_url } #{ URI . parse ( match [ " url " ] ) . full_path } "
document = document . gsub ( match [ 0 ] , " <uri> #{ content } </uri> " )
end
document
end
2019-03-30 02:20:18 +05:30
get " /feeds/videos.xml " do | env |
if ucid = env . params . query [ " channel_id " ]?
env . redirect " /feed/channel/ #{ ucid } "
elsif user = env . params . query [ " user " ]?
env . redirect " /feed/channel/ #{ user } "
elsif plid = env . params . query [ " playlist_id " ]?
env . redirect " /feed/playlist/ #{ plid } "
end
end
2019-03-04 22:16:58 +05:30
# Support push notifications via PubSubHubbub
2019-03-04 06:48:23 +05:30
2019-03-04 08:10:24 +05:30
get " /feed/webhook/:token " do | env |
verify_token = env . params . url [ " token " ]
2019-07-10 21:52:10 +05:30
mode = env . params . query [ " hub.mode " ]?
topic = env . params . query [ " hub.topic " ]?
challenge = env . params . query [ " hub.challenge " ]?
if ! mode || ! topic || ! challenge
env . response . status_code = 400
next
else
mode = mode . not_nil!
topic = topic . not_nil!
challenge = challenge . not_nil!
end
2019-03-04 06:48:23 +05:30
2019-06-08 06:26:41 +05:30
case verify_token
when . starts_with? " v1 "
2019-03-04 19:23:31 +05:30
_ , time , nonce , signature = verify_token . split ( " : " )
data = " #{ time } : #{ nonce } "
2019-06-08 06:26:41 +05:30
when . starts_with? " v2 "
2019-03-04 19:23:31 +05:30
time , signature = verify_token . split ( " : " )
data = " #{ time } "
2019-06-08 06:26:41 +05:30
else
env . response . status_code = 400
next
2019-03-04 19:23:31 +05:30
end
2019-03-04 06:48:23 +05:30
2019-04-04 18:19:53 +05:30
# The hub will sometimes check if we're still subscribed after delivery errors,
# so we reply with a 200 as long as the request hasn't expired
2019-06-08 06:26:41 +05:30
if Time . utc . to_unix - time . to_i > 432000
2019-03-23 20:54:30 +05:30
env . response . status_code = 400
next
2019-03-04 06:48:23 +05:30
end
2019-03-04 19:23:31 +05:30
if OpenSSL :: HMAC . hexdigest ( :sha1 , HMAC_KEY , data ) != signature
2019-03-23 20:54:30 +05:30
env . response . status_code = 400
next
2019-03-04 06:48:23 +05:30
end
2019-06-08 06:26:41 +05:30
if ucid = HTTP :: Params . parse ( URI . parse ( topic ) . query . not_nil! ) [ " channel_id " ]?
PG_DB . exec ( " UPDATE channels SET subscribed = $1 WHERE id = $2 " , Time . utc , ucid )
elsif plid = HTTP :: Params . parse ( URI . parse ( topic ) . query . not_nil! ) [ " playlist_id " ]?
PG_DB . exec ( " UPDATE playlists SET subscribed = $1 WHERE id = $2 " , Time . utc , ucid )
else
env . response . status_code = 400
next
end
2019-03-04 06:48:23 +05:30
2019-03-23 20:54:30 +05:30
env . response . status_code = 200
2019-06-08 06:26:41 +05:30
challenge
2019-03-04 06:48:23 +05:30
end
2019-03-04 08:10:24 +05:30
post " /feed/webhook/:token " do | env |
2019-04-11 04:28:42 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-03-04 22:37:27 +05:30
token = env . params . url [ " token " ]
2019-03-04 06:48:23 +05:30
body = env . request . body . not_nil! . gets_to_end
signature = env . request . headers [ " X-Hub-Signature " ] . lchop ( " sha1= " )
if signature != OpenSSL :: HMAC . hexdigest ( :sha1 , HMAC_KEY , body )
2019-06-08 06:37:55 +05:30
logger . puts ( " #{ token } : Invalid signature " )
2019-03-23 20:54:30 +05:30
env . response . status_code = 200
next
2019-03-04 06:48:23 +05:30
end
2019-03-04 22:37:27 +05:30
spawn do
rss = XML . parse_html ( body )
rss . xpath_nodes ( " //feed/entry " ) . each do | entry |
id = entry . xpath_node ( " videoid " ) . not_nil! . content
2019-04-04 18:19:53 +05:30
author = entry . xpath_node ( " author/name " ) . not_nil! . content
2019-03-08 09:19:52 +05:30
published = Time . parse_rfc3339 ( entry . xpath_node ( " published " ) . not_nil! . content )
2019-03-08 08:43:54 +05:30
updated = Time . parse_rfc3339 ( entry . xpath_node ( " updated " ) . not_nil! . content )
2019-03-04 06:48:23 +05:30
2019-06-29 07:47:56 +05:30
video = get_video ( id , PG_DB , force_refresh : true )
2019-04-11 04:28:42 +05:30
# Deliver notifications to `/api/v1/auth/notifications`
payload = {
2019-04-20 23:11:51 +05:30
" topic " = > video . ucid ,
" videoId " = > video . id ,
" published " = > published . to_unix ,
2019-04-11 04:28:42 +05:30
} . to_json
PG_DB . exec ( " NOTIFY notifications, E' #{ payload } ' " )
video = ChannelVideo . new (
id : id ,
title : video . title ,
published : published ,
updated : updated ,
ucid : video . ucid ,
author : author ,
length_seconds : video . length_seconds ,
live_now : video . live_now ,
premiere_timestamp : video . premiere_timestamp ,
2019-05-31 01:39:39 +05:30
views : video . views ,
2019-04-11 04:28:42 +05:30
)
2019-03-04 06:48:23 +05:30
2020-02-28 23:43:48 +05:30
PG_DB . query_all ( " UPDATE users SET feed_needs_update = true, notifications = array_append(notifications, $1) \
WHERE updated < $2 AND $3 = ANY ( subscriptions ) AND $1 < > ALL ( notifications ) " ,
2019-05-26 21:58:54 +05:30
video . id , video . published , video . ucid , as : String )
2019-03-04 06:48:23 +05:30
2019-03-04 22:37:27 +05:30
video_array = video . to_a
args = arg_array ( video_array )
2019-03-04 06:48:23 +05:30
2019-05-31 20:59:17 +05:30
PG_DB . exec ( " INSERT INTO channel_videos VALUES ( #{ args } ) \
2019-05-26 21:58:54 +05:30
ON CONFLICT ( id ) DO UPDATE SET title = $2 , published = $3 , \
updated = $4 , ucid = $5 , author = $6 , length_seconds = $7 , \
2019-09-24 23:07:06 +05:30
live_now = $8 , premiere_timestamp = $9 , views = $10 " , args: video_array)
2019-03-04 22:37:27 +05:30
end
2019-03-04 06:48:23 +05:30
end
2019-03-04 07:20:23 +05:30
2019-03-23 20:54:30 +05:30
env . response . status_code = 200
next
2019-03-04 06:48:23 +05:30
end
2018-08-05 02:00:44 +05:30
# Channels
2019-04-28 22:17:16 +05:30
{ " /channel/:ucid/live " , " /user/:user/live " , " /c/:user/live " } . each do | route |
get route do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
# Appears to be a bug in routing, having several routes configured
# as `/a/:a`, `/b/:a`, `/c/:a` results in 404
value = env . request . resource . split ( " / " ) [ 2 ]
body = " "
{ " channel " , " user " , " c " } . each do | type |
2019-10-25 22:28:16 +05:30
response = YT_POOL . client & . get ( " / #{ type } / #{ value } /live?disable_polymer=1 " )
2019-04-28 22:17:16 +05:30
if response . status_code == 200
body = response . body
end
end
video_id = body . match ( / 'VIDEO_ID': "(?<id>[a-zA-Z0-9_-]{11})" / ) . try & . [ " id " ]?
if video_id
params = [ ] of String
env . params . query . each do | k , v |
params << " #{ k } = #{ v } "
end
params = params . join ( " & " )
url = " /watch?v= #{ video_id } "
if ! params . empty?
url += " & #{ params } "
end
env . redirect url
else
env . redirect " /channel/ #{ value } "
end
end
end
2018-09-04 19:43:58 +05:30
# YouTube appears to let users set a "brand" URL that
# is different from their username, so we convert that here
get " /c/:user " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-04 19:43:58 +05:30
user = env . params . url [ " user " ]
2019-10-25 22:28:16 +05:30
response = YT_POOL . client & . get ( " /c/ #{ user } " )
2020-04-05 02:01:24 +05:30
html = XML . parse_html ( response . body )
2018-09-04 19:43:58 +05:30
2020-04-05 02:01:24 +05:30
ucid = html . xpath_node ( % q ( / / link [ @rel = " canonical " ] ) ) . try & . [ " href " ] . split ( " / " ) [ - 1 ]
next env . redirect " / " if ! ucid
2018-09-04 19:43:58 +05:30
2020-04-05 02:01:24 +05:30
env . redirect " /channel/ #{ ucid } "
2018-09-04 19:43:58 +05:30
end
2019-01-24 10:42:48 +05:30
# Legacy endpoint for /user/:username
get " /profile " do | env |
user = env . params . query [ " user " ]?
if ! user
env . redirect " / "
else
env . redirect " /user/ #{ user } "
end
end
2019-06-08 21:43:00 +05:30
get " /attribution_link " do | env |
if query = env . params . query [ " u " ]?
url = URI . parse ( query ) . full_path
else
url = " / "
end
env . redirect url
end
2019-05-27 00:19:35 +05:30
# Page used by YouTube to provide captioning widget, since we
# don't support it we redirect to '/'
get " /timedtext_video " do | env |
env . redirect " / "
end
2018-08-05 02:00:44 +05:30
get " /user/:user " do | env |
user = env . params . url [ " user " ]
env . redirect " /channel/ #{ user } "
2018-03-25 09:08:35 +05:30
end
2018-09-06 09:42:11 +05:30
get " /user/:user/videos " do | env |
user = env . params . url [ " user " ]
env . redirect " /channel/ #{ user } /videos "
end
2019-07-09 20:01:04 +05:30
get " /user/:user/about " do | env |
user = env . params . url [ " user " ]
env . redirect " /channel/ #{ user } "
end
2019-08-17 06:36:21 +05:30
get " /channel/:ucid/about " do | env |
2019-07-09 20:01:04 +05:30
ucid = env . params . url [ " ucid " ]
env . redirect " /channel/ #{ ucid } "
end
2018-08-05 02:00:44 +05:30
get " /channel/:ucid " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
user = env . get? " user "
if user
user = user . as ( User )
subscriptions = user . subscriptions
end
subscriptions || = [ ] of String
2018-07-28 18:54:53 +05:30
ucid = env . params . url [ " ucid " ]
2018-08-05 02:00:44 +05:30
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2019-02-25 04:09:44 +05:30
continuation = env . params . query [ " continuation " ]?
2018-11-14 06:34:25 +05:30
sort_by = env . params . query [ " sort_by " ]? . try & . downcase
2018-09-21 20:10:04 +05:30
begin
2019-06-29 07:18:24 +05:30
channel = get_about_info ( ucid , locale )
2019-09-08 21:38:59 +05:30
rescue ex : ChannelRedirect
next env . redirect env . request . resource . gsub ( ucid , ex . channel_id )
2018-09-21 20:10:04 +05:30
rescue ex
2018-10-24 07:28:07 +05:30
error_message = ex . message
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2018-09-21 20:10:04 +05:30
next templated " error "
2018-08-05 02:00:44 +05:30
end
2019-06-29 07:18:24 +05:30
if channel . auto_generated
2019-03-03 22:24:23 +05:30
sort_options = { " last " , " oldest " , " newest " }
sort_by || = " last "
2019-06-29 07:18:24 +05:30
items , continuation = fetch_channel_playlists ( channel . ucid , channel . author , channel . auto_generated , continuation , sort_by )
2019-03-18 05:01:11 +05:30
items . uniq! do | item |
if item . responds_to? ( :title )
item . title
elsif item . responds_to? ( :author )
item . author
end
end
2019-08-17 02:16:37 +05:30
items = items . select { | item | item . is_a? ( SearchPlaylist ) } . map { | item | item . as ( SearchPlaylist ) }
2019-02-25 21:22:44 +05:30
items . each { | item | item . author = " " }
2019-02-25 04:09:44 +05:30
else
2019-03-03 22:24:23 +05:30
sort_options = { " newest " , " oldest " , " popular " }
sort_by || = " newest "
2019-07-02 17:59:01 +05:30
items , count = get_60_videos ( channel . ucid , channel . author , page , channel . auto_generated , sort_by )
2019-02-25 04:09:44 +05:30
items . select! { | item | ! item . paid }
2019-06-29 07:18:24 +05:30
env . set " search " , " channel: #{ channel . ucid } "
2019-02-25 04:09:44 +05:30
end
2018-08-05 02:00:44 +05:30
templated " channel "
end
get " /channel/:ucid/videos " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-08-05 02:00:44 +05:30
ucid = env . params . url [ " ucid " ]
params = env . request . query
if ! params || params . empty?
params = " "
else
params = " ? #{ params } "
2018-08-01 21:14:02 +05:30
end
2018-08-05 02:00:44 +05:30
env . redirect " /channel/ #{ ucid } #{ params } "
end
2018-07-28 18:54:53 +05:30
2019-03-03 22:24:23 +05:30
get " /channel/:ucid/playlists " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-03-03 22:24:23 +05:30
user = env . get? " user "
if user
user = user . as ( User )
subscriptions = user . subscriptions
end
subscriptions || = [ ] of String
ucid = env . params . url [ " ucid " ]
continuation = env . params . query [ " continuation " ]?
sort_by = env . params . query [ " sort_by " ]? . try & . downcase
sort_by || = " last "
begin
2019-06-29 07:18:24 +05:30
channel = get_about_info ( ucid , locale )
2019-09-08 21:38:59 +05:30
rescue ex : ChannelRedirect
next env . redirect env . request . resource . gsub ( ucid , ex . channel_id )
2019-03-03 22:24:23 +05:30
rescue ex
error_message = ex . message
2019-06-18 00:36:02 +05:30
env . response . status_code = 500
2019-03-03 22:24:23 +05:30
next templated " error "
end
2019-06-29 07:18:24 +05:30
if channel . auto_generated
next env . redirect " /channel/ #{ channel . ucid } "
2019-03-03 22:24:23 +05:30
end
2019-06-29 07:18:24 +05:30
items , continuation = fetch_channel_playlists ( channel . ucid , channel . author , channel . auto_generated , continuation , sort_by )
2019-08-17 06:36:21 +05:30
items = items . select { | item | item . is_a? ( SearchPlaylist ) } . map { | item | item . as ( SearchPlaylist ) }
2019-03-03 22:24:23 +05:30
items . each { | item | item . author = " " }
2019-07-27 19:21:10 +05:30
env . set " search " , " channel: #{ channel . ucid } "
2019-03-03 22:24:23 +05:30
templated " playlists "
end
2019-07-09 20:01:04 +05:30
get " /channel/:ucid/community " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
user = env . get? " user "
if user
user = user . as ( User )
subscriptions = user . subscriptions
end
subscriptions || = [ ] of String
ucid = env . params . url [ " ucid " ]
thin_mode = env . params . query [ " thin_mode " ]? || env . get ( " preferences " ) . as ( Preferences ) . thin_mode
thin_mode = thin_mode == " true "
continuation = env . params . query [ " continuation " ]?
# sort_by = env.params.query["sort_by"]?.try &.downcase
begin
channel = get_about_info ( ucid , locale )
2019-09-08 21:38:59 +05:30
rescue ex : ChannelRedirect
next env . redirect env . request . resource . gsub ( ucid , ex . channel_id )
2019-07-09 20:01:04 +05:30
rescue ex
error_message = ex . message
env . response . status_code = 500
next templated " error "
end
if ! channel . tabs . includes? " community "
next env . redirect " /channel/ #{ channel . ucid } "
end
begin
items = JSON . parse ( fetch_channel_community ( ucid , continuation , locale , config , Kemal . config , " json " , thin_mode ) )
rescue ex
env . response . status_code = 500
error_message = ex . message
end
2019-07-27 19:21:10 +05:30
env . set " search " , " channel: #{ channel . ucid } "
2019-07-09 20:01:04 +05:30
templated " community "
end
2018-08-05 02:00:44 +05:30
# API Endpoints
2018-07-28 18:54:53 +05:30
2019-03-02 06:55:16 +05:30
get " /api/v1/stats " do | env |
env . response . content_type = " application/json "
if ! config . statistics_enabled
error_message = { " error " = > " Statistics are not enabled. " } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 400
next error_message
2019-03-02 06:55:16 +05:30
end
2019-03-05 02:13:17 +05:30
if statistics [ " error " ]?
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next statistics . to_json
2019-03-05 02:13:17 +05:30
end
2019-03-24 00:35:13 +05:30
statistics . to_json
end
2019-03-02 06:55:16 +05:30
2019-05-21 06:52:01 +05:30
# YouTube provides "storyboards", which are sprites containing x * y
2019-05-03 00:50:19 +05:30
# preview thumbnails for individual scenes in a video.
# See https://support.jwplayer.com/articles/how-to-add-preview-thumbnails
get " /api/v1/storyboards/:id " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
env . response . content_type = " application/json "
id = env . params . url [ " id " ]
region = env . params . query [ " region " ]?
begin
2019-06-29 07:47:56 +05:30
video = get_video ( id , PG_DB , region : region )
2019-05-03 00:50:19 +05:30
rescue ex : VideoRedirect
2019-09-08 21:38:59 +05:30
error_message = { " error " = > " Video is unavailable " , " videoId " = > ex . video_id } . to_json
env . response . status_code = 302
env . response . headers [ " Location " ] = env . request . resource . gsub ( id , ex . video_id )
next error_message
2019-05-03 00:50:19 +05:30
rescue ex
env . response . status_code = 500
next
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
generate_storyboards ( json , id , storyboards , config , Kemal . config )
end
end
end
next response
end
env . response . content_type = " text/vtt "
storyboard = storyboards . select { | storyboard | width == " #{ storyboard [ :width ] } " || height == " #{ storyboard [ :height ] } " }
if storyboard . empty?
env . response . status_code = 404
next
else
storyboard = storyboard [ 0 ]
end
2019-08-27 18:38:26 +05:30
String . build do | str |
str << <<-END_VTT
WEBVTT
2019-05-03 00:50:19 +05:30
2019-08-27 18:38:26 +05:30
END_VTT
2019-05-03 00:50:19 +05:30
2019-08-27 18:38:26 +05:30
start_time = 0 . milliseconds
end_time = storyboard [ :interval ] . milliseconds
2019-05-03 00:50:19 +05:30
2019-08-27 18:38:26 +05:30
storyboard [ :storyboard_count ] . times do | i |
host_url = make_host_url ( config , Kemal . config )
url = storyboard [ :url ] . gsub ( " $M " , i ) . gsub ( " https://i9.ytimg.com " , host_url )
2019-05-03 00:50:19 +05:30
2019-08-27 18:38:26 +05:30
storyboard [ :storyboard_height ] . times do | j |
storyboard [ :storyboard_width ] . times do | k |
str << <<-END_CUE
#{start_time}.000 --> #{end_time}.000
#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width]},#{storyboard[:height]}
2019-05-03 00:50:19 +05:30
2019-08-27 18:38:26 +05:30
END_CUE
2019-05-03 00:50:19 +05:30
2019-08-27 18:38:26 +05:30
start_time += storyboard [ :interval ] . milliseconds
end_time += storyboard [ :interval ] . milliseconds
end
2019-05-03 00:50:19 +05:30
end
end
end
end
2018-08-05 02:00:44 +05:30
get " /api/v1/captions/:id " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-27 05:14:37 +05:30
env . response . content_type = " application/json "
2018-08-05 02:00:44 +05:30
id = env . params . url [ " id " ]
2018-11-18 05:07:57 +05:30
region = env . params . query [ " region " ]?
2018-07-30 07:31:28 +05:30
2019-05-21 06:52:01 +05:30
# 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.
2018-08-05 02:00:44 +05:30
begin
2019-06-29 07:47:56 +05:30
video = get_video ( id , PG_DB , region : region )
2018-10-07 08:52:22 +05:30
rescue ex : VideoRedirect
2019-09-08 21:38:59 +05:30
error_message = { " error " = > " Video is unavailable " , " videoId " = > ex . video_id } . to_json
env . response . status_code = 302
env . response . headers [ " Location " ] = env . request . resource . gsub ( id , ex . video_id )
next error_message
2018-08-05 02:00:44 +05:30
rescue ex
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next
2018-07-28 18:54:53 +05:30
end
2018-08-05 09:37:38 +05:30
captions = video . captions
2018-07-28 18:54:53 +05:30
2018-08-05 02:00:44 +05:30
label = env . params . query [ " label " ]?
2018-09-30 20:43:07 +05:30
lang = env . params . query [ " lang " ]?
tlang = env . params . query [ " tlang " ]?
if ! label && ! lang
2018-08-05 02:00:44 +05:30
response = JSON . build do | json |
json . object do
json . field " captions " do
json . array do
2018-08-05 09:37:38 +05:30
captions . each do | caption |
2018-08-05 02:00:44 +05:30
json . object do
2018-08-07 04:55:25 +05:30
json . field " label " , caption . name . simpleText
json . field " languageCode " , caption . languageCode
2019-09-24 23:01:33 +05:30
json . field " url " , " /api/v1/captions/ #{ id } ?label= #{ URI . encode_www_form ( caption . name . simpleText ) } "
2018-08-05 02:00:44 +05:30
end
end
end
end
2018-07-28 18:54:53 +05:30
end
2018-08-05 02:00:44 +05:30
end
2018-07-28 18:54:53 +05:30
2019-03-24 00:35:13 +05:30
next response
end
2018-07-28 18:54:53 +05:30
2019-05-21 06:52:01 +05:30
env . response . content_type = " text/vtt; charset=UTF-8 "
2018-09-30 20:43:07 +05:30
if lang
caption = captions . select { | caption | caption . languageCode == lang }
2020-01-09 06:57:21 +05:30
else
caption = captions . select { | caption | caption . name . simpleText == label }
2018-09-30 20:43:07 +05:30
end
2018-08-05 09:37:38 +05:30
if caption . empty?
2019-03-23 20:54:30 +05:30
env . response . status_code = 404
next
2018-08-05 02:00:44 +05:30
else
2018-08-05 09:37:38 +05:30
caption = caption [ 0 ]
2018-08-05 02:00:44 +05:30
end
2018-07-28 18:54:53 +05:30
2019-12-02 04:22:39 +05:30
url = URI . parse ( " #{ caption . baseUrl } &tlang= #{ tlang } " ) . full_path
2018-07-28 18:54:53 +05:30
2019-05-19 06:57:19 +05:30
# 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 . simpleText . includes? " auto-generated "
2019-10-25 22:28:16 +05:30
caption_xml = YT_POOL . client & . get ( url ) . body
2019-05-19 06:57:19 +05:30
caption_xml = XML . parse ( caption_xml )
2018-07-28 18:54:53 +05:30
2019-08-27 18:38:26 +05:30
webvtt = String . build do | str |
str << <<-END_VTT
WEBVTT
Kind : captions
Language : #{tlang || caption.languageCode}
2018-07-28 18:54:53 +05:30
2019-08-27 18:38:26 +05:30
END_VTT
2018-08-07 18:06:51 +05:30
2019-08-27 18:38:26 +05:30
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
2019-05-19 06:57:19 +05:30
2019-08-27 18:38:26 +05:30
if caption_nodes . size > i + 1
end_time = caption_nodes [ i + 1 ] [ " start " ] . to_f . seconds
else
end_time = start_time + duration
end
2018-07-28 18:54:53 +05:30
2019-08-27 18:38:26 +05:30
start_time = " #{ start_time . hours . to_s . rjust ( 2 , '0' ) } : #{ start_time . minutes . to_s . rjust ( 2 , '0' ) } : #{ start_time . seconds . to_s . rjust ( 2 , '0' ) } . #{ start_time . milliseconds . to_s . rjust ( 3 , '0' ) } "
end_time = " #{ end_time . hours . to_s . rjust ( 2 , '0' ) } : #{ end_time . minutes . to_s . rjust ( 2 , '0' ) } : #{ end_time . seconds . to_s . rjust ( 2 , '0' ) } . #{ end_time . milliseconds . to_s . rjust ( 3 , '0' ) } "
2018-08-05 02:00:44 +05:30
2019-08-27 18:38:26 +05:30
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
2018-08-05 02:00:44 +05:30
2019-08-27 18:38:26 +05:30
str << <<-END_CUE
#{start_time} --> #{end_time}
#{text}
2018-08-05 02:00:44 +05:30
2019-08-27 18:38:26 +05:30
END_CUE
end
2019-05-19 06:57:19 +05:30
end
else
2019-10-25 22:28:16 +05:30
webvtt = YT_POOL . client & . get ( " #{ url } &format=vtt " ) . body
2018-07-28 18:54:53 +05:30
end
2019-04-11 22:38:43 +05:30
if title = env . params . query [ " title " ]?
# https://blog.fastmail.com/2011/06/24/download-non-english-filenames/
2019-09-24 23:01:33 +05:30
env . response . headers [ " Content-Disposition " ] = " attachment; filename= \" #{ URI . encode_www_form ( title ) } \" ; filename*=UTF-8'' #{ URI . encode_www_form ( title ) } "
2019-04-11 22:38:43 +05:30
end
2018-08-05 02:00:44 +05:30
webvtt
2018-07-28 18:54:53 +05:30
end
2018-08-05 02:00:44 +05:30
get " /api/v1/comments/:id " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-02-07 05:25:22 +05:30
region = env . params . query [ " region " ]?
2018-12-21 03:02:09 +05:30
2018-09-27 05:14:37 +05:30
env . response . content_type = " application/json "
2018-08-05 02:00:44 +05:30
id = env . params . url [ " id " ]
2018-07-20 21:49:49 +05:30
2018-08-05 02:00:44 +05:30
source = env . params . query [ " source " ]?
source || = " youtube "
2018-07-20 21:49:49 +05:30
2019-03-27 22:01:05 +05:30
thin_mode = env . params . query [ " thin_mode " ]?
thin_mode = thin_mode == " true "
2018-08-05 02:00:44 +05:30
format = env . params . query [ " format " ]?
format || = " json "
2018-07-20 21:49:49 +05:30
2018-11-01 03:17:53 +05:30
continuation = env . params . query [ " continuation " ]?
2019-04-15 04:38:00 +05:30
sort_by = env . params . query [ " sort_by " ]? . try & . downcase
2018-08-05 02:00:44 +05:30
2018-11-01 03:17:53 +05:30
if source == " youtube "
2019-04-15 04:38:00 +05:30
sort_by || = " top "
2018-11-01 03:17:53 +05:30
begin
2019-06-29 07:47:56 +05:30
comments = fetch_youtube_comments ( id , PG_DB , continuation , format , locale , thin_mode , region , sort_by : sort_by )
2018-11-01 03:17:53 +05:30
rescue ex
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2018-07-20 21:49:49 +05:30
end
2018-11-10 20:35:26 +05:30
next comments
2018-08-05 02:00:44 +05:30
elsif source == " reddit "
2019-04-15 04:38:00 +05:30
sort_by || = " confidence "
2018-08-05 02:00:44 +05:30
begin
2019-04-15 04:38:00 +05:30
comments , reddit_thread = fetch_reddit_comments ( id , sort_by : sort_by )
2018-12-21 03:02:09 +05:30
content_html = template_reddit_comments ( comments , locale )
2018-03-31 07:22:10 +05:30
2018-08-05 02:00:44 +05:30
content_html = fill_links ( content_html , " https " , " www.reddit.com " )
2018-09-04 08:45:47 +05:30
content_html = replace_links ( content_html )
2018-08-05 02:00:44 +05:30
rescue ex
2018-09-06 20:49:28 +05:30
comments = nil
2018-08-05 02:00:44 +05:30
reddit_thread = nil
content_html = " "
end
2018-07-16 21:54:24 +05:30
2018-09-06 20:49:28 +05:30
if ! reddit_thread || ! comments
2019-03-23 20:54:30 +05:30
env . response . status_code = 404
next
2018-08-05 02:00:44 +05:30
end
2018-03-31 07:22:10 +05:30
2018-09-06 20:49:28 +05:30
if format == " json "
reddit_thread = JSON . parse ( reddit_thread . to_json ) . as_h
reddit_thread [ " comments " ] = JSON . parse ( comments . to_json )
2019-01-25 22:20:18 +05:30
2019-03-24 00:35:13 +05:30
next reddit_thread . to_json
2018-09-06 20:49:28 +05:30
else
2019-01-25 22:20:18 +05:30
response = {
2018-09-06 20:49:28 +05:30
" title " = > reddit_thread . title ,
2018-09-07 04:48:36 +05:30
" permalink " = > reddit_thread . permalink ,
" contentHtml " = > content_html ,
2019-01-25 22:20:18 +05:30
}
2019-03-24 00:35:13 +05:30
next response . to_json
2018-09-07 04:48:36 +05:30
end
2018-08-05 02:00:44 +05:30
end
2019-03-24 00:35:13 +05:30
end
2018-03-31 07:22:10 +05:30
2018-09-18 06:38:26 +05:30
get " /api/v1/insights/:id " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-18 06:38:26 +05:30
id = env . params . url [ " id " ]
env . response . content_type = " application/json "
2019-10-25 21:55:19 +05:30
error_message = { " error " = > " YouTube has removed publicly available analytics. " } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 410
2019-10-25 21:55:19 +05:30
error_message
2019-03-24 00:35:13 +05:30
end
2018-09-18 06:38:26 +05:30
2019-04-01 08:37:06 +05:30
get " /api/v1/annotations/:id " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
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} / )
env . response . status_code = 400
next
end
annotations = " "
case source
when " archive "
2019-04-15 21:43:09 +05:30
if CONFIG . cache_annotations && ( cached_annotation = PG_DB . query_one? ( " SELECT * FROM annotations WHERE id = $1 " , id , as : Annotation ) )
annotations = cached_annotation . annotations
else
index = CHARS_SAFE . index ( id [ 0 ] ) . not_nil! . to_s . rjust ( 2 , '0' )
2019-04-01 08:37:06 +05:30
2019-04-15 21:43:09 +05:30
# 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
2019-04-01 08:37:06 +05:30
2019-09-24 23:01:33 +05:30
file = URI . encode_www_form ( " #{ id [ 0 , 3 ] } / #{ id } .xml " )
2019-04-01 08:37:06 +05:30
2019-04-15 21:43:09 +05:30
client = make_client ( ARCHIVE_URL )
location = client . get ( " /download/youtubeannotations_ #{ index } / #{ id [ 0 , 2 ] } .tar/ #{ file } " )
2019-04-01 08:37:06 +05:30
2019-04-15 21:43:09 +05:30
if ! location . headers [ " Location " ]?
env . response . status_code = location . status_code
end
2019-04-01 08:37:06 +05:30
2019-04-17 19:36:31 +05:30
response = make_client ( URI . parse ( location . headers [ " Location " ] ) ) . get ( location . headers [ " Location " ] )
2019-04-01 08:37:06 +05:30
2019-04-15 21:43:09 +05:30
if response . body . empty?
env . response . status_code = 404
next
end
2019-04-13 18:58:59 +05:30
2019-04-15 21:43:09 +05:30
if response . status_code != 200
env . response . status_code = response . status_code
next
end
2019-04-01 08:37:06 +05:30
2019-04-15 21:43:09 +05:30
annotations = response . body
cache_annotation ( PG_DB , id , annotations )
end
2020-04-09 22:48:09 +05:30
else # "youtube"
2019-10-25 22:28:16 +05:30
response = YT_POOL . client & . get ( " /annotations_invideo?video_id= #{ id } " )
2019-04-01 08:37:06 +05:30
if response . status_code != 200
env . response . status_code = response . status_code
next
end
annotations = response . body
end
2019-11-10 08:35:17 +05:30
etag = sha256 ( annotations ) [ 0 , 16 ]
if env . request . headers [ " If-None-Match " ]? . try & . == etag
env . response . status_code = 304
else
env . response . headers [ " ETag " ] = etag
annotations
end
2019-04-01 08:37:06 +05:30
end
2018-08-05 02:00:44 +05:30
get " /api/v1/videos/:id " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-29 09:42:35 +05:30
env . response . content_type = " application/json "
2018-08-05 02:00:44 +05:30
id = env . params . url [ " id " ]
2018-11-18 05:03:30 +05:30
region = env . params . query [ " region " ]?
2018-03-31 07:22:10 +05:30
2018-08-05 02:00:44 +05:30
begin
2019-06-29 07:47:56 +05:30
video = get_video ( id , PG_DB , region : region )
2018-10-07 08:52:22 +05:30
rescue ex : VideoRedirect
2019-09-08 21:38:59 +05:30
error_message = { " error " = > " Video is unavailable " , " videoId " = > ex . video_id } . to_json
env . response . status_code = 302
env . response . headers [ " Location " ] = env . request . resource . gsub ( id , ex . video_id )
next error_message
2018-08-05 02:00:44 +05:30
rescue ex
2018-09-21 20:10:04 +05:30
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2018-08-05 02:00:44 +05:30
end
2018-03-31 07:22:10 +05:30
2019-04-11 04:28:42 +05:30
video . to_json ( locale , config , Kemal . config , decrypt_function )
2019-03-24 00:35:13 +05:30
end
2018-07-30 23:04:57 +05:30
2018-08-05 02:00:44 +05:30
get " /api/v1/trending " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2019-01-25 22:20:18 +05:30
env . response . content_type = " application/json "
2018-11-20 22:48:12 +05:30
region = env . params . query [ " region " ]?
trending_type = env . params . query [ " type " ]?
begin
2019-06-29 07:47:56 +05:30
trending , plid = fetch_trending ( trending_type , region , locale )
2018-11-20 22:48:12 +05:30
rescue ex
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2018-11-20 22:48:12 +05:30
end
2018-08-05 02:00:44 +05:30
videos = JSON . build do | json |
json . array do
2018-11-20 22:48:12 +05:30
trending . each do | video |
2019-06-09 00:01:41 +05:30
video . to_json ( locale , config , Kemal . config , json )
2018-08-05 02:00:44 +05:30
end
2019-06-23 23:24:46 +05:30
end
end
2018-07-30 23:04:57 +05:30
2019-03-24 00:35:13 +05:30
videos
end
2018-08-05 02:00:44 +05:30
2018-11-26 05:43:11 +05:30
get " /api/v1/popular " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2019-01-25 22:20:18 +05:30
env . response . content_type = " application/json "
2019-04-29 04:44:16 +05:30
JSON . build do | json |
2018-11-26 05:43:11 +05:30
json . array do
popular_videos . each do | video |
2019-04-29 04:44:16 +05:30
video . to_json ( locale , config , Kemal . config , json )
2018-11-26 05:43:11 +05:30
end
end
end
2019-03-24 00:35:13 +05:30
end
2018-11-26 05:43:11 +05:30
2018-08-05 02:00:44 +05:30
get " /api/v1/top " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2019-01-25 22:20:18 +05:30
env . response . content_type = " application/json "
2019-03-02 03:36:45 +05:30
if ! config . top_enabled
error_message = { " error " = > " Administrator has disabled this endpoint. " } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 400
next error_message
2019-03-02 03:36:45 +05:30
end
2019-06-09 00:01:41 +05:30
JSON . build do | json |
2018-08-05 02:00:44 +05:30
json . array do
top_videos . each do | video |
2019-06-09 00:01:41 +05:30
# Top videos have much more information than provided below (adaptiveFormats, etc)
# but can be very out of date, so we only provide a subset here
2018-08-05 02:00:44 +05:30
json . object do
json . field " title " , video . title
json . field " videoId " , video . id
json . field " videoThumbnails " do
2019-03-09 02:12:37 +05:30
generate_thumbnails ( json , video . id , config , Kemal . config )
2018-07-30 23:04:57 +05:30
end
2018-08-05 02:00:44 +05:30
2019-07-30 06:11:45 +05:30
json . field " lengthSeconds " , video . length_seconds
2018-08-05 02:00:44 +05:30
json . field " viewCount " , video . views
json . field " author " , video . author
2018-09-19 21:07:00 +05:30
json . field " authorId " , video . ucid
2018-08-05 02:00:44 +05:30
json . field " authorUrl " , " /channel/ #{ video . ucid } "
2018-11-04 21:07:12 +05:30
json . field " published " , video . published . to_unix
2019-02-20 20:19:39 +05:30
json . field " publishedText " , translate ( locale , " `x` ago " , recode_date ( video . published , locale ) )
2018-08-05 02:00:44 +05:30
2019-06-09 01:38:27 +05:30
json . field " description " , html_to_content ( video . description_html )
json . field " descriptionHtml " , video . description_html
2018-07-30 23:04:57 +05:30
end
end
end
end
2019-03-24 00:35:13 +05:30
end
2018-07-30 23:04:57 +05:30
2018-08-05 02:00:44 +05:30
get " /api/v1/channels/:ucid " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-21 20:10:04 +05:30
env . response . content_type = " application/json "
2018-08-10 20:14:19 +05:30
2018-09-21 20:10:04 +05:30
ucid = env . params . url [ " ucid " ]
2018-11-14 06:34:25 +05:30
sort_by = env . params . query [ " sort_by " ]? . try & . downcase
sort_by || = " newest "
2018-09-05 07:34:40 +05:30
2018-09-21 20:10:04 +05:30
begin
2019-06-29 07:18:24 +05:30
channel = get_about_info ( ucid , locale )
2019-09-08 21:38:59 +05:30
rescue ex : ChannelRedirect
error_message = { " error " = > " Channel is unavailable " , " authorId " = > ex . channel_id } . to_json
env . response . status_code = 302
env . response . headers [ " Location " ] = env . request . resource . gsub ( ucid , ex . channel_id )
next error_message
2018-09-21 20:10:04 +05:30
rescue ex
2018-10-24 07:28:07 +05:30
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2018-09-05 07:34:40 +05:30
end
2018-09-06 19:13:22 +05:30
page = 1
2019-06-29 07:18:24 +05:30
if channel . auto_generated
2019-02-25 04:09:44 +05:30
videos = [ ] of SearchVideo
count = 0
else
begin
2019-07-03 05:23:19 +05:30
videos , count = get_60_videos ( channel . ucid , channel . author , page , channel . auto_generated , sort_by )
2019-02-25 04:09:44 +05:30
rescue ex
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2019-02-25 04:09:44 +05:30
end
2018-11-09 04:40:14 +05:30
end
2018-08-29 06:59:08 +05:30
2019-06-29 07:18:24 +05:30
JSON . build do | json |
2019-06-09 00:01:41 +05:30
# TODO: Refactor into `to_json` for InvidiousChannel
2018-08-05 02:00:44 +05:30
json . object do
2019-06-29 07:18:24 +05:30
json . field " author " , channel . author
json . field " authorId " , channel . ucid
json . field " authorUrl " , channel . author_url
2018-08-05 02:00:44 +05:30
json . field " authorBanners " do
json . array do
2019-06-30 23:29:38 +05:30
if channel . banner
qualities = {
{ width : 2560 , height : 424 } ,
{ width : 2120 , height : 351 } ,
{ width : 1060 , height : 175 } ,
}
qualities . each do | quality |
json . object do
2019-07-03 05:23:19 +05:30
json . field " url " , channel . banner . not_nil! . gsub ( " =w1060- " , " =w #{ quality [ :width ] } - " )
2019-06-30 23:29:38 +05:30
json . field " width " , quality [ :width ]
json . field " height " , quality [ :height ]
end
2018-08-05 02:00:44 +05:30
end
2019-06-30 23:29:38 +05:30
json . object do
2019-07-03 05:23:19 +05:30
json . field " url " , channel . banner . not_nil! . split ( " =w1060- " ) [ 0 ]
2019-06-30 23:29:38 +05:30
json . field " width " , 512
json . field " height " , 288
end
2018-08-05 02:00:44 +05:30
end
2018-07-19 00:56:02 +05:30
end
end
2018-03-31 20:21:14 +05:30
2018-08-05 02:00:44 +05:30
json . field " authorThumbnails " do
json . array do
2019-03-25 20:30:18 +05:30
qualities = { 32 , 48 , 76 , 100 , 176 , 512 }
2018-07-19 00:56:02 +05:30
2018-08-05 02:00:44 +05:30
qualities . each do | quality |
json . object do
2020-04-10 22:19:51 +05:30
json . field " url " , channel . author_thumbnail . gsub ( / =s \ d+ / , " =s #{ quality } " )
2018-08-05 02:00:44 +05:30
json . field " width " , quality
json . field " height " , quality
end
end
2018-07-19 00:56:02 +05:30
end
2018-03-31 20:21:14 +05:30
end
2019-06-29 07:18:24 +05:30
json . field " subCount " , channel . sub_count
json . field " totalViews " , channel . total_views
json . field " joined " , channel . joined . to_unix
json . field " paid " , channel . paid
2018-03-31 20:21:14 +05:30
2019-06-29 07:18:24 +05:30
json . field " autoGenerated " , channel . auto_generated
json . field " isFamilyFriendly " , channel . is_family_friendly
json . field " description " , html_to_content ( channel . description_html )
json . field " descriptionHtml " , channel . description_html
2018-09-05 05:57:10 +05:30
2019-06-29 07:18:24 +05:30
json . field " allowedRegions " , channel . allowed_regions
2018-07-29 07:10:59 +05:30
2018-08-05 02:00:44 +05:30
json . field " latestVideos " do
json . array do
2018-08-29 06:59:08 +05:30
videos . each do | video |
2019-06-09 00:01:41 +05:30
video . to_json ( locale , config , Kemal . config , json )
2018-08-05 02:00:44 +05:30
end
end
end
2018-11-28 09:37:45 +05:30
json . field " relatedChannels " do
json . array do
2019-06-29 07:18:24 +05:30
channel . related_channels . each do | related_channel |
2018-11-28 09:37:45 +05:30
json . object do
2019-06-29 07:18:24 +05:30
json . field " author " , related_channel . author
json . field " authorId " , related_channel . ucid
json . field " authorUrl " , related_channel . author_url
2018-11-28 09:37:45 +05:30
json . field " authorThumbnails " do
json . array do
2019-03-25 20:30:18 +05:30
qualities = { 32 , 48 , 76 , 100 , 176 , 512 }
2018-11-28 09:37:45 +05:30
qualities . each do | quality |
json . object do
2019-08-01 05:46:09 +05:30
json . field " url " , related_channel . author_thumbnail . gsub ( / = \ d+ / , " =s #{ quality } " )
2018-11-28 09:37:45 +05:30
json . field " width " , quality
json . field " height " , quality
end
end
end
end
end
end
end
end
2018-08-05 02:00:44 +05:30
end
2018-07-29 07:10:59 +05:30
end
2019-03-24 00:35:13 +05:30
end
2018-07-16 18:48:59 +05:30
2019-04-28 22:17:16 +05:30
{ " /api/v1/channels/:ucid/videos " , " /api/v1/channels/videos/:ucid " } . each do | route |
2018-09-18 21:17:22 +05:30
get route do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-21 20:10:04 +05:30
env . response . content_type = " application/json "
2018-09-20 20:06:09 +05:30
ucid = env . params . url [ " ucid " ]
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2019-02-16 04:58:54 +05:30
sort_by = env . params . query [ " sort " ]? . try & . downcase
sort_by || = env . params . query [ " sort_by " ]? . try & . downcase
2018-11-14 06:41:16 +05:30
sort_by || = " newest "
2018-07-16 18:48:59 +05:30
2018-09-21 20:10:04 +05:30
begin
2019-06-29 07:18:24 +05:30
channel = get_about_info ( ucid , locale )
2019-09-08 21:38:59 +05:30
rescue ex : ChannelRedirect
error_message = { " error " = > " Channel is unavailable " , " authorId " = > ex . channel_id } . to_json
env . response . status_code = 302
env . response . headers [ " Location " ] = env . request . resource . gsub ( ucid , ex . channel_id )
next error_message
2018-09-21 20:10:04 +05:30
rescue ex
2018-10-24 07:28:07 +05:30
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2018-09-20 20:06:09 +05:30
end
2018-07-16 18:48:59 +05:30
2018-11-09 04:40:14 +05:30
begin
2019-07-02 17:59:01 +05:30
videos , count = get_60_videos ( channel . ucid , channel . author , page , channel . auto_generated , sort_by )
2018-11-09 04:40:14 +05:30
rescue ex
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2018-11-09 04:40:14 +05:30
end
2018-07-30 07:31:28 +05:30
2019-06-09 00:01:41 +05:30
JSON . build do | json |
2018-09-20 20:06:09 +05:30
json . array do
videos . each do | video |
2019-06-09 00:01:41 +05:30
video . to_json ( locale , config , Kemal . config , json )
2018-08-05 02:00:44 +05:30
end
end
2019-06-23 23:24:46 +05:30
end
end
end
2018-07-16 18:48:59 +05:30
2019-04-28 22:17:16 +05:30
{ " /api/v1/channels/:ucid/latest " , " /api/v1/channels/latest/:ucid " } . each do | route |
2019-02-20 04:30:06 +05:30
get route do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-02-20 20:19:39 +05:30
2019-02-20 04:30:06 +05:30
env . response . content_type = " application/json "
ucid = env . params . url [ " ucid " ]
begin
videos = get_latest_videos ( ucid )
rescue ex
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2019-02-20 04:30:06 +05:30
end
2019-04-29 04:44:16 +05:30
JSON . build do | json |
2019-02-20 04:30:06 +05:30
json . array do
videos . each do | video |
2019-06-09 00:01:41 +05:30
video . to_json ( locale , config , Kemal . config , json )
2019-02-20 04:30:06 +05:30
end
end
end
2019-06-23 23:24:46 +05:30
end
end
2019-02-20 04:30:06 +05:30
2019-04-28 22:17:16 +05:30
{ " /api/v1/channels/:ucid/playlists " , " /api/v1/channels/playlists/:ucid " } . each do | route |
2019-02-20 04:35:27 +05:30
get route do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-02-20 04:35:27 +05:30
env . response . content_type = " application/json "
ucid = env . params . url [ " ucid " ]
continuation = env . params . query [ " continuation " ]?
sort_by = env . params . query [ " sort " ]? . try & . downcase
sort_by || = env . params . query [ " sort_by " ]? . try & . downcase
sort_by || = " last "
begin
2019-06-29 07:18:24 +05:30
channel = get_about_info ( ucid , locale )
2019-09-08 21:38:59 +05:30
rescue ex : ChannelRedirect
error_message = { " error " = > " Channel is unavailable " , " authorId " = > ex . channel_id } . to_json
env . response . status_code = 302
env . response . headers [ " Location " ] = env . request . resource . gsub ( ucid , ex . channel_id )
next error_message
2019-02-20 04:35:27 +05:30
rescue ex
2019-03-23 20:54:30 +05:30
error_message = { " error " = > ex . message } . to_json
env . response . status_code = 500
next error_message
2019-02-20 04:35:27 +05:30
end
2019-06-29 07:18:24 +05:30
items , continuation = fetch_channel_playlists ( channel . ucid , channel . author , channel . auto_generated , continuation , sort_by )
2019-02-20 04:35:27 +05:30
2019-06-29 07:18:24 +05:30
JSON . build do | json |
2019-02-20 04:35:27 +05:30
json . object do
json . field " playlists " do
json . array do
items . each do | item |
2019-06-09 00:01:41 +05:30
if item . is_a? ( SearchPlaylist )
item . to_json ( locale , config , Kemal . config , json )
2019-02-20 04:35:27 +05:30
end
end
end
end
json . field " continuation " , continuation
end
end
end
2019-03-24 00:35:13 +05:30
end
2019-02-20 04:35:27 +05:30
2019-07-03 05:23:19 +05:30
{ " /api/v1/channels/:ucid/comments " , " /api/v1/channels/comments/:ucid " } . each do | route |
get route do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
env . response . content_type = " application/json "
ucid = env . params . url [ " ucid " ]
2019-07-09 20:01:04 +05:30
thin_mode = env . params . query [ " thin_mode " ]?
thin_mode = thin_mode == " true "
2019-07-03 05:23:19 +05:30
2019-07-09 20:01:04 +05:30
format = env . params . query [ " format " ]?
format || = " json "
continuation = env . params . query [ " continuation " ]?
2019-07-03 05:23:19 +05:30
# sort_by = env.params.query["sort_by"]?.try &.downcase
begin
2019-07-09 20:01:04 +05:30
fetch_channel_community ( ucid , continuation , locale , config , Kemal . config , format , thin_mode )
2019-07-03 05:23:19 +05:30
rescue ex
env . response . status_code = 400
error_message = { " error " = > ex . message } . to_json
next error_message
end
end
end
2018-09-22 21:19:42 +05:30
get " /api/v1/channels/search/:ucid " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-22 21:19:42 +05:30
env . response . content_type = " application/json "
ucid = env . params . url [ " ucid " ]
query = env . params . query [ " q " ]?
query || = " "
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
count , search_results = channel_search ( query , page , ucid )
2019-06-09 00:01:41 +05:30
JSON . build do | json |
2018-09-22 21:19:42 +05:30
json . array do
search_results . each do | item |
2019-06-09 00:01:41 +05:30
item . to_json ( locale , config , Kemal . config , json )
2018-09-22 21:19:42 +05:30
end
end
end
2019-03-24 00:35:13 +05:30
end
2018-09-22 21:19:42 +05:30
2018-08-05 02:00:44 +05:30
get " /api/v1/search " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-02-07 05:51:40 +05:30
region = env . params . query [ " region " ]?
2018-12-21 03:02:09 +05:30
2018-09-22 21:19:42 +05:30
env . response . content_type = " application/json "
2018-08-05 09:37:38 +05:30
query = env . params . query [ " q " ]?
query || = " "
2018-08-03 03:48:57 +05:30
2018-08-05 02:00:44 +05:30
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
2018-08-05 03:42:58 +05:30
sort_by = env . params . query [ " sort_by " ]? . try & . downcase
sort_by || = " relevance "
date = env . params . query [ " date " ]? . try & . downcase
date || = " "
2019-02-27 02:01:37 +05:30
duration = env . params . query [ " duration " ]? . try & . downcase
2018-08-05 03:42:58 +05:30
duration || = " "
features = env . params . query [ " features " ]? . try & . split ( " , " ) . map { | feature | feature . downcase }
features || = [ ] of String
2018-09-20 20:06:09 +05:30
content_type = env . params . query [ " type " ]? . try & . downcase
content_type || = " video "
2018-08-05 03:42:58 +05:30
begin
2018-09-18 03:08:18 +05:30
search_params = produce_search_params ( sort_by , date , content_type , duration , features )
2018-08-05 03:42:58 +05:30
rescue ex
2018-09-20 20:06:09 +05:30
env . response . status_code = 400
2019-07-02 17:59:01 +05:30
error_message = { " error " = > ex . message } . to_json
next error_message
2018-08-05 03:42:58 +05:30
end
2019-06-29 07:47:56 +05:30
count , search_results = search ( query , page , search_params , region ) . as ( Tuple )
2019-06-09 00:01:41 +05:30
JSON . build do | json |
2018-08-05 02:00:44 +05:30
json . array do
2018-09-20 20:06:09 +05:30
search_results . each do | item |
2019-06-09 00:01:41 +05:30
item . to_json ( locale , config , Kemal . config , json )
2018-08-05 02:00:44 +05:30
end
end
end
2019-03-24 00:35:13 +05:30
end
2018-08-03 03:48:57 +05:30
2019-05-21 17:45:15 +05:30
get " /api/v1/search/suggestions " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
region = env . params . query [ " region " ]?
env . response . content_type = " application/json "
query = env . params . query [ " q " ]?
query || = " "
begin
2020-05-08 19:30:53 +05:30
headers = HTTP :: Headers { " :authority " = > " suggestqueries.google.com " }
response = YT_POOL . client & . get ( " /complete/search?hl=en&gl= #{ region } &client=youtube&ds=yt&q= #{ URI . encode_www_form ( query ) } &callback=suggestCallback " , headers ) . body
2019-11-28 19:50:44 +05:30
2019-05-21 17:45:15 +05:30
body = response [ 35 .. - 2 ]
body = JSON . parse ( body ) . as_a
suggestions = body [ 1 ] . as_a [ 0 .. - 2 ]
JSON . build do | json |
json . object do
json . field " query " , body [ 0 ] . as_s
json . field " suggestions " do
json . array do
suggestions . each do | suggestion |
json . string suggestion [ 0 ] . as_s
end
end
end
end
end
rescue ex
env . response . status_code = 500
error_message = { " error " = > ex . message } . to_json
next error_message
end
end
2019-08-06 05:19:13 +05:30
{ " /api/v1/playlists/:plid " , " /api/v1/auth/playlists/:plid " } . each do | route |
get route do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-10-08 07:41:33 +05:30
2019-08-06 05:19:13 +05:30
env . response . content_type = " application/json "
plid = env . params . url [ " plid " ]
2018-10-07 08:48:50 +05:30
2019-08-06 05:19:13 +05:30
offset = env . params . query [ " index " ]? . try & . to_i?
offset || = env . params . query [ " page " ]? . try & . to_i? . try { | page | ( page - 1 ) * 100 }
offset || = 0
2018-08-15 20:52:36 +05:30
2019-08-06 05:19:13 +05:30
continuation = env . params . query [ " continuation " ]?
2018-09-28 20:24:45 +05:30
2019-08-06 05:19:13 +05:30
format = env . params . query [ " format " ]?
format || = " json "
2018-08-15 20:52:36 +05:30
2019-08-06 05:19:13 +05:30
if plid . starts_with? " RD "
next env . redirect " /api/v1/mixes/ #{ plid } "
end
2018-08-15 20:52:36 +05:30
2019-08-06 05:19:13 +05:30
begin
playlist = get_playlist ( PG_DB , plid , locale )
rescue ex
env . response . status_code = 404
error_message = { " error " = > " Playlist does not exist. " } . to_json
next error_message
end
2018-09-25 20:58:40 +05:30
2019-08-06 05:19:13 +05:30
user = env . get? ( " user " ) . try & . as ( User )
2019-10-16 17:51:26 +05:30
if ! playlist || playlist . privacy . private? && playlist . author != user . try & . email
2019-08-06 05:19:13 +05:30
env . response . status_code = 404
error_message = { " error " = > " Playlist does not exist. " } . to_json
next error_message
end
2018-09-25 20:58:40 +05:30
2019-08-06 05:19:13 +05:30
response = playlist . to_json ( offset , locale , config , Kemal . config , continuation : continuation )
2018-08-15 20:52:36 +05:30
2019-08-06 05:19:13 +05:30
if format == " html "
response = JSON . parse ( response )
playlist_html = template_playlist ( response )
2019-10-22 07:10:03 +05:30
index , next_video = response [ " videos " ] . as_a . skip ( 1 ) . select { | video | ! video [ " author " ] . as_s . empty? } [ 0 ]? . try { | v | { v [ " index " ] , v [ " videoId " ] } } || { nil , nil }
2018-08-15 20:52:36 +05:30
2019-08-06 05:19:13 +05:30
response = {
" playlistHtml " = > playlist_html ,
" index " = > index ,
" nextVideo " = > next_video ,
} . to_json
2018-08-15 20:52:36 +05:30
end
2018-10-08 07:41:33 +05:30
2019-08-06 05:19:13 +05:30
response
2018-10-08 07:41:33 +05:30
end
2019-03-24 00:35:13 +05:30
end
2018-08-15 20:52:36 +05:30
2018-09-29 09:42:35 +05:30
get " /api/v1/mixes/:rdid " do | env |
2019-03-11 23:14:25 +05:30
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2018-12-21 03:02:09 +05:30
2018-09-29 09:42:35 +05:30
env . response . content_type = " application/json "
rdid = env . params . url [ " rdid " ]
continuation = env . params . query [ " continuation " ]?
2019-02-16 04:58:54 +05:30
continuation || = rdid . lchop ( " RD " ) [ 0 , 11 ]
2018-09-29 09:42:35 +05:30
2018-10-08 07:41:33 +05:30
format = env . params . query [ " format " ]?
format || = " json "
2018-09-29 09:42:35 +05:30
begin
2018-12-21 03:02:09 +05:30
mix = fetch_mix ( rdid , continuation , locale : locale )
2018-10-31 19:45:17 +05:30
if ! rdid . ends_with? continuation
mix = fetch_mix ( rdid , mix . videos [ 1 ] . id )
index = mix . videos . index ( mix . videos . select { | video | video . id == continuation } [ 0 ]? )
end
mix . videos = mix . videos [ index .. - 1 ]
2018-09-29 09:42:35 +05:30
rescue ex
error_message = { " error " = > ex . message } . to_json
2019-03-23 20:54:30 +05:30
env . response . status_code = 500
next error_message
2018-09-29 09:42:35 +05:30
end
response = JSON . build do | json |
json . object do
json . field " title " , mix . title
json . field " mixId " , mix . id
json . field " videos " do
json . array do
mix . videos . each do | video |
json . object do
json . field " title " , video . title
json . field " videoId " , video . id
json . field " author " , video . author
json . field " authorId " , video . ucid
json . field " authorUrl " , " /channel/ #{ video . ucid } "
json . field " videoThumbnails " do
json . array do
2019-03-09 02:12:37 +05:30
generate_thumbnails ( json , video . id , config , Kemal . config )
2018-09-29 09:42:35 +05:30
end
end
json . field " index " , video . index
json . field " lengthSeconds " , video . length_seconds
end
end
end
end
end
end
2018-10-08 07:41:33 +05:30
if format == " html "
response = JSON . parse ( response )
playlist_html = template_mix ( response )
2019-10-22 04:30:56 +05:30
next_video = response [ " videos " ] . as_a . select { | video | ! video [ " author " ] . as_s . empty? } [ 0 ]? . try & . [ " videoId " ]
2018-10-08 07:41:33 +05:30
response = {
" playlistHtml " = > playlist_html ,
" nextVideo " = > next_video ,
} . to_json
end
2019-03-23 20:54:30 +05:30
response
2018-09-29 09:42:35 +05:30
end
2019-06-07 23:09:12 +05:30
# Authenticated endpoints
2019-04-11 04:28:42 +05:30
get " /api/v1/auth/notifications " do | env |
2019-06-02 18:11:53 +05:30
env . response . content_type = " text/event-stream "
2019-04-11 04:28:42 +05:30
topics = env . params . query [ " topics " ]? . try & . split ( " , " ) . uniq . first ( 1000 )
topics || = [ ] of String
2019-06-29 07:47:56 +05:30
create_notification_stream ( env , config , Kemal . config , decrypt_function , topics , connection_channel )
2019-05-21 19:31:17 +05:30
end
2019-04-11 04:28:42 +05:30
2019-05-21 19:31:17 +05:30
post " /api/v1/auth/notifications " do | env |
2019-06-02 18:11:53 +05:30
env . response . content_type = " text/event-stream "
2019-05-21 19:31:17 +05:30
topics = env . params . body [ " topics " ]? . try & . split ( " , " ) . uniq . first ( 1000 )
topics || = [ ] of String
2019-04-11 04:28:42 +05:30
2019-06-29 07:47:56 +05:30
create_notification_stream ( env , config , Kemal . config , decrypt_function , topics , connection_channel )
2019-04-11 04:28:42 +05:30
end
2019-05-01 07:31:57 +05:30
get " /api/v1/auth/preferences " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
user . preferences . to_json
end
2019-04-19 02:53:50 +05:30
2019-05-01 07:31:57 +05:30
post " /api/v1/auth/preferences " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
begin
2019-05-31 05:01:22 +05:30
preferences = Preferences . from_json ( env . request . body || " {} " , user . preferences )
2019-05-01 07:31:57 +05:30
rescue
preferences = user . preferences
end
PG_DB . exec ( " UPDATE users SET preferences = $1 WHERE email = $2 " , preferences . to_json , user . email )
env . response . status_code = 204
end
2019-04-19 02:53:50 +05:30
2019-06-07 23:09:12 +05:30
get " /api/v1/auth/feed " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
2019-06-09 02:34:55 +05:30
max_results = env . params . query [ " max_results " ]? . try & . to_i?
max_results || = user . preferences . max_results
max_results || = CONFIG . default_user_preferences . max_results
2019-06-07 23:09:12 +05:30
page = env . params . query [ " page " ]? . try & . to_i?
page || = 1
videos , notifications = get_subscription_feed ( PG_DB , user , max_results , page )
JSON . build do | json |
json . object do
json . field " notifications " do
json . array do
notifications . each do | video |
video . to_json ( locale , config , Kemal . config , json )
end
end
end
json . field " videos " do
json . array do
videos . each do | video |
video . to_json ( locale , config , Kemal . config , json )
end
end
end
end
end
end
2019-04-22 21:10:29 +05:30
get " /api/v1/auth/subscriptions " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
2019-04-19 02:53:50 +05:30
2019-04-22 21:10:29 +05:30
if user . subscriptions . empty?
values = " '{}' "
else
values = " VALUES #{ user . subscriptions . map { | id | %( ( ' #{ id } ' ) ) } . join ( " , " ) } "
end
2019-04-19 02:53:50 +05:30
2019-04-22 21:10:29 +05:30
subscriptions = PG_DB . query_all ( " SELECT * FROM channels WHERE id = ANY( #{ values } ) " , as : InvidiousChannel )
JSON . build do | json |
json . array do
subscriptions . each do | subscription |
json . object do
json . field " author " , subscription . author
json . field " authorId " , subscription . id
end
end
end
end
end
post " /api/v1/auth/subscriptions/:ucid " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
ucid = env . params . url [ " ucid " ]
if ! user . subscriptions . includes? ucid
get_channel ( ucid , PG_DB , false , false )
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = array_append(subscriptions,$1) WHERE email = $2 " , ucid , user . email )
2019-04-22 21:10:29 +05:30
end
2019-05-15 22:56:29 +05:30
# For Google accounts, access tokens don't have enough information to
# make a request on the user's behalf, which is why we don't sync with
# YouTube.
2019-04-22 21:10:29 +05:30
env . response . status_code = 204
end
delete " /api/v1/auth/subscriptions/:ucid " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
ucid = env . params . url [ " ucid " ]
2019-06-01 20:49:18 +05:30
PG_DB . exec ( " UPDATE users SET feed_needs_update = true, subscriptions = array_remove(subscriptions, $1) WHERE email = $2 " , ucid , user . email )
2019-04-22 21:10:29 +05:30
env . response . status_code = 204
end
2019-04-19 02:53:50 +05:30
2019-08-06 05:19:13 +05:30
get " /api/v1/auth/playlists " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
playlists = PG_DB . query_all ( " SELECT * FROM playlists WHERE author = $1 " , user . email , as : InvidiousPlaylist )
JSON . build do | json |
json . array do
playlists . each do | playlist |
playlist . to_json ( 0 , locale , config , Kemal . config , json )
end
end
end
end
post " /api/v1/auth/playlists " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
title = env . params . json [ " title " ]? . try & . as ( String ) . delete ( " <> " ) . byte_slice ( 0 , 150 )
if ! title
error_message = { " error " = > " Invalid title. " } . to_json
env . response . status_code = 400
next error_message
end
privacy = env . params . json [ " privacy " ]? . try { | privacy | PlaylistPrivacy . parse ( privacy . as ( String ) . downcase ) }
if ! privacy
error_message = { " error " = > " Invalid privacy setting. " } . to_json
env . response . status_code = 400
next error_message
end
if PG_DB . query_one ( " SELECT count(*) FROM playlists WHERE author = $1 " , user . email , as : Int64 ) >= 100
error_message = { " error " = > " User cannot have more than 100 playlists. " } . to_json
env . response . status_code = 400
next error_message
end
host_url = make_host_url ( config , Kemal . config )
playlist = create_playlist ( PG_DB , title , privacy , user )
env . response . headers [ " Location " ] = " #{ host_url } /api/v1/auth/playlists/ #{ playlist . id } "
env . response . status_code = 201
{
" title " = > title ,
" playlistId " = > playlist . id ,
} . to_json
end
patch " /api/v1/auth/playlists/:plid " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
plid = env . params . url [ " plid " ]
playlist = PG_DB . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
2019-10-16 17:51:26 +05:30
if ! playlist || playlist . author != user . email && playlist . privacy . private?
2019-08-06 05:19:13 +05:30
env . response . status_code = 404
error_message = { " error " = > " Playlist does not exist. " } . to_json
next error_message
end
if playlist . author != user . email
env . response . status_code = 403
error_message = { " error " = > " Invalid user " } . to_json
next error_message
end
title = env . params . json [ " title " ] . try & . as ( String ) . delete ( " <> " ) . byte_slice ( 0 , 150 ) || playlist . title
privacy = env . params . json [ " privacy " ]? . try { | privacy | PlaylistPrivacy . parse ( privacy . as ( String ) . downcase ) } || playlist . privacy
description = env . params . json [ " description " ]? . try & . as ( String ) . delete ( " \ r " ) || playlist . description
if title != playlist . title ||
privacy != playlist . privacy ||
description != playlist . description
updated = Time . utc
else
updated = playlist . updated
end
PG_DB . exec ( " UPDATE playlists SET title = $1, privacy = $2, description = $3, updated = $4 WHERE id = $5 " , title , privacy , description , updated , plid )
env . response . status_code = 204
end
delete " /api/v1/auth/playlists/:plid " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
plid = env . params . url [ " plid " ]
playlist = PG_DB . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
2019-10-16 17:51:26 +05:30
if ! playlist || playlist . author != user . email && playlist . privacy . private?
2019-08-06 05:19:13 +05:30
env . response . status_code = 404
error_message = { " error " = > " Playlist does not exist. " } . to_json
next error_message
end
if playlist . author != user . email
env . response . status_code = 403
error_message = { " error " = > " Invalid user " } . to_json
next error_message
end
PG_DB . exec ( " DELETE FROM playlist_videos * WHERE plid = $1 " , plid )
PG_DB . exec ( " DELETE FROM playlists * WHERE id = $1 " , plid )
env . response . status_code = 204
end
post " /api/v1/auth/playlists/:plid/videos " do | env |
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
plid = env . params . url [ " plid " ]
playlist = PG_DB . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
2019-10-16 17:51:26 +05:30
if ! playlist || playlist . author != user . email && playlist . privacy . private?
2019-08-06 05:19:13 +05:30
env . response . status_code = 404
error_message = { " error " = > " Playlist does not exist. " } . to_json
next error_message
end
if playlist . author != user . email
env . response . status_code = 403
error_message = { " error " = > " Invalid user " } . to_json
next error_message
end
if playlist . index . size >= 500
env . response . status_code = 400
error_message = { " error " = > " Playlist cannot have more than 500 videos " } . to_json
next error_message
end
video_id = env . params . json [ " videoId " ] . try & . as ( String )
if ! video_id
env . response . status_code = 403
error_message = { " error " = > " Invalid videoId " } . to_json
next error_message
end
begin
video = get_video ( video_id , PG_DB )
rescue ex
error_message = { " error " = > ex . message } . to_json
env . response . status_code = 500
next error_message
end
playlist_video = PlaylistVideo . new (
title : video . title ,
id : video . id ,
author : video . author ,
ucid : video . ucid ,
length_seconds : video . length_seconds ,
published : video . published ,
plid : plid ,
live_now : video . live_now ,
index : Random :: Secure . rand ( 0 _i64 .. Int64 :: MAX )
)
video_array = playlist_video . to_a
args = arg_array ( video_array )
PG_DB . exec ( " INSERT INTO playlist_videos VALUES ( #{ args } ) " , args : video_array )
PG_DB . exec ( " UPDATE playlists SET index = array_append(index, $1), video_count = video_count + 1, updated = $2 WHERE id = $3 " , playlist_video . index , Time . utc , plid )
host_url = make_host_url ( config , Kemal . config )
2019-10-16 07:39:01 +05:30
env . response . headers [ " Location " ] = " #{ host_url } /api/v1/auth/playlists/ #{ plid } /videos/ #{ playlist_video . index . to_u64 . to_s ( 16 ) . upcase } "
2019-08-06 05:19:13 +05:30
env . response . status_code = 201
playlist_video . to_json ( locale , config , Kemal . config , index : playlist . index . size )
end
delete " /api/v1/auth/playlists/:plid/videos/:index " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
plid = env . params . url [ " plid " ]
index = env . params . url [ " index " ] . to_i64 ( 16 )
playlist = PG_DB . query_one? ( " SELECT * FROM playlists WHERE id = $1 " , plid , as : InvidiousPlaylist )
2019-10-16 17:51:26 +05:30
if ! playlist || playlist . author != user . email && playlist . privacy . private?
2019-08-06 05:19:13 +05:30
env . response . status_code = 404
error_message = { " error " = > " Playlist does not exist. " } . to_json
next error_message
end
if playlist . author != user . email
env . response . status_code = 403
error_message = { " error " = > " Invalid user " } . to_json
next error_message
end
if ! playlist . index . includes? index
env . response . status_code = 404
error_message = { " error " = > " Playlist does not contain index " } . to_json
next error_message
end
PG_DB . exec ( " DELETE FROM playlist_videos * WHERE index = $1 " , index )
PG_DB . exec ( " UPDATE playlists SET index = array_remove(index, $1), video_count = video_count - 1, updated = $2 WHERE id = $3 " , index , Time . utc , plid )
env . response . status_code = 204
end
# patch "/api/v1/auth/playlists/:plid/videos/:index" do |env|
# TODO: Playlist stub
# end
2019-04-19 02:53:50 +05:30
get " /api/v1/auth/tokens " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
scopes = env . get ( " scopes " ) . as ( Array ( String ) )
tokens = PG_DB . query_all ( " SELECT id, issued FROM session_ids WHERE email = $1 " , user . email , as : { session : String , issued : Time } )
JSON . build do | json |
json . array do
tokens . each do | token |
json . object do
json . field " session " , token [ :session ]
json . field " issued " , token [ :issued ] . to_unix
end
end
end
end
end
post " /api/v1/auth/tokens/register " do | env |
user = env . get ( " user " ) . as ( User )
locale = LOCALES [ env . get ( " preferences " ) . as ( Preferences ) . locale ]?
case env . request . headers [ " Content-Type " ]?
when " application/x-www-form-urlencoded "
scopes = env . params . body . select { | k , v | k . match ( / ^scopes \ [ \ d+ \ ]$ / ) } . map { | k , v | v }
callback_url = env . params . body [ " callbackUrl " ]?
expire = env . params . body [ " expire " ]? . try & . to_i?
when " application/json "
scopes = env . params . json [ " scopes " ] . as ( Array ) . map { | v | v . as_s }
callback_url = env . params . json [ " callbackUrl " ]? . try & . as ( String )
expire = env . params . json [ " expire " ]? . try & . as ( Int64 )
else
error_message = { " error " = > " Invalid or missing header 'Content-Type' " } . to_json
env . response . status_code = 400
next error_message
end
if callback_url && callback_url . empty?
callback_url = nil
end
if callback_url
callback_url = URI . parse ( callback_url )
end
if sid = env . get? ( " sid " ) . try & . as ( String )
env . response . content_type = " text/html "
csrf_token = generate_response ( sid , { " :authorize_token " } , HMAC_KEY , PG_DB , use_nonce : true )
next templated " authorize_token "
else
env . response . content_type = " application/json "
superset_scopes = env . get ( " scopes " ) . as ( Array ( String ) )
authorized_scopes = [ ] of String
scopes . each do | scope |
if scopes_include_scope ( superset_scopes , scope )
authorized_scopes << scope
end
end
access_token = generate_token ( user . email , authorized_scopes , expire , HMAC_KEY , PG_DB )
if callback_url
2019-09-24 23:01:33 +05:30
access_token = URI . encode_www_form ( access_token )
2019-04-19 02:53:50 +05:30
if query = callback_url . query
query = HTTP :: Params . parse ( query . not_nil! )
else
query = HTTP :: Params . new
end
query [ " token " ] = access_token
callback_url . query = query . to_s
env . redirect callback_url . to_s
else
access_token
end
end
end
post " /api/v1/auth/tokens/unregister " do | env |
env . response . content_type = " application/json "
user = env . get ( " user " ) . as ( User )
scopes = env . get ( " scopes " ) . as ( Array ( String ) )
session = env . params . json [ " session " ]? . try & . as ( String )
session || = env . get ( " session " ) . as ( String )
# Allow tokens to revoke other tokens with correct scope
if session == env . get ( " session " ) . as ( String )
PG_DB . exec ( " DELETE FROM session_ids * WHERE id = $1 " , session )
elsif scopes_include_scope ( scopes , " GET:tokens " )
PG_DB . exec ( " DELETE FROM session_ids * WHERE id = $1 " , session )
else
error_message = { " error " = > " Cannot revoke session #{ session } " } . to_json
env . response . status_code = 400
next error_message
end
env . response . status_code = 204
end
2018-08-07 23:40:52 +05:30
get " /api/manifest/dash/id/videoplayback " do | env |
2019-04-12 21:38:33 +05:30
env . response . headers . delete ( " Content-Type " )
2018-08-07 23:53:27 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2018-08-07 23:40:52 +05:30
env . redirect " /videoplayback? #{ env . params . query } "
end
get " /api/manifest/dash/id/videoplayback/* " do | env |
2019-04-12 21:38:33 +05:30
env . response . headers . delete ( " Content-Type " )
2018-08-07 23:53:27 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2018-08-07 23:40:52 +05:30
env . redirect env . request . path . lchop ( " /api/manifest/dash/id " )
end
2018-07-16 18:48:59 +05:30
get " /api/manifest/dash/id/:id " do | env |
env . response . headers . add ( " Access-Control-Allow-Origin " , " * " )
env . response . content_type = " application/dash+xml "
local = env . params . query [ " local " ]? . try & . == " true "
id = env . params . url [ " id " ]
2018-11-18 05:07:57 +05:30
region = env . params . query [ " region " ]?
2018-07-16 18:48:59 +05:30
2019-06-05 07:24:38 +05:30
# Since some implementations create playlists based on resolution regardless of different codecs,
2019-06-07 08:02:39 +05:30
# we can opt to only add a source to a representation if it has a unique height within that representation
2020-01-09 06:57:21 +05:30
unique_res = env . params . query [ " unique_res " ]? . try { | q | ( q == " true " || q == " 1 " ) . to_unsafe }
2019-06-05 07:24:38 +05:30
2018-07-16 18:48:59 +05:30
begin
2019-06-29 07:47:56 +05:30
video = get_video ( id , PG_DB , region : region )
2018-10-07 08:52:22 +05:30
rescue ex : VideoRedirect
2019-09-08 21:38:59 +05:30
next env . redirect env . request . resource . gsub ( id , ex . video_id )
2018-07-16 18:48:59 +05:30
rescue ex
2019-03-23 20:54:30 +05:30
env . response . status_code = 403
next
2018-07-16 18:48:59 +05:30
end
2019-03-22 21:02:42 +05:30
if dashmpd = video . player_response [ " streamingData " ]? . try & . [ " dashManifestUrl " ]? . try & . as_s
2020-01-09 06:57:21 +05:30
manifest = YT_POOL . client & . get ( URI . parse ( dashmpd ) . full_path ) . body
2018-07-16 18:48:59 +05:30
manifest = manifest . gsub ( / <BaseURL>[^<]+< \/ BaseURL> / ) do | baseurl |
url = baseurl . lchop ( " <BaseURL> " )
url = url . rchop ( " </BaseURL> " )
if local
2019-07-05 09:58:25 +05:30
url = URI . parse ( url ) . full_path
2018-07-16 18:48:59 +05:30
end
" <BaseURL> #{ url } </BaseURL> "
end
next manifest
end
2018-08-05 09:37:38 +05:30
adaptive_fmts = video . adaptive_fmts ( decrypt_function )
2018-07-16 18:48:59 +05:30
if local
adaptive_fmts . each do | fmt |
2019-07-05 09:58:25 +05:30
fmt [ " url " ] = URI . parse ( fmt [ " url " ] ) . full_path
2018-07-16 18:48:59 +05:30
end
end
2019-04-12 18:34:59 +05:30
audio_streams = video . audio_streams ( adaptive_fmts )
2020-01-09 06:57:21 +05:30
video_streams = video . video_streams ( adaptive_fmts ) . sort_by { | stream | { stream [ " size " ] . split ( " x " ) [ 0 ] . to_i , stream [ " fps " ] . to_i } } . reverse
2018-08-05 09:37:38 +05:30
2019-04-12 21:38:33 +05:30
XML . build ( indent : " " , encoding : " UTF-8 " ) do | xml |
2018-08-12 01:31:22 +05:30
xml . element ( " MPD " , " xmlns " : " urn:mpeg:dash:schema:mpd:2011 " ,
2019-06-05 07:24:38 +05:30
" profiles " : " urn:mpeg:dash:profile:full:2011 " , minBufferTime : " PT1.5S " , type : " static " ,
2019-07-30 06:11:45 +05:30
mediaPresentationDuration : " PT #{ video . length_seconds } S " ) do
2018-07-16 18:48:59 +05:30
xml . element ( " Period " ) do
2019-04-12 21:49:54 +05:30
i = 0
2019-04-12 18:34:59 +05:30
{ " audio/mp4 " , " audio/webm " } . each do | mime_type |
2019-07-09 20:38:00 +05:30
mime_streams = audio_streams . select { | stream | stream [ " type " ] . starts_with? mime_type }
if mime_streams . empty?
next
end
2019-04-12 21:49:54 +05:30
xml . element ( " AdaptationSet " , id : i , mimeType : mime_type , startWithSAP : 1 , subsegmentAlignment : true ) do
2019-07-09 20:38:00 +05:30
mime_streams . each do | fmt |
2019-04-12 18:34:59 +05:30
codecs = fmt [ " type " ] . split ( " codecs= " ) [ 1 ] . strip ( '"' )
2019-06-05 07:24:38 +05:30
bandwidth = fmt [ " bitrate " ] . to_i * 1000
2019-04-12 18:34:59 +05:30
itag = fmt [ " itag " ]
url = fmt [ " url " ]
xml . element ( " Representation " , id : fmt [ " itag " ] , codecs : codecs , bandwidth : bandwidth ) do
xml . element ( " AudioChannelConfiguration " , schemeIdUri : " urn:mpeg:dash:23003:3:audio_channel_configuration:2011 " ,
value : " 2 " )
xml . element ( " BaseURL " ) { xml . text url }
xml . element ( " SegmentBase " , indexRange : fmt [ " index " ] ) do
xml . element ( " Initialization " , range : fmt [ " init " ] )
end
2018-07-16 18:48:59 +05:30
end
end
end
2019-04-12 21:49:54 +05:30
i += 1
2018-07-16 18:48:59 +05:30
end
2019-04-12 18:34:59 +05:30
{ " video/mp4 " , " video/webm " } . each do | mime_type |
2019-07-09 20:38:00 +05:30
mime_streams = video_streams . select { | stream | stream [ " type " ] . starts_with? mime_type }
2020-01-09 06:57:21 +05:30
next if mime_streams . empty?
2019-07-09 20:38:00 +05:30
2019-06-07 08:02:39 +05:30
heights = [ ] of Int32
2019-04-12 21:49:54 +05:30
xml . element ( " AdaptationSet " , id : i , mimeType : mime_type , startWithSAP : 1 , subsegmentAlignment : true , scanType : " progressive " ) do
2019-07-09 20:38:00 +05:30
mime_streams . each do | fmt |
2019-04-12 18:34:59 +05:30
codecs = fmt [ " type " ] . split ( " codecs= " ) [ 1 ] . strip ( '"' )
bandwidth = fmt [ " bitrate " ]
itag = fmt [ " itag " ]
url = fmt [ " url " ]
2019-05-31 02:09:02 +05:30
width , height = fmt [ " size " ] . split ( " x " ) . map { | i | i . to_i }
# Resolutions reported by YouTube player (may not accurately reflect source)
height = [ 4320 , 2160 , 1440 , 1080 , 720 , 480 , 360 , 240 , 144 ] . sort_by { | i | ( height - i ) . abs } [ 0 ]
2019-06-05 07:24:38 +05:30
next if unique_res && heights . includes? height
heights << height
2019-04-12 18:34:59 +05:30
xml . element ( " Representation " , id : itag , codecs : codecs , width : width , height : height ,
startWithSAP : " 1 " , maxPlayoutRate : " 1 " ,
bandwidth : bandwidth , frameRate : fmt [ " fps " ] ) do
xml . element ( " BaseURL " ) { xml . text url }
xml . element ( " SegmentBase " , indexRange : fmt [ " index " ] ) do
xml . element ( " Initialization " , range : fmt [ " init " ] )
end
2018-07-16 18:48:59 +05:30
end
end
end
2019-04-12 21:49:54 +05:30
i += 1
2018-07-16 18:48:59 +05:30
end
end
end
end
end
2018-07-28 04:55:58 +05:30
get " /api/manifest/hls_variant/* " do | env |
2019-10-25 22:28:16 +05:30
manifest = YT_POOL . client & . get ( env . request . path )
2018-07-28 04:55:58 +05:30
if manifest . status_code != 200
2019-03-23 20:54:30 +05:30
env . response . status_code = manifest . status_code
next
2018-07-28 04:55:58 +05:30
end
2019-04-25 23:11:35 +05:30
local = env . params . query [ " local " ]? . try & . == " true "
2018-07-28 04:55:58 +05:30
env . response . content_type = " application/x-mpegURL "
env . response . headers . add ( " Access-Control-Allow-Origin " , " * " )
2018-08-05 09:37:38 +05:30
2019-03-06 00:26:59 +05:30
host_url = make_host_url ( config , Kemal . config )
2019-01-19 20:40:52 +05:30
2018-08-05 09:37:38 +05:30
manifest = manifest . body
2019-04-25 23:11:35 +05:30
if local
manifest = manifest . gsub ( " https://www.youtube.com " , host_url )
manifest = manifest . gsub ( " index.m3u8 " , " index.m3u8?local=true " )
end
manifest
2018-07-28 04:55:58 +05:30
end
get " /api/manifest/hls_playlist/* " do | env |
2019-10-25 22:28:16 +05:30
manifest = YT_POOL . client & . get ( env . request . path )
2018-07-28 04:55:58 +05:30
if manifest . status_code != 200
2019-03-23 20:54:30 +05:30
env . response . status_code = manifest . status_code
next
2018-07-28 04:55:58 +05:30
end
2019-04-25 23:11:35 +05:30
local = env . params . query [ " local " ]? . try & . == " true "
env . response . content_type = " application/x-mpegURL "
env . response . headers . add ( " Access-Control-Allow-Origin " , " * " )
2019-03-06 00:26:59 +05:30
host_url = make_host_url ( config , Kemal . config )
2018-07-28 04:55:58 +05:30
2019-04-25 23:11:35 +05:30
manifest = manifest . body
if local
2019-07-05 22:38:39 +05:30
manifest = manifest . gsub ( / ^https: \/ \/ r \ d---.{11} \ .c \ .youtube \ .com[^ \ n]* /m ) do | match |
path = URI . parse ( match ) . path
path = path . lchop ( " /videoplayback/ " )
path = path . rchop ( " / " )
path = path . gsub ( / mime \/ \ w+ \/ \ w+ / ) do | mimetype |
mimetype = mimetype . split ( " / " )
mimetype [ 0 ] + " / " + mimetype [ 1 ] + " %2F " + mimetype [ 2 ]
end
path = path . split ( " / " )
raw_params = { } of String = > Array ( String )
path . each_slice ( 2 ) do | pair |
key , value = pair
2019-09-24 23:01:33 +05:30
value = URI . decode_www_form ( value )
2019-07-05 22:38:39 +05:30
if raw_params [ key ]?
raw_params [ key ] << value
else
raw_params [ key ] = [ value ]
end
end
raw_params = HTTP :: Params . new ( raw_params )
if fvip = raw_params [ " hls_chunk_host " ] . match ( / r(?<fvip> \ d+)--- / )
raw_params [ " fvip " ] = fvip [ " fvip " ]
end
raw_params [ " local " ] = " true "
2019-04-25 23:11:35 +05:30
2019-07-05 22:38:39 +05:30
" #{ host_url } /videoplayback? #{ raw_params } "
end
end
2018-07-28 04:55:58 +05:30
manifest
end
2019-01-28 08:05:32 +05:30
# YouTube /videoplayback links expire after 6 hours,
# so we have a mechanism here to redirect to the latest version
get " /latest_version " do | env |
2019-02-24 22:34:46 +05:30
if env . params . query [ " download_widget " ]?
download_widget = JSON . parse ( env . params . query [ " download_widget " ] )
2019-04-11 22:38:43 +05:30
2019-02-24 22:34:46 +05:30
id = download_widget [ " id " ] . as_s
title = download_widget [ " title " ] . as_s
2019-04-11 22:38:43 +05:30
if label = download_widget [ " label " ]?
env . redirect " /api/v1/captions/ #{ id } ?label= #{ label } &title= #{ title } "
next
else
2019-04-12 17:59:47 +05:30
itag = download_widget [ " itag " ] . as_s
local = " true "
end
2019-04-11 22:38:43 +05:30
end
2019-02-24 22:34:46 +05:30
id || = env . params . query [ " id " ]?
itag || = env . params . query [ " itag " ]?
2019-01-28 08:05:32 +05:30
2019-02-09 23:58:43 +05:30
region = env . params . query [ " region " ]?
2019-02-24 22:34:46 +05:30
local || = env . params . query [ " local " ]?
2019-01-28 08:50:52 +05:30
local || = " false "
local = local == " true "
2019-01-28 08:05:32 +05:30
if ! id || ! itag
2019-03-23 20:54:30 +05:30
env . response . status_code = 400
next
2019-01-28 08:05:32 +05:30
end
2019-06-29 07:47:56 +05:30
video = get_video ( id , PG_DB , region : region )
2019-01-28 08:05:32 +05:30
fmt_stream = video . fmt_stream ( decrypt_function )
adaptive_fmts = video . adaptive_fmts ( decrypt_function )
urls = ( fmt_stream + adaptive_fmts ) . select { | fmt | fmt [ " itag " ] == itag }
if urls . empty?
2019-03-23 20:54:30 +05:30
env . response . status_code = 404
next
2019-01-28 08:05:32 +05:30
elsif urls . size > 1
2019-03-23 20:54:30 +05:30
env . response . status_code = 409
next
2019-01-28 08:05:32 +05:30
end
2019-01-28 08:50:52 +05:30
url = urls [ 0 ] [ " url " ]
if local
url = URI . parse ( url ) . full_path . not_nil!
end
2019-02-24 22:34:46 +05:30
if title
url += " &title= #{ title } "
end
2019-01-28 08:50:52 +05:30
env . redirect url
2019-01-28 08:05:32 +05:30
end
2018-08-07 23:55:22 +05:30
options " /videoplayback " do | env |
2019-04-12 21:38:33 +05:30
env . response . headers . delete ( " Content-Type " )
2018-08-05 02:00:44 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2018-08-09 20:13:47 +05:30
env . response . headers [ " Access-Control-Allow-Methods " ] = " GET, OPTIONS "
env . response . headers [ " Access-Control-Allow-Headers " ] = " Content-Type, Range "
2018-08-05 02:00:44 +05:30
end
2018-08-07 22:09:56 +05:30
options " /videoplayback/* " do | env |
2019-04-12 21:38:33 +05:30
env . response . headers . delete ( " Content-Type " )
2018-08-07 22:09:56 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2018-08-09 20:13:47 +05:30
env . response . headers [ " Access-Control-Allow-Methods " ] = " GET, OPTIONS "
env . response . headers [ " Access-Control-Allow-Headers " ] = " Content-Type, Range "
2018-08-07 22:09:56 +05:30
end
2018-08-07 23:48:38 +05:30
options " /api/manifest/dash/id/videoplayback " do | env |
2019-04-12 21:38:33 +05:30
env . response . headers . delete ( " Content-Type " )
2018-08-07 23:48:38 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2018-08-09 20:13:47 +05:30
env . response . headers [ " Access-Control-Allow-Methods " ] = " GET, OPTIONS "
env . response . headers [ " Access-Control-Allow-Headers " ] = " Content-Type, Range "
2018-08-07 23:48:38 +05:30
end
options " /api/manifest/dash/id/videoplayback/* " do | env |
2019-04-12 21:38:33 +05:30
env . response . headers . delete ( " Content-Type " )
2018-08-07 23:48:38 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2018-08-09 20:13:47 +05:30
env . response . headers [ " Access-Control-Allow-Methods " ] = " GET, OPTIONS "
env . response . headers [ " Access-Control-Allow-Headers " ] = " Content-Type, Range "
2018-08-07 23:48:38 +05:30
end
2018-08-07 22:09:56 +05:30
get " /videoplayback/* " do | env |
2018-06-07 04:25:51 +05:30
path = env . request . path
2018-08-07 22:09:56 +05:30
2018-08-07 22:19:14 +05:30
path = path . lchop ( " /videoplayback/ " )
path = path . rchop ( " / " )
2018-07-16 08:23:24 +05:30
2018-08-07 22:19:14 +05:30
path = path . gsub ( / mime \/ \ w+ \/ \ w+ / ) do | mimetype |
mimetype = mimetype . split ( " / " )
mimetype [ 0 ] + " / " + mimetype [ 1 ] + " %2F " + mimetype [ 2 ]
end
2018-07-16 08:23:24 +05:30
2018-08-07 22:19:14 +05:30
path = path . split ( " / " )
2018-06-07 04:25:51 +05:30
2018-08-07 22:19:14 +05:30
raw_params = { } of String = > Array ( String )
path . each_slice ( 2 ) do | pair |
key , value = pair
2019-09-24 23:01:33 +05:30
value = URI . decode_www_form ( value )
2018-06-07 04:25:51 +05:30
2018-08-07 22:19:14 +05:30
if raw_params [ key ]?
raw_params [ key ] << value
else
raw_params [ key ] = [ value ]
2018-06-07 04:25:51 +05:30
end
2018-08-07 22:19:14 +05:30
end
2018-06-07 04:25:51 +05:30
2018-08-07 22:19:14 +05:30
query_params = HTTP :: Params . new ( raw_params )
2018-08-07 22:09:56 +05:30
2018-08-12 00:59:51 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2018-08-07 22:09:56 +05:30
env . redirect " /videoplayback? #{ query_params } "
end
get " /videoplayback " do | env |
2018-08-07 22:19:14 +05:30
query_params = env . params . query
2018-04-16 07:17:37 +05:30
2019-03-12 00:37:55 +05:30
fvip = query_params [ " fvip " ]? || " 3 "
2019-05-31 07:17:04 +05:30
mns = query_params [ " mn " ]? . try & . split ( " , " )
mns || = [ ] of String
2019-03-12 00:37:55 +05:30
2019-03-28 01:29:53 +05:30
if query_params [ " region " ]?
region = query_params [ " region " ]
query_params . delete ( " region " )
end
2019-03-11 23:44:30 +05:30
if query_params [ " host " ]? && ! query_params [ " host " ] . empty?
host = " https:// #{ query_params [ " host " ] } "
2019-03-12 00:02:46 +05:30
query_params . delete ( " host " )
2019-03-11 23:44:30 +05:30
else
2019-03-12 00:37:55 +05:30
host = " https://r #{ fvip } --- #{ mns . pop } .googlevideo.com "
2019-03-11 23:44:30 +05:30
end
2018-04-16 07:17:37 +05:30
url = " /videoplayback? #{ query_params . to_s } "
2019-03-11 22:13:48 +05:30
headers = HTTP :: Headers . new
2019-06-23 19:09:14 +05:30
REQUEST_HEADERS_WHITELIST . each do | header |
2019-03-11 22:13:48 +05:30
if env . request . headers [ header ]?
headers [ header ] = env . request . headers [ header ]
end
end
2019-01-25 01:22:33 +05:30
2019-08-27 20:23:44 +05:30
client = make_client ( URI . parse ( host ) , region )
2019-10-26 21:13:28 +05:30
response = HTTP :: Client :: Response . new ( 500 )
2020-03-07 00:20:00 +05:30
error = " "
2019-08-27 20:23:44 +05:30
5 . times do
begin
response = client . head ( url , headers )
if response . headers [ " Location " ]?
location = URI . parse ( response . headers [ " Location " ] )
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
host = " #{ location . scheme } :// #{ location . host } "
client = make_client ( URI . parse ( host ) , region )
url = " #{ location . full_path } &host= #{ location . host } #{ region ? " ®ion= #{ region } " : " " } "
else
break
end
rescue Socket :: Addrinfo :: Error
if ! mns . empty?
mn = mns . pop
end
fvip = " 3 "
host = " https://r #{ fvip } --- #{ mn } .googlevideo.com "
client = make_client ( URI . parse ( host ) , region )
rescue ex
2020-03-07 00:20:00 +05:30
error = ex . message
2019-08-27 20:23:44 +05:30
end
end
if response . status_code >= 400
env . response . status_code = response . status_code
2020-03-07 00:20:00 +05:30
env . response . content_type = " text/plain "
next error
2019-08-27 20:23:44 +05:30
end
2019-07-05 22:04:22 +05:30
if url . includes? " &file=seg.ts "
2019-07-07 19:37:53 +05:30
if CONFIG . disabled? ( " livestreams " )
env . response . status_code = 403
error_message = " Administrator has disabled this endpoint. "
next templated " error "
end
2019-07-05 22:04:22 +05:30
begin
client = make_client ( URI . parse ( host ) , region )
client . get ( url , headers ) do | response |
response . headers . each do | key , value |
2019-11-25 00:11:47 +05:30
if ! RESPONSE_HEADERS_BLACKLIST . includes? ( key . downcase )
2019-07-05 22:04:22 +05:30
env . response . headers [ key ] = value
end
end
2018-04-16 07:17:37 +05:30
2019-07-05 22:04:22 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2019-07-05 02:00:00 +05:30
2019-07-05 22:04:22 +05:30
if location = response . headers [ " Location " ]?
location = URI . parse ( location )
location = " #{ location . full_path } &host= #{ location . host } "
2019-07-05 02:00:00 +05:30
2019-07-05 22:04:22 +05:30
if region
location += " ®ion= #{ region } "
end
2019-05-26 21:23:56 +05:30
2019-07-05 22:04:22 +05:30
next env . redirect location
end
IO . copy ( response . body_io , env . response )
end
rescue ex
end
else
2019-07-07 19:37:53 +05:30
if query_params [ " title " ]? && CONFIG . disabled? ( " downloads " ) ||
CONFIG . disabled? ( " dash " )
env . response . status_code = 403
error_message = " Administrator has disabled this endpoint. "
next templated " error "
end
2019-07-05 22:04:22 +05:30
content_length = nil
first_chunk = true
range_start , range_end = parse_range ( env . request . headers [ " Range " ]? )
chunk_start = range_start
chunk_end = range_end
if ! chunk_end || chunk_end - chunk_start > HTTP_CHUNK_SIZE
chunk_end = chunk_start + HTTP_CHUNK_SIZE - 1
2019-07-05 02:00:00 +05:30
end
2019-05-19 17:42:45 +05:30
2019-08-27 20:23:44 +05:30
client = make_client ( URI . parse ( host ) , region )
2019-07-05 22:04:22 +05:30
# TODO: Record bytes written so we can restart after a chunk fails
while true
if ! range_end && content_length
range_end = content_length
end
2019-03-28 01:29:53 +05:30
2019-07-05 22:04:22 +05:30
if range_end && chunk_start > range_end
break
end
if range_end && chunk_end > range_end
chunk_end = range_end
end
2019-03-28 01:29:53 +05:30
2019-07-05 22:04:22 +05:30
headers [ " Range " ] = " bytes= #{ chunk_start } - #{ chunk_end } "
2019-07-05 02:00:00 +05:30
2019-07-05 22:04:22 +05:30
begin
client . get ( url , headers ) do | response |
if first_chunk
if ! env . request . headers [ " Range " ]? && response . status_code == 206
env . response . status_code = 200
else
env . response . status_code = response . status_code
end
2019-07-05 02:00:00 +05:30
2019-07-05 22:04:22 +05:30
response . headers . each do | key , value |
2019-11-25 00:11:47 +05:30
if ! RESPONSE_HEADERS_BLACKLIST . includes? ( key . downcase ) && key . downcase != " content-range "
2019-07-05 22:04:22 +05:30
env . response . headers [ key ] = value
end
2019-07-05 02:00:00 +05:30
end
2019-07-05 22:04:22 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2019-07-05 02:00:00 +05:30
2019-07-05 22:04:22 +05:30
if location = response . headers [ " Location " ]?
location = URI . parse ( location )
2019-08-27 18:38:26 +05:30
location = " #{ location . full_path } &host= #{ location . host } #{ region ? " ®ion= #{ region } " : " " } "
2019-07-05 22:04:22 +05:30
env . redirect location
break
end
if title = query_params [ " title " ]?
# https://blog.fastmail.com/2011/06/24/download-non-english-filenames/
2019-09-24 23:01:33 +05:30
env . response . headers [ " Content-Disposition " ] = " attachment; filename= \" #{ URI . encode_www_form ( title ) } \" ; filename*=UTF-8'' #{ URI . encode_www_form ( title ) } "
2019-07-05 22:04:22 +05:30
end
if ! response . headers . includes_word? ( " Transfer-Encoding " , " chunked " )
content_length = response . headers [ " Content-Range " ] . split ( " / " ) [ - 1 ] . to_i64
if env . request . headers [ " Range " ]?
env . response . headers [ " Content-Range " ] = " bytes #{ range_start } - #{ range_end || ( content_length - 1 ) } / #{ content_length } "
env . response . content_length = ( ( range_end . try & . + 1 ) || content_length ) - range_start
else
env . response . content_length = content_length
end
2019-07-05 21:32:12 +05:30
end
2019-07-05 02:00:00 +05:30
end
2019-07-01 21:15:09 +05:30
2019-07-05 22:04:22 +05:30
proxy_file ( response , env )
end
rescue ex
if ex . message != " Error reading socket: Connection reset by peer "
break
2019-08-27 20:23:44 +05:30
else
client = make_client ( URI . parse ( host ) , region )
2019-07-05 22:04:22 +05:30
end
2019-07-01 21:15:09 +05:30
end
2019-07-05 21:32:12 +05:30
2019-07-05 22:04:22 +05:30
chunk_start = chunk_end + 1
chunk_end += HTTP_CHUNK_SIZE
first_chunk = false
2019-03-26 03:02:11 +05:30
end
2019-05-26 21:23:56 +05:30
end
2018-09-15 07:54:28 +05:30
end
2018-09-18 05:09:28 +05:30
get " /ggpht/* " do | env |
url = env . request . path . lchop ( " /ggpht " )
2020-05-08 19:30:53 +05:30
headers = HTTP :: Headers { " :authority " = > " yt3.ggpht.com " }
2019-06-23 19:09:14 +05:30
REQUEST_HEADERS_WHITELIST . each do | header |
2019-04-12 03:30:00 +05:30
if env . request . headers [ header ]?
headers [ header ] = env . request . headers [ header ]
end
end
2019-05-26 20:11:12 +05:30
begin
2020-03-07 00:23:35 +05:30
YT_POOL . client & . get ( url , headers ) do | response |
2019-07-03 23:43:40 +05:30
env . response . status_code = response . status_code
2019-05-26 21:23:56 +05:30
response . headers . each do | key , value |
2019-11-25 00:11:47 +05:30
if ! RESPONSE_HEADERS_BLACKLIST . includes? ( key . downcase )
2019-05-26 21:23:56 +05:30
env . response . headers [ key ] = value
end
2019-05-19 17:42:45 +05:30
end
2019-04-12 03:30:00 +05:30
2019-07-03 23:43:40 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
if response . status_code >= 300
2019-07-04 00:24:15 +05:30
env . response . headers . delete ( " Transfer-Encoding " )
2019-05-26 21:23:56 +05:30
break
end
2019-04-12 03:30:00 +05:30
2019-05-26 21:23:56 +05:30
proxy_file ( response , env )
end
2019-05-26 20:11:12 +05:30
rescue ex
end
2019-04-12 03:30:00 +05:30
end
2019-05-03 00:50:19 +05:30
options " /sb/:id/:storyboard/:index " do | env |
env . response . headers . delete ( " Content-Type " )
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
env . response . headers [ " Access-Control-Allow-Methods " ] = " GET, OPTIONS "
env . response . headers [ " Access-Control-Allow-Headers " ] = " Content-Type, Range "
end
2019-04-12 03:30:00 +05:30
get " /sb/:id/:storyboard/:index " do | env |
id = env . params . url [ " id " ]
storyboard = env . params . url [ " storyboard " ]
index = env . params . url [ " index " ]
2020-03-07 00:23:35 +05:30
url = " /sb/ #{ id } / #{ storyboard } / #{ index } ? #{ env . params . query } "
headers = HTTP :: Headers . new
2019-04-12 03:30:00 +05:30
if storyboard . starts_with? " storyboard_live "
2020-03-07 00:23:35 +05:30
headers [ " :authority " ] = " i.ytimg.com "
2019-04-12 03:30:00 +05:30
else
2020-03-07 00:23:35 +05:30
headers [ " :authority " ] = " i9.ytimg.com "
2019-04-12 03:30:00 +05:30
end
2019-06-23 19:09:14 +05:30
REQUEST_HEADERS_WHITELIST . each do | header |
2019-03-11 22:13:48 +05:30
if env . request . headers [ header ]?
headers [ header ] = env . request . headers [ header ]
end
end
2018-09-18 05:09:28 +05:30
2019-05-26 20:11:12 +05:30
begin
2020-03-07 00:23:35 +05:30
YT_POOL . client & . get ( url , headers ) do | response |
2019-05-26 21:23:56 +05:30
env . response . status_code = response . status_code
response . headers . each do | key , value |
2019-11-25 00:11:47 +05:30
if ! RESPONSE_HEADERS_BLACKLIST . includes? ( key . downcase )
2019-05-26 21:23:56 +05:30
env . response . headers [ key ] = value
end
2019-05-19 17:42:45 +05:30
end
2018-09-18 05:09:28 +05:30
2020-03-05 02:06:39 +05:30
env . response . headers [ " Connection " ] = " close "
2019-07-03 23:43:40 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
if response . status_code >= 300
2019-07-04 00:24:15 +05:30
env . response . headers . delete ( " Transfer-Encoding " )
2019-05-26 21:23:56 +05:30
break
end
2018-09-18 05:09:28 +05:30
2019-05-26 21:23:56 +05:30
proxy_file ( response , env )
end
2019-05-26 20:11:12 +05:30
rescue ex
end
2018-09-18 05:09:28 +05:30
end
2019-08-17 02:16:37 +05:30
get " /s_p/:id/:name " do | env |
id = env . params . url [ " id " ]
name = env . params . url [ " name " ]
url = env . request . resource
2020-05-08 19:30:53 +05:30
headers = HTTP :: Headers { " :authority " = > " i9.ytimg.com " }
2019-08-17 02:16:37 +05:30
REQUEST_HEADERS_WHITELIST . each do | header |
if env . request . headers [ header ]?
headers [ header ] = env . request . headers [ header ]
end
end
begin
2020-03-07 00:23:35 +05:30
YT_POOL . client & . get ( url , headers ) do | response |
2019-08-17 02:16:37 +05:30
env . response . status_code = response . status_code
response . headers . each do | key , value |
2019-11-25 00:11:47 +05:30
if ! RESPONSE_HEADERS_BLACKLIST . includes? ( key . downcase )
2019-08-17 02:16:37 +05:30
env . response . headers [ key ] = value
end
2019-11-01 21:32:38 +05:30
end
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
if response . status_code >= 300 && response . status_code != 404
env . response . headers . delete ( " Transfer-Encoding " )
break
end
proxy_file ( response , env )
end
rescue ex
end
end
get " /yts/img/:name " do | env |
headers = HTTP :: Headers . new
REQUEST_HEADERS_WHITELIST . each do | header |
if env . request . headers [ header ]?
headers [ header ] = env . request . headers [ header ]
end
end
begin
YT_POOL . client & . get ( env . request . resource , headers ) do | response |
env . response . status_code = response . status_code
response . headers . each do | key , value |
2019-11-25 00:11:47 +05:30
if ! RESPONSE_HEADERS_BLACKLIST . includes? ( key . downcase )
2019-11-01 21:32:38 +05:30
env . response . headers [ key ] = value
end
2019-08-17 02:16:37 +05:30
end
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
if response . status_code >= 300 && response . status_code != 404
env . response . headers . delete ( " Transfer-Encoding " )
break
end
proxy_file ( response , env )
end
rescue ex
end
end
2018-09-15 07:54:28 +05:30
get " /vi/:id/:name " do | env |
id = env . params . url [ " id " ]
name = env . params . url [ " name " ]
2020-05-08 19:30:53 +05:30
headers = HTTP :: Headers { " :authority " = > " i.ytimg.com " }
2020-03-07 00:23:35 +05:30
2018-09-15 07:54:28 +05:30
if name == " maxres.jpg "
2019-03-09 02:12:37 +05:30
build_thumbnails ( id , config , Kemal . config ) . each do | thumb |
2020-03-07 00:23:35 +05:30
if YT_POOL . client & . head ( " /vi/ #{ id } / #{ thumb [ :url ] } .jpg " , headers ) . status_code == 200
2018-09-15 07:54:28 +05:30
name = thumb [ :url ] + " .jpg "
break
end
end
end
url = " /vi/ #{ id } / #{ name } "
2019-06-23 19:09:14 +05:30
REQUEST_HEADERS_WHITELIST . each do | header |
2019-03-11 22:13:48 +05:30
if env . request . headers [ header ]?
headers [ header ] = env . request . headers [ header ]
end
end
2018-09-15 07:54:28 +05:30
2019-05-26 20:11:12 +05:30
begin
2020-03-07 00:23:35 +05:30
YT_POOL . client & . get ( url , headers ) do | response |
2019-05-26 21:23:56 +05:30
env . response . status_code = response . status_code
response . headers . each do | key , value |
2019-11-25 00:11:47 +05:30
if ! RESPONSE_HEADERS_BLACKLIST . includes? ( key . downcase )
2019-05-26 21:23:56 +05:30
env . response . headers [ key ] = value
end
2019-05-19 17:42:45 +05:30
end
2018-09-15 07:54:28 +05:30
2019-07-03 23:43:40 +05:30
env . response . headers [ " Access-Control-Allow-Origin " ] = " * "
2019-07-04 00:24:15 +05:30
if response . status_code >= 300 && response . status_code != 404
env . response . headers . delete ( " Transfer-Encoding " )
2019-05-26 21:23:56 +05:30
break
end
2018-09-15 07:54:28 +05:30
2019-05-26 21:23:56 +05:30
proxy_file ( response , env )
end
2019-05-26 20:11:12 +05:30
rescue ex
end
2018-04-16 07:17:37 +05:30
end
2019-10-27 09:49:05 +05:30
get " /Captcha " do | env |
2020-05-08 19:30:53 +05:30
headers = HTTP :: Headers { " :authority " = > " accounts.google.com " }
response = YT_POOL . client & . get ( env . request . resource , headers )
2019-10-27 09:49:05 +05:30
env . response . headers [ " Content-Type " ] = response . headers [ " Content-Type " ]
response . body
end
2019-08-22 04:53:20 +05:30
# Undocumented, creates anonymous playlist with specified 'video_ids', max 50 videos
2019-05-03 19:41:27 +05:30
get " /watch_videos " do | env |
2019-10-25 22:28:16 +05:30
response = YT_POOL . client & . get ( env . request . resource )
2019-05-03 19:41:27 +05:30
if url = response . headers [ " Location " ]?
url = URI . parse ( url ) . full_path
next env . redirect url
end
env . response . status_code = response . status_code
end
2018-02-10 20:45:23 +05:30
error 404 do | env |
2019-03-27 15:58:53 +05:30
if md = env . request . path . match ( / ^ \/ (?<id>([a-zA-Z0-9_-]{11})|( \ w+))$ / )
2019-04-18 01:16:00 +05:30
item = md [ " id " ]
2018-10-07 08:49:36 +05:30
2019-04-18 01:16:00 +05:30
# Check if item is branding URL e.g. https://youtube.com/gaming
2019-10-25 22:28:16 +05:30
response = YT_POOL . client & . get ( " / #{ item } " )
2019-03-27 15:58:53 +05:30
if response . status_code == 301
2020-01-14 18:51:17 +05:30
response = YT_POOL . client & . get ( URI . parse ( response . headers [ " Location " ] ) . full_path )
2019-03-27 15:58:53 +05:30
end
2019-06-07 23:12:07 +05:30
if response . body . empty?
env . response . headers [ " Location " ] = " / "
halt env , status_code : 302
end
2019-03-27 15:58:53 +05:30
html = XML . parse_html ( response . body )
2020-01-14 18:51:17 +05:30
ucid = html . xpath_node ( % q ( / / link [ @rel = " canonical " ] ) ) . try & . [ " href " ] . split ( " / " ) [ - 1 ]
2019-03-27 15:58:53 +05:30
if ucid
2020-01-14 18:51:17 +05:30
env . response . headers [ " Location " ] = " /channel/ #{ ucid } "
2019-03-27 15:58:53 +05:30
halt env , status_code : 302
end
2018-10-07 08:49:36 +05:30
params = [ ] of String
env . params . query . each do | k , v |
params << " #{ k } = #{ v } "
end
params = params . join ( " & " )
2019-04-18 01:16:00 +05:30
url = " /watch?v= #{ item } "
2018-10-07 08:49:36 +05:30
if ! params . empty?
url += " & #{ params } "
end
2019-04-18 01:16:00 +05:30
# Check if item is video ID
2019-10-25 22:28:16 +05:30
if item . match ( / ^[a-zA-Z0-9_-]{11}$ / ) && YT_POOL . client & . head ( " /watch?v= #{ item } " ) . status_code != 404
2019-02-22 02:37:22 +05:30
env . response . headers [ " Location " ] = url
halt env , status_code : 302
end
end
2019-01-13 00:48:08 +05:30
env . response . headers [ " Location " ] = " / "
halt env , status_code : 302
2017-12-31 02:51:43 +05:30
end
error 500 do | env |
2019-01-13 00:48:08 +05:30
error_message = <<-END_HTML
2019-02-11 20:48:40 +05:30
Looks like you ' ve found a bug in Invidious . Feel free to open a new issue
2019-10-27 23:48:07 +05:30
< a href = " https://github.com/omarroth/invidious/issues " > here < / a>
2019-03-24 00:35:13 +05:30
or send an email to
2019-10-27 23:48:07 +05:30
< a href = " mailto: #{ CONFIG . admin_email } " > #{CONFIG.admin_email}</a>.
2019-01-13 00:48:08 +05:30
END_HTML
2018-02-10 20:45:23 +05:30
templated " error "
2017-12-31 02:51:43 +05:30
end
2018-03-09 22:58:57 +05:30
static_headers do | response , filepath , filestat |
2019-05-08 19:28:10 +05:30
response . headers . add ( " Cache-Control " , " max-age=2629800 " )
2018-03-09 22:58:57 +05:30
end
2017-11-23 13:18:55 +05:30
public_folder " assets "
2018-04-16 09:26:58 +05:30
2018-07-31 05:12:45 +05:30
Kemal . config . powered_by_header = false
2018-04-16 09:26:58 +05:30
add_handler FilteredCompressHandler . new
2019-02-03 10:18:47 +05:30
add_handler APIHandler . new
2019-04-19 02:53:50 +05:30
add_handler AuthHandler . new
2019-03-23 20:54:30 +05:30
add_handler DenyFrame . new
2019-04-19 02:53:50 +05:30
add_context_storage_type ( Array ( String ) )
2019-02-24 21:19:48 +05:30
add_context_storage_type ( Preferences )
2019-04-19 02:53:50 +05:30
add_context_storage_type ( User )
2017-11-23 13:18:55 +05:30
2019-01-24 01:45:19 +05:30
Kemal . config . logger = logger
2019-09-23 22:35:29 +05:30
Kemal . config . host_binding = Kemal . config . host_binding != " 0.0.0.0 " ? Kemal . config . host_binding : CONFIG . host_binding
Kemal . config . port = Kemal . config . port != 3000 ? Kemal . config . port : CONFIG . port
2017-11-23 13:18:55 +05:30
Kemal . run