From 5680d5a7bebbb8e3c715e7f546bce49116b4a599 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 2 Aug 2019 15:24:38 -0500 Subject: [PATCH 01/31] Sort dash representations by framerate --- src/invidious.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious.cr b/src/invidious.cr index d6479002..c2de0dcf 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -4493,7 +4493,7 @@ get "/api/manifest/dash/id/:id" do |env| end audio_streams = video.audio_streams(adaptive_fmts) - video_streams = video.video_streams(adaptive_fmts) + video_streams = video.video_streams(adaptive_fmts).sort_by { |stream| stream["fps"].to_i }.reverse XML.build(indent: " ", encoding: "UTF-8") do |xml| xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011", From ea39bb4334227804b95a2a084e51ae004d8f5f9e Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Thu, 1 Aug 2019 12:49:12 +0200 Subject: [PATCH 02/31] docker: various improvements to Dockerfile This includes the following changes: - Use multi-stage build to run application in an optimized environment, see https://docs.docker.com/develop/develop-images/multistage-build/ - Run application on alpine instead of archlinux to further reduce image size - Build Crystal application with --release for improved runtime performance - Run application as non-root user for better security, see https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#user - Only rebuild Docker layers when required --- docker/Dockerfile | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index 043d950e..c9fa6367 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,15 +1,28 @@ -FROM archlinux/base - -RUN pacman -Sy --noconfirm shards crystal imagemagick librsvg \ - which pkgconf gcc ttf-liberation glibc -# base-devel contains many other basic packages, that are normally assumed to already exist on a clean arch system - -ADD . /invidious - +FROM alpine:latest AS builder +RUN apk add -u crystal shards libc-dev \ + yaml-dev libxml2-dev sqlite-dev sqlite-static zlib-dev openssl-dev WORKDIR /invidious +COPY ./shard.yml ./shard.yml +RUN shards update && shards install +COPY ./src/ ./src/ +# TODO: .git folder is required for building – this is destructive. +# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION. +COPY ./.git/ ./.git/ +RUN crystal build --static --release \ +# TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946 + -Dmusl \ + ./src/invidious.cr -RUN sed -i 's/host: localhost/host: postgres/' config/config.yml && \ - shards update && shards install && \ - crystal build src/invidious.cr - +FROM alpine:latest +RUN apk add -u imagemagick ttf-opensans +WORKDIR /invidious +RUN addgroup -g 1000 -S invidious && \ + adduser -u 1000 -S invidious -G invidious +COPY ./assets/ ./assets/ +COPY ./config/config.yml ./config/config.yml +COPY ./config/sql/ ./config/sql/ +COPY ./locales/ ./locales/ +RUN sed -i 's/host: localhost/host: postgres/' config/config.yml +COPY --from=builder /invidious/invidious . +USER invidious CMD [ "/invidious/invidious" ] From 824150f89b8d9fccf88fa393f155bb10dcdae543 Mon Sep 17 00:00:00 2001 From: leonklingele <5585491+leonklingele@users.noreply.github.com> Date: Sun, 4 Aug 2019 16:10:32 +0200 Subject: [PATCH 03/31] Add Travis CI and pin dependencies (#655) --- .travis.yml | 17 +++++++++++++++++ README.md | 2 ++ shard.yml | 3 +++ 3 files changed, 22 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..da787cf1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,17 @@ +language: crystal + +crystal: + - latest + +dist: bionic + +before_install: + - shards update + - shards install + +install: + - crystal build --error-on-warnings src/invidious.cr + +script: + - crystal tool format --check + - crystal spec diff --git a/README.md b/README.md index 44254c62..be7c5580 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Invidious +[![Build Status](https://travis-ci.org/omarroth/invidious.svg?branch=master)](https://travis-ci.org/omarroth/invidious) + ## Invidious is an alternative front-end to YouTube - Audio-only mode (and no need to keep window open on mobile) diff --git a/shard.yml b/shard.yml index 3e2b3d03..b1b500d8 100644 --- a/shard.yml +++ b/shard.yml @@ -11,10 +11,13 @@ targets: dependencies: pg: github: will/crystal-pg + version: ~> 0.17.0 sqlite3: github: crystal-lang/crystal-sqlite3 + version: ~> 0.12.0 kemal: github: kemalcr/kemal + version: ~> 0.25.2 crystal: 0.29.0 From 37d064d8369c675c28f71e481163513163cad2de Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 4 Aug 2019 09:16:29 -0500 Subject: [PATCH 04/31] Bump Crystal version --- shard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index b1b500d8..7718cd24 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: invidious -version: 0.19.0 +version: 0.19.1 authors: - Omar Roth @@ -19,6 +19,6 @@ dependencies: github: kemalcr/kemal version: ~> 0.25.2 -crystal: 0.29.0 +crystal: 0.30.0 license: AGPLv3 From 4f120e19fd0e287c2202ceef8a20cf09263df0ed Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 4 Aug 2019 09:46:26 -0500 Subject: [PATCH 05/31] Fix overflow for channel description --- src/invidious/views/channel.ecr | 2 +- src/invidious/views/community.ecr | 2 +- src/invidious/views/playlists.ecr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index 9e7ec88c..b5eb46ea 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -28,7 +28,7 @@
-

<%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content %>

+

<%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content %>

diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 732ae9bd..218cc2d4 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -27,7 +27,7 @@
-

<%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content %>

+

<%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content %>

diff --git a/src/invidious/views/playlists.ecr b/src/invidious/views/playlists.ecr index 216f4475..a32192b5 100644 --- a/src/invidious/views/playlists.ecr +++ b/src/invidious/views/playlists.ecr @@ -27,7 +27,7 @@
-

<%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content %>

+

