summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/ci.yml3
-rw-r--r--README.md4
-rw-r--r--assets/css/player.css4
-rw-r--r--assets/js/watch.js2
-rwxr-xr-xconfig/migrate-scripts/migrate-db-17cf077.sh7
-rwxr-xr-xconfig/migrate-scripts/migrate-db-1c8075c.sh11
-rwxr-xr-xconfig/migrate-scripts/migrate-db-1eca969.sh37
-rwxr-xr-xconfig/migrate-scripts/migrate-db-30e6d29.sh7
-rwxr-xr-xconfig/migrate-scripts/migrate-db-3646395.sh9
-rwxr-xr-xconfig/migrate-scripts/migrate-db-3bcb98e.sh5
-rwxr-xr-xconfig/migrate-scripts/migrate-db-52cb239.sh5
-rwxr-xr-xconfig/migrate-scripts/migrate-db-6e51189.sh7
-rwxr-xr-xconfig/migrate-scripts/migrate-db-701b5ea.sh5
-rwxr-xr-xconfig/migrate-scripts/migrate-db-88b7097.sh5
-rwxr-xr-xconfig/migrate-scripts/migrate-db-8e884fe.sh9
-rw-r--r--docker-compose.yml2
-rw-r--r--docker/Dockerfile.arm642
-rwxr-xr-xdocker/init-invidious-db.sh4
-rw-r--r--locales/ar.json13
-rw-r--r--locales/en-US.json4
-rw-r--r--locales/eo.json9
-rw-r--r--locales/es.json11
-rw-r--r--locales/ja.json16
-rw-r--r--locales/ko.json10
-rw-r--r--locales/lt.json11
-rw-r--r--locales/nb-NO.json12
-rw-r--r--locales/pt.json8
-rw-r--r--locales/tr.json11
-rw-r--r--locales/zh-CN.json9
-rw-r--r--locales/zh-TW.json11
-rw-r--r--src/invidious.cr196
-rw-r--r--src/invidious/comments.cr6
-rw-r--r--src/invidious/helpers/helpers.cr37
-rw-r--r--src/invidious/helpers/serialized_yt_data.cr9
-rw-r--r--src/invidious/helpers/utils.cr96
-rw-r--r--src/invidious/mixes.cr2
-rw-r--r--src/invidious/playlists.cr51
-rw-r--r--src/invidious/routes/api/v1/misc.cr30
-rw-r--r--src/invidious/routes/images.cr191
-rw-r--r--src/invidious/views/components/item.ecr12
-rw-r--r--src/invidious/views/template.ecr4
-rw-r--r--src/invidious/views/watch.ecr2
-rw-r--r--src/invidious/yt_backend/connection_pool.cr96
-rw-r--r--src/invidious/yt_backend/extractors.cr (renamed from src/invidious/helpers/extractors.cr)47
-rw-r--r--src/invidious/yt_backend/extractors_utils.cr67
-rw-r--r--src/invidious/yt_backend/proxy.cr (renamed from src/invidious/helpers/proxy.cr)0
-rw-r--r--src/invidious/yt_backend/youtube_api.cr (renamed from src/invidious/helpers/youtube_api.cr)0
47 files changed, 630 insertions, 469 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3bb4c491..b99ecf18 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -40,6 +40,7 @@ jobs:
crystal:
- 1.0.0
- 1.1.1
+ - 1.2.0
include:
- crystal: nightly
stable: false
@@ -48,7 +49,7 @@ jobs:
- uses: actions/checkout@v2
- name: Install Crystal
- uses: oprypin/install-crystal@v1.2.4
+ uses: crystal-lang/install-crystal@v1.5.3
with:
crystal: ${{ matrix.crystal }}
diff --git a/README.md b/README.md
index 3f7fa8a7..8beb4380 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
<h1>Invidious</h1>
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html">
- <img alt="License: AGPLv3+" src="https://shields.io/badge/License-AGPL%20v3+-blue.svg">
+ <img alt="License: AGPLv3" src="https://shields.io/badge/License-AGPL%20v3-blue.svg">
</a>
<a href="https://github.com/iv-org/invidious/actions">
<img alt="Build Status" src="https://github.com/iv-org/invidious/workflows/Invidious%20CI/badge.svg">
@@ -58,7 +58,7 @@
- No JavaScript required
- Light/Dark themes
- Customizable homepage
-- Subscriptions independant from Google
+- Subscriptions independent from Google
- Notifications for all subscribed channels
- Audio-only mode (with background play on mobile)
- Support for Reddit comments
diff --git a/assets/css/player.css b/assets/css/player.css
index 656fb48c..120fd2f8 100644
--- a/assets/css/player.css
+++ b/assets/css/player.css
@@ -218,6 +218,10 @@ video.video-js {
#player-container {
position: relative;
+ padding-left: 0;
+ padding-right: 0;
+ margin-left: 1em;
+ margin-right: 1em;
padding-bottom: 82vh;
height: 0;
}
diff --git a/assets/js/watch.js b/assets/js/watch.js
index 3909edd4..1579abf4 100644
--- a/assets/js/watch.js
+++ b/assets/js/watch.js
@@ -149,6 +149,8 @@ function get_playlist(plid, retries) {
if (xhr.readyState == 4) {
if (xhr.status == 200) {
playlist.innerHTML = xhr.response.playlistHtml;
+ var nextVideo = document.getElementById(xhr.response.nextVideo);
+ nextVideo.parentNode.parentNode.scrollTop = nextVideo.offsetTop;
if (xhr.response.nextVideo) {
player.on('ended', function () {
diff --git a/config/migrate-scripts/migrate-db-17cf077.sh b/config/migrate-scripts/migrate-db-17cf077.sh
index 5e5bb214..1597311d 100755
--- a/config/migrate-scripts/migrate-db-17cf077.sh
+++ b/config/migrate-scripts/migrate-db-17cf077.sh
@@ -1,4 +1,7 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE channels ADD COLUMN subscribed bool;"
-psql invidious kemal -c "UPDATE channels SET subscribed = false;"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channels ADD COLUMN subscribed bool;"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "UPDATE channels SET subscribed = false;"
diff --git a/config/migrate-scripts/migrate-db-1c8075c.sh b/config/migrate-scripts/migrate-db-1c8075c.sh
index 63954397..b6f7b89c 100755
--- a/config/migrate-scripts/migrate-db-1c8075c.sh
+++ b/config/migrate-scripts/migrate-db-1c8075c.sh
@@ -1,7 +1,10 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE channel_videos DROP COLUMN live_now CASCADE"
-psql invidious kemal -c "ALTER TABLE channel_videos DROP COLUMN premiere_timestamp CASCADE"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
-psql invidious kemal -c "ALTER TABLE channel_videos ADD COLUMN live_now bool"
-psql invidious kemal -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos DROP COLUMN live_now CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos DROP COLUMN premiere_timestamp CASCADE"
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos ADD COLUMN live_now bool"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz"
diff --git a/config/migrate-scripts/migrate-db-1eca969.sh b/config/migrate-scripts/migrate-db-1eca969.sh
index f840d924..770a76d3 100755
--- a/config/migrate-scripts/migrate-db-1eca969.sh
+++ b/config/migrate-scripts/migrate-db-1eca969.sh
@@ -1,19 +1,22 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN title CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN views CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN likes CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN dislikes CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN wilson_score CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN published CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN description CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN language CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN author CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN ucid CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN allowed_regions CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN is_family_friendly CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN genre CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN genre_url CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN license CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN sub_count_text CASCADE"
-psql invidious kemal -c "ALTER TABLE videos DROP COLUMN author_thumbnail CASCADE"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN title CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN views CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN likes CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN dislikes CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN wilson_score CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN published CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN description CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN language CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN author CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN ucid CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN allowed_regions CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN is_family_friendly CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN genre CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN genre_url CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN license CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN sub_count_text CASCADE"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE videos DROP COLUMN author_thumbnail CASCADE"
diff --git a/config/migrate-scripts/migrate-db-30e6d29.sh b/config/migrate-scripts/migrate-db-30e6d29.sh
index 3a377461..9d0b2d30 100755
--- a/config/migrate-scripts/migrate-db-30e6d29.sh
+++ b/config/migrate-scripts/migrate-db-30e6d29.sh
@@ -1,4 +1,7 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE channels ADD COLUMN deleted bool;"
-psql invidious kemal -c "UPDATE channels SET deleted = false;"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channels ADD COLUMN deleted bool;"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "UPDATE channels SET deleted = false;"
diff --git a/config/migrate-scripts/migrate-db-3646395.sh b/config/migrate-scripts/migrate-db-3646395.sh
index 830b85f2..b6efe239 100755
--- a/config/migrate-scripts/migrate-db-3646395.sh
+++ b/config/migrate-scripts/migrate-db-3646395.sh
@@ -1,5 +1,8 @@
#!/bin/sh
-psql invidious kemal < config/sql/session_ids.sql
-psql invidious kemal -c "INSERT INTO session_ids (SELECT unnest(id), email, CURRENT_TIMESTAMP FROM users) ON CONFLICT (id) DO NOTHING"
-psql invidious kemal -c "ALTER TABLE users DROP COLUMN id"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" < config/sql/session_ids.sql
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "INSERT INTO session_ids (SELECT unnest(id), email, CURRENT_TIMESTAMP FROM users) ON CONFLICT (id) DO NOTHING"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE users DROP COLUMN id"
diff --git a/config/migrate-scripts/migrate-db-3bcb98e.sh b/config/migrate-scripts/migrate-db-3bcb98e.sh
index cb9fa6ab..444f65ed 100755
--- a/config/migrate-scripts/migrate-db-3bcb98e.sh
+++ b/config/migrate-scripts/migrate-db-3bcb98e.sh
@@ -1,3 +1,6 @@
#!/bin/sh
-psql invidious kemal < config/sql/annotations.sql
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" < config/sql/annotations.sql
diff --git a/config/migrate-scripts/migrate-db-52cb239.sh b/config/migrate-scripts/migrate-db-52cb239.sh
index db8efeab..da977d97 100755
--- a/config/migrate-scripts/migrate-db-52cb239.sh
+++ b/config/migrate-scripts/migrate-db-52cb239.sh
@@ -1,3 +1,6 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE channel_videos ADD COLUMN views bigint;"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos ADD COLUMN views bigint;"
diff --git a/config/migrate-scripts/migrate-db-6e51189.sh b/config/migrate-scripts/migrate-db-6e51189.sh
index ce728118..9132d3d7 100755
--- a/config/migrate-scripts/migrate-db-6e51189.sh
+++ b/config/migrate-scripts/migrate-db-6e51189.sh
@@ -1,4 +1,7 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE channel_videos ADD COLUMN live_now bool;"
-psql invidious kemal -c "UPDATE channel_videos SET live_now = false;"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos ADD COLUMN live_now bool;"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "UPDATE channel_videos SET live_now = false;"
diff --git a/config/migrate-scripts/migrate-db-701b5ea.sh b/config/migrate-scripts/migrate-db-701b5ea.sh
index 429531a2..46d60c00 100755
--- a/config/migrate-scripts/migrate-db-701b5ea.sh
+++ b/config/migrate-scripts/migrate-db-701b5ea.sh
@@ -1,3 +1,6 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE users ADD COLUMN feed_needs_update boolean"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE users ADD COLUMN feed_needs_update boolean"
diff --git a/config/migrate-scripts/migrate-db-88b7097.sh b/config/migrate-scripts/migrate-db-88b7097.sh
index 6bde8399..146ee92d 100755
--- a/config/migrate-scripts/migrate-db-88b7097.sh
+++ b/config/migrate-scripts/migrate-db-88b7097.sh
@@ -1,3 +1,6 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz;"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channel_videos ADD COLUMN premiere_timestamp timestamptz;"
diff --git a/config/migrate-scripts/migrate-db-8e884fe.sh b/config/migrate-scripts/migrate-db-8e884fe.sh
index 1c8dafd1..0d5de828 100755
--- a/config/migrate-scripts/migrate-db-8e884fe.sh
+++ b/config/migrate-scripts/migrate-db-8e884fe.sh
@@ -1,5 +1,8 @@
#!/bin/sh
-psql invidious kemal -c "ALTER TABLE channels DROP COLUMN subscribed"
-psql invidious kemal -c "ALTER TABLE channels ADD COLUMN subscribed timestamptz"
-psql invidious kemal -c "UPDATE channels SET subscribed = '2019-01-01 00:00:00+00'"
+[ -z "$POSTGRES_USER" ] && POSTGRES_USER=kemal
+[ -z "$POSTGRES_DB" ] && POSTGRES_DB=invidious
+
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channels DROP COLUMN subscribed"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "ALTER TABLE channels ADD COLUMN subscribed timestamptz"
+psql "$POSTGRES_DB" "$POSTGRES_USER" -c "UPDATE channels SET subscribed = '2019-01-01 00:00:00+00'"
diff --git a/docker-compose.yml b/docker-compose.yml
index b94f9813..ea1d2993 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -12,7 +12,7 @@ services:
POSTGRES_PASSWORD: kemal
POSTGRES_USER: kemal
healthcheck:
- test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER"]
+ test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER $$POSTGRES_DB"]
invidious:
build:
context: .
diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64
index 063ba6d2..193ed09d 100644
--- a/docker/Dockerfile.arm64
+++ b/docker/Dockerfile.arm64
@@ -1,5 +1,5 @@
FROM alpine:edge AS builder
-RUN apk add --no-cache 'crystal=1.1.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev
+RUN apk add --no-cache 'crystal=1.1.1-r1' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev
ARG release
diff --git a/docker/init-invidious-db.sh b/docker/init-invidious-db.sh
index cc0e1c3f..22b4cc5f 100755
--- a/docker/init-invidious-db.sh
+++ b/docker/init-invidious-db.sh
@@ -1,10 +1,6 @@
#!/bin/bash
set -eou pipefail
-psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
- CREATE USER postgres;
-EOSQL
-
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channels.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/videos.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < config/sql/channel_videos.sql
diff --git a/locales/ar.json b/locales/ar.json
index 4f7f1e2c..50cdbe80 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -145,7 +145,7 @@
},
"search": "بحث",
"Log out": "تسجيل الخروج",
- "Released under the AGPLv3 on Github.": "تم إصداره بموجب AGPLv3 على Github.",
+ "Released under the AGPLv3 on Github.": "صدر تحت AGPLv3 على Github.",
"Source available here.": "الأكواد متوفرة هنا.",
"View JavaScript license information.": "مشاهدة معلومات حول تراخيص الجافاسكريبت.",
"View privacy policy.": "عرض سياسة الخصوصية.",
@@ -382,7 +382,7 @@
"News": "الأخبار",
"Movies": "الأفلام",
"Download": "نزّل",
- "Download as: ": "نزله كـ:. ",
+ "Download as: ": "نزله ك:. ",
"%A %B %-d, %Y": "%A %-d %B %Y",
"(edited)": "(تم تعديلة)",
"YouTube comment permalink": "رابط التعليق على اليوتيوب",
@@ -425,5 +425,12 @@
"next_steps_error_message_refresh": "تحديث",
"next_steps_error_message_go_to_youtube": "انتقل إلى يوتيوب",
"short": "قصير (< 4 دقائق)",
- "long": "طويل (> 20 دقيقة)"
+ "long": "طويل (> 20 دقيقة)",
+ "footer_source_code": "شفرة المصدر",
+ "footer_original_source_code": "شفرة المصدر الأصلية",
+ "footer_modfied_source_code": "شفرة المصدر المعدلة",
+ "adminprefs_modified_source_code_url_label": "URL إلى مستودع التعليمات البرمجية المصدرية المعدلة",
+ "footer_documentation": "التوثيق",
+ "footer_donate": "تبرّع: ",
+ "footer_donate_page": "تبرّع"
}
diff --git a/locales/en-US.json b/locales/en-US.json
index 1fa1983d..230d96ad 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -72,7 +72,7 @@
"Player volume: ": "Player volume: ",
"Default comments: ": "Default comments: ",
"youtube": "YouTube",
- "reddit": "reddit",
+ "reddit": "Reddit",
"Default captions: ": "Default captions: ",
"Fallback captions: ": "Fallback captions: ",
"Show related videos: ": "Show related videos: ",
@@ -426,7 +426,7 @@
"next_steps_error_message": "After which you should try to: ",
"next_steps_error_message_refresh": "Refresh",
"next_steps_error_message_go_to_youtube": "Go to YouTube",
- "footer_donate": "Donate: ",
+ "footer_donate_page": "Donate",
"footer_documentation": "Documentation",
"footer_source_code": "Source code",
"footer_original_source_code": "Original source code",
diff --git a/locales/eo.json b/locales/eo.json
index a00da6aa..054b8dd6 100644
--- a/locales/eo.json
+++ b/locales/eo.json
@@ -425,5 +425,12 @@
"next_steps_error_message_refresh": "Reŝargi",
"next_steps_error_message_go_to_youtube": "Iri al JuTubo",
"long": "Longa (> 20 minutos)",
- "short": "Mallonga (< 4 minutos)"
+ "short": "Mallonga (< 4 minutos)",
+ "footer_donate": "Doni: ",
+ "footer_documentation": "Dokumentaro",
+ "footer_source_code": "Fontkodo",
+ "adminprefs_modified_source_code_url_label": "URL al modifita deponejo de fontkodo",
+ "footer_modfied_source_code": "Modifita Fontkodo",
+ "footer_original_source_code": "Originala fontkodo",
+ "footer_donate_page": "Donaci"
}
diff --git a/locales/es.json b/locales/es.json
index 3cbe9be4..1a1b6753 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -424,6 +424,13 @@
"next_steps_error_message": "Después de lo cual deberías intentar: ",
"next_steps_error_message_refresh": "Recargar",
"next_steps_error_message_go_to_youtube": "Ir a YouTube",
- "short": "Corto (< minutos)",
- "long": "Largo (> minutos)"
+ "short": "Corto (< 4 minutos)",
+ "long": "Largo (> 20 minutos)",
+ "footer_documentation": "Documentación",
+ "footer_original_source_code": "Código fuente original",
+ "adminprefs_modified_source_code_url_label": "URL al repositorio de código fuente modificado",
+ "footer_source_code": "Código fuente",
+ "footer_donate": "Donar: ",
+ "footer_modfied_source_code": "Código fuente modificado",
+ "footer_donate_page": "Donar"
}
diff --git a/locales/ja.json b/locales/ja.json
index c4f78f96..4c2e692d 100644
--- a/locales/ja.json
+++ b/locales/ja.json
@@ -78,7 +78,7 @@
"Show related videos: ": "関連動画を表示: ",
"Show annotations by default: ": "デフォルトでアノテーションを表示: ",
"Automatically extend video description: ": "動画の説明文を自動的に拡張: ",
- "Interactive 360 degree videos: ": "インタラクティブ360°動画: ",
+ "Interactive 360 degree videos: ": "対話的な360°動画: ",
"Visual preferences": "外観設定",
"Player style: ": "プレイヤースタイル: ",
"Dark mode: ": "ダークモード: ",
@@ -137,7 +137,7 @@
},
"Import/export": "インポート/エクスポート",
"unsubscribe": "登録解除",
- "revoke": "revoke",
+ "revoke": "取り消す",
"Subscriptions": "登録チャンネル",
"`x` unseen notifications": {
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 個の未読通知",
@@ -145,7 +145,7 @@
},
"search": "検索",
"Log out": "ログアウト",
- "Released under the AGPLv3 on Github.": "Github 上で AGPLv3 の下で公開されています",
+ "Released under the AGPLv3 on Github.": "Github 上で AGPLv3 の元で公開されています",
"Source available here.": "ソースはここで閲覧可能です。",
"View JavaScript license information.": "JavaScript ライセンス情報",
"View privacy policy.": "プライバシーポリシー",
@@ -423,5 +423,13 @@
"Current version: ": "現在のバージョン: ",
"next_steps_error_message": "下記のものを試して下さい: ",
"next_steps_error_message_refresh": "再読込",
- "next_steps_error_message_go_to_youtube": "YouTubeへ"
+ "next_steps_error_message_go_to_youtube": "YouTubeへ",
+ "short": "4 分未満",
+ "footer_donate": "寄金: ",
+ "footer_documentation": "文書",
+ "footer_source_code": "ソースコード",
+ "footer_original_source_code": "ソースコード(元)",
+ "footer_modfied_source_code": "ソースコード(編集)",
+ "adminprefs_modified_source_code_url_label": "編集したソースコードのレポジトリーURL",
+ "long": "20 分以上"
}
diff --git a/locales/ko.json b/locales/ko.json
index 16d4797e..27fdc683 100644
--- a/locales/ko.json
+++ b/locales/ko.json
@@ -423,5 +423,13 @@
"today": "오늘",
"hour": "지난 1시간",
"sort": "정렬기준",
- "features": "기능별"
+ "features": "기능별",
+ "short": "4분 미만",
+ "long": "20분 초과",
+ "footer_donate": "후원: ",
+ "footer_documentation": "문서",
+ "footer_source_code": "소스 코드",
+ "footer_original_source_code": "원본 소스 코드",
+ "footer_modfied_source_code": "수정된 소스 코드",
+ "adminprefs_modified_source_code_url_label": "수정된 소스 코드 저장소의 URL"
}
diff --git a/locales/lt.json b/locales/lt.json
index 4b2fa5aa..f1667156 100644
--- a/locales/lt.json
+++ b/locales/lt.json
@@ -72,7 +72,7 @@
"Player volume: ": "Grotuvo garsas: ",
"Default comments: ": "Numatytieji komentarai: ",
"youtube": "YouTube",
- "reddit": "reddit",
+ "reddit": "Reddit",
"Default captions: ": "Numatytieji subtitrai: ",
"Fallback captions: ": "Atsarginiai subtitrai: ",
"Show related videos: ": "Rodyti susijusius vaizdo įrašus: ",
@@ -425,5 +425,12 @@
"next_steps_error_message_refresh": "Atnaujinti",
"next_steps_error_message_go_to_youtube": "Eiti į YouTube",
"short": "Trumpas (< 4 minučių)",
- "long": "Ilgas (> 20 minučių)"
+ "long": "Ilgas (> 20 minučių)",
+ "footer_documentation": "Dokumentacija",
+ "footer_source_code": "Pirminis kodas",
+ "footer_donate": "Paaukoti: ",
+ "footer_original_source_code": "Pradinis pirminis kodas",
+ "adminprefs_modified_source_code_url_label": "URL į pakeisto pirminio kodo repozitoriją",
+ "footer_modfied_source_code": "Pakeistas pirminis kodas",
+ "footer_donate_page": "Paaukoti"
}
diff --git a/locales/nb-NO.json b/locales/nb-NO.json
index 9e39a6c7..1a6dcb38 100644
--- a/locales/nb-NO.json
+++ b/locales/nb-NO.json
@@ -145,7 +145,7 @@
},
"search": "søk",
"Log out": "Logg ut",
- "Released under the AGPLv3 on Github.": "",
+ "Released under the AGPLv3 on Github.": "Tilgjengelig med AGPLv3-lisens på Github.",
"Source available here.": "Kildekode tilgjengelig her.",
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
"View privacy policy.": "Vis personvernspraksis.",
@@ -423,5 +423,13 @@
"Current version: ": "Gjeldende versjon: ",
"next_steps_error_message": "Etterpå bør du prøve dette: ",
"next_steps_error_message_refresh": "Gjenoppfrisk",
- "next_steps_error_message_go_to_youtube": "Gå til YouTube"
+ "next_steps_error_message_go_to_youtube": "Gå til YouTube",
+ "long": "Lang (> 20 minutter)",
+ "footer_donate_page": "Doner",
+ "short": "Kort (< 4 minutter)",
+ "footer_documentation": "Dokumentasjon",
+ "footer_source_code": "Kildekode",
+ "footer_original_source_code": "Opprinnelig kildekode",
+ "footer_modfied_source_code": "Endret kildekode",
+ "adminprefs_modified_source_code_url_label": "Nettadresse til kodelager inneholdende endret kildekode"
}
diff --git a/locales/pt.json b/locales/pt.json
index 66de7d10..918ab4c0 100644
--- a/locales/pt.json
+++ b/locales/pt.json
@@ -425,5 +425,11 @@
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` subscritores"
},
"short": "Curto (< 4 minutos)",
- "long": "Longo (> 20 minutos)"
+ "long": "Longo (> 20 minutos)",
+ "footer_source_code": "Código-fonte",
+ "footer_original_source_code": "Código-fonte original",
+ "adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado",
+ "footer_donate": "Fazer um donativo: ",
+ "footer_documentation": "Documentação",
+ "footer_modfied_source_code": "Código-fonte alterado"
}
diff --git a/locales/tr.json b/locales/tr.json
index 8432e8a9..60236d2f 100644
--- a/locales/tr.json
+++ b/locales/tr.json
@@ -72,7 +72,7 @@
"Player volume: ": "Oynatıcı ses seviyesi: ",
"Default comments: ": "Öntanımlı yorumlar: ",
"youtube": "YouTube",
- "reddit": "reddit",
+ "reddit": "Reddit",
"Default captions: ": "Öntanımlı altyazılar: ",
"Fallback captions: ": "Yedek altyazılar: ",
"Show related videos: ": "İlgili videoları göster: ",
@@ -425,5 +425,12 @@
"next_steps_error_message_refresh": "Yenile",
"next_steps_error_message_go_to_youtube": "YouTube'a git",
"short": "Kısa (4 dakikadan az)",
- "long": "Uzun (20 dakikadan fazla)"
+ "long": "Uzun (20 dakikadan fazla)",
+ "footer_donate": "Bağış yap: ",
+ "footer_documentation": "Belgelendirme",
+ "footer_source_code": "Kaynak kodları",
+ "footer_original_source_code": "Orijinal kaynak kodları",
+ "footer_modfied_source_code": "Değiştirilmiş kaynak kodları",
+ "adminprefs_modified_source_code_url_label": "Değiştirilmiş kaynak kodları deposunun URL'si",
+ "footer_donate_page": "Bağış yap"
}
diff --git a/locales/zh-CN.json b/locales/zh-CN.json
index 1033e77e..db6da6a8 100644
--- a/locales/zh-CN.json
+++ b/locales/zh-CN.json
@@ -425,5 +425,12 @@
"next_steps_error_message_refresh": "刷新",
"next_steps_error_message_go_to_youtube": "转到 YouTube",
"short": "短(少于4分钟)",
- "long": "长(多于 20 分钟)"
+ "long": "长(多于 20 分钟)",
+ "footer_donate": "捐赠: ",
+ "footer_documentation": "文档",
+ "footer_source_code": "源代码",
+ "footer_modfied_source_code": "修改的源代码",
+ "adminprefs_modified_source_code_url_label": "更改的源代码仓库网址",
+ "footer_original_source_code": "原始源代码",
+ "footer_donate_page": "捐赠"
}
diff --git a/locales/zh-TW.json b/locales/zh-TW.json
index 403221c4..22aaf643 100644
--- a/locales/zh-TW.json
+++ b/locales/zh-TW.json
@@ -72,7 +72,7 @@
"Player volume: ": "播放器音量: ",
"Default comments: ": "預設留言: ",
"youtube": "YouTube",
- "reddit": "reddit",
+ "reddit": "Reddit",
"Default captions: ": "預設字幕: ",
"Fallback captions: ": "汰退字幕: ",
"Show related videos: ": "顯示相關的影片: ",
@@ -425,5 +425,12 @@
"next_steps_error_message_refresh": "重新整理",
"next_steps_error_message_go_to_youtube": "到 YouTube",
"short": "短(小於4分鐘)",
- "long": "長(多於20分鐘)"
+ "long": "長(多於20分鐘)",
+ "footer_donate": "抖內: ",
+ "footer_documentation": "文件",
+ "footer_source_code": "原始碼",
+ "footer_original_source_code": "原本的原始碼",
+ "footer_modfied_source_code": "修改後的原始碼",
+ "adminprefs_modified_source_code_url_label": "修改後的原始碼倉庫 URL",
+ "footer_donate_page": "捐款"
}
diff --git a/src/invidious.cr b/src/invidious.cr
index f8f0784a..570c33e6 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -27,6 +27,7 @@ require "yaml"
require "compress/zip"
require "protodec/utils"
require "./invidious/helpers/*"
+require "./invidious/yt_backend/*"
require "./invidious/*"
require "./invidious/channels/*"
require "./invidious/user/*"
@@ -390,6 +391,13 @@ end
Invidious::Routing.post "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_post
{% end %}
+Invidious::Routing.get "/ggpht/*", Invidious::Routes::Images, :ggpht
+Invidious::Routing.options "/sb/:authority/:id/:storyboard/:index", Invidious::Routes::Images, :options_storyboard
+Invidious::Routing.get "/sb/:authority/:id/:storyboard/:index", Invidious::Routes::Images, :get_storyboard
+Invidious::Routing.get "/s_p/:id/:name", Invidious::Routes::Images, :s_p_image
+Invidious::Routing.get "/yts/img/:name", Invidious::Routes::Images, :yts_image
+Invidious::Routing.get "/vi/:id/:name", Invidious::Routes::Images, :thumbnails
+
# API routes (macro)
define_v1_api_routes()
@@ -1274,194 +1282,6 @@ post "/api/v1/auth/notifications" do |env|
create_notification_stream(env, topics, connection_channel)
end
-get "/ggpht/*" do |env|
- url = env.request.path.lchop("/ggpht")
-
- headers = HTTP::Headers{":authority" => "yt3.ggpht.com"}
- REQUEST_HEADERS_WHITELIST.each do |header|
- if env.request.headers[header]?
- headers[header] = env.request.headers[header]
- end
- end
-
- begin
- YT_POOL.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.downcase)
- env.response.headers[key] = value
- end
- end
-
- env.response.headers["Access-Control-Allow-Origin"] = "*"
-
- if response.status_code >= 300
- env.response.headers.delete("Transfer-Encoding")
- break
- end
-
- proxy_file(response, env)
- end
- rescue ex
- end
-end
-
-options "/sb/:authority/:id/:storyboard/:index" do |env|
- env.response.headers["Access-Control-Allow-Origin"] = "*"
- env.response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
- env.response.headers["Access-Control-Allow-Headers"] = "Content-Type, Range"
-end
-
-get "/sb/:authority/:id/:storyboard/:index" do |env|
- authority = env.params.url["authority"]
- id = env.params.url["id"]
- storyboard = env.params.url["storyboard"]
- index = env.params.url["index"]
-
- url = "/sb/#{id}/#{storyboard}/#{index}?#{env.params.query}"
-
- headers = HTTP::Headers.new
-
- headers[":authority"] = "#{authority}.ytimg.com"
-
- REQUEST_HEADERS_WHITELIST.each do |header|
- if env.request.headers[header]?
- headers[header] = env.request.headers[header]
- end
- end
-
- begin
- YT_POOL.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.downcase)
- env.response.headers[key] = value
- end
- end
-
- env.response.headers["Connection"] = "close"
- env.response.headers["Access-Control-Allow-Origin"] = "*"
-
- if response.status_code >= 300
- env.response.headers.delete("Transfer-Encoding")
- break
- end
-
- proxy_file(response, env)
- end
- rescue ex
- end
-end
-
-get "/s_p/:id/:name" do |env|
- id = env.params.url["id"]
- name = env.params.url["name"]
-
- url = env.request.resource
-
- headers = HTTP::Headers{":authority" => "i9.ytimg.com"}
- REQUEST_HEADERS_WHITELIST.each do |header|
- if env.request.headers[header]?
- headers[header] = env.request.headers[header]
- end
- end
-
- begin
- YT_POOL.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.downcase)
- 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 "/yts/img/:name" do |env|
- headers = HTTP::Headers.new
- REQUEST_HEADERS_WHITELIST.each do |header|
- if env.request.headers[header]?
- headers[header] = env.request.headers[header]
- end
- end
-
- begin
- YT_POOL.client &.get(env.request.resource, headers) do |response|
- env.response.status_code = response.status_code
- response.headers.each do |key, value|
- if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
- 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"]
-
- headers = HTTP::Headers{":authority" => "i.ytimg.com"}
-
- if name == "maxres.jpg"
- build_thumbnails(id).each do |thumb|
- if YT_POOL.client &.head("/vi/#{id}/#{thumb[:url]}.jpg", headers).status_code == 200
- name = thumb[:url] + ".jpg"
- break
- end
- end
- end
- url = "/vi/#{id}/#{name}"
-
- REQUEST_HEADERS_WHITELIST.each do |header|
- if env.request.headers[header]?
- headers[header] = env.request.headers[header]
- end
- end
-
- begin
- YT_POOL.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.downcase)
- 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 "/Captcha" do |env|
headers = HTTP::Headers{":authority" => "accounts.google.com"}
response = YT_POOL.client &.get(env.request.resource, headers)
diff --git a/src/invidious/comments.cr b/src/invidious/comments.cr
index a5506b03..9c788253 100644
--- a/src/invidious/comments.cr
+++ b/src/invidious/comments.cr
@@ -329,7 +329,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false)
html << <<-END_HTML
<div class="pure-g" style="width:100%">
<div class="channel-profile pure-u-4-24 pure-u-md-2-24">
- <img style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}">
+ <img loading="lazy" style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}">
</div>
<div class="pure-u-20-24 pure-u-md-22-24">
<p>
@@ -349,7 +349,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false)
html << <<-END_HTML
<div class="pure-g">
<div class="pure-u-1 pure-u-md-1-2">
- <img style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}">
+ <img loading="lazy" style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}">
</div>
</div>
END_HTML
@@ -410,7 +410,7 @@ def template_youtube_comments(comments, locale, thin_mode, is_replies = false)
html << <<-END_HTML
<span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
<div class="creator-heart">
- <img class="creator-heart-background-hearted" src="#{creator_thumbnail}"></img>
+ <img loading="lazy" class="creator-heart-background-hearted" src="#{creator_thumbnail}"></img>
<div class="creator-heart-small-hearted">
<div class="icon ion-ios-heart creator-heart-small-container"></div>
</div>
diff --git a/src/invidious/helpers/helpers.cr b/src/invidious/helpers/helpers.cr
index 91377b0d..2e61d21f 100644
--- a/src/invidious/helpers/helpers.cr
+++ b/src/invidious/helpers/helpers.cr
@@ -60,43 +60,6 @@ def html_to_content(description_html : String)
return description
end
-def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
- extracted = extract_items(initial_data, author_fallback, author_id_fallback)
-
- target = [] of SearchItem
- extracted.each do |i|
- if i.is_a?(Category)
- i.contents.each { |cate_i| target << cate_i if !cate_i.is_a? Video }
- else
- target << i
- end
- end
- return target.select(&.is_a?(SearchVideo)).map(&.as(SearchVideo))
-end
-
-def extract_selected_tab(tabs)
- # Extract the selected tab from the array of tabs Youtube returns
- return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"].as_bool)[0]["tabRenderer"]
-end
-
-def fetch_continuation_token(items : Array(JSON::Any))
- # Fetches the continuation token from an array of items
- return items.last["continuationItemRenderer"]?
- .try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
-end
-
-def fetch_continuation_token(initial_data : Hash(String, JSON::Any))
- # Fetches the continuation token from initial data
- if initial_data["onResponseReceivedActions"]?
- continuation_items = initial_data["onResponseReceivedActions"][0]["appendContinuationItemsAction"]["continuationItems"]
- else
- tab = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])
- continuation_items = tab["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"][0]["gridRenderer"]["items"]
- end
-
- return fetch_continuation_token(continuation_items.as_a)
-end
-
def check_enum(db, enum_name, struct_type = nil)
return # TODO
diff --git a/src/invidious/helpers/serialized_yt_data.cr b/src/invidious/helpers/serialized_yt_data.cr
index 61356555..a9798f0c 100644
--- a/src/invidious/helpers/serialized_yt_data.cr
+++ b/src/invidious/helpers/serialized_yt_data.cr
@@ -237,8 +237,15 @@ class Category
def to_json(locale, json : JSON::Builder)
json.object do
+ json.field "type", "category"
json.field "title", self.title
- json.field "contents", self.contents
+ json.field "contents" do
+ json.array do
+ self.contents.each do |item|
+ item.to_json(locale, json)
+ end
+ end
+ end
end
end
diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr
index af5f553b..603b4e1f 100644
--- a/src/invidious/helpers/utils.cr
+++ b/src/invidious/helpers/utils.cr
@@ -1,70 +1,5 @@
-require "lsquic"
require "db"
-def add_yt_headers(request)
- request.headers["user-agent"] ||= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
- request.headers["accept-charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
- request.headers["accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
- request.headers["accept-language"] ||= "en-us,en;q=0.5"
- return if request.resource.starts_with? "/sorry/index"
- request.headers["x-youtube-client-name"] ||= "1"
- request.headers["x-youtube-client-version"] ||= "2.20200609"
- # Preserve original cookies and add new YT consent cookie for EU servers
- request.headers["cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+"
- if !CONFIG.cookies.empty?
- request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
- end
-end
-
-struct YoutubeConnectionPool
- property! url : URI
- property! capacity : Int32
- property! timeout : Float64
- property pool : DB::Pool(QUIC::Client | HTTP::Client)
-
- def initialize(url : URI, @capacity = 5, @timeout = 5.0, use_quic = true)
- @url = url
- @pool = build_pool(use_quic)
- end
-
- def client(region = nil, &block)
- if region
- conn = make_client(url, region)
- response = yield conn
- else
- conn = pool.checkout
- begin
- response = yield conn
- rescue ex
- conn.close
- conn = QUIC::Client.new(url)
- conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
- conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
- conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
- response = yield conn
- ensure
- pool.release(conn)
- end
- end
-
- response
- end
-
- private def build_pool(use_quic)
- DB::Pool(QUIC::Client | HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
- if use_quic
- conn = QUIC::Client.new(url)
- else
- conn = HTTP::Client.new(url)
- end
- conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
- conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
- conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
- conn
- end
- end
-end
-
# See http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
def ci_lower_bound(pos, n)
if n == 0
@@ -85,37 +20,6 @@ def elapsed_text(elapsed)
"#{(millis * 1000).round(2)}µs"
end
-def make_client(url : URI, region = nil)
- # TODO: Migrate any applicable endpoints to QUIC
- client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure)
- client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC
- client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
- client.read_timeout = 10.seconds
- client.connect_timeout = 10.seconds
-
- if region
- PROXY_LIST[region]?.try &.sample(40).each do |proxy|
- begin
- proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port])
- client.set_proxy(proxy)
- break
- rescue ex
- end
- end
- end
-
- return client
-end
-
-def make_client(url : URI, region = nil, &block)
- client = make_client(url, region)
- begin
- yield client
- ensure
- client.close
- end
-end
-
def decode_length_seconds(string)
length_seconds = string.gsub(/[^0-9:]/, "").split(":").map &.to_i
length_seconds = [0] * (3 - length_seconds.size) + length_seconds
diff --git a/src/invidious/mixes.cr b/src/invidious/mixes.cr
index 55b01174..63ea434f 100644
--- a/src/invidious/mixes.cr
+++ b/src/invidious/mixes.cr
@@ -97,7 +97,7 @@ def template_mix(mix)
<li class="pure-menu-item">
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
<div class="thumbnail">
- <img class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
+ <img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
</div>
<p style="width:100%">#{video["title"]}</p>
diff --git a/src/invidious/playlists.cr b/src/invidious/playlists.cr
index f56cc2ea..7940dc1f 100644
--- a/src/invidious/playlists.cr
+++ b/src/invidious/playlists.cr
@@ -107,7 +107,7 @@ struct Playlist
property updated : Time
property thumbnail : String?
- def to_json(offset, locale, json : JSON::Builder, continuation : String? = nil)
+ def to_json(offset, locale, json : JSON::Builder, video_id : String? = nil)
json.object do
json.field "type", "playlist"
json.field "title", self.title
@@ -142,7 +142,7 @@ struct Playlist
json.field "videos" do
json.array do
- videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, continuation: continuation)
+ videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, video_id: video_id)
videos.each_with_index do |video, index|
video.to_json(locale, json)
end
@@ -151,12 +151,12 @@ struct Playlist
end
end
- def to_json(offset, locale, json : JSON::Builder? = nil, continuation : String? = nil)
+ def to_json(offset, locale, json : JSON::Builder? = nil, video_id : String? = nil)
if json
- to_json(offset, locale, json, continuation: continuation)
+ to_json(offset, locale, json, video_id: video_id)
else
JSON.build do |json|
- to_json(offset, locale, json, continuation: continuation)
+ to_json(offset, locale, json, video_id: video_id)
end
end
end
@@ -196,7 +196,7 @@ struct InvidiousPlaylist
end
end
- def to_json(offset, locale, json : JSON::Builder, continuation : String? = nil)
+ def to_json(offset, locale, json : JSON::Builder, video_id : String? = nil)
json.object do
json.field "type", "invidiousPlaylist"
json.field "title", self.title
@@ -218,11 +218,11 @@ struct InvidiousPlaylist
json.field "videos" do
json.array do
if !offset || offset == 0
- index = PG_DB.query_one?("SELECT index FROM playlist_videos WHERE plid = $1 AND id = $2 LIMIT 1", self.id, continuation, as: Int64)
+ index = PG_DB.query_one?("SELECT index FROM playlist_videos WHERE plid = $1 AND id = $2 LIMIT 1", self.id, video_id, as: Int64)
offset = self.index.index(index) || 0
end
- videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, continuation: continuation)
+ videos = get_playlist_videos(PG_DB, self, offset: offset, locale: locale, video_id: video_id)
videos.each_with_index do |video, index|
video.to_json(locale, json, offset + index)
end
@@ -231,12 +231,12 @@ struct InvidiousPlaylist
end
end
- def to_json(offset, locale, json : JSON::Builder? = nil, continuation : String? = nil)
+ def to_json(offset, locale, json : JSON::Builder? = nil, video_id : String? = nil)
if json
- to_json(offset, locale, json, continuation: continuation)
+ to_json(offset, locale, json, video_id: video_id)
else
JSON.build do |json|
- to_json(offset, locale, json, continuation: continuation)
+ to_json(offset, locale, json, video_id: video_id)
end
end
end
@@ -426,7 +426,7 @@ def fetch_playlist(plid, locale)
})
end
-def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil)
+def get_playlist_videos(db, playlist, offset, locale = nil, video_id = nil)
# Show empy playlist if requested page is out of range
# (e.g, when a new playlist has been created, offset will be negative)
if offset >= playlist.video_count || offset < 0
@@ -437,17 +437,26 @@ def get_playlist_videos(db, playlist, offset, locale = nil, continuation = nil)
db.query_all("SELECT * FROM playlist_videos WHERE plid = $1 ORDER BY array_position($2, index) LIMIT 100 OFFSET $3",
playlist.id, playlist.index, offset, as: PlaylistVideo)
else
- if offset >= 100
- # Normalize offset to match youtube's behavior (100 videos chunck per request)
- offset = (offset / 100).to_i64 * 100_i64
+ if video_id
+ initial_data = YoutubeAPI.next({
+ "videoId" => video_id,
+ "playlistId" => playlist.id,
+ })
+ offset = initial_data.dig?("contents", "twoColumnWatchNextResults", "playlist", "playlist", "currentIndex").try &.as_i || offset
+ end
+
+ videos = [] of PlaylistVideo
+ until videos.size >= 200 || videos.size == playlist.video_count || offset >= playlist.video_count
+ # 100 videos per request
ctoken = produce_playlist_continuation(playlist.id, offset)
initial_data = YoutubeAPI.browse(ctoken)
- else
- initial_data = YoutubeAPI.browse("VL" + playlist.id, params: "")
+ videos += extract_playlist_videos(initial_data)
+
+ offset += 100
end
- return extract_playlist_videos(initial_data)
+ return videos
end
end
@@ -523,10 +532,10 @@ def template_playlist(playlist)
playlist["videos"].as_a.each do |video|
html += <<-END_HTML
- <li class="pure-menu-item">
- <a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}">
+ <li class="pure-menu-item" id="#{video["videoId"]}">
+ <a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}">
<div class="thumbnail">
- <img class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
+ <img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
</div>
<p style="width:100%">#{video["title"]}</p>
diff --git a/src/invidious/routes/api/v1/misc.cr b/src/invidious/routes/api/v1/misc.cr
index cf95bd9b..80b59fd5 100644
--- a/src/invidious/routes/api/v1/misc.cr
+++ b/src/invidious/routes/api/v1/misc.cr
@@ -24,7 +24,7 @@ module Invidious::Routes::API::V1::Misc
offset ||= env.params.query["page"]?.try &.to_i?.try { |page| (page - 1) * 100 }
offset ||= 0
- continuation = env.params.query["continuation"]?
+ video_id = env.params.query["continuation"]?
format = env.params.query["format"]?
format ||= "json"
@@ -46,12 +46,32 @@ module Invidious::Routes::API::V1::Misc
return error_json(404, "Playlist does not exist.")
end
- response = playlist.to_json(offset, locale, continuation: continuation)
+ # includes into the playlist a maximum of 20 videos, before the offset
+ if offset > 0
+ lookback = offset < 50 ? offset : 50
+ response = playlist.to_json(offset - lookback, locale)
+ json_response = JSON.parse(response)
+ else
+ # Unless the continuation is really the offset 0, it becomes expensive.
+ # It happens when the offset is not set.
+ # First we find the actual offset, and then we lookback
+ # it shouldn't happen often though
+
+ lookback = 0
+ response = playlist.to_json(offset, locale, video_id: video_id)
+ json_response = JSON.parse(response)
+
+ if json_response["videos"].as_a[0]["index"] != offset
+ offset = json_response["videos"].as_a[0]["index"].as_i
+ lookback = offset < 50 ? offset : 50
+ response = playlist.to_json(offset - lookback, locale)
+ json_response = JSON.parse(response)
+ end
+ end
if format == "html"
- response = JSON.parse(response)
- playlist_html = template_playlist(response)
- index, next_video = response["videos"].as_a.skip(1).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
+ playlist_html = template_playlist(json_response)
+ index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
response = {
"playlistHtml" => playlist_html,
diff --git a/src/invidious/routes/images.cr b/src/invidious/routes/images.cr
new file mode 100644
index 00000000..bb924cdf
--- /dev/null
+++ b/src/invidious/routes/images.cr
@@ -0,0 +1,191 @@
+module Invidious::Routes::Images
+ # Avatars, banners and other large image assets.
+ def self.ggpht(env)
+ url = env.request.path.lchop("/ggpht")
+
+ headers = HTTP::Headers{":authority" => "yt3.ggpht.com"}
+ REQUEST_HEADERS_WHITELIST.each do |header|
+ if env.request.headers[header]?
+ headers[header] = env.request.headers[header]
+ end
+ end
+
+ begin
+ YT_POOL.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.downcase)
+ env.response.headers[key] = value
+ end
+ end
+
+ env.response.headers["Access-Control-Allow-Origin"] = "*"
+
+ if response.status_code >= 300
+ env.response.headers.delete("Transfer-Encoding")
+ break
+ end
+
+ proxy_file(response, env)
+ end
+ rescue ex
+ end
+ end
+
+ def self.options_storyboard(env)
+ env.response.headers["Access-Control-Allow-Origin"] = "*"
+ env.response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
+ env.response.headers["Access-Control-Allow-Headers"] = "Content-Type, Range"
+ end
+
+ def self.get_storyboard(env)
+ authority = env.params.url["authority"]
+ id = env.params.url["id"]
+ storyboard = env.params.url["storyboard"]
+ index = env.params.url["index"]
+
+ url = "/sb/#{id}/#{storyboard}/#{index}?#{env.params.query}"
+
+ headers = HTTP::Headers.new
+
+ headers[":authority"] = "#{authority}.ytimg.com"
+
+ REQUEST_HEADERS_WHITELIST.each do |header|
+ if env.request.headers[header]?
+ headers[header] = env.request.headers[header]
+ end
+ end
+
+ begin
+ YT_POOL.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.downcase)
+ env.response.headers[key] = value
+ end
+ end
+
+ env.response.headers["Connection"] = "close"
+ env.response.headers["Access-Control-Allow-Origin"] = "*"
+
+ if response.status_code >= 300
+ env.response.headers.delete("Transfer-Encoding")
+ break
+ end
+
+ proxy_file(response, env)
+ end
+ rescue ex
+ end
+ end
+
+ # ??? maybe also for storyboards?
+ def self.s_p_image(env)
+ id = env.params.url["id"]
+ name = env.params.url["name"]
+
+ url = env.request.resource
+
+ headers = HTTP::Headers{":authority" => "i9.ytimg.com"}
+ REQUEST_HEADERS_WHITELIST.each do |header|
+ if env.request.headers[header]?
+ headers[header] = env.request.headers[header]
+ end
+ end
+
+ begin
+ YT_POOL.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.downcase)
+ 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
+
+ def self.yts_image(env)
+ headers = HTTP::Headers.new
+ REQUEST_HEADERS_WHITELIST.each do |header|
+ if env.request.headers[header]?
+ headers[header] = env.request.headers[header]
+ end
+ end
+
+ begin
+ YT_POOL.client &.get(env.request.resource, headers) do |response|
+ env.response.status_code = response.status_code
+ response.headers.each do |key, value|
+ if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
+ 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
+
+ def self.thumbnails(env)
+ id = env.params.url["id"]
+ name = env.params.url["name"]
+
+ headers = HTTP::Headers{":authority" => "i.ytimg.com"}
+
+ if name == "maxres.jpg"
+ build_thumbnails(id).each do |thumb|
+ if YT_POOL.client &.head("/vi/#{id}/#{thumb[:url]}.jpg", headers).status_code == 200
+ name = thumb[:url] + ".jpg"
+ break
+ end
+ end
+ end
+ url = "/vi/#{id}/#{name}"
+
+ REQUEST_HEADERS_WHITELIST.each do |header|
+ if env.request.headers[header]?
+ headers[header] = env.request.headers[header]
+ end
+ end
+
+ begin
+ YT_POOL.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.downcase)
+ 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
+end
diff --git a/src/invidious/views/components/item.ecr b/src/invidious/views/components/item.ecr
index b15ae255..5788bf51 100644
--- a/src/invidious/views/components/item.ecr
+++ b/src/invidious/views/components/item.ecr
@@ -5,7 +5,7 @@
<a href="/channel/<%= item.ucid %>">
<% if !env.get("preferences").as(Preferences).thin_mode %>
<center>
- <img style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/>
+ <img loading="lazy" style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/>
</center>
<% end %>
<p dir="auto"><%= HTML.escape(item.author) %></p>
@@ -23,7 +23,7 @@
<a style="width:100%" href="<%= url %>">
<% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail">
- <img class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>"/>
+ <img loading="lazy" class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>"/>
<p class="length"><%= number_with_separator(item.video_count) %> videos</p>
</div>
<% end %>
@@ -36,7 +36,7 @@
<a href="/watch?v=<%= item.id %>&list=<%= item.rdid %>">
<% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail">
- <img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
+ <img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if item.length_seconds != 0 %>
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
<% end %>
@@ -48,10 +48,10 @@
<p dir="auto"><b><%= HTML.escape(item.author) %></b></p>
</a>
<% when PlaylistVideo %>
- <a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>">
+ <a style="width:100%" href="/watch?v=<%= item.id %>&list=<%= item.plid %>&index=<%= item.index %>">
<% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail">
- <img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
+ <img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if plid = env.get?("remove_playlist_items") %>
<form data-onsubmit="return_false" action="/playlist_ajax?action_remove_video=1&set_video_id=<%= item.index %>&playlist_id=<%= plid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
@@ -114,7 +114,7 @@
<a style="width:100%" href="/watch?v=<%= item.id %>">
<% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail">
- <img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
+ <img loading="lazy" class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if env.get? "show_watched" %>
<form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
diff --git a/src/invidious/views/template.ecr b/src/invidious/views/template.ecr
index b7020598..3fb2fe18 100644
--- a/src/invidious/views/template.ecr
+++ b/src/invidious/views/template.ecr
@@ -149,9 +149,7 @@
<div class="pure-u-1 pure-u-md-1-3">
<span>
<i class="icon ion-ios-wallet"></i>
- <%= translate(locale, "footer_donate") %>
- <a href="bitcoin:bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr">BTC</a>&nbsp;/
- <a href="monero:41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR">XMR</a>
+ <a href="https://invidious.io/donate/"><%= translate(locale, "footer_donate_page") %></a>
</span>
<span><%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %></span>
</div>
diff --git a/src/invidious/views/watch.ecr b/src/invidious/views/watch.ecr
index 68e7eb80..398e25b6 100644
--- a/src/invidious/views/watch.ecr
+++ b/src/invidious/views/watch.ecr
@@ -303,7 +303,7 @@ we're going to need to do it here in order to allow for translations.
<a href="/watch?v=<%= rv["id"] %>">
<% if !env.get("preferences").as(Preferences).thin_mode %>
<div class="thumbnail">
- <img class="thumbnail" src="/vi/<%= rv["id"] %>/mqdefault.jpg">
+ <img loading="lazy" class="thumbnail" src="/vi/<%= rv["id"] %>/mqdefault.jpg">
<p class="length"><%= recode_length_seconds(rv["length_seconds"]?.try &.to_i? || 0) %></p>
</div>
<% end %>
diff --git a/src/invidious/yt_backend/connection_pool.cr b/src/invidious/yt_backend/connection_pool.cr
new file mode 100644
index 00000000..5ba2d73c
--- /dev/null
+++ b/src/invidious/yt_backend/connection_pool.cr
@@ -0,0 +1,96 @@
+require "lsquic"
+
+def add_yt_headers(request)
+ request.headers["user-agent"] ||= "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36"
+ request.headers["accept-charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
+ request.headers["accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
+ request.headers["accept-language"] ||= "en-us,en;q=0.5"
+ return if request.resource.starts_with? "/sorry/index"
+ request.headers["x-youtube-client-name"] ||= "1"
+ request.headers["x-youtube-client-version"] ||= "2.20200609"
+ # Preserve original cookies and add new YT consent cookie for EU servers
+ request.headers["cookie"] = "#{request.headers["cookie"]?}; CONSENT=YES+"
+ if !CONFIG.cookies.empty?
+ request.headers["cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
+ end
+end
+
+struct YoutubeConnectionPool
+ property! url : URI
+ property! capacity : Int32
+ property! timeout : Float64
+ property pool : DB::Pool(QUIC::Client | HTTP::Client)
+
+ def initialize(url : URI, @capacity = 5, @timeout = 5.0, use_quic = true)
+ @url = url
+ @pool = build_pool(use_quic)
+ end
+
+ def client(region = nil, &block)
+ if region
+ conn = make_client(url, region)
+ response = yield conn
+ else
+ conn = pool.checkout
+ begin
+ response = yield conn
+ rescue ex
+ conn.close
+ conn = QUIC::Client.new(url)
+ conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
+ conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
+ conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
+ response = yield conn
+ ensure
+ pool.release(conn)
+ end
+ end
+
+ response
+ end
+
+ private def build_pool(use_quic)
+ DB::Pool(QUIC::Client | HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
+ if use_quic
+ conn = QUIC::Client.new(url)
+ else
+ conn = HTTP::Client.new(url)
+ end
+ conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
+ conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
+ conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
+ conn
+ end
+ end
+end
+
+def make_client(url : URI, region = nil)
+ # TODO: Migrate any applicable endpoints to QUIC
+ client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure)
+ client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC
+ client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
+ client.read_timeout = 10.seconds
+ client.connect_timeout = 10.seconds
+
+ if region
+ PROXY_LIST[region]?.try &.sample(40).each do |proxy|
+ begin
+ proxy = HTTPProxy.new(proxy_host: proxy[:ip], proxy_port: proxy[:port])
+ client.set_proxy(proxy)
+ break
+ rescue ex
+ end
+ end
+ end
+
+ return client
+end
+
+def make_client(url : URI, region = nil, &block)
+ client = make_client(url, region)
+ begin
+ yield client
+ ensure
+ client.close
+ end
+end
diff --git a/src/invidious/helpers/extractors.cr b/src/invidious/yt_backend/extractors.cr
index c8a6cd4a..8398ca8e 100644
--- a/src/invidious/helpers/extractors.cr
+++ b/src/invidious/yt_backend/extractors.cr
@@ -43,7 +43,7 @@ private module Parsers
private def self.parse(item_contents, author_fallback)
video_id = item_contents["videoId"].as_s
- title = extract_text(item_contents["title"]) || ""
+ title = extract_text(item_contents["title"]?) || ""
# Extract author information
if author_info = item_contents.dig?("ownerText", "runs", 0)
@@ -321,11 +321,13 @@ private module Parsers
content_container = item_contents["contents"]
end
- raw_contents = content_container["items"].as_a
- raw_contents.each do |item|
- result = extract_item(item)
- if !result.nil?
- contents << result
+ raw_contents = content_container["items"]?.try &.as_a
+ if !raw_contents.nil?
+ raw_contents.each do |item|
+ result = extract_item(item)
+ if !result.nil?
+ contents << result
+ end
end
end
@@ -399,7 +401,7 @@ private module Extractors
items_container = renderer_container_contents
end
- items_container["items"].as_a.each do |item|
+ items_container["items"]?.try &.as_a.each do |item|
raw_items << item
end
end
@@ -531,37 +533,6 @@ private module HelperExtractors
end
end
-# Extracts text from InnerTube response
-#
-# InnerTube can package text in three different formats
-# "runs": [
-# {"text": "something"},
-# {"text": "cont"},
-# ...
-# ]
-#
-# "SimpleText": "something"
-#
-# Or sometimes just none at all as with the data returned from
-# category continuations.
-#
-# In order to facilitate calling this function with `#[]?`:
-# A nil will be accepted. Of course, since nil cannot be parsed,
-# another nil will be returned.
-def extract_text(item : JSON::Any?) : String?
- if item.nil?
- return nil
- end
-
- if text_container = item["simpleText"]?
- return text_container.as_s
- elsif text_container = item["runs"]?
- return text_container.as_a.map(&.["text"].as_s).join("")
- else
- nil
- end
-end
-
# Parses an item from Youtube's JSON response into a more usable structure.
# The end result can either be a SearchVideo, SearchPlaylist or SearchChannel.
def extract_item(item : JSON::Any, author_fallback : String? = "",
diff --git a/src/invidious/yt_backend/extractors_utils.cr b/src/invidious/yt_backend/extractors_utils.cr
new file mode 100644
index 00000000..97cc0997
--- /dev/null
+++ b/src/invidious/yt_backend/extractors_utils.cr
@@ -0,0 +1,67 @@
+# Extracts text from InnerTube response
+#
+# InnerTube can package text in three different formats
+# "runs": [
+# {"text": "something"},
+# {"text": "cont"},
+# ...
+# ]
+#
+# "SimpleText": "something"
+#
+# Or sometimes just none at all as with the data returned from
+# category continuations.
+#
+# In order to facilitate calling this function with `#[]?`:
+# A nil will be accepted. Of course, since nil cannot be parsed,
+# another nil will be returned.
+def extract_text(item : JSON::Any?) : String?
+ if item.nil?
+ return nil
+ end
+
+ if text_container = item["simpleText"]?
+ return text_container.as_s
+ elsif text_container = item["runs"]?
+ return text_container.as_a.map(&.["text"].as_s).join("")
+ else
+ nil
+ end
+end
+
+def extract_videos(initial_data : Hash(String, JSON::Any), author_fallback : String? = nil, author_id_fallback : String? = nil)
+ extracted = extract_items(initial_data, author_fallback, author_id_fallback)
+
+ target = [] of SearchItem
+ extracted.each do |i|
+ if i.is_a?(Category)
+ i.contents.each { |cate_i| target << cate_i if !cate_i.is_a? Video }
+ else
+ target << i
+ end
+ end
+ return target.select(&.is_a?(SearchVideo)).map(&.as(SearchVideo))
+end
+
+def extract_selected_tab(tabs)
+ # Extract the selected tab from the array of tabs Youtube returns
+ return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"].as_bool)[0]["tabRenderer"]
+end
+
+def fetch_continuation_token(items : Array(JSON::Any))
+ # Fetches the continuation token from an array of items
+ return items.last["continuationItemRenderer"]?
+ .try &.["continuationEndpoint"]["continuationCommand"]["token"].as_s
+end
+
+def fetch_continuation_token(initial_data : Hash(String, JSON::Any))
+ # Fetches the continuation token from initial data
+ if initial_data["onResponseReceivedActions"]?
+ continuation_items = initial_data["onResponseReceivedActions"][0]["appendContinuationItemsAction"]["continuationItems"]
+ else
+ tab = extract_selected_tab(initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"])
+ continuation_items = tab["content"]["sectionListRenderer"]["contents"][0]["itemSectionRenderer"]["contents"][0]["gridRenderer"]["items"]
+ end
+
+ return fetch_continuation_token(continuation_items.as_a)
+end
diff --git a/src/invidious/helpers/proxy.cr b/src/invidious/yt_backend/proxy.cr
index 3418d887..3418d887 100644
--- a/src/invidious/helpers/proxy.cr
+++ b/src/invidious/yt_backend/proxy.cr
diff --git a/src/invidious/helpers/youtube_api.cr b/src/invidious/yt_backend/youtube_api.cr
index b3815f6a..b3815f6a 100644
--- a/src/invidious/helpers/youtube_api.cr
+++ b/src/invidious/yt_backend/youtube_api.cr