summaryrefslogtreecommitdiffstats
path: root/src/invidious.cr
diff options
context:
space:
mode:
Diffstat (limited to 'src/invidious.cr')
-rw-r--r--src/invidious.cr519
1 files changed, 20 insertions, 499 deletions
diff --git a/src/invidious.cr b/src/invidious.cr
index d3ad18bd..b09f31c2 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -114,6 +114,18 @@ LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level)
# Check table integrity
Invidious::Database.check_integrity(CONFIG)
+# Resolve player dependencies. This is done at compile time.
+#
+# Running the script by itself would show some colorful feedback while this doesn't.
+# Perhaps we should just move the script to runtime in order to get that feedback?
+
+{% puts "\nChecking player dependencies...\n" %}
+{% if flag?(:minified_player_dependencies) %}
+ {% puts run("../scripts/fetch-player-dependencies.cr", "--minified").stringify %}
+{% else %}
+ {% puts run("../scripts/fetch-player-dependencies.cr").stringify %}
+{% end %}
+
# Start jobs
if CONFIG.channel_threads > 0
@@ -324,6 +336,7 @@ end
end
Invidious::Routing.get "/watch", Invidious::Routes::Watch, :handle
+ Invidious::Routing.post "/watch_ajax", Invidious::Routes::Watch, :mark_watched
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
@@ -357,6 +370,8 @@ end
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
+ Invidious::Routing.get "/data_control", Invidious::Routes::PreferencesRoute, :data_control
+ Invidious::Routing.post "/data_control", Invidious::Routes::PreferencesRoute, :update_data_control
# Feeds
Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Feeds, :view_all_playlists_redirect
@@ -375,6 +390,11 @@ end
# 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
+
+ Invidious::Routing.get "/modify_notifications", Invidious::Routes::Notifications, :modify
+
+ Invidious::Routing.post "/subscription_ajax", Invidious::Routes::Subscriptions, :toggle_subscription
+ Invidious::Routing.get "/subscription_manager", Invidious::Routes::Subscriptions, :subscription_manager
{% end %}
Invidious::Routing.get "/ggpht/*", Invidious::Routes::Images, :ggpht
@@ -391,505 +411,6 @@ define_v1_api_routes()
define_api_manifest_routes()
define_video_playback_routes()
-# Users
-
-post "/watch_ajax" do |env|
- locale = env.get("preferences").as(Preferences).locale
-
- user = env.get? "user"
- sid = env.get? "sid"
- referer = get_referer(env, "/feed/subscriptions")
-
- redirect = env.params.query["redirect"]?
- redirect ||= "true"
- redirect = redirect == "true"
-
- if !user
- if redirect
- next env.redirect referer
- else
- next error_json(403, "No such user")
- end
- end
-
- user = user.as(User)
- sid = sid.as(String)
- token = env.params.body["csrf_token"]?
-
- id = env.params.query["id"]?
- if !id
- env.response.status_code = 400
- next
- end
-
- begin
- validate_request(token, sid, env.request, HMAC_KEY, locale)
- rescue ex
- if redirect
- next error_template(400, ex)
- else
- next error_json(400, ex)
- 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
- Invidious::Database::Users.mark_watched(user, id)
- end
- when "action_mark_unwatched"
- Invidious::Database::Users.mark_unwatched(user, id)
- else
- next error_json(400, "Unsupported action #{action}")
- end
-
- if redirect
- env.redirect referer
- else
- env.response.content_type = "application/json"
- "{}"
- end
-end
-
-# /modify_notifications
-# will "ding" all subscriptions.
-# /modify_notifications?receive_all_updates=false&receive_no_updates=false
-# will "unding" all subscriptions.
-get "/modify_notifications" do |env|
- locale = env.get("preferences").as(Preferences).locale
-
- user = env.get? "user"
- sid = env.get? "sid"
- referer = get_referer(env, "/")
-
- redirect = env.params.query["redirect"]?
- redirect ||= "false"
- redirect = redirect == "true"
-
- if !user
- if redirect
- next env.redirect referer
- else
- next error_json(403, "No such user")
- end
- end
-
- user = user.as(User)
-
- if !user.password
- channel_req = {} of String => String
-
- 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"
-
- channel_req.reject! { |k, v| v != "true" && v != "false" }
-
- headers = HTTP::Headers.new
- headers["Cookie"] = env.request.headers["Cookie"]
-
- html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
-
- cookies = HTTP::Cookies.from_client_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)
-
- if match = html.body.match(/'XSRF_TOKEN': "(?<session_token>[^"]+)"/)
- session_token = match["session_token"]
- else
- next env.redirect referer
- end
-
- headers["content-type"] = "application/x-www-form-urlencoded"
- channel_req["session_token"] = session_token
-
- subs = XML.parse_html(html.body)
- 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
-
- YT_POOL.client &.post("/subscription_ajax?action_update_subscription_preferences=1", headers, form: channel_req)
- end
- end
-
- if redirect
- env.redirect referer
- else
- env.response.content_type = "application/json"
- "{}"
- end
-end
-
-post "/subscription_ajax" do |env|
- locale = 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
- next error_json(403, "No such user")
- 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, locale)
- rescue ex
- if redirect
- next error_template(400, ex)
- else
- next error_json(400, ex)
- end
- end
-
- if env.params.query["action_create_subscription_to_channel"]?.try &.to_i?.try &.== 1
- action = "action_create_subscription_to_channel"
- elsif env.params.query["action_remove_subscriptions"]?.try &.to_i?.try &.== 1
- action = "action_remove_subscriptions"
- else
- next env.redirect referer
- end
-
- channel_id = env.params.query["c"]?
- channel_id ||= ""
-
- if !user.password
- # Sync subscriptions with YouTube
- subscribe_ajax(channel_id, action, env.request.headers)
- end
-
- case action
- when "action_create_subscription_to_channel"
- if !user.subscriptions.includes? channel_id
- get_channel(channel_id, false, false)
- Invidious::Database::Users.subscribe_channel(user, channel_id)
- end
- when "action_remove_subscriptions"
- Invidious::Database::Users.unsubscribe_channel(user, channel_id)
- else
- next error_json(400, "Unsupported action #{action}")
- end
-
- if redirect
- env.redirect referer
- else
- env.response.content_type = "application/json"
- "{}"
- end
-end
-
-get "/subscription_manager" do |env|
- locale = env.get("preferences").as(Preferences).locale
-
- user = env.get? "user"
- sid = env.get? "sid"
- referer = get_referer(env)
-
- if !user
- next env.redirect referer
- end
-
- user = user.as(User)
- sid = sid.as(String)
-
- if !user.password
- # Refresh account
- headers = HTTP::Headers.new
- headers["Cookie"] = env.request.headers["Cookie"]
-
- user, sid = get_user(sid, headers)
- end
-
- action_takeout = env.params.query["action_takeout"]?.try &.to_i?
- action_takeout ||= 0
- action_takeout = action_takeout == 1
-
- format = env.params.query["format"]?
- format ||= "rss"
-
- subscriptions = Invidious::Database::Channels.select(user.subscriptions)
- subscriptions.sort_by!(&.author.downcase)
-
- if action_takeout
- if format == "json"
- env.response.content_type = "application/json"
- env.response.headers["content-disposition"] = "attachment"
- playlists = Invidious::Database::Playlists.select_like_iv(user.email)
-
- 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
- Invidious::Database::PlaylistVideos.select_ids(playlist.id, playlist.index, limit: 500).each do |video_id|
- json.string video_id
- end
- end
- end
- end
- end
- end
- end
- end
- end
- 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
-
- xml.element("outline", text: title, title: title) do
- subscriptions.each do |channel|
- if format == "newpipe"
- xml_url = "https://www.youtube.com/feeds/videos.xml?channel_id=#{channel.id}"
- else
- xml_url = "#{HOST_URL}/feed/channel/#{channel.id}"
- end
-
- xml.element("outline", text: channel.author, title: channel.author,
- "type": "rss", xmlUrl: xml_url)
- end
- end
- end
- end
- end
-
- next export.gsub(%(<?xml version="1.0"?>\n), "")
- end
- end
-
- templated "subscription_manager"
-end
-
-get "/data_control" do |env|
- locale = env.get("preferences").as(Preferences).locale
-
- user = env.get? "user"
- referer = get_referer(env)
-
- if !user
- next env.redirect referer
- end
-
- user = user.as(User)
-
- templated "data_control"
-end
-
-post "/data_control" do |env|
- locale = env.get("preferences").as(Preferences).locale
-
- user = env.get? "user"
- referer = get_referer(env)
-
- if user
- user = user.as(User)
-
- # TODO: Find a way to prevent browser timeout
-
- HTTP::FormData.parse(env.request) do |part|
- body = part.body.gets_to_end
- type = part.headers["Content-Type"]
-
- next if body.empty?
-
- # TODO: Unify into single import based on content-type
- case part.name
- when "import_invidious"
- body = JSON.parse(body)
-
- if body["subscriptions"]?
- user.subscriptions += body["subscriptions"].as_a.map(&.as_s)
- user.subscriptions.uniq!
-
- user.subscriptions = get_batch_channels(user.subscriptions, false, false)
-
- Invidious::Database::Users.update_subscriptions(user)
- end
-
- if body["watch_history"]?
- user.watched += body["watch_history"].as_a.map(&.as_s)
- user.watched.uniq!
- Invidious::Database::Users.update_watch_history(user)
- end
-
- if body["preferences"]?
- user.preferences = Preferences.from_json(body["preferences"].to_json)
- Invidious::Database::Users.update_preferences(user)
- end
-
- 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(title, privacy, user)
- Invidious::Database::Playlists.update_description(playlist.id, description)
-
- videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx|
- raise InfoException.new("Playlist cannot have more than 500 videos") if idx > 500
-
- video_id = video_id.try &.as_s?
- next if !video_id
-
- begin
- video = get_video(video_id)
- rescue ex
- next
- 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),
- })
-
- Invidious::Database::PlaylistVideos.insert(playlist_video)
- Invidious::Database::Playlists.update_video_added(playlist.id, playlist_video.index)
- end
- end
- end
- when "import_youtube"
- filename = part.filename || ""
- extension = filename.split(".").last
-
- if extension == "xml" || type == "application/xml" || type == "text/xml"
- 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
- elsif extension == "json" || type == "application/json"
- subscriptions = JSON.parse(body)
- user.subscriptions += subscriptions.as_a.compact_map do |entry|
- entry["snippet"]["resourceId"]["channelId"].as_s
- end
- elsif extension == "csv" || type == "text/csv"
- subscriptions = parse_subscription_export_csv(body)
- user.subscriptions += subscriptions
- else
- halt(env, status_code: 415,
- response: error_template(415, "Invalid subscription file uploaded")
- )
- end
-
- user.subscriptions.uniq!
- user.subscriptions = get_batch_channels(user.subscriptions, false, false)
-
- Invidious::Database::Users.update_subscriptions(user)
- 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!
-
- user.subscriptions = get_batch_channels(user.subscriptions, false, false)
-
- Invidious::Database::Users.update_subscriptions(user)
- when "import_newpipe_subscriptions"
- body = JSON.parse(body)
- 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>.+)/)
- response = YT_POOL.client &.get("/user/#{match["user"]}?disable_polymer=1&hl=en&gl=US")
- html = XML.parse_html(response.body)
- ucid = html.xpath_node(%q(//link[@rel="canonical"])).try &.["href"].split("/")[-1]
- next ucid if ucid
- end
-
- nil
- end
- user.subscriptions.uniq!
-
- user.subscriptions = get_batch_channels(user.subscriptions, false, false)
-
- Invidious::Database::Users.update_subscriptions(user)
- when "import_newpipe"
- Compress::Zip::Reader.open(IO::Memory.new(body)) do |file|
- file.each_entry do |entry|
- if entry.filename == "newpipe.db"
- tempfile = File.tempfile(".db")
- File.write(tempfile.path, entry.io.gets_to_end)
- db = DB.open("sqlite3://" + tempfile.path)
-
- user.watched += db.query_all("SELECT url FROM streams", as: String).map(&.lchop("https://www.youtube.com/watch?v="))
- user.watched.uniq!
-
- Invidious::Database::Users.update_watch_history(user)
-
- user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String).map(&.lchop("https://www.youtube.com/channel/"))
- user.subscriptions.uniq!
-
- user.subscriptions = get_batch_channels(user.subscriptions, false, false)
-
- Invidious::Database::Users.update_subscriptions(user)
-
- db.close
- tempfile.delete
- end
- end
- end
- else nil # Ignore
- end
- end
- end
-
- env.redirect referer
-end
-
get "/change_password" do |env|
locale = env.get("preferences").as(Preferences).locale