diff options
| -rw-r--r-- | README.md | 2 | ||||
| -rw-r--r-- | assets/css/default.css | 16 | ||||
| -rw-r--r-- | kubernetes/Chart.yaml | 2 | ||||
| -rw-r--r-- | kubernetes/templates/deployment.yaml | 8 | ||||
| -rw-r--r-- | kubernetes/templates/service.yaml | 6 | ||||
| -rw-r--r-- | kubernetes/values.yaml | 5 | ||||
| -rw-r--r-- | src/invidious.cr | 67 | ||||
| -rw-r--r-- | src/invidious/channels.cr | 86 | ||||
| -rw-r--r-- | src/invidious/helpers/helpers.cr | 16 | ||||
| -rw-r--r-- | src/invidious/helpers/jobs.cr | 4 | ||||
| -rw-r--r-- | src/invidious/jobs.cr | 13 | ||||
| -rw-r--r-- | src/invidious/jobs/base_job.cr | 3 | ||||
| -rw-r--r-- | src/invidious/jobs/pull_popular_videos_job.cr | 27 | ||||
| -rw-r--r-- | src/invidious/playlists.cr | 2 | ||||
| -rw-r--r-- | src/invidious/routes/base_route.cr | 8 | ||||
| -rw-r--r-- | src/invidious/routes/home.cr | 34 | ||||
| -rw-r--r-- | src/invidious/routes/licenses.cr | 6 | ||||
| -rw-r--r-- | src/invidious/routes/privacy.cr | 6 | ||||
| -rw-r--r-- | src/invidious/routing.cr | 8 | ||||
| -rw-r--r-- | src/invidious/views/components/feed_menu.ecr | 28 | ||||
| -rw-r--r-- | src/invidious/views/components/player.ecr | 2 |
21 files changed, 200 insertions, 149 deletions
@@ -92,7 +92,7 @@ $ curl -sSL https://dist.crystal-lang.org/apt/setup.sh | sudo bash $ curl -sL "https://keybase.io/crystal/pgp_keys.asc" | sudo apt-key add - $ echo "deb https://dist.crystal-lang.org/apt crystal main" | sudo tee /etc/apt/sources.list.d/crystal.list $ sudo apt-get update -$ sudo apt install crystal libssl-dev libxml2-dev libyaml-dev libgmp-dev libreadline-dev postgresql librsvg2-bin libsqlite3-dev +$ sudo apt install crystal libssl-dev libxml2-dev libyaml-dev libgmp-dev libreadline-dev postgresql librsvg2-bin libsqlite3-dev zlib1g-dev ``` #### Add an Invidious user and clone the repository diff --git a/assets/css/default.css b/assets/css/default.css index ea139b40..b7a77be6 100644 --- a/assets/css/default.css +++ b/assets/css/default.css @@ -60,6 +60,22 @@ body { color: rgb(255, 0, 0); } +.feed-menu { + display: flex; + justify-content: center; + flex-wrap: wrap; +} + +.feed-menu-item { + text-align: center; +} + +@media screen and (max-width: 640px) { + .feed-menu-item { + flex: 0 0 40%; + } +} + .h-box { padding-left: 1em; padding-right: 1em; diff --git a/kubernetes/Chart.yaml b/kubernetes/Chart.yaml index 0d7791d7..bb0838ad 100644 --- a/kubernetes/Chart.yaml +++ b/kubernetes/Chart.yaml @@ -1,7 +1,7 @@ apiVersion: v2 name: invidious description: Invidious is an alternative front-end to YouTube -version: 1.0.0 +version: 1.1.0 appVersion: 0.20.1 keywords: - youtube diff --git a/kubernetes/templates/deployment.yaml b/kubernetes/templates/deployment.yaml index 34156127..bb0b832f 100644 --- a/kubernetes/templates/deployment.yaml +++ b/kubernetes/templates/deployment.yaml @@ -23,6 +23,13 @@ spec: runAsUser: {{ .Values.securityContext.runAsUser }} runAsGroup: {{ .Values.securityContext.runAsGroup }} fsGroup: {{ .Values.securityContext.fsGroup }} + initContainers: + - name: wait-for-postgresql + image: postgres + args: + - /bin/sh + - -c + - until pg_isready -h {{ .Values.config.db.host }} -p {{ .Values.config.db.port }} -U {{ .Values.config.db.user }}; do echo waiting for database; sleep 2; done; containers: - name: {{ .Chart.Name }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" @@ -50,4 +57,5 @@ spec: httpGet: port: 3000 path: / + initialDelaySeconds: 15 restartPolicy: Always diff --git a/kubernetes/templates/service.yaml b/kubernetes/templates/service.yaml index 56bdea2e..01454d4e 100644 --- a/kubernetes/templates/service.yaml +++ b/kubernetes/templates/service.yaml @@ -7,10 +7,14 @@ metadata: chart: {{ .Chart.Name }} release: {{ .Release.Name }} spec: + type: {{ .Values.service.type }} ports: - name: http - port: 3000 + port: {{ .Values.service.port }} targetPort: 3000 selector: app: {{ template "invidious.name" . }} release: {{ .Release.Name }} +{{- if .Values.service.loadBalancerIP }} + loadBalancerIP: {{ .Values.service.loadBalancerIP }} +{{- end }} diff --git a/kubernetes/values.yaml b/kubernetes/values.yaml index ce32b257..4d037022 100644 --- a/kubernetes/values.yaml +++ b/kubernetes/values.yaml @@ -13,6 +13,11 @@ autoscaling: maxReplicas: 16 targetCPUUtilizationPercentage: 50 +service: + type: clusterIP + port: 3000 + #loadBalancerIP: + resources: {} #requests: # cpu: 100m diff --git a/src/invidious.cr b/src/invidious.cr index 2a4c373c..ad63fcad 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -27,6 +27,8 @@ require "compress/zip" require "protodec/utils" require "./invidious/helpers/*" require "./invidious/*" +require "./invidious/routes/**" +require "./invidious/jobs/**" ENV_CONFIG_NAME = "INVIDIOUS_CONFIG" @@ -196,11 +198,11 @@ if config.statistics_enabled end end -popular_videos = [] of ChannelVideo -spawn do - pull_popular_videos(PG_DB) do |videos| - popular_videos = videos - end +Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB) +Invidious::Jobs.start_all + +def popular_videos + Invidious::Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get end DECRYPT_FUNCTION = [] of {SigProc, Int32} @@ -350,44 +352,9 @@ before_all do |env| env.set "current_page", URI.encode_www_form(current_page) end -get "/" do |env| - preferences = env.get("preferences").as(Preferences) - locale = LOCALES[preferences.locale]? - user = env.get? "user" - - case preferences.default_home - when "" - templated "empty" - when "Popular" - templated "popular" - when "Trending" - env.redirect "/feed/trending" - when "Subscriptions" - if user - env.redirect "/feed/subscriptions" - else - templated "popular" - end - when "Playlists" - if user - env.redirect "/view_all_playlists" - else - templated "popular" - end - else - templated "empty" - end -end - -get "/privacy" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - templated "privacy" -end - -get "/licenses" do |env| - locale = LOCALES[env.get("preferences").as(Preferences).locale]? - rendered "licenses" -end +Invidious::Routing.get "/", Invidious::Routes::Home +Invidious::Routing.get "/privacy", Invidious::Routes::Privacy +Invidious::Routing.get "/licenses", Invidious::Routes::Licenses # Videos @@ -3412,17 +3379,13 @@ post "/feed/webhook/:token" do |env| views: video.views, }) - PG_DB.query_all("UPDATE users SET feed_needs_update = true, notifications = array_append(notifications, $1) \ - WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", - video.id, video.published, video.ucid, as: String) - - video_array = video.to_a - args = arg_array(video_array) - - PG_DB.exec("INSERT INTO channel_videos VALUES (#{args}) \ + was_insert = PG_DB.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \ ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \ updated = $4, ucid = $5, author = $6, length_seconds = $7, \ - live_now = $8, premiere_timestamp = $9, views = $10", args: video_array) + live_now = $8, premiere_timestamp = $9, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool) + + PG_DB.exec("UPDATE users SET notifications = array_append(notifications, $1), \ + feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert end end diff --git a/src/invidious/channels.cr b/src/invidious/channels.cr index 007aa06c..18bdac09 100644 --- a/src/invidious/channels.cr +++ b/src/invidious/channels.cr @@ -97,6 +97,14 @@ struct ChannelVideo end end end + + def to_tuple + {% begin %} + { + {{*@type.instance_vars.map { |var| var.name }}} + } + {% end %} + end end struct AboutRelatedChannel @@ -260,28 +268,15 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) views: views, }) - emails = db.query_all("UPDATE users SET notifications = array_append(notifications, $1) \ - WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications) RETURNING email", - video.id, video.published, ucid, as: String) - - video_array = video.to_a - args = arg_array(video_array) - # We don't include the 'premiere_timestamp' here because channel pages don't include them, # meaning the above timestamp is always null - db.exec("INSERT INTO channel_videos VALUES (#{args}) \ + was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \ ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \ updated = $4, ucid = $5, author = $6, length_seconds = $7, \ - live_now = $8, views = $10", args: video_array) - - # Update all users affected by insert - if emails.empty? - values = "'{}'" - else - values = "VALUES #{emails.map { |email| %((E'#{email.gsub({'\'' => "\\'", '\\' => "\\\\"})}')) }.join(",")}" - end + live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool) - db.exec("UPDATE users SET feed_needs_update = true WHERE email = ANY(#{values})") + db.exec("UPDATE users SET notifications = array_append(notifications, $1), \ + feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert end if pull_all_videos @@ -315,39 +310,19 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil) # We are notified of Red videos elsewhere (PubSub), which includes a correct published date, # so since they don't provide a published date here we can safely ignore them. if Time.utc - video.published > 1.minute - emails = db.query_all("UPDATE users SET notifications = array_append(notifications, $1) \ - WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications) RETURNING email", - video.id, video.published, video.ucid, as: String) - - video_array = video.to_a - args = arg_array(video_array) - - # We don't update the 'premire_timestamp' here because channel pages don't include them - db.exec("INSERT INTO channel_videos VALUES (#{args}) \ + was_insert = db.query_one("INSERT INTO channel_videos VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \ ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \ updated = $4, ucid = $5, author = $6, length_seconds = $7, \ - live_now = $8, views = $10", args: video_array) + live_now = $8, views = $10 returning (xmax=0) as was_insert", *video.to_tuple, as: Bool) - # Update all users affected by insert - if emails.empty? - values = "'{}'" - else - values = "VALUES #{emails.map { |email| %((E'#{email.gsub({'\'' => "\\'", '\\' => "\\\\"})}')) }.join(",")}" - end - - db.exec("UPDATE users SET feed_needs_update = true WHERE email = ANY(#{values})") + db.exec("UPDATE users SET notifications = array_append(notifications, $1), \ + feed_needs_update = true WHERE $2 = ANY(subscriptions)", video.id, video.ucid) if was_insert end end - if count < 25 - break - end - + break if count < 25 page += 1 end - - # When a video is deleted from a channel, we find and remove it here - db.exec("DELETE FROM channel_videos * WHERE NOT id = ANY ('{#{ids.map { |id| %("#{id}") }.join(",")}}') AND ucid = $1", ucid) end channel = InvidiousChannel.new({ @@ -427,14 +402,9 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = " object["80226972:embedded"]["3:base64"].as(Hash)["4:varint"] = 0_i64 object["80226972:embedded"]["3:base64"].as(Hash)["61:string"] = Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({ - "1:embedded" => { - "1:varint" => 6307666885028338688_i64, - "2:embedded" => { - "1:string" => Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({ - "1:varint" => 30_i64 * (page - 1), - }))), - }, - }, + "1:string" => Base64.urlsafe_encode(Protodec::Any.from_json(Protodec::Any.cast_json({ + "1:varint" => 30_i64 * (page - 1), + }))), }))) end @@ -915,20 +885,8 @@ def get_about_info(ucid, locale) end def get_channel_videos_response(ucid, page = 1, auto_generated = nil, sort_by = "newest") - url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: false) - response = YT_POOL.client &.get(url) - initial_data = JSON.parse(response.body).as_a.find &.["response"]? - return response if !initial_data - needs_v2 = initial_data - .try &.["response"]?.try &.["alerts"]? - .try &.as_a.any? { |alert| - alert.try &.["alertRenderer"]?.try &.["type"]?.try { |t| t == "ERROR" } - } - if needs_v2 - url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true) - response = YT_POOL.client &.get(url) - end - response + url = produce_channel_videos_url(ucid, page, auto_generated: auto_generated, sort_by: sort_by, v2: true) + return YT_POOL.client &.get(url) end def get_60_videos(ucid, author, page, auto_generated, sort_by = "newest") diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr index 6571f818..fb220eab 100644 --- a/src/invidious/helpers/helpers.cr +++ b/src/invidious/helpers/helpers.cr @@ -177,8 +177,8 @@ def extract_item(item : JSON::Any, author_fallback : String? = nil, author_id_fa view_count = i["viewCountText"]?.try &.["simpleText"]?.try &.as_s.gsub(/\D+/, "").to_i64? || 0_i64 description_html = i["descriptionSnippet"]?.try { |t| parse_content(t) } || "" length_seconds = i["lengthText"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } || - i["thumbnailOverlays"]?.try &.as_a.find(&.["thumbnailOverlayTimeStatusRenderer"]?).try &.["thumbnailOverlayTimeStatusRenderer"]? - .try &.["text"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } || 0 + i["thumbnailOverlays"]?.try &.as_a.find(&.["thumbnailOverlayTimeStatusRenderer"]?).try &.["thumbnailOverlayTimeStatusRenderer"]? + .try &.["text"]?.try &.["simpleText"]?.try &.as_s.try { |t| decode_length_seconds(t) } || 0 live_now = false paid = false @@ -302,14 +302,14 @@ def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : Stri channel_v2_response = initial_data .try &.["response"]? - .try &.["continuationContents"]? - .try &.["gridContinuation"]? - .try &.["items"]? + .try &.["continuationContents"]? + .try &.["gridContinuation"]? + .try &.["items"]? if channel_v2_response channel_v2_response.try &.as_a.each { |item| - extract_item(item, author_fallback, author_id_fallback) - .try { |t| items << t } + extract_item(item, author_fallback, author_id_fallback) + .try { |t| items << t } } else initial_data.try { |t| t["contents"]? || t["response"]? } @@ -325,7 +325,7 @@ def extract_items(initial_data : Hash(String, JSON::Any), author_fallback : Stri extract_item(item, author_fallback, author_id_fallback) .try { |t| items << t } } } - end + end items end diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index 43334718..ca3d44d0 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -258,7 +258,7 @@ def bypass_captcha(captcha_key, logger) end inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s - headers["Cookies"] = response["solution"]["cookies"].as_h.map { |k, v| "#{k}=#{v}" }.join("; ") + headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || "" response = YT_POOL.client &.post("/das_captcha", headers, form: inputs) yield response.cookies.select { |cookie| cookie.name != "PREF" } @@ -308,7 +308,7 @@ def bypass_captcha(captcha_key, logger) end inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s - headers["Cookies"] = response["solution"]["cookies"].as_h.map { |k, v| "#{k}=#{v}" }.join("; ") + headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || "" response = YT_POOL.client &.post("/sorry/index", headers: headers, form: inputs) headers = HTTP::Headers{ "Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0], diff --git a/src/invidious/jobs.cr b/src/invidious/jobs.cr new file mode 100644 index 00000000..ec0cad64 --- /dev/null +++ b/src/invidious/jobs.cr @@ -0,0 +1,13 @@ +module Invidious::Jobs + JOBS = [] of BaseJob + + def self.register(job : BaseJob) + JOBS << job + end + + def self.start_all + JOBS.each do |job| + spawn { job.begin } + end + end +end diff --git a/src/invidious/jobs/base_job.cr b/src/invidious/jobs/base_job.cr new file mode 100644 index 00000000..47e75864 --- /dev/null +++ b/src/invidious/jobs/base_job.cr @@ -0,0 +1,3 @@ +abstract class Invidious::Jobs::BaseJob + abstract def begin +end diff --git a/src/invidious/jobs/pull_popular_videos_job.cr b/src/invidious/jobs/pull_popular_videos_job.cr new file mode 100644 index 00000000..7a8ab84e --- /dev/null +++ b/src/invidious/jobs/pull_popular_videos_job.cr @@ -0,0 +1,27 @@ +class Invidious::Jobs::PullPopularVideosJob < Invidious::Jobs::BaseJob + QUERY = <<-SQL + SELECT DISTINCT ON (ucid) * + FROM channel_videos + WHERE ucid IN (SELECT channel FROM (SELECT UNNEST(subscriptions) AS channel FROM users) AS d + GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40) + ORDER BY ucid, published DESC + SQL + POPULAR_VIDEOS = Atomic.new([] of ChannelVideo) + private getter db : DB::Database + + def initialize(@db) + end + + def begin + loop do + videos = db.query_all(QUERY, as: ChannelVideo) + .sort_by(&.published) + .reverse + + POPULAR_VIDEOS.set(videos) + + sleep 1.minute + Fiber.yield + end + end +end diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr index 9190e4e6..c984a12a 100644 --- a/src/invidious/playlists.cr +++ b/src/invidious/playlists.cr @@ -483,7 +483,7 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any)) published: Time.utc, plid: plid, live_now: live, - index: index - 1, + index: index, }) end end diff --git a/src/invidious/routes/base_route.cr b/src/invidious/routes/base_route.cr new file mode 100644 index 00000000..576c1746 --- /dev/null +++ b/src/invidious/routes/base_route.cr @@ -0,0 +1,8 @@ +abstract class Invidious::Routes::BaseRoute + private getter config : Config + + def initialize(@config) + end + + abstract def handle(env) +end diff --git a/src/invidious/routes/home.cr b/src/invidious/routes/home.cr new file mode 100644 index 00000000..9b1bf61b --- /dev/null +++ b/src/invidious/routes/home.cr @@ -0,0 +1,34 @@ +class Invidious::Routes::Home < Invidious::Routes::BaseRoute + def handle(env) + preferences = env.get("preferences").as(Preferences) + locale = LOCALES[preferences.locale]? + user = env.get? "user" + + case preferences.default_home + when "" + templated "empty" + when "Popular" + templated "popular" + when "Trending" + env.redirect "/feed/trending" + when "Subscriptions" + if user + env.redirect "/feed/subscriptions" + else + templated "popular" + end + when "Playlists" + if user + env.redirect "/view_all_playlists" + else + templated "popular" + end + else + templated "empty" + end + end + + private def popular_videos + Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get + end +end diff --git a/src/invidious/routes/licenses.cr b/src/invidious/routes/licenses.cr new file mode 100644 index 00000000..38fde7bb --- /dev/null +++ b/src/invidious/routes/licenses.cr @@ -0,0 +1,6 @@ +class Invidious::Routes::Licenses < Invidious::Routes::BaseRoute + def handle(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + rendered "licenses" + end +end diff --git a/src/invidious/routes/privacy.cr b/src/invidious/routes/privacy.cr new file mode 100644 index 00000000..4565c94c --- /dev/null +++ b/src/invidious/routes/privacy.cr @@ -0,0 +1,6 @@ +class Invidious::Routes::Privacy < Invidious::Routes::BaseRoute + def handle(env) + locale = LOCALES[env.get("preferences").as(Preferences).locale]? + templated "privacy" + end +end diff --git a/src/invidious/routing.cr b/src/invidious/routing.cr new file mode 100644 index 00000000..a096db44 --- /dev/null +++ b/src/invidious/routing.cr @@ -0,0 +1,8 @@ +module Invidious::Routing + macro get(path, controller) + get {{ path }} do |env| + controller_instance = {{ controller }}.new(config) + controller_instance.handle(env) + end + end +end diff --git a/src/invidious/views/components/feed_menu.ecr b/src/invidious/views/components/feed_menu.ecr index f72db2da..3dbeaf37 100644 --- a/src/invidious/views/components/feed_menu.ecr +++ b/src/invidious/views/components/feed_menu.ecr @@ -1,19 +1,11 @@ -<div class="h-box pure-g"> - <div class="pure-u-1 pure-u-md-1-4"></div> - <div class="pure-u-1 pure-u-md-1-2"> - <div class="pure-g"> - <% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %> - <% if !env.get?("user") %> - <% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %> - <% end %> - <% feed_menu.each do |feed| %> - <div class="pure-u-1-2 pure-u-md-1-<%= feed_menu.size %>"> - <a href="/feed/<%= feed.downcase %>" class="pure-menu-heading" style="text-align:center"> - <%= translate(locale, feed) %> - </a> - </div> - <% end %> - </div> - </div> - <div class="pure-u-1 pure-u-md-1-4"></div> +<div class="feed-menu"> + <% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %> + <% if !env.get?("user") %> + <% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %> + <% end %> + <% feed_menu.each do |feed| %> + <a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading"> + <%= translate(locale, feed) %> + </a> + <% end %> </div> diff --git a/src/invidious/views/components/player.ecr b/src/invidious/views/components/player.ecr index 6b01d25f..0e6664fa 100644 --- a/src/invidious/views/components/player.ecr +++ b/src/invidious/views/components/player.ecr @@ -1,4 +1,4 @@ -<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>" title="<%= HTML.escape(video.title) %>" +<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>" id="player" class="on-video_player video-js player-style-<%= params.player_style %>" <% if params.autoplay %>autoplay<% end %> <% if params.video_loop %>loop<% end %> |
