2018-09-04 10:22:10 -04:00
# "Invidious" (which is an alternative front-end to YouTube)
2019-03-15 12:44:53 -04:00
# Copyright (C) 2019 Omar Roth
2018-01-28 12:32:40 -05:00
#
# 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-22 14:26:08 -05:00
require " digest/md5 "
2019-01-23 15:15:19 -05:00
require " file_utils "
2017-11-23 02:48:55 -05:00
require " kemal "
2021-08-24 15:59:27 -04:00
require " athena-negotiation "
2018-07-18 15:26:02 -04:00
require " openssl/hmac "
2018-02-03 17:13:14 -05:00
require " option_parser "
2018-11-21 18:12:13 -05:00
require " sqlite3 "
2018-01-16 15:02:35 -05:00
require " xml "
2018-03-09 13:42:23 -05:00
require " yaml "
2020-06-15 18:57:20 -04:00
require " compress/zip "
2019-10-27 13:50:42 -04:00
require " protodec/utils "
2021-11-26 13:36:31 -05:00
require " ./invidious/database/* "
2018-08-04 16:30:44 -04:00
require " ./invidious/helpers/* "
2021-10-07 16:32:04 -04:00
require " ./invidious/yt_backend/* "
2018-07-06 08:59:56 -04:00
require " ./invidious/* "
2021-07-14 11:46:12 -04:00
require " ./invidious/channels/* "
2021-10-07 16:00:50 -04:00
require " ./invidious/user/* "
2020-10-06 00:41:18 -04:00
require " ./invidious/routes/** "
require " ./invidious/jobs/** "
2017-11-29 16:33:46 -05:00
2021-01-23 12:58:13 -05:00
CONFIG = Config . load
HMAC_KEY = CONFIG . hmac_key || Random :: Secure . hex ( 32 )
2018-03-09 13:42:23 -05:00
2021-01-30 09:52:48 -05:00
PG_DB = DB . open CONFIG . database_url
2019-06-23 09:39:14 -04:00
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 11:12:11 -04:00
TEXTCAPTCHA_URL = URI . parse ( " https://textcaptcha.com " )
2019-06-23 09:39:14 -04:00
YT_URL = URI . parse ( " https://www.youtube.com " )
2021-01-23 13:39:04 -05:00
HOST_URL = make_host_url ( Kemal . config )
2019-06-23 09:39:14 -04:00
2019-06-07 13:39:12 -04:00
CHARS_SAFE = " ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ "
TEST_IDS = { " AgbeGFYluEA " , " BaW_jenozKc " , " a9LDPn-MO4I " , " ddFvjfvPnqk " , " iqKdEhx-dD4 " }
2019-06-08 17:04:55 -04:00
MAX_ITEMS_PER_PAGE = 1500
2018-03-04 23:25:03 -05:00
2019-11-24 13:41:47 -05:00
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-04 16:30:00 -04:00
HTTP_CHUNK_SIZE = 10485760 # ~10MB
2019-06-23 09:39:14 -04:00
2020-02-15 13:52:28 -05:00
CURRENT_BRANCH = {{ " #{ ` git branch | sed -n '/* /s///p' ` . strip } " }}
2019-06-23 09:39:14 -04:00
CURRENT_COMMIT = {{ " #{ ` git rev-list HEAD --max-count=1 --abbrev-commit ` . strip } " }}
2020-12-05 14:06:24 -05:00
CURRENT_VERSION = {{ " #{ ` git log -1 --format=%ci | awk '{print $1}' | sed s/-/./g ` . strip } " }}
2019-06-23 09:39:14 -04:00
2019-05-09 12:52:37 -04:00
# 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 09:28:53 -04:00
SOFTWARE = {
" name " = > " invidious " ,
" version " = > " #{ CURRENT_VERSION } - #{ CURRENT_COMMIT } " ,
" branch " = > " #{ CURRENT_BRANCH } " ,
}
2021-09-26 17:03:45 -04:00
YT_POOL = YoutubeConnectionPool . new ( YT_URL , capacity : CONFIG . pool_size , use_quic : CONFIG . use_quic )
2019-10-25 12:58:16 -04:00
2021-01-04 10:51:06 -05:00
# CLI
2019-04-06 09:28:53 -04:00
Kemal . config . extra_options do | parser |
parser . banner = " Usage: invidious [arguments] "
2021-01-04 10:51:06 -05:00
parser . on ( " -c THREADS " , " --channel-threads=THREADS " , " Number of threads for refreshing channels (default: #{ CONFIG . channel_threads } ) " ) do | number |
2019-04-06 09:28:53 -04:00
begin
2021-01-04 10:51:06 -05:00
CONFIG . channel_threads = number . to_i
2019-04-06 09:28:53 -04:00
rescue ex
puts " THREADS must be integer "
exit
end
end
2021-01-04 10:51:06 -05:00
parser . on ( " -f THREADS " , " --feed-threads=THREADS " , " Number of threads for refreshing feeds (default: #{ CONFIG . feed_threads } ) " ) do | number |
2019-04-06 09:28:53 -04:00
begin
2021-01-04 10:51:06 -05:00
CONFIG . feed_threads = number . to_i
2019-04-06 09:28:53 -04:00
rescue ex
puts " THREADS must be integer "
exit
end
end
2021-01-04 10:51:06 -05:00
parser . on ( " -o OUTPUT " , " --output=OUTPUT " , " Redirect output (default: #{ CONFIG . output } ) " ) do | output |
CONFIG . output = output
2019-04-06 09:28:53 -04:00
end
2021-01-04 10:51:06 -05:00
parser . on ( " -l LEVEL " , " --log-level=LEVEL " , " Log level, one of #{ LogLevel . values } (default: #{ CONFIG . log_level } ) " ) do | log_level |
CONFIG . log_level = LogLevel . parse ( log_level )
2020-12-21 10:05:35 -05:00
end
parser . on ( " -v " , " --version " , " Print version " ) do
2019-04-06 09:28:53 -04:00
puts SOFTWARE . to_pretty_json
exit
end
end
Kemal :: CLI . new ARGV
2021-01-04 10:51:06 -05:00
if CONFIG . output . upcase != " STDOUT "
FileUtils . mkdir_p ( File . dirname ( CONFIG . output ) )
2021-01-04 10:05:15 -05:00
end
2021-01-04 10:51:06 -05:00
OUTPUT = CONFIG . output . upcase == " STDOUT " ? STDOUT : File . open ( CONFIG . output , mode : " a " )
LOGGER = Invidious :: LogHandler . new ( OUTPUT , CONFIG . log_level )
2021-01-04 10:05:15 -05:00
2019-04-15 12:13:09 -04:00
# Check table integrity
2019-04-11 13:13:25 -04:00
if CONFIG . check_tables
2021-01-04 10:51:06 -05:00
check_enum ( PG_DB , " privacy " , PlaylistPrivacy )
2019-08-05 19:49:13 -04:00
2021-01-04 10:51:06 -05:00
check_table ( PG_DB , " channels " , InvidiousChannel )
check_table ( PG_DB , " channel_videos " , ChannelVideo )
check_table ( PG_DB , " playlists " , InvidiousPlaylist )
check_table ( PG_DB , " playlist_videos " , PlaylistVideo )
check_table ( PG_DB , " nonces " , Nonce )
check_table ( PG_DB , " session_ids " , SessionId )
check_table ( PG_DB , " users " , User )
check_table ( PG_DB , " videos " , Video )
2019-04-15 12:13:09 -04:00
if CONFIG . cache_annotations
2021-01-04 10:51:06 -05:00
check_table ( PG_DB , " annotations " , Annotation )
2019-04-15 12:13:09 -04:00
end
2019-04-11 13:13:25 -04:00
end
2018-03-25 23:18:29 -04:00
2019-04-10 17:23:37 -04:00
# Start jobs
2019-05-15 13:26:29 -04:00
2021-01-23 13:41:50 -05:00
if CONFIG . channel_threads > 0
Invidious :: Jobs . register Invidious :: Jobs :: RefreshChannelsJob . new ( PG_DB )
end
if CONFIG . feed_threads > 0
Invidious :: Jobs . register Invidious :: Jobs :: RefreshFeedsJob . new ( PG_DB )
end
2020-09-27 13:19:44 -04:00
DECRYPT_FUNCTION = DecryptFunction . new ( CONFIG . decrypt_polling )
2021-01-23 13:39:04 -05:00
if CONFIG . decrypt_polling
2021-01-04 10:51:06 -05:00
Invidious :: Jobs . register Invidious :: Jobs :: UpdateDecryptFunctionJob . new
2020-09-27 13:19:44 -04:00
end
2019-03-03 20:18:23 -05:00
2021-01-23 13:39:04 -05:00
if CONFIG . statistics_enabled
Invidious :: Jobs . register Invidious :: Jobs :: StatisticsRefreshJob . new ( PG_DB , SOFTWARE )
2020-10-17 08:25:57 -04:00
end
2019-08-27 09:08:26 -04:00
2021-01-23 13:39:04 -05:00
if ( CONFIG . use_pubsub_feeds . is_a? ( Bool ) && CONFIG . use_pubsub_feeds . as ( Bool ) ) || ( CONFIG . use_pubsub_feeds . is_a? ( Int32 ) && CONFIG . use_pubsub_feeds . as ( Int32 ) > 0 )
Invidious :: Jobs . register Invidious :: Jobs :: SubscribeToFeedsJob . new ( PG_DB , HMAC_KEY )
2021-01-07 14:15:26 -05:00
end
2021-01-23 13:39:04 -05:00
if CONFIG . popular_enabled
2020-12-27 00:12:43 -05:00
Invidious :: Jobs . register Invidious :: Jobs :: PullPopularVideosJob . new ( PG_DB )
end
2020-10-17 08:25:57 -04:00
connection_channel = Channel ( { Bool , Channel ( PQ :: Notification ) } ) . new ( 32 )
2021-01-30 09:52:48 -05:00
Invidious :: Jobs . register Invidious :: Jobs :: NotificationJob . new ( connection_channel , CONFIG . database_url )
2020-10-17 08:25:57 -04:00
2020-10-06 00:41:18 -04:00
Invidious :: Jobs . start_all
def popular_videos
Invidious :: Jobs :: PullPopularVideosJob :: POPULAR_VIDEOS . get
2018-11-08 21:08:03 -05:00
end
2018-03-24 23:56:41 -04:00
before_all do | env |
2021-08-24 15:59:27 -04:00
preferences = Preferences . from_json ( " {} " )
begin
if prefs_cookie = env . request . cookies [ " PREFS " ]?
preferences = Preferences . from_json ( URI . decode_www_form ( prefs_cookie . value ) )
else
if language_header = env . request . headers [ " Accept-Language " ]?
if language = ANG . language_negotiator . best ( language_header , LOCALES . keys )
preferences . locale = language . header
end
end
end
2020-03-15 17:46:08 -04:00
rescue
2021-08-24 15:59:27 -04:00
preferences = Preferences . from_json ( " {} " )
2020-03-15 17:46:08 -04:00
end
2020-10-16 06:23:18 -04:00
env . set " preferences " , preferences
2019-05-10 17:48:38 -04:00
env . response . headers [ " X-XSS-Protection " ] = " 1; mode=block "
2018-09-05 22:51:40 -04:00
env . response . headers [ " X-Content-Type-Options " ] = " nosniff "
2021-06-17 13:45:20 -04:00
2021-06-19 03:40:33 -04:00
# Allow media resources to be loaded from google servers
2021-06-17 13:45:20 -04:00
# TODO: check if *.youtube.com can be removed
2020-03-15 17:46:08 -04:00
if CONFIG . disabled? ( " local " ) || ! preferences . local
2021-06-17 13:45:20 -04:00
extra_media_csp = " https://*.googlevideo.com:443 https://*.youtube.com:443 "
else
extra_media_csp = " "
end
# Only allow the pages at /embed/* to be embedded
if env . request . resource . starts_with? ( " /embed " )
frame_ancestors = " 'self' http: https: "
else
2021-06-20 12:43:00 -04:00
frame_ancestors = " 'none' "
2020-03-15 17:46:08 -04:00
end
2021-06-17 13:45:20 -04:00
# TODO: Remove style-src's 'unsafe-inline', requires to remove all
# inline styles (<style> [..] </style>, style=" [..] ")
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 ,
" child-src 'self' blob: " ,
" frame-src 'self' " ,
" frame-ancestors " + frame_ancestors ,
} . join ( " ; " )
2019-04-07 15:01:08 -04:00
env . response . headers [ " Referrer-Policy " ] = " same-origin "
2021-06-17 13:45:20 -04:00
# Ask the chrom*-based browsers to disable FLoC
# See: https://blog.runcloud.io/google-floc/
env . response . headers [ " Permissions-Policy " ] = " interest-cohort=() "
2021-01-23 13:39:04 -05:00
if ( Kemal . config . ssl || CONFIG . https_only ) && CONFIG . hsts
2019-04-30 21:53:56 -04:00
env . response . headers [ " Strict-Transport-Security " ] = " max-age=31536000; includeSubDomains; preload "
2019-04-07 15:01:08 -04:00
end
2019-03-28 14:43:40 -04:00
2019-11-20 12:03:52 -05:00
next if {
" /sb/ " ,
" /vi/ " ,
" /s_p/ " ,
" /yts/ " ,
" /ggpht/ " ,
" /api/manifest/ " ,
" /videoplayback " ,
" /latest_version " ,
} . any? { | r | env . request . resource . starts_with? r }
2018-07-16 12:24:24 -04:00
if env . request . cookies . has_key? " SID "
2018-03-31 20:09:27 -04:00
sid = env . request . cookies [ " SID " ] . value
2018-07-05 19:43:26 -04:00
2019-04-18 17:23:50 -04:00
if sid . starts_with? " v1: "
raise " Cannot use token as SID "
end
2018-07-18 15:26:02 -04:00
# Invidious users only have SID
if ! env . request . cookies . has_key? " SSID "
2021-12-02 17:57:13 -05:00
if email = Invidious :: Database :: SessionIDs . select_email ( sid )
2021-12-02 20:27:51 -05:00
user = Invidious :: Database :: Users . select! ( email : email )
2019-08-05 19:49:13 -04:00
csrf_token = generate_response ( sid , {
" :authorize_token " ,
" :playlist_ajax " ,
" :signout " ,
" :subscription_ajax " ,
" :token_ajax " ,
" :watch_ajax " ,
} , HMAC_KEY , PG_DB , 1 . week )
2018-11-08 18:42:25 -05:00
2019-03-11 13:44:25 -04:00
preferences = user . preferences
2020-10-16 06:23:18 -04:00
env . set " preferences " , preferences
2019-03-11 13:44:25 -04:00
2018-08-15 13:40:42 -04:00
env . set " sid " , sid
2019-04-18 17:23:50 -04:00
env . set " csrf_token " , csrf_token
2019-04-16 00:23:40 -04:00
env . set " user " , user
2018-07-18 15:26:02 -04:00
end
else
2019-04-16 00:23:40 -04:00
headers = HTTP :: Headers . new
headers [ " Cookie " ] = env . request . headers [ " Cookie " ]
2018-07-18 15:26:02 -04:00
begin
2021-01-04 10:51:06 -05:00
user , sid = get_user ( sid , headers , PG_DB , false )
2019-08-05 19:49:13 -04:00
csrf_token = generate_response ( sid , {
" :authorize_token " ,
" :playlist_ajax " ,
" :signout " ,
" :subscription_ajax " ,
" :token_ajax " ,
" :watch_ajax " ,
} , HMAC_KEY , PG_DB , 1 . week )
2018-11-15 21:23:17 -05:00
2019-03-11 13:44:25 -04:00
preferences = user . preferences
2020-10-16 06:23:18 -04:00
env . set " preferences " , preferences
2019-03-11 13:44:25 -04:00
2018-08-15 13:40:42 -04:00
env . set " sid " , sid
2019-04-18 17:23:50 -04:00
env . set " csrf_token " , csrf_token
2019-04-16 00:23:40 -04:00
env . set " user " , user
2018-07-18 15:26:02 -04:00
rescue ex
end
2018-07-16 13:50:41 -04:00
end
2018-04-13 22:32:14 -04:00
end
2018-08-17 11:19:20 -04:00
2019-08-15 12:29:55 -04:00
dark_mode = convert_theme ( env . params . query [ " dark_mode " ]? ) || preferences . dark_mode . to_s
2019-03-11 13:44:25 -04:00
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
2021-03-17 15:07:38 -04:00
env . set " preferences " , preferences
2018-12-20 16:32:09 -05:00
2018-08-17 11:19:20 -04:00
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 13:31:33 -04:00
env . set " current_page " , URI . encode_www_form ( current_page )
2018-03-22 13:44:36 -04:00
end
2021-10-02 16:04:02 -04:00
{% unless flag? ( :api_only ) %}
Invidious :: Routing . get " / " , Invidious :: Routes :: Misc , :home
Invidious :: Routing . get " /privacy " , Invidious :: Routes :: Misc , :privacy
Invidious :: Routing . get " /licenses " , Invidious :: Routes :: Misc , :licenses
Invidious :: Routing . get " /channel/:ucid " , Invidious :: Routes :: Channels , :home
Invidious :: Routing . get " /channel/:ucid/home " , Invidious :: Routes :: Channels , :home
Invidious :: Routing . get " /channel/:ucid/videos " , Invidious :: Routes :: Channels , :videos
Invidious :: Routing . get " /channel/:ucid/playlists " , Invidious :: Routes :: Channels , :playlists
Invidious :: Routing . get " /channel/:ucid/community " , Invidious :: Routes :: Channels , :community
Invidious :: Routing . get " /channel/:ucid/about " , Invidious :: Routes :: Channels , :about
[ " " , " /videos " , " /playlists " , " /community " , " /about " ] . each do | path |
# /c/LinusTechTips
Invidious :: Routing . get " /c/:user #{ path } " , Invidious :: Routes :: Channels , :brand_redirect
# /user/linustechtips | Not always the same as /c/
Invidious :: Routing . get " /user/:user #{ path } " , Invidious :: Routes :: Channels , :brand_redirect
# /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow
Invidious :: Routing . get " /attribution_link #{ path } " , Invidious :: Routes :: Channels , :brand_redirect
# /profile?user=linustechtips
Invidious :: Routing . get " /profile/ #{ path } " , Invidious :: Routes :: Channels , :profile
end
2021-08-03 17:46:15 -04:00
2021-10-02 16:04:02 -04:00
Invidious :: Routing . get " /watch " , Invidious :: Routes :: Watch , :handle
Invidious :: Routing . get " /watch/:id " , Invidious :: Routes :: Watch , :redirect
Invidious :: Routing . get " /shorts/:id " , Invidious :: Routes :: Watch , :redirect
Invidious :: Routing . get " /w/:id " , Invidious :: Routes :: Watch , :redirect
Invidious :: Routing . get " /v/:id " , Invidious :: Routes :: Watch , :redirect
Invidious :: Routing . get " /e/:id " , Invidious :: Routes :: Watch , :redirect
Invidious :: Routing . get " /redirect " , Invidious :: Routes :: Misc , :cross_instance_redirect
Invidious :: Routing . get " /embed/ " , Invidious :: Routes :: Embed , :redirect
Invidious :: Routing . get " /embed/:id " , Invidious :: Routes :: Embed , :show
Invidious :: Routing . get " /create_playlist " , Invidious :: Routes :: Playlists , :new
Invidious :: Routing . post " /create_playlist " , Invidious :: Routes :: Playlists , :create
Invidious :: Routing . get " /subscribe_playlist " , Invidious :: Routes :: Playlists , :subscribe
Invidious :: Routing . get " /delete_playlist " , Invidious :: Routes :: Playlists , :delete_page
Invidious :: Routing . post " /delete_playlist " , Invidious :: Routes :: Playlists , :delete
Invidious :: Routing . get " /edit_playlist " , Invidious :: Routes :: Playlists , :edit
Invidious :: Routing . post " /edit_playlist " , Invidious :: Routes :: Playlists , :update
Invidious :: Routing . get " /add_playlist_items " , Invidious :: Routes :: Playlists , :add_playlist_items_page
Invidious :: Routing . post " /playlist_ajax " , Invidious :: Routes :: Playlists , :playlist_ajax
Invidious :: Routing . get " /playlist " , Invidious :: Routes :: Playlists , :show
Invidious :: Routing . get " /mix " , Invidious :: Routes :: Playlists , :mix
Invidious :: Routing . get " /opensearch.xml " , Invidious :: Routes :: Search , :opensearch
Invidious :: Routing . get " /results " , Invidious :: Routes :: Search , :results
Invidious :: Routing . get " /search " , Invidious :: Routes :: Search , :search
Invidious :: Routing . get " /login " , Invidious :: Routes :: Login , :login_page
Invidious :: Routing . post " /login " , Invidious :: Routes :: Login , :login
Invidious :: Routing . post " /signout " , Invidious :: Routes :: Login , :signout
Invidious :: Routing . get " /preferences " , Invidious :: Routes :: PreferencesRoute , :show
Invidious :: Routing . post " /preferences " , Invidious :: Routes :: PreferencesRoute , :update
Invidious :: Routing . get " /toggle_theme " , Invidious :: Routes :: PreferencesRoute , :toggle_theme
# Feeds
Invidious :: Routing . get " /view_all_playlists " , Invidious :: Routes :: Feeds , :view_all_playlists_redirect
Invidious :: Routing . get " /feed/playlists " , Invidious :: Routes :: Feeds , :playlists
Invidious :: Routing . get " /feed/popular " , Invidious :: Routes :: Feeds , :popular
Invidious :: Routing . get " /feed/trending " , Invidious :: Routes :: Feeds , :trending
Invidious :: Routing . get " /feed/subscriptions " , Invidious :: Routes :: Feeds , :subscriptions
Invidious :: Routing . get " /feed/history " , Invidious :: Routes :: Feeds , :history
# RSS Feeds
Invidious :: Routing . get " /feed/channel/:ucid " , Invidious :: Routes :: Feeds , :rss_channel
Invidious :: Routing . get " /feed/private " , Invidious :: Routes :: Feeds , :rss_private
Invidious :: Routing . get " /feed/playlist/:plid " , Invidious :: Routes :: Feeds , :rss_playlist
Invidious :: Routing . get " /feeds/videos.xml " , Invidious :: Routes :: Feeds , :rss_videos
# Support push notifications via PubSubHubbub
Invidious :: Routing . get " /feed/webhook/:token " , Invidious :: Routes :: Feeds , :push_notifications_get
Invidious :: Routing . post " /feed/webhook/:token " , Invidious :: Routes :: Feeds , :push_notifications_post
{% end %}
2021-08-30 10:58:24 -04:00
2021-10-11 16:22:11 -04:00
Invidious :: Routing . get " /ggpht/* " , Invidious :: Routes :: Images , :ggpht
Invidious :: Routing . options " /sb/:authority/:id/:storyboard/:index " , Invidious :: Routes :: Images , :options_storyboard
Invidious :: Routing . get " /sb/:authority/:id/:storyboard/:index " , Invidious :: Routes :: Images , :get_storyboard
Invidious :: Routing . get " /s_p/:id/:name " , Invidious :: Routes :: Images , :s_p_image
Invidious :: Routing . get " /yts/img/:name " , Invidious :: Routes :: Images , :yts_image
Invidious :: Routing . get " /vi/:id/:name " , Invidious :: Routes :: Images , :thumbnails
2021-08-30 12:17:20 -04:00
# API routes (macro)
2021-07-22 00:34:16 -04:00
define_v1_api_routes ( )
2021-08-30 12:17:20 -04:00
# Video playback (macros)
2021-08-13 02:31:12 -04:00
define_api_manifest_routes ( )
define_video_playback_routes ( )
2021-07-22 00:34:16 -04:00
2018-08-04 16:30:44 -04:00
# Users
2018-07-21 21:56:11 -04:00
2019-04-16 00:23:40 -04:00
post " /watch_ajax " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-11-19 23:06:59 -05:00
user = env . get? " user "
2019-04-16 00:23:40 -04:00
sid = env . get? " sid "
2018-11-19 23:06:59 -05:00
referer = get_referer ( env , " /feed/subscriptions " )
redirect = env . params . query [ " redirect " ]?
2019-04-16 00:23:40 -04:00
redirect || = " true "
2018-11-19 23:06:59 -05:00
redirect = redirect == " true "
2019-04-16 00:23:40 -04:00
if ! user
2019-04-18 17:23:50 -04:00
if redirect
next env . redirect referer
else
2020-11-30 04:59:21 -05:00
next error_json ( 403 , " No such user " )
2019-04-18 17:23:50 -04:00
end
2018-11-19 23:06:59 -05:00
end
2018-12-20 16:32:09 -05:00
2019-04-16 00:23:40 -04:00
user = user . as ( User )
sid = sid . as ( String )
2019-04-18 17:23:50 -04:00
token = env . params . body [ " csrf_token " ]?
2018-11-19 23:06:59 -05:00
id = env . params . query [ " id " ]?
if ! id
2019-03-23 11:24:30 -04:00
env . response . status_code = 400
next
2018-11-19 23:06:59 -05:00
end
2019-04-16 00:23:40 -04:00
begin
2019-04-18 17:23:50 -04:00
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
2019-04-16 00:23:40 -04:00
rescue ex
if redirect
2020-11-30 04:59:21 -05:00
next error_template ( 400 , ex )
2019-04-16 00:23:40 -04:00
else
2020-11-30 04:59:21 -05:00
next error_json ( 400 , ex )
2019-04-16 00:23:40 -04:00
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
2021-12-02 20:27:51 -05:00
Invidious :: Database :: Users . mark_watched ( user , id )
2019-04-16 00:23:40 -04:00
end
when " action_mark_unwatched "
2021-12-02 20:27:51 -05:00
Invidious :: Database :: Users . mark_unwatched ( user , id )
2020-04-09 13:18:09 -04:00
else
2020-11-30 04:59:21 -05:00
next error_json ( 400 , " Unsupported action #{ action } " )
2018-11-19 23:06:59 -05:00
end
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
end
2018-08-05 00:07:38 -04:00
# /modify_notifications
# will "ding" all subscriptions.
2018-08-04 16:30:44 -04:00
# /modify_notifications?receive_all_updates=false&receive_no_updates=false
# will "unding" all subscriptions.
get " /modify_notifications " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-08-04 16:30:44 -04:00
user = env . get? " user "
2019-04-07 13:59:12 -04:00
sid = env . get? " sid "
referer = get_referer ( env , " / " )
2018-07-14 09:36:31 -04:00
2019-04-07 13:59:12 -04:00
redirect = env . params . query [ " redirect " ]?
redirect || = " false "
redirect = redirect == " true "
2018-07-29 22:05:40 -04:00
2019-04-18 17:23:50 -04:00
if ! user
if redirect
next env . redirect referer
else
2020-11-30 04:59:21 -05:00
next error_json ( 403 , " No such user " )
2019-04-18 17:23:50 -04:00
end
2019-04-07 13:59:12 -04:00
end
user = user . as ( User )
if ! user . password
2018-08-04 16:30:44 -04:00
channel_req = { } of String = > String
2018-02-26 19:59:02 -05:00
2018-08-04 16:30:44 -04:00
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 12:42:24 -05:00
2018-08-04 16:30:44 -04:00
channel_req . reject! { | k , v | v != " true " && v != " false " }
2018-01-06 21:39:24 -05:00
2018-08-04 16:30:44 -04:00
headers = HTTP :: Headers . new
headers [ " Cookie " ] = env . request . headers [ " Cookie " ]
2017-12-30 16:21:43 -05:00
2019-10-25 12:58:16 -04:00
html = YT_POOL . client & . get ( " /subscription_manager?disable_polymer=1 " , headers )
2019-04-07 13:59:12 -04:00
2021-05-24 09:45:50 -04:00
cookies = HTTP :: Cookies . from_client_headers ( headers )
2019-04-07 13:59:12 -04:00
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 )
2020-06-15 18:33:23 -04:00
if match = html . body . match ( / 'XSRF_TOKEN': "(?<session_token>[^"]+)" / )
2018-08-04 16:30:44 -04:00
session_token = match [ " session_token " ]
else
next env . redirect referer
end
2018-07-18 15:26:02 -04:00
2019-04-07 13:59:12 -04:00
headers [ " content-type " ] = " application/x-www-form-urlencoded "
2018-08-04 16:30:44 -04:00
channel_req [ " session_token " ] = session_token
2018-04-07 22:36:09 -04:00
2019-04-07 13:59:12 -04:00
subs = XML . parse_html ( html . body )
2018-08-04 16:30:44 -04:00
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 12:58:16 -04:00
YT_POOL . client & . post ( " /subscription_ajax?action_update_subscription_preferences=1 " , headers , form : channel_req )
2018-08-04 16:30:44 -04:00
end
2018-07-18 15:26:02 -04:00
end
2019-04-07 13:59:12 -04:00
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
end
2019-04-16 00:23:40 -04:00
post " /subscription_ajax " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2019-04-07 13:59:12 -04:00
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env , " / " )
redirect = env . params . query [ " redirect " ]?
2019-04-16 00:23:40 -04:00
redirect || = " true "
2019-04-07 13:59:12 -04:00
redirect = redirect == " true "
2019-04-16 00:23:40 -04:00
if ! user
2019-04-18 17:23:50 -04:00
if redirect
next env . redirect referer
else
2020-11-30 04:59:21 -05:00
next error_json ( 403 , " No such user " )
2019-04-18 17:23:50 -04:00
end
2019-04-07 13:59:12 -04:00
end
user = user . as ( User )
2019-04-16 00:23:40 -04:00
sid = sid . as ( String )
2019-04-18 17:23:50 -04:00
token = env . params . body [ " csrf_token " ]?
2019-04-16 00:23:40 -04:00
begin
2019-04-18 17:23:50 -04:00
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
2019-04-16 00:23:40 -04:00
rescue ex
if redirect
2020-11-30 04:59:21 -05:00
next error_template ( 400 , ex )
2019-04-16 00:23:40 -04:00
else
2020-11-30 04:59:21 -05:00
next error_json ( 400 , ex )
2019-04-16 00:23:40 -04:00
end
end
2019-04-07 13:59:12 -04:00
2019-06-07 20:56:41 -04:00
if env . params . query [ " action_create_subscription_to_channel " ]? . try & . to_i? . try & . == 1
2019-04-07 13:59:12 -04:00
action = " action_create_subscription_to_channel "
2019-06-07 20:56:41 -04:00
elsif env . params . query [ " action_remove_subscriptions " ]? . try & . to_i? . try & . == 1
2019-04-07 13:59:12 -04:00
action = " action_remove_subscriptions "
else
next env . redirect referer
end
channel_id = env . params . query [ " c " ]?
channel_id || = " "
if ! user . password
2019-04-14 19:08:00 -04:00
# Sync subscriptions with YouTube
2019-05-15 13:26:29 -04:00
subscribe_ajax ( channel_id , action , env . request . headers )
2019-04-07 13:59:12 -04:00
end
case action
2019-06-07 20:56:41 -04:00
when " action_create_subscription_to_channel "
2019-04-07 13:59:12 -04:00
if ! user . subscriptions . includes? channel_id
2021-01-04 10:51:06 -05:00
get_channel ( channel_id , PG_DB , false , false )
2021-12-02 20:27:51 -05:00
Invidious :: Database :: Users . subscribe_channel ( user , channel_id )
2019-04-07 13:59:12 -04:00
end
2019-06-07 20:56:41 -04:00
when " action_remove_subscriptions "
2021-12-02 20:27:51 -05:00
Invidious :: Database :: Users . unsubscribe_channel ( user , channel_id )
2020-04-09 13:18:09 -04:00
else
2020-11-30 04:59:21 -05:00
next error_json ( 400 , " Unsupported action #{ action } " )
2019-04-07 13:59:12 -04:00
end
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
2018-08-04 16:30:44 -04:00
end
2018-04-29 10:40:33 -04:00
2018-08-04 16:30:44 -04:00
get " /subscription_manager " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-08-04 16:30:44 -04:00
user = env . get? " user "
2019-02-10 13:33:29 -05:00
sid = env . get? " sid "
2019-04-18 17:23:50 -04:00
referer = get_referer ( env )
2018-08-08 21:26:02 -04:00
2019-04-18 17:23:50 -04:00
if ! user
2018-08-08 21:26:02 -04:00
next env . redirect referer
2018-04-28 10:27:05 -04:00
end
2018-08-04 16:30:44 -04:00
user = user . as ( User )
2021-12-02 17:57:13 -05:00
sid = sid . as ( String )
2018-03-16 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
if ! user . password
# Refresh account
headers = HTTP :: Headers . new
headers [ " Cookie " ] = env . request . headers [ " Cookie " ]
2018-04-07 22:36:09 -04:00
2021-01-04 10:51:06 -05:00
user , sid = get_user ( sid , headers , PG_DB )
2018-08-04 16:30:44 -04:00
end
2018-03-16 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
action_takeout = env . params . query [ " action_takeout " ]? . try & . to_i?
action_takeout || = 0
action_takeout = action_takeout == 1
2018-07-18 15:26:02 -04:00
2018-08-04 16:30:44 -04:00
format = env . params . query [ " format " ]?
format || = " rss "
2018-07-18 15:26:02 -04:00
2021-12-02 13:16:41 -05:00
subscriptions = Invidious :: Database :: Channels . select ( user . subscriptions )
2021-09-24 22:42:43 -04:00
subscriptions . sort_by! ( & . author . downcase )
2018-03-16 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
if action_takeout
if format == " json "
env . response . content_type = " application/json "
env . response . headers [ " content-disposition " ] = " attachment "
2020-07-25 13:30:28 -04:00
playlists = PG_DB . query_all ( " SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created " , user . email , as : InvidiousPlaylist )
next JSON . build do | json |
json . object do
json . field " subscriptions " , user . subscriptions
json . field " watch_history " , user . watched
json . field " preferences " , user . preferences
json . field " playlists " do
json . array do
playlists . each do | playlist |
json . object do
json . field " title " , playlist . title
json . field " description " , html_to_content ( playlist . description_html )
json . field " privacy " , playlist . privacy . to_s
json . field " videos " do
json . array do
2020-07-28 17:21:39 -04:00
PG_DB . query_all ( " SELECT id FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 500 " , playlist . id , playlist . index , as : String ) . each do | video_id |
json . string video_id
2020-07-25 13:30:28 -04:00
end
end
end
end
end
end
end
end
end
2018-08-04 16:30:44 -04:00
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 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
xml . element ( " outline " , text : title , title : title ) do
subscriptions . each do | channel |
if format == " newpipe "
2021-09-24 22:15:23 -04:00
xml_url = " https://www.youtube.com/feeds/videos.xml?channel_id= #{ channel . id } "
2018-08-04 16:30:44 -04:00
else
2021-09-24 22:15:23 -04:00
xml_url = " #{ HOST_URL } /feed/channel/ #{ channel . id } "
2018-08-04 16:30:44 -04:00
end
2018-03-16 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
xml . element ( " outline " , text : channel . author , title : channel . author ,
2021-09-24 22:15:23 -04:00
" type " : " rss " , xmlUrl : xml_url )
2018-08-04 16:30:44 -04:00
end
end
end
2018-07-18 15:26:02 -04:00
end
2018-03-16 12:40:29 -04:00
end
2018-08-04 16:30:44 -04:00
next export . gsub ( %( <?xml version="1.0"?> \n ) , " " )
end
end
2018-03-16 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
templated " subscription_manager "
end
2018-03-16 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
get " /data_control " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-08-04 16:30:44 -04:00
user = env . get? " user "
2018-08-08 21:26:02 -04:00
referer = get_referer ( env )
2018-03-16 12:40:29 -04:00
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
2018-08-04 16:30:44 -04:00
end
2019-07-12 22:00:50 -04:00
user = user . as ( User )
templated " data_control "
2018-08-04 16:30:44 -04:00
end
2018-03-16 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
post " /data_control " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-08-04 16:30:44 -04:00
user = env . get? " user "
2018-08-08 21:26:02 -04:00
referer = get_referer ( env )
2018-03-16 12:40:29 -04:00
2018-08-04 16:30:44 -04:00
if user
user = user . as ( User )
2018-04-29 10:40:33 -04:00
2020-07-26 10:58:50 -04:00
# TODO: Find a way to prevent browser timeout
2019-04-24 21:18:35 -04:00
2018-08-04 16:30:44 -04:00
HTTP :: FormData . parse ( env . request ) do | part |
body = part . body . gets_to_end
2020-07-25 13:30:28 -04:00
next if body . empty?
2018-04-17 18:54:33 -04:00
2020-04-09 13:18:09 -04:00
# TODO: Unify into single import based on content-type
2018-08-04 16:30:44 -04:00
case part . name
when " import_invidious "
body = JSON . parse ( body )
2018-07-26 11:20:15 -04:00
2018-11-09 18:25:24 -05:00
if body [ " subscriptions " ]?
2021-09-24 22:42:43 -04:00
user . subscriptions += body [ " subscriptions " ] . as_a . map ( & . as_s )
2018-11-09 18:25:24 -05:00
user . subscriptions . uniq!
2021-01-04 10:51:06 -05:00
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-11-09 18:25:24 -05:00
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_subscriptions ( user )
2018-08-04 16:30:44 -04:00
end
2018-07-26 11:20:15 -04:00
2018-11-08 17:43:28 -05:00
if body [ " watch_history " ]?
2021-09-24 22:42:43 -04:00
user . watched += body [ " watch_history " ] . as_a . map ( & . as_s )
2018-11-09 18:25:24 -05:00
user . watched . uniq!
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_watch_history ( user )
2018-07-26 11:20:15 -04:00
end
2018-04-29 10:40:33 -04:00
2018-11-08 17:35:26 -05:00
if body [ " preferences " ]?
2020-07-26 10:58:50 -04:00
user . preferences = Preferences . from_json ( body [ " preferences " ] . to_json )
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_preferences ( user )
2018-11-08 17:35:26 -05:00
end
2020-07-25 13:30:28 -04:00
if playlists = body [ " playlists " ]? . try & . as_a?
playlists . each do | item |
title = item [ " title " ]? . try & . as_s? . try & . delete ( " <> " )
description = item [ " description " ]? . try & . as_s? . try & . delete ( " \ r " )
privacy = item [ " privacy " ]? . try & . as_s? . try { | privacy | PlaylistPrivacy . parse? privacy }
next if ! title
next if ! description
next if ! privacy
playlist = create_playlist ( PG_DB , title , privacy , user )
2021-11-29 21:11:21 -05:00
Invidious :: Database :: Playlists . update_description ( playlist . id , description )
2020-07-25 13:30:28 -04:00
2020-07-28 17:21:39 -04:00
videos = item [ " videos " ]? . try & . as_a? . try & . each_with_index do | video_id , idx |
2020-11-30 04:59:21 -05:00
raise InfoException . new ( " Playlist cannot have more than 500 videos " ) if idx > 500
2020-07-28 17:21:39 -04:00
2020-07-25 13:30:28 -04:00
video_id = video_id . try & . as_s?
next if ! video_id
begin
video = get_video ( video_id , PG_DB )
rescue ex
next
end
2020-07-26 10:58:50 -04:00
playlist_video = PlaylistVideo . new ( {
title : video . title ,
id : video . id ,
author : video . author ,
ucid : video . ucid ,
2020-07-25 13:30:28 -04:00
length_seconds : video . length_seconds ,
2020-07-26 10:58:50 -04:00
published : video . published ,
plid : playlist . id ,
live_now : video . live_now ,
index : Random :: Secure . rand ( 0 _i64 .. Int64 :: MAX ) ,
} )
2020-07-25 13:30:28 -04:00
2021-11-29 20:24:24 -05:00
Invidious :: Database :: PlaylistVideos . insert ( playlist_video )
Invidious :: Database :: Playlists . update_video_added ( playlist . id , playlist_video . index )
2020-07-25 13:30:28 -04:00
end
end
end
2018-08-04 16:30:44 -04:00
when " import_youtube "
2021-02-27 15:59:09 -05:00
if body [ 0 .. 4 ] == " <opml "
2021-02-27 12:58:55 -05:00
subscriptions = XML . parse ( body )
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
else
subscriptions = JSON . parse ( body )
user . subscriptions += subscriptions . as_a . compact_map do | entry |
entry [ " snippet " ] [ " resourceId " ] [ " channelId " ] . as_s
2021-02-27 13:12:01 -05:00
end
2018-11-09 18:25:24 -05:00
end
user . subscriptions . uniq!
2021-01-04 10:51:06 -05:00
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-10-06 19:19:47 -04:00
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_subscriptions ( user )
2018-11-09 18:25:24 -05:00
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!
2021-01-04 10:51:06 -05:00
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-11-09 18:25:24 -05:00
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_subscriptions ( user )
2018-08-04 16:30:44 -04:00
when " import_newpipe_subscriptions "
body = JSON . parse ( body )
2019-04-22 16:39:57 -04:00
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 12:58:16 -04:00
response = YT_POOL . client & . get ( " /user/ #{ match [ " user " ] } ?disable_polymer=1&hl=en&gl=US " )
2020-01-14 08:21:17 -05:00
html = XML . parse_html ( response . body )
ucid = html . xpath_node ( % q ( / / link [ @rel = " canonical " ] ) ) . try & . [ " href " ] . split ( " / " ) [ - 1 ]
next ucid if ucid
2019-04-22 16:39:57 -04:00
end
nil
2018-11-09 18:25:24 -05:00
end
user . subscriptions . uniq!
2021-01-04 10:51:06 -05:00
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-11-09 18:25:24 -05:00
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_subscriptions ( user )
2018-08-04 16:30:44 -04:00
when " import_newpipe "
2020-06-15 18:57:20 -04:00
Compress :: Zip :: Reader . open ( IO :: Memory . new ( body ) ) do | file |
2018-08-04 16:30:44 -04:00
file . each_entry do | entry |
if entry . filename == " newpipe.db "
2018-11-21 18:12:13 -05:00
tempfile = File . tempfile ( " .db " )
File . write ( tempfile . path , entry . io . gets_to_end )
db = DB . open ( " sqlite3:// " + tempfile . path )
2018-04-29 10:40:33 -04:00
2021-09-24 22:42:43 -04:00
user . watched += db . query_all ( " SELECT url FROM streams " , as : String ) . map ( & . lchop ( " https://www.youtube.com/watch?v= " ) )
2018-11-09 18:25:24 -05:00
user . watched . uniq!
2018-07-18 15:26:02 -04:00
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_watch_history ( user )
2018-10-06 19:19:47 -04:00
2021-09-24 22:42:43 -04:00
user . subscriptions += db . query_all ( " SELECT url FROM subscriptions " , as : String ) . map ( & . lchop ( " https://www.youtube.com/channel/ " ) )
2018-11-09 18:25:24 -05:00
user . subscriptions . uniq!
2021-01-04 10:51:06 -05:00
user . subscriptions = get_batch_channels ( user . subscriptions , PG_DB , false , false )
2018-11-09 18:25:24 -05:00
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_subscriptions ( user )
2018-11-21 18:12:13 -05:00
db . close
tempfile . delete
2018-08-04 16:30:44 -04:00
end
2018-07-18 15:26:02 -04:00
end
2018-07-08 09:57:06 -04:00
end
2020-04-09 13:18:09 -04:00
else nil # Ignore
2018-07-18 15:26:02 -04:00
end
2018-08-04 16:30:44 -04:00
end
end
2018-07-18 15:26:02 -04:00
2018-08-04 16:30:44 -04:00
env . redirect referer
end
2018-07-18 15:26:02 -04:00
2019-04-22 11:18:17 -04:00
get " /change_password " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2019-04-22 11:18:17 -04:00
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
2019-04-22 11:18:17 -04:00
end
2019-07-12 22:00:50 -04:00
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 11:18:17 -04:00
end
post " /change_password " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2019-04-22 11:18:17 -04:00
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
end
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
# We don't store passwords for Google accounts
if ! user . password
2020-11-30 04:59:21 -05:00
next error_template ( 400 , " Cannot change password for Google accounts " )
2019-07-12 22:00:50 -04:00
end
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
2020-11-30 04:59:21 -05:00
next error_template ( 400 , ex )
2019-07-12 22:00:50 -04:00
end
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
password = env . params . body [ " password " ]?
if ! password
2020-11-30 04:59:21 -05:00
next error_template ( 401 , " Password is a required field " )
2019-07-12 22:00:50 -04:00
end
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
new_passwords = env . params . body . select { | k , v | k . match ( / ^new_password \ [ \ d+ \ ]$ / ) } . map { | k , v | v }
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
if new_passwords . size <= 1 || new_passwords . uniq . size != 1
2020-11-30 04:59:21 -05:00
next error_template ( 400 , " New passwords must match " )
2019-07-12 22:00:50 -04:00
end
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
new_password = new_passwords . uniq [ 0 ]
if new_password . empty?
2020-11-30 04:59:21 -05:00
next error_template ( 401 , " Password cannot be empty " )
2019-07-12 22:00:50 -04:00
end
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
if new_password . bytesize > 55
2020-11-30 04:59:21 -05:00
next error_template ( 400 , " Password cannot be longer than 55 characters " )
2019-07-12 22:00:50 -04:00
end
2019-04-22 11:18:17 -04:00
2019-07-12 22:00:50 -04:00
if ! Crypto :: Bcrypt :: Password . new ( user . password . not_nil! ) . verify ( password . byte_slice ( 0 , 55 ) )
2020-11-30 04:59:21 -05:00
next error_template ( 401 , " Incorrect password " )
2019-04-22 11:18:17 -04:00
end
2019-07-12 22:00:50 -04:00
new_password = Crypto :: Bcrypt :: Password . create ( new_password , cost : 10 )
2021-12-02 21:29:52 -05:00
Invidious :: Database :: Users . update_password ( user , new_password . to_s )
2019-07-12 22:00:50 -04:00
2019-04-22 11:18:17 -04:00
env . redirect referer
end
2018-11-08 01:12:14 -05:00
get " /delete_account " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-11-08 01:12:14 -05:00
user = env . get? " user "
2019-04-16 00:23:40 -04:00
sid = env . get? " sid "
2018-11-08 01:12:14 -05:00
referer = get_referer ( env )
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
2018-11-08 01:12:14 -05:00
end
2019-07-12 22:00:50 -04:00
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 01:12:14 -05:00
end
post " /delete_account " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-11-08 01:12:14 -05:00
user = env . get? " user "
2019-04-16 00:23:40 -04:00
sid = env . get? " sid "
2018-11-08 01:12:14 -05:00
referer = get_referer ( env )
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
end
2018-11-08 01:12:14 -05:00
2019-07-12 22:00:50 -04:00
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2018-11-08 01:12:14 -05:00
2019-07-12 22:00:50 -04:00
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
2020-11-30 04:59:21 -05:00
next error_template ( 400 , ex )
2019-07-12 22:00:50 -04:00
end
2018-11-08 01:12:14 -05:00
2019-07-12 22:00:50 -04:00
view_name = " subscriptions_ #{ sha256 ( user . email ) } "
2021-12-02 20:27:51 -05:00
Invidious :: Database :: Users . delete ( user )
2021-12-02 17:57:13 -05:00
Invidious :: Database :: SessionIDs . delete ( email : user . email )
2019-07-12 22:00:50 -04:00
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 01:12:14 -05:00
end
env . redirect referer
end
2018-08-04 16:30:44 -04:00
get " /clear_watch_history " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-08-04 16:30:44 -04:00
user = env . get? " user "
2019-04-16 00:23:40 -04:00
sid = env . get? " sid "
2018-11-08 01:12:14 -05:00
referer = get_referer ( env )
2018-08-08 21:26:02 -04:00
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
2018-11-08 01:12:14 -05:00
end
2019-07-12 22:00:50 -04:00
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 01:12:14 -05:00
end
post " /clear_watch_history " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2018-12-20 16:32:09 -05:00
2018-11-08 01:12:14 -05:00
user = env . get? " user "
2019-04-16 00:23:40 -04:00
sid = env . get? " sid "
2018-08-08 21:26:02 -04:00
referer = get_referer ( env )
2018-03-16 12:40:29 -04:00
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
end
2018-11-08 01:12:14 -05:00
2019-07-12 22:00:50 -04:00
user = user . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2018-11-08 01:12:14 -05:00
2019-07-12 22:00:50 -04:00
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
2020-11-30 04:59:21 -05:00
next error_template ( 400 , ex )
2018-08-04 16:30:44 -04:00
end
2021-12-02 20:27:51 -05:00
Invidious :: Database :: Users . clear_watch_history ( user )
2018-08-04 16:30:44 -04:00
env . redirect referer
end
2019-05-15 13:26:29 -04:00
get " /authorize_token " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2019-05-15 13:26:29 -04:00
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
end
2019-05-15 13:26:29 -04:00
2019-07-12 22:00:50 -04:00
user = user . as ( User )
sid = sid . as ( String )
csrf_token = generate_response ( sid , { " :authorize_token " } , HMAC_KEY , PG_DB )
2019-05-15 13:26:29 -04:00
2019-07-12 22:00:50 -04:00
scopes = env . params . query [ " scopes " ]? . try & . split ( " , " )
scopes || = [ ] of String
2019-05-15 13:26:29 -04:00
2019-07-12 22:00:50 -04:00
callback_url = env . params . query [ " callback_url " ]?
if callback_url
callback_url = URI . parse ( callback_url )
2019-05-15 13:26:29 -04:00
end
2019-07-12 22:00:50 -04:00
expire = env . params . query [ " expire " ]? . try & . to_i?
templated " authorize_token "
2019-05-15 13:26:29 -04:00
end
2019-04-18 17:23:50 -04:00
post " /authorize_token " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2019-04-18 17:23:50 -04:00
user = env . get? " user "
sid = env . get? " sid "
referer = get_referer ( env )
2019-07-12 22:00:50 -04:00
if ! user
next env . redirect referer
end
2019-04-18 17:23:50 -04:00
2019-07-12 22:00:50 -04:00
user = env . get ( " user " ) . as ( User )
sid = sid . as ( String )
token = env . params . body [ " csrf_token " ]?
2019-04-18 17:23:50 -04:00
2019-07-12 22:00:50 -04:00
begin
validate_request ( token , sid , env . request , HMAC_KEY , PG_DB , locale )
rescue ex
2020-11-30 04:59:21 -05:00
next error_template ( 400 , ex )
2019-07-12 22:00:50 -04:00
end
2019-04-18 17:23:50 -04:00
2019-07-12 22:00:50 -04:00
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-18 17:23:50 -04:00
2019-07-12 22:00:50 -04:00
access_token = generate_token ( user . email , scopes , expire , HMAC_KEY , PG_DB )
2019-04-18 17:23:50 -04:00
2019-07-12 22:00:50 -04:00
if callback_url
2019-09-24 13:31:33 -04:00
access_token = URI . encode_www_form ( access_token )
2019-07-12 22:00:50 -04:00
url = URI . parse ( callback_url )
2019-04-18 17:23:50 -04:00
2019-07-12 22:00:50 -04:00
if url . query
query = HTTP :: Params . parse ( url . query . not_nil! )
2019-04-18 17:23:50 -04:00
else
2019-07-12 22:00:50 -04:00
query = HTTP :: Params . new
2019-04-18 17:23:50 -04:00
end
2019-07-12 22:00:50 -04:00
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-18 17:23:50 -04:00
end
end
get " /token_manager " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2019-04-18 17:23:50 -04:00
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 )
2021-12-02 17:57:13 -05:00
tokens = Invidious :: Database :: SessionIDs . select_all ( user . email )
2019-04-18 17:23:50 -04:00
templated " token_manager "
end
post " /token_ajax " do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2019-04-18 17:23:50 -04:00
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
2020-11-30 04:59:21 -05:00
next error_json ( 403 , " No such user " )
2019-04-18 17:23:50 -04:00
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
2020-11-30 04:59:21 -05:00
next error_template ( 400 , ex )
2019-04-18 17:23:50 -04:00
else
2020-11-30 04:59:21 -05:00
next error_json ( 400 , ex )
2019-04-18 17:23:50 -04:00
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 "
2021-12-02 17:57:13 -05:00
Invidious :: Database :: SessionIDs . delete ( sid : session , email : user . email )
2020-04-09 13:18:09 -04:00
else
2020-11-30 04:59:21 -05:00
next error_json ( 400 , " Unsupported action #{ action } " )
2019-04-18 17:23:50 -04:00
end
if redirect
env . redirect referer
else
env . response . content_type = " application/json "
" {} "
end
end
2018-08-04 16:30:44 -04:00
# Channels
2019-04-28 12:47:16 -04:00
{ " /channel/:ucid/live " , " /user/:user/live " , " /c/:user/live " } . each do | route |
get route do | env |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2019-04-28 12:47:16 -04:00
# 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 12:58:16 -04:00
response = YT_POOL . client & . get ( " / #{ type } / #{ value } /live?disable_polymer=1 " )
2019-04-28 12:47:16 -04:00
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
2019-06-07 13:39:12 -04:00
# Authenticated endpoints
2018-03-31 10:51:14 -04:00
2021-08-14 03:08:46 -04:00
# The notification APIs can't be extracted yet
# due to the requirement of the `connection_channel`
# used by the `NotificationJob`
2018-09-04 20:27:10 -04:00
2019-04-10 18:58:42 -04:00
get " /api/v1/auth/notifications " do | env |
2019-06-02 08:41:53 -04:00
env . response . content_type = " text/event-stream "
2018-07-28 21:40:59 -04:00
2019-04-10 18:58:42 -04:00
topics = env . params . query [ " topics " ]? . try & . split ( " , " ) . uniq . first ( 1000 )
topics || = [ ] of String
2018-11-27 23:07:45 -05:00
2020-06-15 18:10:30 -04:00
create_notification_stream ( env , topics , connection_channel )
2019-03-23 15:05:13 -04:00
end
2018-07-16 09:18:59 -04:00
2019-05-21 10:01:17 -04:00
post " /api/v1/auth/notifications " do | env |
2019-06-02 08:41:53 -04:00
env . response . content_type = " text/event-stream "
2018-12-20 16:32:09 -05:00
2019-05-21 10:01:17 -04:00
topics = env . params . body [ " topics " ]? . try & . split ( " , " ) . uniq . first ( 1000 )
topics || = [ ] of String
2019-04-10 18:58:42 -04:00
2020-06-15 18:10:30 -04:00
create_notification_stream ( env , topics , connection_channel )
2019-04-10 18:58:42 -04:00
end
2019-10-27 00:19:05 -04:00
get " /Captcha " do | env |
2020-05-08 10:00:53 -04:00
headers = HTTP :: Headers { " :authority " = > " accounts.google.com " }
response = YT_POOL . client & . get ( env . request . resource , headers )
2019-10-27 00:19:05 -04:00
env . response . headers [ " Content-Type " ] = response . headers [ " Content-Type " ]
response . body
end
2019-08-21 19:23:20 -04:00
# Undocumented, creates anonymous playlist with specified 'video_ids', max 50 videos
2019-05-03 10:11:27 -04:00
get " /watch_videos " do | env |
2019-10-25 12:58:16 -04:00
response = YT_POOL . client & . get ( env . request . resource )
2019-05-03 10:11:27 -04:00
if url = response . headers [ " Location " ]?
2021-01-31 13:52:32 -05:00
url = URI . parse ( url ) . request_target
2019-05-03 10:11:27 -04:00
next env . redirect url
end
env . response . status_code = response . status_code
end
2018-02-10 10:15:23 -05:00
error 404 do | env |
2019-03-27 06:28:53 -04:00
if md = env . request . path . match ( / ^ \/ (?<id>([a-zA-Z0-9_-]{11})|( \ w+))$ / )
2019-04-17 15:46:00 -04:00
item = md [ " id " ]
2018-10-06 23:19:36 -04:00
2019-04-17 15:46:00 -04:00
# Check if item is branding URL e.g. https://youtube.com/gaming
2019-10-25 12:58:16 -04:00
response = YT_POOL . client & . get ( " / #{ item } " )
2019-03-27 06:28:53 -04:00
if response . status_code == 301
2021-01-31 13:52:32 -05:00
response = YT_POOL . client & . get ( URI . parse ( response . headers [ " Location " ] ) . request_target )
2019-03-27 06:28:53 -04:00
end
2019-06-07 13:42:07 -04:00
if response . body . empty?
env . response . headers [ " Location " ] = " / "
halt env , status_code : 302
end
2019-03-27 06:28:53 -04:00
html = XML . parse_html ( response . body )
2020-01-14 08:21:17 -05:00
ucid = html . xpath_node ( % q ( / / link [ @rel = " canonical " ] ) ) . try & . [ " href " ] . split ( " / " ) [ - 1 ]
2019-03-27 06:28:53 -04:00
if ucid
2020-01-14 08:21:17 -05:00
env . response . headers [ " Location " ] = " /channel/ #{ ucid } "
2019-03-27 06:28:53 -04:00
halt env , status_code : 302
end
2018-10-06 23:19:36 -04:00
params = [ ] of String
env . params . query . each do | k , v |
params << " #{ k } = #{ v } "
end
params = params . join ( " & " )
2019-04-17 15:46:00 -04:00
url = " /watch?v= #{ item } "
2018-10-06 23:19:36 -04:00
if ! params . empty?
url += " & #{ params } "
end
2019-04-17 15:46:00 -04:00
# Check if item is video ID
2019-10-25 12:58:16 -04:00
if item . match ( / ^[a-zA-Z0-9_-]{11}$ / ) && YT_POOL . client & . head ( " /watch?v= #{ item } " ) . status_code != 404
2019-02-21 16:07:22 -05:00
env . response . headers [ " Location " ] = url
halt env , status_code : 302
end
end
2019-01-12 14:18:08 -05:00
env . response . headers [ " Location " ] = " / "
halt env , status_code : 302
2017-12-30 16:21:43 -05:00
end
2020-11-30 04:59:21 -05:00
error 500 do | env , ex |
2021-11-08 17:52:55 -05:00
locale = env . get ( " preferences " ) . as ( Preferences ) . locale
2020-11-30 04:59:21 -05:00
error_template ( 500 , ex )
2017-12-30 16:21:43 -05:00
end
2021-09-24 22:15:23 -04:00
static_headers do | response |
2019-05-08 09:58:10 -04:00
response . headers . add ( " Cache-Control " , " max-age=2629800 " )
2018-03-09 12:28:57 -05:00
end
2017-11-23 02:48:55 -05:00
public_folder " assets "
2018-04-15 23:56:58 -04:00
2018-07-30 19:42:45 -04:00
Kemal . config . powered_by_header = false
2018-04-15 23:56:58 -04:00
add_handler FilteredCompressHandler . new
2019-02-02 23:48:47 -05:00
add_handler APIHandler . new
2019-04-18 17:23:50 -04:00
add_handler AuthHandler . new
2019-03-23 11:24:30 -04:00
add_handler DenyFrame . new
2019-04-18 17:23:50 -04:00
add_context_storage_type ( Array ( String ) )
2019-02-24 10:49:48 -05:00
add_context_storage_type ( Preferences )
2019-04-18 17:23:50 -04:00
add_context_storage_type ( User )
2017-11-23 02:48:55 -05:00
2021-01-04 10:51:06 -05:00
Kemal . config . logger = LOGGER
2019-09-23 13:05:29 -04:00
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
2021-09-10 03:42:15 -04:00
Kemal . config . app_name = " Invidious "
2021-10-11 08:42:22 -04:00
# Use in kemal's production mode.
# Users can also set the KEMAL_ENV environmental variable for this to be set automatically.
{% if flag? ( :release ) || flag? ( :production ) %}
Kemal . config . env = " production " if ! ENV . has_key? ( " KEMAL_ENV " )
{% end %}
2017-11-23 02:48:55 -05:00
Kemal . run