<%= XML.parse_html(channel.description_html).xpath_node(%q(.//pre)).try &.content %>

From 7a33831d1471da0d117692539b739f0da146e540 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Sun, 4 Aug 2019 20:56:24 -0500 Subject: [PATCH 06/31] Fix detection of premium content --- src/invidious/videos.cr | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 49ff0494..e1dff5aa 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -803,8 +803,11 @@ struct Video end def premium - premium = self.player_response.to_s.includes? "Get YouTube without the ads." - return premium + if info["premium"]? + self.info["premium"] == "true" + else + false + end end def captions @@ -1189,6 +1192,8 @@ def fetch_video(id, region) author = player_json["videoDetails"]["author"]?.try &.as_s || "" ucid = player_json["videoDetails"]["channelId"]?.try &.as_s || "" + info["premium"] = html.xpath_node(%q(.//span[text()="Premium"])) ? "true" : "false" + views = html.xpath_node(%q(//meta[@itemprop="interactionCount"])) .try &.["content"].to_i64? || 0_i64 From 5e6d7f5d16724174c87b5aaea9e1b824b397f7c1 Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Mon, 5 Aug 2019 03:35:30 +0200 Subject: [PATCH 07/31] shard: update dependencies --- shard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 7718cd24..09c54a51 100644 --- a/shard.yml +++ b/shard.yml @@ -11,10 +11,10 @@ targets: dependencies: pg: github: will/crystal-pg - version: ~> 0.17.0 + version: ~> 0.18.0 sqlite3: github: crystal-lang/crystal-sqlite3 - version: ~> 0.12.0 + version: ~> 0.13.0 kemal: github: kemalcr/kemal version: ~> 0.25.2 From cc956583fb70df1c1add33e977ef4ae8747ceffb Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 5 Aug 2019 14:19:02 -0500 Subject: [PATCH 08/31] Fix detection of unavailable videos --- src/invidious/videos.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index e1dff5aa..7d0c7838 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -1178,7 +1178,7 @@ def fetch_video(id, region) end end - if info["errorcode"]?.try &.== "2" || !info["player_response"] + if !info["player_response"]? || info["errorcode"]?.try &.== "2" raise "Video unavailable." end From c9a05187fbd67ca59d02e503210beb5b5861790f Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 5 Aug 2019 18:54:39 -0500 Subject: [PATCH 09/31] Update icon for unlisted videos --- src/invidious/views/watch.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr index f8882c6e..542fe9e5 100644 --- a/src/invidious/views/watch.ecr +++ b/src/invidious/views/watch.ecr @@ -65,7 +65,7 @@ var video_data = { <% if !video.is_listed %>

- <%= translate(locale, "Unlisted") %> + <%= translate(locale, "Unlisted") %>

<% end %> From 66b949bed1b2d685ec2f76c99897a13b94a9373b Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 5 Aug 2019 18:55:23 -0500 Subject: [PATCH 10/31] Format history.ecr --- src/invidious/views/history.ecr | 50 ++++++++++++++++----------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/invidious/views/history.ecr b/src/invidious/views/history.ecr index a4a947db..e5154560 100644 --- a/src/invidious/views/history.ecr +++ b/src/invidious/views/history.ecr @@ -26,32 +26,32 @@ var watched_data = {
-<% watched.each_slice(4) do |slice| %> - <% slice.each do |item| %> -
- - <% end %> -<% end %> + <% end %> + <% end %>
From 46577fb1285e99ca60438d575c9eb0542cde52b0 Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Fri, 9 Aug 2019 02:00:22 +0200 Subject: [PATCH 11/31] Add support for player styles This currently includes the following styles: - Invidious, the default - YouTube, using a centered play button and always visible video control bar Implements https://github.com/omarroth/invidious/issues/670. Supersedes https://github.com/omarroth/invidious/pull/661. --- assets/css/default.css | 19 +++++++++++++++++++ src/invidious.cr | 4 ++++ src/invidious/helpers/helpers.cr | 1 + src/invidious/users.cr | 1 + src/invidious/videos.cr | 5 +++++ src/invidious/views/components/player.ecr | 2 +- src/invidious/views/preferences.ecr | 9 +++++++++ 7 files changed, 40 insertions(+), 1 deletion(-) diff --git a/assets/css/default.css b/assets/css/default.css index c527cbca..4b81d9f5 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -433,3 +433,22 @@ video.video-js { .pure-control-group label { word-wrap: normal; } + +.video-js.player-style-invidious { + /* This is already the default */ +} + +.video-js.player-style-youtube .vjs-control-bar { + display: flex; + flex-direction: row; +} +.video-js.player-style-youtube .vjs-big-play-button { + /* + Styles copied from video-js.min.css, definition of + .vjs-big-play-centered .vjs-big-play-button + */ + top: 50%; + left: 50%; + margin-top: -.81666em; + margin-left: -1.5em; +} diff --git a/src/invidious.cr b/src/invidious.cr index c2de0dcf..85fb87d9 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -1478,6 +1478,9 @@ post "/preferences" do |env| speed = env.params.body["speed"]?.try &.as(String).to_f32? speed ||= CONFIG.default_user_preferences.speed + player_style = env.params.body["player_style"]?.try &.as(String) + player_style ||= CONFIG.default_user_preferences.player_style + quality = env.params.body["quality"]?.try &.as(String) quality ||= CONFIG.default_user_preferences.quality @@ -1546,6 +1549,7 @@ post "/preferences" do |env| locale: locale, max_results: max_results, notifications_only: notifications_only, + player_style: player_style, quality: quality, redirect_feed: redirect_feed, related_videos: related_videos, diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 9cefcf14..9256f6b1 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -73,6 +73,7 @@ struct ConfigPreferences locale: {type: String, default: "en-US"}, max_results: {type: Int32, default: 40}, notifications_only: {type: Bool, default: false}, + video_player: {type: String, default: "invidious"}, quality: {type: String, default: "hd720"}, redirect_feed: {type: Bool, default: false}, related_videos: {type: Bool, default: true}, diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 1b5d34c6..35d8a49e 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -138,6 +138,7 @@ struct Preferences locale: {type: String, default: CONFIG.default_user_preferences.locale, converter: ProcessString}, max_results: {type: Int32, default: CONFIG.default_user_preferences.max_results, converter: ClampInt}, notifications_only: {type: Bool, default: CONFIG.default_user_preferences.notifications_only}, + player_style: {type: String, default: CONFIG.default_user_preferences.player_style, converter: ProcessString}, quality: {type: String, default: CONFIG.default_user_preferences.quality, converter: ProcessString}, redirect_feed: {type: Bool, default: CONFIG.default_user_preferences.redirect_feed}, related_videos: {type: Bool, default: CONFIG.default_user_preferences.related_videos}, diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 7d0c7838..cf83d6c2 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -258,6 +258,7 @@ struct VideoPreferences listen: Bool, local: Bool, preferred_captions: Array(String), + player_style: String, quality: String, raw: Bool, region: String?, @@ -1264,6 +1265,7 @@ def process_video_params(query, preferences) continue_autoplay = query["continue_autoplay"]?.try &.to_i? listen = query["listen"]? && (query["listen"] == "true" || query["listen"] == "1").to_unsafe local = query["local"]? && (query["local"] == "true" || query["local"] == "1").to_unsafe + player_style = query["player_style"]? preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase } quality = query["quality"]? region = query["region"]? @@ -1281,6 +1283,7 @@ def process_video_params(query, preferences) continue_autoplay ||= preferences.continue_autoplay.to_unsafe listen ||= preferences.listen.to_unsafe local ||= preferences.local.to_unsafe + player_style ||= preferences.player_style preferred_captions ||= preferences.captions quality ||= preferences.quality related_videos ||= preferences.related_videos.to_unsafe @@ -1296,6 +1299,7 @@ def process_video_params(query, preferences) continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe listen ||= CONFIG.default_user_preferences.listen.to_unsafe local ||= CONFIG.default_user_preferences.local.to_unsafe + player_style ||= CONFIG.default_user_preferences.player_style preferred_captions ||= CONFIG.default_user_preferences.captions quality ||= CONFIG.default_user_preferences.quality related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe @@ -1354,6 +1358,7 @@ def process_video_params(query, preferences) controls: controls, listen: listen, local: local, + player_style: player_style, preferred_captions: preferred_captions, quality: quality, raw: raw, diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index 491e8fb1..ba6311cb 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -1,5 +1,5 @@
+
+ + +
+
checked<% end %>> From 2d955dae487200dba652d540110ed4ffb79bfb97 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 8 Aug 2019 22:09:34 -0500 Subject: [PATCH 12/31] Force redirect for videos without audio --- src/invidious.cr | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index c2de0dcf..46d38414 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -463,8 +463,16 @@ get "/watch" do |env| # Older videos may not have audio sources available. # We redirect here so they're not unplayable - if params.listen && audio_streams.empty? - next env.redirect "/watch?#{env.params.query}&listen=0" + if audio_streams.empty? + 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 end captions = video.captions @@ -689,6 +697,17 @@ get "/embed/:id" do |env| video_streams = video.video_streams(adaptive_fmts) audio_streams = video.audio_streams(adaptive_fmts) + if audio_streams.empty? + if params.quality == "dash" + env.params.query.delete_all("quality") + next env.redirect "/embed/#{video_id}?#{env.params.query}" + elsif params.listen + env.params.query.delete_all("listen") + env.params.query["listen"] = "0" + next env.redirect "/embed/#{video_id}?#{env.params.query}" + end + end + captions = video.captions preferred_captions = captions.select { |caption| From 3de37a61c51e0fea89fdd9d5be713a82bb366da5 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Thu, 8 Aug 2019 23:02:45 -0500 Subject: [PATCH 13/31] Update videojs-http-source-selector --- assets/css/videojs-http-source-selector.css | 2 +- assets/js/videojs-http-source-selector.min.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/css/videojs-http-source-selector.css b/assets/css/videojs-http-source-selector.css index c0cd24e3..18ba17ce 100644 --- a/assets/css/videojs-http-source-selector.css +++ b/assets/css/videojs-http-source-selector.css @@ -1,6 +1,6 @@ /** * videojs-http-source-selector - * @version 1.1.5 + * @version 1.1.6 * @copyright 2019 Justin Fujita * @license MIT */ diff --git a/assets/js/videojs-http-source-selector.min.js b/assets/js/videojs-http-source-selector.min.js index 597282e2..d4923c66 100644 --- a/assets/js/videojs-http-source-selector.min.js +++ b/assets/js/videojs-http-source-selector.min.js @@ -1,7 +1,7 @@ /** * videojs-http-source-selector - * @version 1.1.5 + * @version 1.1.6 * @copyright 2019 Justin Fujita * @license MIT */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("video.js")):"function"==typeof define&&define.amd?define(["video.js"],t):(e=e||self)["videojs-http-source-selector"]=t(e.videojs)}(this,function(i){"use strict";function o(e,t){e.prototype=Object.create(t.prototype),(e.prototype.constructor=e).__proto__=t}var a=function(n){function e(e,t){var o;return o=n.call(this,e,t)||this,t.selectable=!0,o}o(e,n);var t=e.prototype;return t.handleClick=function(){var e=this.options_;console.log("Changing quality to:",e.label),this.selected_=!0,this.selected(!0);for(var t=this.player().qualityLevels(),o=0;ot.options_.sortVal?-1:0}),e},e}(r),l={},e=i.registerPlugin||i.plugin,t=function(e){var t=this;this.ready(function(){!function(n,e){if(n.addClass("vjs-http-source-selector"),console.log("videojs-http-source-selector initialized!"),console.log("player.techName_:"+n.techName_),"Html5"!=n.techName_)return;n.on(["loadedmetadata"],function(e){if(n.qualityLevels(),i.log("loadmetadata event"),"undefined"==n.videojs_http_source_selector_initialized||1==n.videojs_http_source_selector_initialized)console.log("player.videojs_http_source_selector_initialized == true");else{console.log("player.videojs_http_source_selector_initialized == false"),n.videojs_http_source_selector_initialized=!0;var t=n.controlBar,o=t.getChild("fullscreenToggle").el();t.el().insertBefore(t.addChild("SourceMenuButton").el(),o)}})}(t,i.mergeOptions(l,e))}),i.registerComponent("SourceMenuButton",n),i.registerComponent("SourceMenuItem",a)};return e("httpSourceSelector",t),t.VERSION="1.1.5",t}); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t(require("video.js")):"function"==typeof define&&define.amd?define(["video.js"],t):(e=e||self)["videojs-http-source-selector"]=t(e.videojs)}(this,function(r){"use strict";function o(e,t){e.prototype=Object.create(t.prototype),(e.prototype.constructor=e).__proto__=t}function s(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}var e=(r=r&&r.hasOwnProperty("default")?r.default:r).getComponent("MenuItem"),t=r.getComponent("Component"),a=function(n){function e(e,t){return t.selectable=!0,t.multiSelectable=!1,n.call(this,e,t)||this}o(e,n);var t=e.prototype;return t.handleClick=function(){var e=this.options_;console.log("Changing quality to:",e.label),n.prototype.handleClick.call(this);for(var t=this.player().qualityLevels(),o=0;ot.options_.sortVal?-1:0}),e},e}(u),l={},i=r.registerPlugin||r.plugin,c=function(e){var t=this;this.ready(function(){!function(n,e){if(n.addClass("vjs-http-source-selector"),console.log("videojs-http-source-selector initialized!"),console.log("player.techName_:"+n.techName_),"Html5"!=n.techName_)return;n.on(["loadedmetadata"],function(e){if(n.qualityLevels(),r.log("loadmetadata event"),"undefined"==n.videojs_http_source_selector_initialized||1==n.videojs_http_source_selector_initialized)console.log("player.videojs_http_source_selector_initialized == true");else{console.log("player.videojs_http_source_selector_initialized == false"),n.videojs_http_source_selector_initialized=!0;var t=n.controlBar,o=t.getChild("fullscreenToggle").el();t.el().insertBefore(t.addChild("SourceMenuButton").el(),o)}})}(t,r.mergeOptions(l,e))}),r.registerComponent("SourceMenuButton",n),r.registerComponent("SourceMenuItem",a)};return i("httpSourceSelector",c),c.VERSION="1.1.6",c}); \ No newline at end of file From b63f469110b6d561e06ba7a39a459b71166ef523 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 9 Aug 2019 14:09:24 -0500 Subject: [PATCH 14/31] Fix typo in ConfigPreferences --- src/invidious/helpers/helpers.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 9256f6b1..ce0ded32 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -73,7 +73,7 @@ struct ConfigPreferences locale: {type: String, default: "en-US"}, max_results: {type: Int32, default: 40}, notifications_only: {type: Bool, default: false}, - video_player: {type: String, default: "invidious"}, + player_style: {type: String, default: "invidious"}, quality: {type: String, default: "hd720"}, redirect_feed: {type: Bool, default: false}, related_videos: {type: Bool, default: true}, From 4c6e92eea1a70215321bb9899f4cd9313c8736fa Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Fri, 9 Aug 2019 19:28:04 +0200 Subject: [PATCH 15/31] travis: also test Docker build --- .travis.yml | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/.travis.yml b/.travis.yml index da787cf1..f5918bb1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,28 @@ -language: crystal - -crystal: - - latest - dist: bionic -before_install: - - shards update - - shards install +jobs: + include: + - stage: build + language: crystal + crystal: latest + before_install: + - shards update + - shards install + install: + - crystal build --error-on-warnings src/invidious.cr + script: + - crystal tool format --check + - crystal spec -install: - - crystal build --error-on-warnings src/invidious.cr - -script: - - crystal tool format --check - - crystal spec + - stage: build_docker + language: minimal + services: + - docker + install: + - docker-compose build + script: + - docker-compose up -d + - sleep 15 # Wait for cluster to become ready, TODO: do not sleep + - HEADERS="$(curl -I -s http://localhost:3000/)" + - STATUS="$(echo $HEADERS | head -n1)" + - if [[ "$STATUS" != *"200 OK"* ]]; then echo "$HEADERS"; exit 1; fi From 00346781bbddd1568b54be908f77216dd87fe454 Mon Sep 17 00:00:00 2001 From: Andrew <30773181+2secslater@users.noreply.github.com> Date: Wed, 14 Aug 2019 20:12:37 +0000 Subject: [PATCH 16/31] Fix annoying typo in Preferences view --- src/invidious/views/preferences.ecr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index d3d8ea0f..b04bcd4d 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -115,7 +115,7 @@ function update_value(element) {
From 2a9a34816442a58a04ceccd05b814034c086535c Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Wed, 14 Aug 2019 23:28:56 +0200 Subject: [PATCH 17/31] Format Crystal files Crystal 0.30.1 apparently introduced some breaking changes to their code formatter which made CI fail. The code was automatically formatted by running crystal tool format --- src/invidious/helpers/handlers.cr | 24 ++++++++++++------------ src/invidious/helpers/proxy.cr | 8 ++++---- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/invidious/helpers/handlers.cr b/src/invidious/helpers/handlers.cr index 7fbfb643..51bc9545 100644 --- a/src/invidious/helpers/handlers.cr +++ b/src/invidious/helpers/handlers.cr @@ -69,20 +69,20 @@ class FilteredCompressHandler < Kemal::Handler return call_next env if exclude_match? env {% if flag?(:without_zlib) %} - call_next env - {% else %} - request_headers = env.request.headers + call_next env + {% else %} + request_headers = env.request.headers - if request_headers.includes_word?("Accept-Encoding", "gzip") - env.response.headers["Content-Encoding"] = "gzip" - env.response.output = Gzip::Writer.new(env.response.output, sync_close: true) - elsif request_headers.includes_word?("Accept-Encoding", "deflate") - env.response.headers["Content-Encoding"] = "deflate" - env.response.output = Flate::Writer.new(env.response.output, sync_close: true) - end + if request_headers.includes_word?("Accept-Encoding", "gzip") + env.response.headers["Content-Encoding"] = "gzip" + env.response.output = Gzip::Writer.new(env.response.output, sync_close: true) + elsif request_headers.includes_word?("Accept-Encoding", "deflate") + env.response.headers["Content-Encoding"] = "deflate" + env.response.output = Flate::Writer.new(env.response.output, sync_close: true) + end - call_next env - {% end %} + call_next env + {% end %} end end diff --git a/src/invidious/helpers/proxy.cr b/src/invidious/helpers/proxy.cr index e3c9d2f5..fde282cd 100644 --- a/src/invidious/helpers/proxy.cr +++ b/src/invidious/helpers/proxy.cr @@ -31,10 +31,10 @@ class HTTPProxy if resp[:code]? == 200 {% if !flag?(:without_openssl) %} - if tls - tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host) - socket = tls_socket - end + if tls + tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host) + socket = tls_socket + end {% end %} return socket From 52f71cdda051ede571c83efd9d120ff6223b3c66 Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Wed, 14 Aug 2019 23:44:03 +0200 Subject: [PATCH 18/31] shard: update dependencies This updates will/crystal-pg to 0.18.1 and kemalcr/kemal tp 0.26.0. --- shard.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shard.yml b/shard.yml index 09c54a51..236e7e1c 100644 --- a/shard.yml +++ b/shard.yml @@ -11,13 +11,13 @@ targets: dependencies: pg: github: will/crystal-pg - version: ~> 0.18.0 + version: ~> 0.18.1 sqlite3: github: crystal-lang/crystal-sqlite3 version: ~> 0.13.0 kemal: github: kemalcr/kemal - version: ~> 0.25.2 + version: ~> 0.26.0 crystal: 0.30.0 From 10d690c8fb8741313c422e237c47e3cf540cfb24 Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Wed, 14 Aug 2019 23:44:27 +0200 Subject: [PATCH 19/31] shard: update to crystal 0.30.1 --- shard.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shard.yml b/shard.yml index 236e7e1c..0f9beaf2 100644 --- a/shard.yml +++ b/shard.yml @@ -19,6 +19,6 @@ dependencies: github: kemalcr/kemal version: ~> 0.26.0 -crystal: 0.30.0 +crystal: 0.30.1 license: AGPLv3 From 900d8790b32a7356d5d0a7c96484f436010c09fc Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Tue, 13 Aug 2019 15:21:00 -0500 Subject: [PATCH 20/31] Refactor geo-bypass --- src/invidious/videos.cr | 40 +++++++++++----------------------------- 1 file changed, 11 insertions(+), 29 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index cf83d6c2..7fd06dd5 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -1132,35 +1132,20 @@ def fetch_video(id, region) info = extract_player_config(response.body, html) info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") - # Try to use proxies for region-blocked videos + allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",") + allowed_regions ||= [] of String + + # Check for region-blocks if info["reason"]? && info["reason"].includes? "your country" - bypass_channel = Channel({XML::Node, HTTP::Params} | Nil).new + region = allowed_regions.sample(1)[0]? + client = make_client(YT_URL, region) + response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999") - PROXY_LIST.each do |proxy_region, list| - spawn do - client = make_client(YT_URL, proxy_region) - proxy_response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999") + html = XML.parse_html(response.body) + info = extract_player_config(response.body, html) - proxy_html = XML.parse_html(proxy_response.body) - proxy_info = extract_player_config(proxy_response.body, proxy_html) - - if !proxy_info["reason"]? - proxy_info["region"] = proxy_region - proxy_info["cookie"] = proxy_response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") - bypass_channel.send({proxy_html, proxy_info}) - else - bypass_channel.send(nil) - end - end - end - - PROXY_LIST.size.times do - response = bypass_channel.receive - if response - html, info = response - break - end - end + info["region"] = region if region + info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") end # Try to pull streams from embed URL @@ -1215,9 +1200,6 @@ def fetch_video(id, region) published ||= Time.utc.to_s("%Y-%m-%d") published = Time.parse(published, "%Y-%m-%d", Time::Location.local) - allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",") - allowed_regions ||= [] of String - is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).try &.["content"] == "True" is_family_friendly ||= true From 567cda4cd35eec6343d6cdba984b7960fe0b52eb Mon Sep 17 00:00:00 2001 From: Leon Klingele Date: Thu, 15 Aug 2019 01:37:25 +0200 Subject: [PATCH 21/31] docker: use alpine:edge base image for building This fixes currently failing Docker builds. kemalcr/kemal in version 0.26.0 requires Crystal 0.30.0 which is not yet available on Alpine 3.10 (previously used as the Docker base image). --- docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index c9fa6367..45fade57 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:latest AS builder +FROM alpine:edge AS builder RUN apk add -u crystal shards libc-dev \ yaml-dev libxml2-dev sqlite-dev sqlite-static zlib-dev openssl-dev WORKDIR /invidious From f54fbd057ebb8d1e9631782b6d182b403598648f Mon Sep 17 00:00:00 2001 From: psvenk <45520974+psvenk@users.noreply.github.com> Date: Thu, 15 Aug 2019 16:29:55 +0000 Subject: [PATCH 22/31] Add prefers-color-scheme support (#601) * Add prefers-color-scheme support This should fix . The cookie storage format has been changed from boolean ("true"/"false") to tri-state ("dark"/"light"/""), so that users without a cookie set will get dark mode if they have enabled the dark theme in their operating system. The code for handling the cookie state, along with the user's operating system theme, has been factored out into a new function `update_mode`, which is called both at window load and at the "storage" event listener, because the "storage" event listener is only trigerred when a change is made to the localStorage from another tab/window (for more info - see ). --- assets/js/themes.js | 37 ++++++++++--- locales/ar.json | 4 ++ locales/de.json | 4 ++ locales/el.json | 4 ++ locales/en-US.json | 4 ++ locales/eo.json | 4 ++ locales/es.json | 4 ++ locales/eu.json | 4 ++ locales/fr.json | 4 ++ locales/is.json | 4 ++ locales/it.json | 4 ++ locales/nb_NO.json | 4 ++ locales/nl.json | 4 ++ locales/pl.json | 4 ++ locales/ru.json | 4 ++ locales/uk.json | 4 ++ locales/zh-CN.json | 4 ++ src/invidious.cr | 30 ++++++++--- src/invidious/helpers/helpers.cr | 74 ++++++++++++++++++++++++-- src/invidious/helpers/patch_mapping.cr | 2 +- src/invidious/helpers/utils.cr | 13 +++++ src/invidious/users.cr | 62 ++------------------- src/invidious/views/preferences.ecr | 8 ++- src/invidious/views/template.ecr | 9 ++-- 24 files changed, 215 insertions(+), 84 deletions(-) diff --git a/assets/js/themes.js b/assets/js/themes.js index 683aea39..90a05c36 100644 --- a/assets/js/themes.js +++ b/assets/js/themes.js @@ -1,8 +1,8 @@ -var toggle_theme = document.getElementById('toggle_theme') +var toggle_theme = document.getElementById('toggle_theme'); toggle_theme.href = 'javascript:void(0);'; toggle_theme.addEventListener('click', function () { - var dark_mode = document.getElementById('dark_theme').media == 'none'; + var dark_mode = document.getElementById('dark_theme').media === 'none'; var url = '/toggle_theme?redirect=false'; var xhr = new XMLHttpRequest(); @@ -11,19 +11,24 @@ toggle_theme.addEventListener('click', function () { xhr.open('GET', url, true); set_mode(dark_mode); - localStorage.setItem('dark_mode', dark_mode); + window.localStorage.setItem('dark_mode', dark_mode ? 'dark' : 'light'); xhr.send(); }); window.addEventListener('storage', function (e) { - if (e.key == 'dark_mode') { - var dark_mode = e.newValue === 'true'; - set_mode(dark_mode); + if (e.key === 'dark_mode') { + update_mode(e.newValue); } }); -function set_mode(bool) { +window.addEventListener('load', function () { + window.localStorage.setItem('dark_mode', document.getElementById('dark_mode_pref').textContent); + // Update localStorage if dark mode preference changed on preferences page + update_mode(window.localStorage.dark_mode); +}); + +function set_mode (bool) { document.getElementById('dark_theme').media = !bool ? 'none' : ''; document.getElementById('light_theme').media = bool ? 'none' : ''; @@ -33,3 +38,21 @@ function set_mode(bool) { toggle_theme.children[0].setAttribute('class', 'icon ion-ios-moon'); } } + +function update_mode (mode) { + if (mode === 'true' /* for backwards compatibility */ || mode === 'dark') { + // If preference for dark mode indicated + set_mode(true); + } + else if (mode === 'false' /* for backwards compaibility */ || mode === 'light') { + // If preference for light mode indicated + set_mode(false); + } + else if (document.getElementById('dark_mode_pref').textContent === '' && window.matchMedia('(prefers-color-scheme: dark)').matches) { + // If no preference indicated here and no preference indicated on the preferences page (backend), but the browser tells us that the operating system has a dark theme + set_mode(true); + } + // else do nothing, falling back to the mode defined by the `dark_mode` preference on the preferences page (backend) +} + + diff --git a/locales/ar.json b/locales/ar.json index e3716008..f2ed450c 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -68,7 +68,11 @@ "Show related videos: ": "عرض مقاطع الفيديو ذات الصلة؟", "Show annotations by default: ": "عرض الملاحظات فى الفيديو تلقائيا ؟", "Visual preferences": "التفضيلات المرئية", + "Player style: ": "", "Dark mode: ": "الوضع الليلى: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "الوضع الخفيف: ", "Subscription preferences": "تفضيلات الإشتراك", "Show annotations by default for subscribed channels: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟", diff --git a/locales/de.json b/locales/de.json index 33edb706..e0ef9d67 100644 --- a/locales/de.json +++ b/locales/de.json @@ -68,7 +68,11 @@ "Show related videos: ": "Ähnliche Videos anzeigen? ", "Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ", "Visual preferences": "Anzeigeeinstellungen", + "Player style: ": "", "Dark mode: ": "Nachtmodus: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Schlanker Modus: ", "Subscription preferences": "Abonnementeinstellungen", "Show annotations by default for subscribed channels: ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ", diff --git a/locales/el.json b/locales/el.json index 03d25533..32f154a1 100644 --- a/locales/el.json +++ b/locales/el.json @@ -74,7 +74,11 @@ "Show related videos: ": "Προβολή σχετικών βίντεο; ", "Show annotations by default: ": "Αυτόματη προβολή σημειώσεων; :", "Visual preferences": "Προτιμήσεις εμφάνισης", + "Player style: ": "", "Dark mode: ": "Σκοτεινή λειτουργία: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Ελαφριά λειτουργία: ", "Subscription preferences": "Προτιμήσεις συνδρομών", "Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ", diff --git a/locales/en-US.json b/locales/en-US.json index 16301b1d..580d9ead 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -74,7 +74,11 @@ "Show related videos: ": "Show related videos: ", "Show annotations by default: ": "Show annotations by default: ", "Visual preferences": "Visual preferences", + "Player style: ": "", "Dark mode: ": "Dark mode: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Thin mode: ", "Subscription preferences": "Subscription preferences", "Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ", diff --git a/locales/eo.json b/locales/eo.json index fe85edf2..2c22af59 100644 --- a/locales/eo.json +++ b/locales/eo.json @@ -68,7 +68,11 @@ "Show related videos: ": "Ĉu montri rilatajn videojn? ", "Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ", "Visual preferences": "Vidaj preferoj", + "Player style: ": "", "Dark mode: ": "Malhela reĝimo: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Maldika reĝimo: ", "Subscription preferences": "Abonaj agordoj", "Show annotations by default for subscribed channels: ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ", diff --git a/locales/es.json b/locales/es.json index fdbf2fb2..5860b882 100644 --- a/locales/es.json +++ b/locales/es.json @@ -68,7 +68,11 @@ "Show related videos: ": "¿Mostrar vídeos relacionados? ", "Show annotations by default: ": "¿Mostrar anotaciones por defecto? ", "Visual preferences": "Preferencias visuales", + "Player style: ": "", "Dark mode: ": "Modo oscuro: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Modo compacto: ", "Subscription preferences": "Preferencias de la suscripción", "Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ", diff --git a/locales/eu.json b/locales/eu.json index 5a756154..cbdbbefc 100644 --- a/locales/eu.json +++ b/locales/eu.json @@ -68,7 +68,11 @@ "Show related videos: ": "", "Show annotations by default: ": "", "Visual preferences": "", + "Player style: ": "", "Dark mode: ": "", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "", "Subscription preferences": "", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/fr.json b/locales/fr.json index 37a773f1..af561a0c 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -68,7 +68,11 @@ "Show related videos: ": "Voir les vidéos liées : ", "Show annotations by default: ": "Voir les annotations par défaut : ", "Visual preferences": "Préférences du site", + "Player style: ": "", "Dark mode: ": "Mode Sombre : ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Mode Simplifié : ", "Subscription preferences": "Préférences de la page d'abonnements", "Show annotations by default for subscribed channels: ": "Voir les annotations par défaut sur les chaînes suivies : ", diff --git a/locales/is.json b/locales/is.json index 43ba26e9..808063c4 100644 --- a/locales/is.json +++ b/locales/is.json @@ -68,7 +68,11 @@ "Show related videos: ": "Sýna tengd myndbönd? ", "Show annotations by default: ": "Á að sýna glósur sjálfgefið? ", "Visual preferences": "Sjónrænar stillingar", + "Player style: ": "", "Dark mode: ": "Myrkur ham: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Þunnt ham: ", "Subscription preferences": "Áskriftarstillingar", "Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ", diff --git a/locales/it.json b/locales/it.json index bf028c91..7f532d0d 100644 --- a/locales/it.json +++ b/locales/it.json @@ -68,7 +68,11 @@ "Show related videos: ": "Mostra video correlati? ", "Show annotations by default: ": "Mostra le annotazioni per impostazione predefinita? ", "Visual preferences": "Preferenze grafiche", + "Player style: ": "", "Dark mode: ": "Tema scuro: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Modalità per connessioni lente: ", "Subscription preferences": "Preferenze iscrizioni", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/nb_NO.json b/locales/nb_NO.json index 589512d4..f50e2290 100644 --- a/locales/nb_NO.json +++ b/locales/nb_NO.json @@ -68,7 +68,11 @@ "Show related videos: ": "Vis relaterte videoer? ", "Show annotations by default: ": "Vis merknader som forvalg? ", "Visual preferences": "Visuelle innstillinger", + "Player style: ": "", "Dark mode: ": "Mørk drakt: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Tynt modus: ", "Subscription preferences": "Abonnementsinnstillinger", "Show annotations by default for subscribed channels: ": "Vis merknader som forvalg for kanaler det abonneres på? ", diff --git a/locales/nl.json b/locales/nl.json index b24b13b8..3e2c6c64 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -68,7 +68,11 @@ "Show related videos: ": "Gerelateerde video's tonen? ", "Show annotations by default: ": "Standaard annotaties tonen? ", "Visual preferences": "Visuele instellingen", + "Player style: ": "", "Dark mode: ": "Donkere modus: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Smalle modus: ", "Subscription preferences": "Abonnementsinstellingen", "Show annotations by default for subscribed channels: ": "Standaard annotaties tonen voor geabonneerde kanalen? ", diff --git a/locales/pl.json b/locales/pl.json index 550e664b..1e3a2068 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -68,7 +68,11 @@ "Show related videos: ": "Pokaż powiązane filmy? ", "Show annotations by default: ": "", "Visual preferences": "Preferencje Wizualne", + "Player style: ": "", "Dark mode: ": "Ciemny motyw: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Tryb minimalny: ", "Subscription preferences": "Preferencje subskrybcji", "Show annotations by default for subscribed channels: ": "", diff --git a/locales/ru.json b/locales/ru.json index f9cc1e2d..90aa4a3b 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -68,7 +68,11 @@ "Show related videos: ": "Показывать похожие видео? ", "Show annotations by default: ": "Всегда показывать аннотации? ", "Visual preferences": "Настройки сайта", + "Player style: ": "", "Dark mode: ": "Тёмное оформление: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Облегчённое оформление: ", "Subscription preferences": "Настройки подписок", "Show annotations by default for subscribed channels: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ", diff --git a/locales/uk.json b/locales/uk.json index 95e5798d..e537008c 100644 --- a/locales/uk.json +++ b/locales/uk.json @@ -68,7 +68,11 @@ "Show related videos: ": "Показувати схожі відео? ", "Show annotations by default: ": "Завжди показувати анотації? ", "Visual preferences": "Налаштування сайту", + "Player style: ": "", "Dark mode: ": "Темне оформлення: ", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "Полегшене оформлення: ", "Subscription preferences": "Налаштування підписок", "Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 0a3f53d9..23617d04 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -68,7 +68,11 @@ "Show related videos: ": "显示相关视频?", "Show annotations by default: ": "默认显示视频注释?", "Visual preferences": "视觉选项", + "Player style: ": "", "Dark mode: ": "暗色模式:", + "Theme: ": "", + "dark": "", + "light": "", "Thin mode: ": "窄页模式:", "Subscription preferences": "订阅设置", "Show annotations by default for subscribed channels: ": "在订阅频道的视频默认显示注释?", diff --git a/src/invidious.cr b/src/invidious.cr index 16695c5f..712a408f 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -267,8 +267,7 @@ before_all do |env| end end - dark_mode = env.params.query["dark_mode"]? || preferences.dark_mode.to_s - dark_mode = dark_mode == "true" + dark_mode = convert_theme(env.params.query["dark_mode"]?) || preferences.dark_mode.to_s thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s thin_mode = thin_mode == "true" @@ -1528,8 +1527,7 @@ post "/preferences" do |env| locale ||= CONFIG.default_user_preferences.locale dark_mode = env.params.body["dark_mode"]?.try &.as(String) - dark_mode ||= "off" - dark_mode = dark_mode == "on" + dark_mode ||= CONFIG.default_user_preferences.dark_mode thin_mode = env.params.body["thin_mode"]?.try &.as(String) thin_mode ||= "off" @@ -1553,6 +1551,7 @@ post "/preferences" do |env| notifications_only ||= "off" notifications_only = notifications_only == "on" + # Convert to JSON and back again to take advantage of converters used for compatability preferences = Preferences.from_json({ annotations: annotations, annotations_subscribed: annotations_subscribed, @@ -1648,12 +1647,27 @@ get "/toggle_theme" do |env| if user = env.get? "user" user = user.as(User) preferences = user.preferences - preferences.dark_mode = !preferences.dark_mode - PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences.to_json, user.email) + 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) else preferences = env.get("preferences").as(Preferences) - preferences.dark_mode = !preferences.dark_mode + + case preferences.dark_mode + when "dark" + preferences.dark_mode = "light" + else + preferences.dark_mode = "dark" + end + preferences = preferences.to_json if Kemal.config.ssl || config.https_only @@ -2026,7 +2040,7 @@ post "/data_control" do |env| env.response.puts %() env.response.puts %() env.response.puts %() - if env.get("preferences").as(Preferences).dark_mode + if env.get("preferences").as(Preferences).dark_mode == "dark" env.response.puts %() else env.response.puts %() diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index ce0ded32..03c1654c 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -24,6 +24,27 @@ end struct ConfigPreferences module StringToArray + def self.to_json(value : Array(String), json : JSON::Builder) + json.array do + value.each do |element| + json.string element + end + end + end + + def self.from_json(value : JSON::PullParser) : Array(String) + begin + result = [] of String + value.read_array do + result << HTML.escape(value.read_string[0, 100]) + end + rescue ex + result = [HTML.escape(value.read_string[0, 100]), ""] + end + + result + end + def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder) yaml.sequence do value.each do |element| @@ -44,11 +65,11 @@ struct ConfigPreferences node.raise "Expected scalar, not #{item.class}" end - result << item.value + result << HTML.escape(item.value[0, 100]) end rescue ex if node.is_a?(YAML::Nodes::Scalar) - result = [node.value, ""] + result = [HTML.escape(node.value[0, 100]), ""] else result = ["", ""] end @@ -58,6 +79,53 @@ struct ConfigPreferences end end + module BoolToString + def self.to_json(value : String, json : JSON::Builder) + json.string value + end + + def self.from_json(value : JSON::PullParser) : String + begin + result = value.read_string + + if result.empty? + CONFIG.default_user_preferences.dark_mode + else + result + end + rescue ex + result = value.read_bool + + if result + "dark" + else + "light" + end + end + end + + def self.to_yaml(value : String, yaml : YAML::Nodes::Builder) + yaml.scalar value + end + + def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String + unless node.is_a?(YAML::Nodes::Scalar) + node.raise "Expected sequence, not #{node.class}" + end + + case node.value + when "true" + "dark" + when "false" + "light" + when "" + CONFIG.default_user_preferences.dark_mode + else + node.value + end + end + end + yaml_mapping({ annotations: {type: Bool, default: false}, annotations_subscribed: {type: Bool, default: false}, @@ -66,7 +134,7 @@ struct ConfigPreferences comments: {type: Array(String), default: ["youtube", ""], converter: StringToArray}, continue: {type: Bool, default: false}, continue_autoplay: {type: Bool, default: true}, - dark_mode: {type: Bool, default: false}, + dark_mode: {type: String, default: "", converter: BoolToString}, latest_only: {type: Bool, default: false}, listen: {type: Bool, default: false}, local: {type: Bool, default: false}, diff --git a/src/invidious/helpers/patch_mapping.cr b/src/invidious/helpers/patch_mapping.cr index 8360caa6..e138aa1c 100644 --- a/src/invidious/helpers/patch_mapping.cr +++ b/src/invidious/helpers/patch_mapping.cr @@ -4,7 +4,7 @@ def Object.from_json(string_or_io, default) : self new parser, default end -# Adds configurable 'default' to +# Adds configurable 'default' macro patched_json_mapping(_properties_, strict = false) {% for key, value in _properties_ %} {% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %} diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 69aae839..b39f65c5 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -356,3 +356,16 @@ def parse_range(range) return 0_i64, nil end + +def convert_theme(theme) + case theme + when "true" + "dark" + when "false" + "light" + when "", nil + nil + else + theme + end +end diff --git a/src/invidious/users.cr b/src/invidious/users.cr index 35d8a49e..8bd82bf1 100644 --- a/src/invidious/users.cr +++ b/src/invidious/users.cr @@ -31,62 +31,6 @@ struct User end struct Preferences - module StringToArray - def self.to_json(value : Array(String), json : JSON::Builder) - json.array do - value.each do |element| - json.string element - end - end - end - - def self.from_json(value : JSON::PullParser) : Array(String) - begin - result = [] of String - value.read_array do - result << HTML.escape(value.read_string[0, 100]) - end - rescue ex - result = [HTML.escape(value.read_string[0, 100]), ""] - end - - result - end - - def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder) - yaml.sequence do - value.each do |element| - yaml.scalar element - end - end - end - - def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(String) - begin - unless node.is_a?(YAML::Nodes::Sequence) - node.raise "Expected sequence, not #{node.class}" - end - - result = [] of String - node.nodes.each do |item| - unless item.is_a?(YAML::Nodes::Scalar) - node.raise "Expected scalar, not #{item.class}" - end - - result << HTML.escape(item.value[0, 100]) - end - rescue ex - if node.is_a?(YAML::Nodes::Scalar) - result = [HTML.escape(node.value[0, 100]), ""] - else - result = ["", ""] - end - end - - result - end - end - module ProcessString def self.to_json(value : String, json : JSON::Builder) json.string value @@ -127,11 +71,11 @@ struct Preferences annotations: {type: Bool, default: CONFIG.default_user_preferences.annotations}, annotations_subscribed: {type: Bool, default: CONFIG.default_user_preferences.annotations_subscribed}, autoplay: {type: Bool, default: CONFIG.default_user_preferences.autoplay}, - captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: StringToArray}, - comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: StringToArray}, + captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: ConfigPreferences::StringToArray}, + comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: ConfigPreferences::StringToArray}, continue: {type: Bool, default: CONFIG.default_user_preferences.continue}, continue_autoplay: {type: Bool, default: CONFIG.default_user_preferences.continue_autoplay}, - dark_mode: {type: Bool, default: CONFIG.default_user_preferences.dark_mode}, + dark_mode: {type: String, default: CONFIG.default_user_preferences.dark_mode, converter: ConfigPreferences::BoolToString}, latest_only: {type: Bool, default: CONFIG.default_user_preferences.latest_only}, listen: {type: Bool, default: CONFIG.default_user_preferences.listen}, local: {type: Bool, default: CONFIG.default_user_preferences.local}, diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr index b04bcd4d..6ea01fba 100644 --- a/src/invidious/views/preferences.ecr +++ b/src/invidious/views/preferences.ecr @@ -122,8 +122,12 @@ function update_value(element) {
- - checked<% end %>> + +
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr index 6272d2be..8d8cec88 100644 --- a/src/invidious/views/template.ecr +++ b/src/invidious/views/template.ecr @@ -18,13 +18,14 @@ - media="none"<% end %>> - media="none"<% end %>> + media="none"<% end %>> + media="none"<% end %>> <% locale = LOCALES[env.get("preferences").as(Preferences).locale]? %> +
@@ -43,7 +44,7 @@ <% if env.get? "user" %>
" class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode %> + <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> <% else %> @@ -76,7 +77,7 @@ <% else %>
" class="pure-menu-heading"> - <% if env.get("preferences").as(Preferences).dark_mode %> + <% if env.get("preferences").as(Preferences).dark_mode == "dark" %> <% else %> From a19cdb5e7251cb5b51d08e0c844119d8267440b3 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 16 Aug 2019 15:46:37 -0500 Subject: [PATCH 23/31] Fix season playlists --- src/invidious.cr | 40 +++++++++++++++++++++++-- src/invidious/helpers/helpers.cr | 30 +++++++------------ src/invidious/search.cr | 14 ++++----- src/invidious/views/components/item.ecr | 4 +-- 4 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 712a408f..21d6544d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3047,8 +3047,7 @@ get "/channel/:ucid" do |env| item.author end end - items.select! { |item| item.responds_to?(:thumbnail_id) && item.thumbnail_id } - items = items.map { |item| item.as(SearchPlaylist) } + items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) } items.each { |item| item.author = "" } else sort_options = {"newest", "oldest", "popular"} @@ -5086,6 +5085,43 @@ get "/sb/:id/:storyboard/:index" do |env| end end +get "/s_p/:id/:name" do |env| + id = env.params.url["id"] + name = env.params.url["name"] + + host = "https://i9.ytimg.com" + client = make_client(URI.parse(host)) + url = env.request.resource + + headers = HTTP::Headers.new + REQUEST_HEADERS_WHITELIST.each do |header| + if env.request.headers[header]? + headers[header] = env.request.headers[header] + end + end + + begin + client.get(url, headers) do |response| + env.response.status_code = response.status_code + response.headers.each do |key, value| + if !RESPONSE_HEADERS_BLACKLIST.includes? key + env.response.headers[key] = value + end + 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 "/vi/:id/:name" do |env| id = env.params.url["id"] name = env.params.url["name"] diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 03c1654c..e018e567 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -312,8 +312,7 @@ end def extract_videos(nodeset, ucid = nil, author_name = nil) videos = extract_items(nodeset, ucid, author_name) - videos.select! { |item| !item.is_a?(SearchChannel | SearchPlaylist) } - videos.map { |video| video.as(SearchVideo) } + videos.select { |item| item.is_a?(SearchVideo) }.map { |video| video.as(SearchVideo) } end def extract_items(nodeset, ucid = nil, author_name = nil) @@ -361,14 +360,14 @@ def extract_items(nodeset, ucid = nil, author_name = nil) anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a)) end - video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) + video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) || + node.xpath_node(%q(.//span[@class="formatted-video-count-label"])) if video_count video_count = video_count.content if video_count == "50+" author = "YouTube" author_id = "UC-9-kyTW8ZkZNDHQJ6FgpwQ" - video_count = video_count.rchop("+") end video_count = video_count.gsub(/\D/, "").to_i? @@ -400,11 +399,6 @@ def extract_items(nodeset, ucid = nil, author_name = nil) playlist_thumbnail = node.xpath_node(%q(.//div/span/img)).try &.["data-thumb"]? playlist_thumbnail ||= node.xpath_node(%q(.//div/span/img)).try &.["src"] - if !playlist_thumbnail || playlist_thumbnail.empty? - thumbnail_id = videos[0]?.try &.id - else - thumbnail_id = playlist_thumbnail.match(/\/vi\/(?[a-zA-Z0-9_-]{11})\/\w+\.jpg/).try &.["video_id"] - end items << SearchPlaylist.new( title, @@ -413,7 +407,7 @@ def extract_items(nodeset, ucid = nil, author_name = nil) author_id, video_count, videos, - thumbnail_id + playlist_thumbnail ) when .includes? "yt-lockup-channel" author = title.strip @@ -586,15 +580,11 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) playlist_thumbnail = child_node.xpath_node(%q(.//span/img)).try &.["data-thumb"]? playlist_thumbnail ||= child_node.xpath_node(%q(.//span/img)).try &.["src"] - if !playlist_thumbnail || playlist_thumbnail.empty? - thumbnail_id = videos[0]?.try &.id - else - thumbnail_id = playlist_thumbnail.match(/\/vi\/(?[a-zA-Z0-9_-]{11})\/\w+\.jpg/).try &.["video_id"] - end - video_count_label = child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"])) - if video_count_label - video_count = video_count_label.content.gsub(/\D/, "").to_i? + video_count = child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) || + child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"])) + if video_count + video_count = video_count.content.gsub(/\D/, "").to_i? end video_count ||= 50 @@ -605,7 +595,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) ucid, video_count, Array(SearchPlaylistVideo).new, - thumbnail_id + playlist_thumbnail ) end end @@ -620,7 +610,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) ucid, videos.size, videos, - videos[0].try &.id + "/vi/#{videos[0].id}/mqdefault.jpg" ) end end diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 1d4805bf..784e3897 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -152,13 +152,13 @@ struct SearchPlaylist end db_mapping({ - title: String, - id: String, - author: String, - ucid: String, - video_count: Int32, - videos: Array(SearchPlaylistVideo), - thumbnail_id: String?, + title: String, + id: String, + author: String, + ucid: String, + video_count: Int32, + videos: Array(SearchPlaylistVideo), + thumbnail: String?, }) end diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr index 28e70058..71ae70df 100644 --- a/src/invidious/views/components/item.ecr +++ b/src/invidious/views/components/item.ecr @@ -15,7 +15,7 @@
<%= item.description_html %>
<% when SearchPlaylist %> <% if item.id.starts_with? "RD" %> - <% url = "/mix?list=#{item.id}&continuation=#{item.thumbnail_id}" %> + <% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").full_path.split("/")[2]}" %> <% else %> <% url = "/playlist?list=#{item.id}" %> <% end %> @@ -23,7 +23,7 @@
<% if !env.get("preferences").as(Preferences).thin_mode %>
- + "/>

<%= number_with_separator(item.video_count) %> videos

<% end %> From 7eaac995bd549b03f53930fbbfc5d77e2b051362 Mon Sep 17 00:00:00 2001 From: Dragnucs Date: Fri, 16 Aug 2019 20:59:05 +0000 Subject: [PATCH 24/31] Change font family to better native selection (#679) --- assets/css/default.css | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 4b81d9f5..451fbc8c 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -1,3 +1,8 @@ +html, +body { + font-family: BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif; +} + .deleted { background-color: rgb(255, 0, 0, 0.5); } @@ -113,7 +118,6 @@ img.thumbnail { border-radius: 2px; padding: 2px; font-size: 16px; - font-family: sans-serif; right: 0.25em; bottom: -0.75em; } @@ -126,7 +130,6 @@ img.thumbnail { border-radius: 2px; padding: 4px 8px 4px 8px; font-size: 16px; - font-family: sans-serif; left: 0.2em; top: -0.7em; } From e6b4e1268945777c5d07dfca4362a1af23f6d970 Mon Sep 17 00:00:00 2001 From: leonklingele <5585491+leonklingele@users.noreply.github.com> Date: Fri, 16 Aug 2019 23:01:14 +0200 Subject: [PATCH 25/31] js: add support for keydown events (#678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * js: add support for keydown events This will modify the player behavior even if the player element is unfocused. Based on the YouTube key bindings, allow to - toggle playback with space and 'k' key - increase and decrease player volume with up / down arrow key - mute and unmute player with 'm' key - jump forwards and backwards by 5 seconds with right / left arrow key - jump forwards and backwards by 10 seconds with 'l' / 'j' key - set video progress with number keys 0–9 - toggle captions with 'c' key - toggle fullscreen mode with 'f' key - play next video with 'N' key - increase and decrease playback speed with '>' / '<' key * js: remove unused dependency 'videojs.hotkeys.min.js' Support for controlling the player volume by scrolling over it is still retained by copying over the relevant code part from the aforementioned library. --- assets/js/player.js | 332 ++++++++++++++---- assets/js/videojs.hotkeys.min.js | 2 - assets/js/watch.js | 44 +-- .../views/components/player_sources.ecr | 1 - src/invidious/views/licenses.ecr | 14 - 5 files changed, 293 insertions(+), 100 deletions(-) delete mode 100644 assets/js/videojs.hotkeys.min.js diff --git a/assets/js/player.js b/assets/js/player.js index 25cbb18b..4a61258c 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -38,69 +38,7 @@ var shareOptions = { embedCode: "" } -var player = videojs('player', options, function () { - this.hotkeys({ - volumeStep: 0.1, - seekStep: 5, - enableModifiersForNumbers: false, - enableHoverScroll: true, - customKeys: { - // Toggle play with K Key - play: { - key: function (e) { - return e.which === 75; - }, - handler: function (player, options, e) { - if (player.paused()) { - player.play(); - } else { - player.pause(); - } - } - }, - // Go backward 10 seconds - backward: { - key: function (e) { - return e.which === 74; - }, - handler: function (player, options, e) { - player.currentTime(player.currentTime() - 10); - } - }, - // Go forward 10 seconds - forward: { - key: function (e) { - return e.which === 76; - }, - handler: function (player, options, e) { - player.currentTime(player.currentTime() + 10); - } - }, - // Increase speed - increase_speed: { - key: function (e) { - return (e.which === 190 && e.shiftKey); - }, - handler: function (player, _, e) { - size = options.playbackRates.length; - index = options.playbackRates.indexOf(player.playbackRate()); - player.playbackRate(options.playbackRates[(index + 1) % size]); - } - }, - // Decrease speed - decrease_speed: { - key: function (e) { - return (e.which === 188 && e.shiftKey); - }, - handler: function (player, _, e) { - size = options.playbackRates.length; - index = options.playbackRates.indexOf(player.playbackRate()); - player.playbackRate(options.playbackRates[(size + index - 1) % size]); - } - } - } - }); -}); +var player = videojs('player', options); if (location.pathname.startsWith('/embed/')) { player.overlay({ @@ -254,5 +192,273 @@ if (!video_data.params.listen && video_data.params.annotations) { xhr.send(); } +function increase_volume(delta) { + const curVolume = player.volume(); + let newVolume = curVolume + delta; + if (newVolume > 1) { + newVolume = 1; + } else if (newVolume < 0) { + newVolume = 0; + } + player.volume(newVolume); +} + +function toggle_muted() { + const isMuted = player.muted(); + player.muted(!isMuted); +} + +function skip_seconds(delta) { + const duration = player.duration(); + const curTime = player.currentTime(); + let newTime = curTime + delta; + if (newTime > duration) { + newTime = duration; + } else if (newTime < 0) { + newTime = 0; + } + player.currentTime(newTime); +} + +function set_time_percent(percent) { + const duration = player.duration(); + const newTime = duration * (percent / 100); + player.currentTime(newTime); +} + +function toggle_play() { + if (player.paused()) { + player.play(); + } else { + player.pause(); + } +} + +const toggle_captions = (function() { + let toggledTrack = null; + const onChange = function(e) { + toggledTrack = null; + }; + const bindChange = function(onOrOff) { + player.textTracks()[onOrOff]('change', onChange); + }; + // Wrapper function to ignore our own emitted events and only listen + // to events emitted by Video.js on click on the captions menu items. + const setMode = function(track, mode) { + bindChange('off'); + track.mode = mode; + window.setTimeout(function() { + bindChange('on'); + }, 0); + }; + bindChange('on'); + return function() { + if (toggledTrack !== null) { + if (toggledTrack.mode !== 'showing') { + setMode(toggledTrack, 'showing'); + } else { + setMode(toggledTrack, 'disabled'); + } + toggledTrack = null; + return; + } + + // Used as a fallback if no captions are currently active. + // TODO: Make this more intelligent by e.g. relying on browser language. + let fallbackCaptionsTrack = null; + + const tracks = player.textTracks(); + for (let i = 0; i < tracks.length; i++) { + const track = tracks[i]; + if (track.kind !== 'captions') { + continue; + } + + if (fallbackCaptionsTrack === null) { + fallbackCaptionsTrack = track; + } + if (track.mode === 'showing') { + setMode(track, 'disabled'); + toggledTrack = track; + return; + } + } + + // Fallback if no captions are currently active. + if (fallbackCaptionsTrack !== null) { + setMode(fallbackCaptionsTrack, 'showing'); + toggledTrack = fallbackCaptionsTrack; + } + }; +})(); + +function toggle_fullscreen() { + if (player.isFullscreen()) { + player.exitFullscreen(); + } else { + player.requestFullscreen(); + } +} + +function increase_playback_rate(steps) { + const maxIndex = options.playbackRates.length - 1; + const curIndex = options.playbackRates.indexOf(player.playbackRate()); + let newIndex = curIndex + steps; + if (newIndex > maxIndex) { + newIndex = maxIndex; + } else if (newIndex < 0) { + newIndex = 0; + } + player.playbackRate(options.playbackRates[newIndex]); +} + +window.addEventListener('keydown', e => { + if (e.target.tagName.toLowerCase() === 'input') { + // Ignore input when focus is on certain elements, e.g. form fields. + return; + } + // See https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L310-L313 + const isPlayerFocused = false + || e.target === document.querySelector('.video-js') + || e.target === document.querySelector('.vjs-tech') + || e.target === document.querySelector('.iframeblocker') + || e.target === document.querySelector('.vjs-control-bar') + ; + let action = null; + + const code = e.keyCode; + const key = e.key; + switch (key) { + case ' ': + case 'k': + action = toggle_play; + break; + + case 'ArrowUp': + if (isPlayerFocused) { + action = increase_volume.bind(this, 0.1); + } + break; + case 'ArrowDown': + if (isPlayerFocused) { + action = increase_volume.bind(this, -0.1); + } + break; + + case 'm': + action = toggle_muted; + break; + + case 'ArrowRight': + action = skip_seconds.bind(this, 5); + break; + case 'ArrowLeft': + action = skip_seconds.bind(this, -5); + break; + case 'l': + action = skip_seconds.bind(this, 10); + break; + case 'j': + action = skip_seconds.bind(this, -10); + break; + + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + const percent = (code - 48) * 10; + action = set_time_percent.bind(this, percent); + break; + + case 'c': + action = toggle_captions; + break; + case 'f': + action = toggle_fullscreen; + break; + + case 'N': + action = next_video; + break; + case 'P': + // TODO: Add support to play back previous video. + break; + + case '.': + // TODO: Add support for next-frame-stepping. + break; + case ',': + // TODO: Add support for previous-frame-stepping. + break; + + case '>': + action = increase_playback_rate.bind(this, 1); + break; + case '<': + action = increase_playback_rate.bind(this, -1); + break; + + default: + console.info('Unhandled key down event: %s:', key, e); + break; + } + + if (action) { + e.preventDefault(); + action(); + } +}, false); + +// Add support for controlling the player volume by scrolling over it. Adapted from +// https://github.com/ctd1500/videojs-hotkeys/blob/bb4a158b2e214ccab87c2e7b95f42bc45c6bfd87/videojs.hotkeys.js#L292-L328 +(function() { + const volumeStep = 0.05; + const enableVolumeScroll = true; + const enableHoverScroll = true; + const doc = document; + const pEl = document.getElementById('player'); + + var volumeHover = false; + var volumeSelector = pEl.querySelector('.vjs-volume-menu-button') || pEl.querySelector('.vjs-volume-panel'); + if (volumeSelector != null) { + volumeSelector.onmouseover = function() { volumeHover = true; }; + volumeSelector.onmouseout = function() { volumeHover = false; }; + } + + var mouseScroll = function mouseScroll(event) { + var activeEl = doc.activeElement; + if (enableHoverScroll) { + // If we leave this undefined then it can match non-existent elements below + activeEl = 0; + } + + // When controls are disabled, hotkeys will be disabled as well + if (player.controls()) { + if (volumeHover) { + if (enableVolumeScroll) { + event = window.event || event; + var delta = Math.max(-1, Math.min(1, (event.wheelDelta || -event.detail))); + event.preventDefault(); + + if (delta == 1) { + increase_volume(volumeStep); + } else if (delta == -1) { + increase_volume(-volumeStep); + } + } + } + } + }; + + player.on('mousewheel', mouseScroll); + player.on("DOMMouseScroll", mouseScroll); +}()); + // Since videojs-share can sometimes be blocked, we defer it until last player.share(shareOptions); diff --git a/assets/js/videojs.hotkeys.min.js b/assets/js/videojs.hotkeys.min.js deleted file mode 100644 index a6cfe6e2..00000000 --- a/assets/js/videojs.hotkeys.min.js +++ /dev/null @@ -1,2 +0,0 @@ -/* videojs-hotkeys v0.2.25 - https://github.com/ctd1500/videojs-hotkeys */ -!function(e,n){"undefined"!=typeof window&&window.videojs?n(window.videojs):"function"==typeof define&&define.amd?define("videojs-hotkeys",["video.js"],function(e){return n(e.default||e)}):"undefined"!=typeof module&&module.exports&&(module.exports=n(require("video.js")))}(0,function(e){"use strict";"undefined"!=typeof window&&(window.videojs_hotkeys={version:"0.2.25"});(e.registerPlugin||e.plugin)("hotkeys",function(n){function t(e){return"function"==typeof s?s(e):s}function r(e){null!=e&&"function"==typeof e.then&&e.then(null,function(e){})}var o=this,u=o.el(),l=document,i={volumeStep:.1,seekStep:5,enableMute:!0,enableVolumeScroll:!0,enableHoverScroll:!1,enableFullscreen:!0,enableNumbers:!0,enableJogStyle:!1,alwaysCaptureHotkeys:!1,enableModifiersForNumbers:!0,enableInactiveFocus:!0,skipInitialFocus:!1,playPauseKey:function(e){return 32===e.which||179===e.which},rewindKey:function(e){return 37===e.which||177===e.which},forwardKey:function(e){return 39===e.which||176===e.which},volumeUpKey:function(e){return 38===e.which},volumeDownKey:function(e){return 40===e.which},muteKey:function(e){return 77===e.which},fullscreenKey:function(e){return 70===e.which},customKeys:{}},c=e.mergeOptions||e.util.mergeOptions,a=(n=c(i,n||{})).volumeStep,s=n.seekStep,m=n.enableMute,f=n.enableVolumeScroll,y=n.enableHoverScroll,v=n.enableFullscreen,d=n.enableNumbers,p=n.enableJogStyle,b=n.alwaysCaptureHotkeys,h=n.enableModifiersForNumbers,w=n.enableInactiveFocus,k=n.skipInitialFocus,S=e.VERSION;u.hasAttribute("tabIndex")||u.setAttribute("tabIndex","-1"),u.style.outline="none",!b&&o.autoplay()||k||o.one("play",function(){u.focus()}),w&&o.on("userinactive",function(){var e=function(){clearTimeout(n)},n=setTimeout(function(){o.off("useractive",e);var n=l.activeElement,t=u.querySelector(".vjs-control-bar");n&&n.parentElement==t&&u.focus()},10);o.one("useractive",e)}),o.on("play",function(){var e=u.querySelector(".iframeblocker");e&&""===e.style.display&&(e.style.display="block",e.style.bottom="39px")});var K=!1,q=u.querySelector(".vjs-volume-menu-button")||u.querySelector(".vjs-volume-panel");null!=q&&(q.onmouseover=function(){K=!0},q.onmouseout=function(){K=!1});var j=function(e){if(y)n=0;else var n=l.activeElement;if(o.controls()&&(b||n==u||n==u.querySelector(".vjs-tech")||n==u.querySelector(".iframeblocker")||n==u.querySelector(".vjs-control-bar")||K)&&f){e=window.event||e;var t=Math.max(-1,Math.min(1,e.wheelDelta||-e.detail));e.preventDefault(),1==t?o.volume(o.volume()+a):-1==t&&o.volume(o.volume()-a)}},F=function(e,t){return n.playPauseKey(e,t)?1:n.rewindKey(e,t)?2:n.forwardKey(e,t)?3:n.volumeUpKey(e,t)?4:n.volumeDownKey(e,t)?5:n.muteKey(e,t)?6:n.fullscreenKey(e,t)?7:void 0};return o.on("keydown",function(e){var i,c,s=e.which,f=e.preventDefault,y=o.duration();if(o.controls()){var w=l.activeElement;if(b||w==u||w==u.querySelector(".vjs-tech")||w==u.querySelector(".vjs-control-bar")||w==u.querySelector(".iframeblocker"))switch(F(e,o)){case 1:f(),b&&e.stopPropagation(),o.paused()?r(o.play()):o.pause();break;case 2:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()-t(e))<=0&&(c=0),o.currentTime(c),i&&r(o.play());break;case 3:i=!o.paused(),f(),i&&o.pause(),(c=o.currentTime()+t(e))>=y&&(c=i?y-.001:y),o.currentTime(c),i&&r(o.play());break;case 5:f(),p?(c=o.currentTime()-1,o.currentTime()<=1&&(c=0),o.currentTime(c)):o.volume(o.volume()-a);break;case 4:f(),p?((c=o.currentTime()+1)>=y&&(c=y),o.currentTime(c)):o.volume(o.volume()+a);break;case 6:m&&o.muted(!o.muted());break;case 7:v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen());break;default:if((s>47&&s<59||s>95&&s<106)&&(h||!(e.metaKey||e.ctrlKey||e.altKey))&&d){var k=48;s>95&&(k=96);var S=s-k;f(),o.currentTime(o.duration()*S*.1)}for(var K in n.customKeys){var q=n.customKeys[K];q&&q.key&&q.handler&&q.key(e)&&(f(),q.handler(o,n,e))}}}}),o.on("dblclick",function(e){if(null!=S&&S<="7.1.0"&&o.controls()){var n=e.relatedTarget||e.toElement||l.activeElement;n!=u&&n!=u.querySelector(".vjs-tech")&&n!=u.querySelector(".iframeblocker")||v&&(o.isFullscreen()?o.exitFullscreen():o.requestFullscreen())}}),o.on("mousewheel",j),o.on("DOMMouseScroll",j),this})}); \ No newline at end of file diff --git a/assets/js/watch.js b/assets/js/watch.js index 05e3b7e2..0f3e8123 100644 --- a/assets/js/watch.js +++ b/assets/js/watch.js @@ -73,29 +73,33 @@ if (continue_button) { continue_button.onclick = continue_autoplay; } +function next_video() { + var url = new URL('https://example.com/watch?v=' + video_data.next_video); + + if (video_data.params.autoplay || video_data.params.continue_autoplay) { + url.searchParams.set('autoplay', '1'); + } + + if (video_data.params.listen !== video_data.preferences.listen) { + url.searchParams.set('listen', video_data.params.listen); + } + + if (video_data.params.speed !== video_data.preferences.speed) { + url.searchParams.set('speed', video_data.params.speed); + } + + if (video_data.params.local !== video_data.preferences.local) { + url.searchParams.set('local', video_data.params.local); + } + + url.searchParams.set('continue', '1'); + location.assign(url.pathname + url.search); +} + function continue_autoplay(event) { if (event.target.checked) { player.on('ended', function () { - var url = new URL('https://example.com/watch?v=' + video_data.next_video); - - if (video_data.params.autoplay || video_data.params.continue_autoplay) { - url.searchParams.set('autoplay', '1'); - } - - if (video_data.params.listen !== video_data.preferences.listen) { - url.searchParams.set('listen', video_data.params.listen); - } - - if (video_data.params.speed !== video_data.preferences.speed) { - url.searchParams.set('speed', video_data.params.speed); - } - - if (video_data.params.local !== video_data.preferences.local) { - url.searchParams.set('local', video_data.params.local); - } - - url.searchParams.set('continue', '1'); - location.assign(url.pathname + url.search); + next_video(); }); } else { player.off('ended'); diff --git a/src/invidious/views/components/player_sources.ecr b/src/invidious/views/components/player_sources.ecr index 003d2c3a..d950e0da 100644 --- a/src/invidious/views/components/player_sources.ecr +++ b/src/invidious/views/components/player_sources.ecr @@ -6,7 +6,6 @@ - diff --git a/src/invidious/views/licenses.ecr b/src/invidious/views/licenses.ecr index 7cffb7fc..aae8bb19 100644 --- a/src/invidious/views/licenses.ecr +++ b/src/invidious/views/licenses.ecr @@ -135,20 +135,6 @@ - - -
videojs.hotkeys.min.js - - - - Apache-2.0-only - - - - <%= translate(locale, "source") %> - - - videojs-http-source-selector.min.js From 2b94975345a3bdd085a99081de521d1c5d3ada48 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Fri, 16 Aug 2019 20:06:21 -0500 Subject: [PATCH 26/31] Fix playlist_thumbnail extractor --- assets/css/default.css | 11 +++++++---- src/invidious.cr | 5 ++--- src/invidious/helpers/helpers.cr | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/assets/css/default.css b/assets/css/default.css index 451fbc8c..4daa9f16 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -1,6 +1,8 @@ html, body { - font-family: BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif; + font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", Helvetica, + Arial, sans-serif; } .deleted { @@ -108,6 +110,7 @@ img.thumbnail { height: 100%; left: 0; top: 0; + object-fit: cover; } .length { @@ -442,8 +445,8 @@ video.video-js { } .video-js.player-style-youtube .vjs-control-bar { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; } .video-js.player-style-youtube .vjs-big-play-button { /* @@ -452,6 +455,6 @@ video.video-js { */ top: 50%; left: 50%; - margin-top: -.81666em; + margin-top: -0.81666em; margin-left: -1.5em; } diff --git a/src/invidious.cr b/src/invidious.cr index 21d6544d..2fa1d66f 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -3003,7 +3003,7 @@ get "/user/:user/about" do |env| env.redirect "/channel/#{user}" end -get "/channel:ucid/about" do |env| +get "/channel/:ucid/about" do |env| ucid = env.params.url["ucid"] env.redirect "/channel/#{ucid}" end @@ -3107,8 +3107,7 @@ get "/channel/:ucid/playlists" do |env| end items, continuation = fetch_channel_playlists(channel.ucid, channel.author, channel.auto_generated, continuation, sort_by) - items.select! { |item| item.is_a?(SearchPlaylist) && !item.videos.empty? } - items = items.map { |item| item.as(SearchPlaylist) } + items = items.select { |item| item.is_a?(SearchPlaylist) }.map { |item| item.as(SearchPlaylist) } items.each { |item| item.author = "" } env.set "search", "channel:#{channel.ucid} " diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index e018e567..1b232072 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -397,8 +397,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil) ) end - playlist_thumbnail = node.xpath_node(%q(.//div/span/img)).try &.["data-thumb"]? - playlist_thumbnail ||= node.xpath_node(%q(.//div/span/img)).try &.["src"] + playlist_thumbnail = node.xpath_node(%q(.//span/img)).try &.["data-thumb"]? + playlist_thumbnail ||= node.xpath_node(%q(.//span/img)).try &.["src"] items << SearchPlaylist.new( title, From acaf7b969add62bdcc32f4bc193bcd5280413cc3 Mon Sep 17 00:00:00 2001 From: leonklingele Date: Mon, 19 Aug 2019 06:22:39 +0200 Subject: [PATCH 27/31] js: add support to detect alt, meta and control key in keydown handler (#704) This fixes a quite severe user experience issue where pressing the 'alt', 'meta' and/or 'ctrl' key along with one of the supported keys (e.g. 'f' to enter video fullscreen mode) would overwrite the default browser behavior. In the case of 'f+meta' we would enter fullscreen mode, and not open the browser search panel as one might expect. This change is required to stay consistent with the way YouTube handles keydown events. --- assets/js/player.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/assets/js/player.js b/assets/js/player.js index 4a61258c..0d0ecebd 100644 --- a/assets/js/player.js +++ b/assets/js/player.js @@ -327,8 +327,13 @@ window.addEventListener('keydown', e => { let action = null; const code = e.keyCode; - const key = e.key; - switch (key) { + const decoratedKey = + e.key + + (e.altKey ? '+alt' : '') + + (e.ctrlKey ? '+ctrl' : '') + + (e.metaKey ? '+meta' : '') + ; + switch (decoratedKey) { case ' ': case 'k': action = toggle_play; @@ -405,7 +410,7 @@ window.addEventListener('keydown', e => { break; default: - console.info('Unhandled key down event: %s:', key, e); + console.info('Unhandled key down event: %s:', decoratedKey, e); break; } From e768e1e27757805d0dbb2c1f3dbaf788c8c6cbfd Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Mon, 19 Aug 2019 09:00:37 -0500 Subject: [PATCH 28/31] Fix allowed_regions for globally blocked videos --- src/invidious/videos.cr | 51 ++++++++++++----------------------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr index 7fd06dd5..03fe9a26 100644 --- a/src/invidious/videos.cr +++ b/src/invidious/videos.cr @@ -108,33 +108,7 @@ CAPTION_LANGUAGES = { "Zulu", } -REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"} -BYPASS_REGIONS = { - "GB", - "DE", - "FR", - "IN", - "CN", - "RU", - "CA", - "JP", - "IT", - "TH", - "ES", - "AE", - "KR", - "IR", - "BR", - "PK", - "ID", - "BD", - "MX", - "PH", - "EG", - "VN", - "CD", - "TR", -} +REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"} # See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L380-#L476 VIDEO_FORMATS = { @@ -1133,19 +1107,24 @@ def fetch_video(id, region) info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",") - allowed_regions ||= [] of String + if !allowed_regions || allowed_regions == [""] + allowed_regions = [] of String + end # Check for region-blocks - if info["reason"]? && info["reason"].includes? "your country" - region = allowed_regions.sample(1)[0]? - client = make_client(YT_URL, region) - response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999") + if info["reason"]? && info["reason"].includes?("your country") + bypass_regions = PROXY_LIST.keys & allowed_regions + if !bypass_regions.empty? + region = bypass_regions[rand(bypass_regions.size)] + client = make_client(YT_URL, region) + response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999") - html = XML.parse_html(response.body) - info = extract_player_config(response.body, html) + html = XML.parse_html(response.body) + info = extract_player_config(response.body, html) - info["region"] = region if region - info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") + info["region"] = region if region + info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ") + end end # Try to pull streams from embed URL From 9f9cc1ffb5abdb9c0b4331a1f69039f0e77ae667 Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 21 Aug 2019 18:23:20 -0500 Subject: [PATCH 29/31] Refactor search extractor --- src/invidious.cr | 2 +- src/invidious/channels.cr | 15 +++--- src/invidious/helpers/helpers.cr | 90 ++++++++++++++------------------ 3 files changed, 48 insertions(+), 59 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 2fa1d66f..143be96d 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -5167,7 +5167,7 @@ get "/vi/:id/:name" do |env| end end -# Undocumented, creates anonymous playlist with specified 'video_ids' +# Undocumented, creates anonymous playlist with specified 'video_ids', max 50 videos get "/watch_videos" do |env| client = make_client(YT_URL) diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 5e01cef2..107039a6 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -387,14 +387,15 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by) html = XML.parse_html(json["content_html"].as_s) nodeset = html.xpath_nodes(%q(//li[contains(@class, "feed-item-container")])) - else - url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list" + elsif auto_generated + url = "/channel/#{ucid}" - if auto_generated - url += "&view=50" - else - url += "&view=1" - end + response = client.get(url) + html = XML.parse_html(response.body) + + nodeset = html.xpath_nodes(%q(//ul[@id="browse-items-primary"]/li[contains(@class, "feed-item-container")])) + else + url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list&view=1" case sort_by when "last", "last_added" diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 1b232072..20ff2de6 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -442,47 +442,20 @@ def extract_items(nodeset, ucid = nil, author_name = nil) else id = id.lchop("/watch?v=") - metadata = node.xpath_nodes(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li)) + metadata = node.xpath_node(%q(.//div[contains(@class,"yt-lockup-meta")]/ul)) - begin - published = decode_date(metadata[0].content.lchop("Streamed ").lchop("Starts ")) - rescue ex - end - begin - published ||= Time.unix(metadata[0].xpath_node(%q(.//span)).not_nil!["data-timestamp"].to_i64) - rescue ex - end + published = metadata.try &.xpath_node(%q(.//li[contains(text(), " ago")])).try { |node| decode_date(node.content.sub(/^[a-zA-Z]+ /, "")) } + published ||= metadata.try &.xpath_node(%q(.//span[@data-timestamp])).try { |node| Time.unix(node["data-timestamp"].to_i64) } published ||= Time.utc - begin - view_count = metadata[0].content.rchop(" watching").delete(",").try &.to_i64? - rescue ex - end - begin - view_count ||= metadata.try &.[1].content.delete("No views,").try &.to_i64? - rescue ex - end + view_count = metadata.try &.xpath_node(%q(.//li[contains(text(), " views")])).try &.content.gsub(/\D/, "").to_i64? view_count ||= 0_i64 - length_seconds = node.xpath_node(%q(.//span[@class="video-time"])) - if length_seconds - length_seconds = decode_length_seconds(length_seconds.content) - else - length_seconds = -1 - end + length_seconds = node.xpath_node(%q(.//span[@class="video-time"])).try { |node| decode_length_seconds(node.content) } + length_seconds ||= -1 - live_now = node.xpath_node(%q(.//span[contains(@class, "yt-badge-live")])) - if live_now - live_now = true - else - live_now = false - end - - if node.xpath_node(%q(.//span[text()="Premium"])) - premium = true - else - premium = false - end + live_now = node.xpath_node(%q(.//span[contains(@class, "yt-badge-live")])) ? true : false + premium = node.xpath_node(%q(.//span[text()="Premium"])) ? true : false if !premium || node.xpath_node(%q(.//span[contains(text(), "Free episode")])) paid = false @@ -520,26 +493,18 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) nodeset.each do |shelf| shelf_anchor = shelf.xpath_node(%q(.//h2[contains(@class, "branded-page-module-title")])) + next if !shelf_anchor - if !shelf_anchor - next - end - - title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")])) - if title - title = title.content.strip - end + title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")])).try &.content.strip title ||= "" id = shelf_anchor.xpath_node(%q(.//a)).try &.["href"] - if !id - next - end + next if !id - is_playlist = false + shelf_is_playlist = false videos = [] of SearchPlaylistVideo - shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list")]/li)).each do |child_node| + shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list") or contains(@class, "expanded-shelf-content-list")]/li)).each do |child_node| type = child_node.xpath_node(%q(./div)) if !type next @@ -547,7 +512,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) case type["class"] when .includes? "yt-lockup-video" - is_playlist = true + shelf_is_playlist = true anchor = child_node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a)) if anchor @@ -588,19 +553,42 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) end video_count ||= 50 + videos = [] of SearchPlaylistVideo + child_node.xpath_nodes(%q(.//*[contains(@class, "yt-lockup-playlist-items")]/li)).each do |video| + anchor = video.xpath_node(%q(.//a)) + if anchor + video_title = anchor.content.strip + id = HTTP::Params.parse(URI.parse(anchor["href"]).query.not_nil!)["v"] + end + video_title ||= "" + id ||= "" + + anchor = video.xpath_node(%q(.//span/span)) + if anchor + length_seconds = decode_length_seconds(anchor.content) + end + length_seconds ||= 0 + + videos << SearchPlaylistVideo.new( + video_title, + id, + length_seconds + ) + end + items << SearchPlaylist.new( playlist_title, plid, author_name, ucid, video_count, - Array(SearchPlaylistVideo).new, + videos, playlist_thumbnail ) end end - if is_playlist + if shelf_is_playlist plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"] items << SearchPlaylist.new( From 4c9975a7d90ec9b3821befb07af02a293925196a Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 21 Aug 2019 18:35:54 -0500 Subject: [PATCH 30/31] Use accurate sub count when available --- src/invidious/views/channel.ecr | 2 +- src/invidious/views/community.ecr | 2 +- src/invidious/views/playlists.ecr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/invidious/views/channel.ecr b/src/invidious/views/channel.ecr index b5eb46ea..1074598d 100644 --- a/src/invidious/views/channel.ecr +++ b/src/invidious/views/channel.ecr @@ -34,7 +34,7 @@
<% ucid = channel.ucid %> <% author = channel.author %> - <% sub_count_text = number_to_short_text(channel.sub_count) %> + <% sub_count_text = channel.sub_count.format %> <%= rendered "components/subscribe_widget" %>
diff --git a/src/invidious/views/community.ecr b/src/invidious/views/community.ecr index 218cc2d4..9d086b5d 100644 --- a/src/invidious/views/community.ecr +++ b/src/invidious/views/community.ecr @@ -33,7 +33,7 @@
<% ucid = channel.ucid %> <% author = channel.author %> - <% sub_count_text = number_to_short_text(channel.sub_count) %> + <% sub_count_text = channel.sub_count.format %> <%= rendered "components/subscribe_widget" %>
diff --git a/src/invidious/views/playlists.ecr b/src/invidious/views/playlists.ecr index a32192b5..400922ff 100644 --- a/src/invidious/views/playlists.ecr +++ b/src/invidious/views/playlists.ecr @@ -33,7 +33,7 @@
<% ucid = channel.ucid %> <% author = channel.author %> - <% sub_count_text = number_to_short_text(channel.sub_count) %> + <% sub_count_text = channel.sub_count.format %> <%= rendered "components/subscribe_widget" %>
From 059f50dad477daab63246314198ec7e274493ece Mon Sep 17 00:00:00 2001 From: Omar Roth Date: Wed, 21 Aug 2019 19:08:11 -0500 Subject: [PATCH 31/31] Add 'playlistThumbnail' to playlist objects --- src/invidious.cr | 2 ++ src/invidious/helpers/helpers.cr | 56 +++++++++++++------------------- src/invidious/playlists.cr | 18 +++++----- src/invidious/search.cr | 1 + 4 files changed, 36 insertions(+), 41 deletions(-) diff --git a/src/invidious.cr b/src/invidious.cr index 143be96d..0203e3b8 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -4101,8 +4101,10 @@ get "/api/v1/playlists/:plid" do |env| response = JSON.build do |json| json.object do + json.field "type", "playlist" json.field "title", playlist.title json.field "playlistId", playlist.id + json.field "playlistThumbnail", playlist.thumbnail json.field "author", playlist.author json.field "authorId", playlist.ucid diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 20ff2de6..331f6360 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -331,18 +331,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil) next end - anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)) - if anchor - author = anchor.content.strip - author_id = anchor["href"].split("/")[-1] - end - - author ||= author_name - author_id ||= ucid - - author ||= "" - author_id ||= "" - + author_id = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).try &.["href"].split("/")[-1] || ucid || "" + author = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).try &.content.strip || author_name || "" description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")])).try &.to_s || "" tile = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-tile")])) @@ -401,13 +391,13 @@ def extract_items(nodeset, ucid = nil, author_name = nil) playlist_thumbnail ||= node.xpath_node(%q(.//span/img)).try &.["src"] items << SearchPlaylist.new( - title, - plid, - author, - author_id, - video_count, - videos, - playlist_thumbnail + title: title, + id: plid, + author: author, + ucid: author_id, + video_count: video_count, + videos: videos, + thumbnail: playlist_thumbnail ) when .includes? "yt-lockup-channel" author = title.strip @@ -577,13 +567,13 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) end items << SearchPlaylist.new( - playlist_title, - plid, - author_name, - ucid, - video_count, - videos, - playlist_thumbnail + title: playlist_title, + id: plid, + author: author_name, + ucid: ucid, + video_count: video_count, + videos: videos, + thumbnail: playlist_thumbnail ) end end @@ -592,13 +582,13 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil) plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"] items << SearchPlaylist.new( - title, - plid, - author_name, - ucid, - videos.size, - videos, - "/vi/#{videos[0].id}/mqdefault.jpg" + title: title, + id: plid, + author: author_name, + ucid: ucid, + video_count: videos.size, + videos: videos, + thumbnail: "https://i.ytimg.com/vi/#{videos[0].id}/mqdefault.jpg" ) end end diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index d28a4149..7965d990 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -51,6 +51,7 @@ struct Playlist video_count: Int32, views: Int64, updated: Time, + thumbnail: String?, }) end @@ -223,6 +224,9 @@ def fetch_playlist(plid, locale) description_html = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1])).try &.to_s || document.xpath_node(%q(//span[@class="pl-header-description-text"])).try &.to_s || "" + playlist_thumbnail = document.xpath_node(%q(//div[@class="pl-header-thumb"]/img)).try &.["data-thumb"]? || + document.xpath_node(%q(//div[@class="pl-header-thumb"]/img)).try &.["src"] + # YouTube allows anonymous playlists, so most of this can be empty or optional anchor = document.xpath_node(%q(//ul[@class="pl-header-details"])) author = anchor.try &.xpath_node(%q(.//li[1]/a)).try &.content @@ -234,15 +238,12 @@ def fetch_playlist(plid, locale) video_count = anchor.try &.xpath_node(%q(.//li[2])).try &.content.gsub(/\D/, "").to_i? video_count ||= 0 - views = anchor.try &.xpath_node(%q(.//li[3])).try &.content.delete("No views, ").to_i64? + + views = anchor.try &.xpath_node(%q(.//li[3])).try &.content.gsub(/\D/, "").to_i64? views ||= 0_i64 - updated = anchor.try &.xpath_node(%q(.//li[4])).try &.content.lchop("Last updated on ").lchop("Updated ") - if updated - updated = decode_date(updated) - else - updated = Time.utc - end + updated = anchor.try &.xpath_node(%q(.//li[4])).try &.content.lchop("Last updated on ").lchop("Updated ").try { |date| decode_date(date) } + updated ||= Time.utc playlist = Playlist.new( title: title, @@ -253,7 +254,8 @@ def fetch_playlist(plid, locale) description_html: description_html, video_count: video_count, views: views, - updated: updated + updated: updated, + thumbnail: playlist_thumbnail, ) return playlist diff --git a/src/invidious/search.cr b/src/invidious/search.cr index 784e3897..7a36f32e 100644 --- a/src/invidious/search.cr +++ b/src/invidious/search.cr @@ -117,6 +117,7 @@ struct SearchPlaylist json.field "type", "playlist" json.field "title", self.title json.field "playlistId", self.id + json.field "playlistThumbnail", self.thumbnail json.field "author", self.author json.field "authorId", self.ucid