summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/container-release.yml21
-rw-r--r--.github/workflows/lock.yml4
-rw-r--r--docker/APKBUILD-boringssl46
-rw-r--r--docker/APKBUILD-lsquic43
-rw-r--r--docker/Dockerfile33
-rw-r--r--docker/Dockerfile.arm6433
-rw-r--r--locales/ar.json4
-rw-r--r--locales/eo.json6
-rw-r--r--locales/es.json14
-rw-r--r--locales/lt.json8
-rw-r--r--locales/pl.json40
-rw-r--r--locales/sv-SE.json26
-rw-r--r--shard.lock4
-rw-r--r--shard.yml3
-rw-r--r--src/invidious.cr458
-rw-r--r--src/invidious/helpers/youtube_api.cr25
-rw-r--r--src/invidious/routes/feeds.cr431
-rw-r--r--src/invidious/routes/login.cr7
-rw-r--r--src/invidious/routes/misc.cr2
-rw-r--r--src/invidious/routes/playlists.cr27
-rw-r--r--src/invidious/videos.cr165
-rw-r--r--src/invidious/views/feeds/history.ecr (renamed from src/invidious/views/history.ecr)0
-rw-r--r--src/invidious/views/feeds/playlists.ecr (renamed from src/invidious/views/view_all_playlists.ecr)0
-rw-r--r--src/invidious/views/feeds/popular.ecr (renamed from src/invidious/views/popular.ecr)0
-rw-r--r--src/invidious/views/feeds/subscriptions.ecr (renamed from src/invidious/views/subscriptions.ecr)0
-rw-r--r--src/invidious/views/feeds/trending.ecr (renamed from src/invidious/views/trending.ecr)0
-rw-r--r--src/invidious/views/playlist.ecr2
-rw-r--r--src/invidious/views/preferences.ecr2
28 files changed, 606 insertions, 798 deletions
diff --git a/.github/workflows/container-release.yml b/.github/workflows/container-release.yml
index 756374da..9cc39783 100644
--- a/.github/workflows/container-release.yml
+++ b/.github/workflows/container-release.yml
@@ -30,15 +30,6 @@ jobs:
username: ${{ secrets.QUAY_USERNAME }}
password: ${{ secrets.QUAY_PASSWORD }}
- - name: Cache Docker layers
- if: github.ref == 'refs/heads/master'
- uses: actions/cache@v2
- with:
- path: /tmp/.buildx-cache
- key: ${{ runner.os }}-multi-buildx-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-multi-buildx
-
- name: Build and push Docker AMD64 image for Push Event
if: github.ref == 'refs/heads/master'
uses: docker/build-push-action@v2
@@ -49,8 +40,6 @@ jobs:
labels: quay.expires-after=12w
push: true
tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest
- cache-from: type=local,src=/tmp/.buildx-cache
- cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
- name: Build and push Docker ARM64 image for Push Event
if: github.ref == 'refs/heads/master'
@@ -61,12 +50,4 @@ jobs:
platforms: linux/arm64/v8
labels: quay.expires-after=12w
push: true
- tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64
- cache-from: type=local,src=/tmp/.buildx-cache
- cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new
-
- - name: Override old Docker cache
- if: github.ref == 'refs/heads/master'
- run: |
- rm -rf /tmp/.buildx-cache
- mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+ tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64 \ No newline at end of file
diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml
index afed2240..aa9e2b31 100644
--- a/.github/workflows/lock.yml
+++ b/.github/workflows/lock.yml
@@ -13,8 +13,8 @@ jobs:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
- issue-lock-inactive-days: '30'
- pr-lock-inactive-days: '30'
+ issue-lock-inactive-days: '240'
+ pr-lock-inactive-days: '240'
issue-lock-reason: 'resolved'
pr-lock-reason: 'resolved'
diff --git a/docker/APKBUILD-boringssl b/docker/APKBUILD-boringssl
deleted file mode 100644
index 61caa4f1..00000000
--- a/docker/APKBUILD-boringssl
+++ /dev/null
@@ -1,46 +0,0 @@
-# Based on https://aur.archlinux.org/packages/boringssl-git/
-# Maintainer: Omar Roth <omarroth@protonmail.com>
-pkgname=boringssl
-pkgver=1.1.0
-pkgrel=0
-pkgdesc="BoringSSL is a fork of OpenSSL that is designed to meet Google's needs"
-url="https://boringssl.googlesource.com/boringssl"
-arch="all"
-license="MIT"
-replaces="openssl libressl"
-depends="!openssl-libs-static"
-makedepends_host="linux-headers"
-makedepends="cmake git go perl"
-subpackages="$pkgname-static $pkgname-dev $pkgname-doc"
-source="251b516.tar.gz::https://github.com/google/boringssl/tarball/251b516"
-builddir="$srcdir/google-boringssl-251b516"
-
-prepare() {
- :
-}
-
-build() {
- cmake -DCMAKE_BUILD_TYPE=Release .
- make ssl crypto
-}
-
-check() {
- make all_tests
-}
-
-package() {
- for i in *.md ; do
- install -Dm644 $i "$pkgdir/usr/share/doc/$pkgname/$i"
- done
- install -d "$pkgdir/usr/lib"
- install -d "$pkgdir/usr/include"
- cp -R include/openssl "$pkgdir/usr/include"
-
- install -Dm755 crypto/libcrypto.a "$pkgdir/usr/lib/libcrypto.a"
- install -Dm755 ssl/libssl.a "$pkgdir/usr/lib/libssl.a"
-# install -Dm755 decrepit/libdecrepit.a "$pkgdir/usr/lib/libdecrepit.a"
-# install -Dm755 libboringssl_gtest.a "$pkgdir/usr/lib/libboringssl_gtest.a"
-}
-sha512sums="
-b1d42ed188cf0cce89d40061fa05de85b387ee4244f1236ea488a431536a2c6b657b4f03daed0ac9328c7f5c4c9330499283b8a67f1444dcf9ba5e97e1199c4e 251b516.tar.gz
-"
diff --git a/docker/APKBUILD-lsquic b/docker/APKBUILD-lsquic
deleted file mode 100644
index 51630a0e..00000000
--- a/docker/APKBUILD-lsquic
+++ /dev/null
@@ -1,43 +0,0 @@
-# Maintainer: Omar Roth <omarroth@protonmail.com>
-pkgname=lsquic
-pkgver=2.18.1
-pkgrel=0
-pkgdesc="LiteSpeed QUIC and HTTP/3 Library"
-url="https://github.com/litespeedtech/lsquic"
-arch="all"
-license="MIT"
-depends="boringssl-dev boringssl-static zlib-static libevent-static"
-makedepends="cmake git go perl bsd-compat-headers linux-headers"
-subpackages="$pkgname-static"
-source="v$pkgver.tar.gz::https://github.com/litespeedtech/lsquic/tarball/v2.18.1
-ls-qpack-$pkgver.tar.gz::https://github.com/litespeedtech/ls-qpack/tarball/a8ae6ef
-ls-hpack-$pkgver.tar.gz::https://github.com/litespeedtech/ls-hpack/tarball/bd5d589"
-builddir="$srcdir/litespeedtech-$pkgname-692a910"
-
-prepare() {
- cp -r -T "$srcdir/litespeedtech-ls-qpack-a8ae6ef" "$builddir/src/liblsquic/ls-qpack"
- cp -r -T "$srcdir/litespeedtech-ls-hpack-bd5d589" "$builddir/src/lshpack"
-}
-
-build() {
- cmake \
- -DCMAKE_BUILD_TYPE=None \
- -DBORINGSSL_INCLUDE=/usr/include/openssl \
- -DBORINGSSL_LIB_crypto=/usr/lib \
- -DBORINGSSL_LIB_ssl=/usr/lib .
- make lsquic
-}
-
-check() {
- make tests
-}
-
-package() {
- install -d "$pkgdir/usr/lib"
- install -Dm755 src/liblsquic/liblsquic.a "$pkgdir/usr/lib/liblsquic.a"
-}
-sha512sums="
-d015a72f1e88750ecb364768a40f532678f11ded09c6447a2e698b20f43fa499ef143a53f4c92a5938dfece0e39e687dc9df4aea97c618faee0c63da771561c3 v2.18.1.tar.gz
-c5629085a3881815fb0b72a321eeba8de093eff9417b8ac7bde1ee1264971be0dca6d61d74799b02ae03a4c629b2a9cf21387deeb814935339a8a2503ea33fee ls-qpack-2.18.1.tar.gz
-1b9f7ce4c82dadfca8154229a415b0335a61761eba698f814d4b94195c708003deb5cb89318a1ab78ac8fa88b141bc9df283fb1c6e40b3ba399660feaae353a0 ls-hpack-2.18.1.tar.gz
-"
diff --git a/docker/Dockerfile b/docker/Dockerfile
index 7742ee39..d3d69af6 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -1,34 +1,3 @@
-FROM alpine:edge AS liblsquic-builder
-WORKDIR /src
-
-RUN apk add --no-cache build-base git apk-tools abuild cmake go perl linux-headers
-
-RUN abuild-keygen -a -n && \
- cp /root/.abuild/-*.rsa.pub /etc/apk/keys/
-
-COPY docker/APKBUILD-boringssl boringssl/APKBUILD
-RUN cd boringssl && abuild -F -r && cd ..
-
-RUN apk add --repository /root/packages/src boringssl boringssl-dev boringssl-static
-
-RUN apk add --no-cache zlib-dev zlib-static libevent-dev libevent-static
-
-COPY docker/APKBUILD-lsquic lsquic/APKBUILD
-RUN cd lsquic && abuild -F -r && cd ..
-
-RUN apk add --repository /root/packages/src lsquic-static
-
-RUN mkdir tmp && cd tmp && \
- ar -x /usr/lib/libssl.a && \
- ar -x /usr/lib/libcrypto.a && \
- ar -x /usr/lib/liblsquic.a && \
- ar rc liblsquic.a *.o && \
- strip --strip-unneeded liblsquic.a && \
- ranlib liblsquic.a && \
- cp liblsquic.a /root/liblsquic.a && \
- cd .. && rm -rf tmp
-
-
FROM crystallang/crystal:1.1.1-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static
@@ -37,7 +6,7 @@ COPY ./shard.yml ./shard.yml
COPY ./shard.lock ./shard.lock
RUN shards install
-COPY --from=liblsquic-builder /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a
+COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a
COPY ./src/ ./src/
# TODO: .git folder is required for building – this is destructive.
diff --git a/docker/Dockerfile.arm64 b/docker/Dockerfile.arm64
index 54226b66..84084a06 100644
--- a/docker/Dockerfile.arm64
+++ b/docker/Dockerfile.arm64
@@ -1,34 +1,3 @@
-FROM alpine:3.14 AS liblsquic-builder
-WORKDIR /src
-
-RUN apk add --no-cache build-base git apk-tools abuild cmake go perl linux-headers
-
-RUN abuild-keygen -a -n && \
- cp /root/.abuild/-*.rsa.pub /etc/apk/keys/
-
-COPY docker/APKBUILD-boringssl boringssl/APKBUILD
-RUN cd boringssl && abuild -F -r && cd ..
-
-RUN apk add --repository /root/packages/src boringssl boringssl-dev boringssl-static
-
-RUN apk add --no-cache zlib-dev zlib-static libevent-dev libevent-static
-
-COPY docker/APKBUILD-lsquic lsquic/APKBUILD
-RUN cd lsquic && abuild -F -r && cd ..
-
-RUN apk add --repository /root/packages/src lsquic-static
-
-RUN mkdir tmp && cd tmp && \
- ar -x /usr/lib/libssl.a && \
- ar -x /usr/lib/libcrypto.a && \
- ar -x /usr/lib/liblsquic.a && \
- ar rc liblsquic.a *.o && \
- strip --strip-unneeded liblsquic.a && \
- ranlib liblsquic.a && \
- cp liblsquic.a /root/liblsquic.a && \
- cd .. && rm -rf tmp
-
-
FROM alpine:3.14 AS builder
RUN apk add --no-cache 'crystal<2' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev
@@ -37,7 +6,7 @@ COPY ./shard.yml ./shard.yml
COPY ./shard.lock ./shard.lock
RUN shards install
-COPY --from=liblsquic-builder /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a
+COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a
COPY ./src/ ./src/
# TODO: .git folder is required for building – this is destructive.
diff --git a/locales/ar.json b/locales/ar.json
index a61b71df..b5e77517 100644
--- a/locales/ar.json
+++ b/locales/ar.json
@@ -28,7 +28,7 @@
"New passwords must match": "يَجبُ أن تكون كلمتي المرور متطابقتان",
"Cannot change password for Google accounts": "لا يُمكن تغيير كلمة المرور لِحسابات جوجل",
"Authorize token?": "رمز التفويض؟",
- "Authorize token for `x`?": "رمز التفويض لِـ `x` ؟",
+ "Authorize token for `x`?": "رمز التفويض لـ `x` ؟",
"Yes": "نعم",
"No": "لا",
"Import and Export Data": "اِستيراد البيانات وتصديرها",
@@ -382,7 +382,7 @@
"News": "الأخبار",
"Movies": "الأفلام",
"Download": "نزّل",
- "Download as: ": "نزّله كـ: ",
+ "Download as: ": "نزله كـ: ",
"%A %B %-d, %Y": "%A %-d %B %Y",
"(edited)": "(تم تعديلة)",
"YouTube comment permalink": "رابط التعليق على اليوتيوب",
diff --git a/locales/eo.json b/locales/eo.json
index e3970159..05359382 100644
--- a/locales/eo.json
+++ b/locales/eo.json
@@ -421,7 +421,7 @@
"hdr": "granddinamikgama",
"filter": "filtri",
"Current version: ": "Nuna versio: ",
- "next_steps_error_message": "",
- "next_steps_error_message_refresh": "",
- "next_steps_error_message_go_to_youtube": ""
+ "next_steps_error_message": "Poste, vi provu: ",
+ "next_steps_error_message_refresh": "Reŝargi",
+ "next_steps_error_message_go_to_youtube": "Iri al JuTubo"
}
diff --git a/locales/es.json b/locales/es.json
index 0b9186aa..dfeb69b8 100644
--- a/locales/es.json
+++ b/locales/es.json
@@ -87,7 +87,7 @@
"light": "claro",
"Thin mode: ": "Modo compacto: ",
"Miscellaneous preferences": "Preferencias misceláneas",
- "Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
+ "Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Redirección automática de instancia (segunda opción a redirect.invidious.io): ",
"Subscription preferences": "Preferencias de la suscripción",
"Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ",
"Redirect homepage to feed: ": "Redirigir la página de inicio a la fuente: ",
@@ -117,7 +117,7 @@
"Administrator preferences": "Preferencias de administrador",
"Default homepage: ": "Página de inicio por defecto: ",
"Feed menu: ": "Menú de fuentes: ",
- "Show nickname on top: ": "",
+ "Show nickname on top: ": "Mostrar nombre de usuario arriba: ",
"Top enabled: ": "¿Habilitar los destacados? ",
"CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ",
"Login enabled: ": "¿Habilitar el inicio de sesión? ",
@@ -164,8 +164,8 @@
"Show more": "Mostrar más",
"Show less": "Mostrar menos",
"Watch on YouTube": "Ver el vídeo en YouTube",
- "Switch Invidious Instance": "",
- "Broken? Try another Invidious Instance": "",
+ "Switch Invidious Instance": "Cambiar Instancia de Invidious",
+ "Broken? Try another Invidious Instance": "¿Algún error? Prueba otra instancia de Invidious",
"Hide annotations": "Ocultar anotaciones",
"Show annotations": "Mostrar anotaciones",
"Genre: ": "Género: ",
@@ -421,7 +421,7 @@
"hdr": "hdr",
"filter": "filtro",
"Current version: ": "Versión actual: ",
- "next_steps_error_message": "",
- "next_steps_error_message_refresh": "",
- "next_steps_error_message_go_to_youtube": ""
+ "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"
}
diff --git a/locales/lt.json b/locales/lt.json
index 89b8223c..94b4556a 100644
--- a/locales/lt.json
+++ b/locales/lt.json
@@ -149,7 +149,7 @@
"Source available here.": "Kodas prieinamas čia.",
"View JavaScript license information.": "Žiūrėti JavaScript licencijos informaciją.",
"View privacy policy.": "Žiūrėti privatumo politiką.",
- "Trending": "Populiarūs",
+ "Trending": "Tendencijos",
"Public": "Viešas",
"Unlisted": "Neįtrauktas į sąrašą",
"Private": "Neviešas",
@@ -227,7 +227,7 @@
"Empty playlist": "Tuščias grojaraštis",
"Not a playlist.": "Ne grojaraštis.",
"Playlist does not exist.": "Grojaraštis neegzistuoja.",
- "Could not pull trending pages.": "Nepavyko pritraukti 'dabar populiaru' puslapių.",
+ "Could not pull trending pages.": "Nepavyko ištraukti tendencijų puslapių.",
"Hidden field \"challenge\" is a required field": "Paslėptas laukas „iššūkis“ yra privalomas laukas",
"Hidden field \"token\" is a required field": "Paslėptas laukas „žetonas“ yra privalomas laukas",
"Erroneous challenge": "Klaidingas iššūkis",
@@ -357,7 +357,7 @@
"": "`x` dienas"
},
"`x` hours": {
- "([^.,0-9]|^)1([^.,0-9]|$)": "`x`valandą",
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` valandą",
"": "`x` valandas"
},
"`x` minutes": {
@@ -369,7 +369,7 @@
"": "`x` sekundes"
},
"Fallback comments: ": "Atsarginiai komentarai: ",
- "Popular": "Šiuo metu populiaru",
+ "Popular": "Populiaru",
"Search": "Paieška",
"Top": "Top",
"About": "Apie",
diff --git a/locales/pl.json b/locales/pl.json
index 2da80747..0f86154e 100644
--- a/locales/pl.json
+++ b/locales/pl.json
@@ -86,8 +86,8 @@
"dark": "ciemny",
"light": "jasny",
"Thin mode: ": "Tryb minimalny: ",
- "Miscellaneous preferences": "",
- "Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
+ "Miscellaneous preferences": "Różne preferencje",
+ "Automaticatic instance redirection (fallback to redirect.invidious.io): ": "Automatyczne przekierowanie instancji (powrót do redirect.invidious.io): ",
"Subscription preferences": "Preferencje subskrybcji",
"Show annotations by default for subscribed channels: ": "Domyślnie wyświetlaj adnotacje dla subskrybowanych kanałów: ",
"Redirect homepage to feed: ": "Przekieruj stronę główną do subskrybcji: ",
@@ -116,8 +116,8 @@
"Delete account": "Usuń konto",
"Administrator preferences": "Preferencje administratora",
"Default homepage: ": "Domyślna strona główna: ",
- "Feed menu: ": "Menu aktualności: ",
- "Show nickname on top: ": "",
+ "Feed menu: ": "Menu aktualności ",
+ "Show nickname on top: ": "Pokaż pseudonim na górze: ",
"Top enabled: ": "\"Top\" aktywne: ",
"CAPTCHA enabled: ": "CAPTCHA aktywna? ",
"Login enabled: ": "Logowanie włączone? ",
@@ -132,8 +132,8 @@
"": "`x` subskrybcji"
},
"`x` tokens": {
- "([^.,0-9]|^)1([^.,0-9]|$)": "",
- "": ""
+ "([^.,0-9]|^)1([^.,0-9]|$)": "`x` token",
+ "": "`x` tokenów"
},
"Import/export": "Import/Eksport",
"unsubscribe": "odsubskrybuj",
@@ -164,8 +164,8 @@
"Show more": "Pokaż więcej",
"Show less": "Pokaż mniej",
"Watch on YouTube": "Zobacz film na YouTube",
- "Switch Invidious Instance": "",
- "Broken? Try another Invidious Instance": "",
+ "Switch Invidious Instance": "Przełącz instancję Invidious",
+ "Broken? Try another Invidious Instance": "Nie działa? Spróbuj innej instancji Invidious",
"Hide annotations": "Ukryj adnotacje",
"Show annotations": "Pokaż adnotacje",
"Genre: ": "Gatunek: ",
@@ -393,20 +393,20 @@
"Videos": "Filmy",
"Playlists": "Playlisty",
"Community": "Społeczność",
- "relevance": "",
- "rating": "",
+ "relevance": "Trafność",
+ "rating": "Ocena",
"date": "data",
- "views": "",
- "content_type": "",
- "duration": "",
- "features": "",
+ "views": "Liczba wyświetleń",
+ "content_type": "Typ",
+ "duration": "Długość",
+ "features": "Funkcje",
"sort": "sortuj",
"hour": "godzina",
"today": "dzisiaj",
"week": "tydzień",
"month": "miesiąc",
"year": "rok",
- "video": "",
+ "video": "Film",
"channel": "kanał",
"playlist": "playlista",
"movie": "film",
@@ -415,13 +415,13 @@
"subtitles": "napisy",
"creative_commons": "creative_commons",
"3d": "3d",
- "live": "",
+ "live": "Na żywo",
"4k": "4k",
- "location": "",
+ "location": "Lokalizacja",
"hdr": "hdr",
"filter": "filtr",
"Current version: ": "Aktualna wersja: ",
- "next_steps_error_message": "",
- "next_steps_error_message_refresh": "",
- "next_steps_error_message_go_to_youtube": ""
+ "next_steps_error_message": "Po czym powinien*ś spróbować: ",
+ "next_steps_error_message_refresh": "Odśwież",
+ "next_steps_error_message_go_to_youtube": "Przejdź do YouTube"
}
diff --git a/locales/sv-SE.json b/locales/sv-SE.json
index 7aaaab7b..42699093 100644
--- a/locales/sv-SE.json
+++ b/locales/sv-SE.json
@@ -77,8 +77,8 @@
"Fallback captions: ": "Ersättningsundertexter: ",
"Show related videos: ": "Visa relaterade videor? ",
"Show annotations by default: ": "Visa länkar-i-videon som förval? ",
- "Automatically extend video description: ": "",
- "Interactive 360 degree videos: ": "",
+ "Automatically extend video description: ": "Förläng videobeskrivning automatiskt: ",
+ "Interactive 360 degree videos: ": "Interaktiva 360-gradervideos: ",
"Visual preferences": "Visuella inställningar",
"Player style: ": "Spelarstil: ",
"Dark mode: ": "Mörkt läge: ",
@@ -86,7 +86,7 @@
"dark": "Mörkt",
"light": "Ljust",
"Thin mode: ": "Lättviktigt läge: ",
- "Miscellaneous preferences": "",
+ "Miscellaneous preferences": "Övriga inställningar",
"Automaticatic instance redirection (fallback to redirect.invidious.io): ": "",
"Subscription preferences": "Prenumerationsinställningar",
"Show annotations by default for subscribed channels: ": "Visa länkar-i-videor som förval för kanaler som prenumereras på? ",
@@ -117,7 +117,7 @@
"Administrator preferences": "Administratörsinställningar",
"Default homepage: ": "Förvald hemsida: ",
"Feed menu: ": "Flödesmeny: ",
- "Show nickname on top: ": "",
+ "Show nickname on top: ": "Visa smeknamn överst: ",
"Top enabled: ": "Topp påslaget? ",
"CAPTCHA enabled: ": "CAPTCHA påslaget? ",
"Login enabled: ": "Inloggning påslaget? ",
@@ -164,8 +164,8 @@
"Show more": "Visa mer",
"Show less": "Visa mindre",
"Watch on YouTube": "Titta på YouTube",
- "Switch Invidious Instance": "",
- "Broken? Try another Invidious Instance": "",
+ "Switch Invidious Instance": "Byt Invidious Instans",
+ "Broken? Try another Invidious Instance": "Trasig? Prova en annan Invidious Instance",
"Hide annotations": "Dölj länkar-i-video",
"Show annotations": "Visa länkar-i-video",
"Genre: ": "Genre: ",
@@ -397,10 +397,10 @@
"rating": "rankning",
"date": "datum",
"views": "visningar",
- "content_type": "",
- "duration": "",
- "features": "",
- "sort": "",
+ "content_type": "Typ",
+ "duration": "Varaktighet",
+ "features": "Funktioner",
+ "sort": "Sortera efter",
"hour": "timme",
"today": "idag",
"week": "vecka",
@@ -419,9 +419,9 @@
"4k": "4k",
"location": "plats",
"hdr": "hdr",
- "filter": "",
+ "filter": "Filter",
"Current version: ": "Nuvarande version: ",
"next_steps_error_message": "",
- "next_steps_error_message_refresh": "",
- "next_steps_error_message_go_to_youtube": ""
+ "next_steps_error_message_refresh": "Uppdatera",
+ "next_steps_error_message_go_to_youtube": "Gå till Youtube"
}
diff --git a/shard.lock b/shard.lock
index 35d1aefd..bfb54ee1 100644
--- a/shard.lock
+++ b/shard.lock
@@ -1,5 +1,9 @@
version: 2.0
shards:
+ athena-negotiation:
+ git: https://github.com/athena-framework/negotiation.git
+ version: 0.1.1
+
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.10.1
diff --git a/shard.yml b/shard.yml
index 2df4909c..3292e505 100644
--- a/shard.yml
+++ b/shard.yml
@@ -25,6 +25,9 @@ dependencies:
lsquic:
github: iv-org/lsquic.cr
version: ~> 2.18.1-2
+ athena-negotiation:
+ github: athena-framework/negotiation
+ version: ~> 0.1.1
crystal: ">= 1.0.0, < 2.0.0"
diff --git a/src/invidious.cr b/src/invidious.cr
index 1962ae65..27ebd735 100644
--- a/src/invidious.cr
+++ b/src/invidious.cr
@@ -17,6 +17,7 @@
require "digest/md5"
require "file_utils"
require "kemal"
+require "athena-negotiation"
require "openssl/hmac"
require "option_parser"
require "pg"
@@ -166,10 +167,20 @@ def popular_videos
end
before_all do |env|
- preferences = begin
- Preferences.from_json(URI.decode_www_form(env.request.cookies["PREFS"]?.try &.value || "{}"))
+ preferences = Preferences.from_json("{}")
+
+ begin
+ if prefs_cookie = env.request.cookies["PREFS"]?
+ preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value))
+ else
+ if language_header = env.request.headers["Accept-Language"]?
+ if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
+ preferences.locale = language.header
+ end
+ end
+ end
rescue
- Preferences.from_json("{}")
+ preferences = Preferences.from_json("{}")
end
env.set "preferences", preferences
@@ -338,7 +349,6 @@ Invidious::Routing.get "/redirect", Invidious::Routes::Misc, :cross_instance_red
Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect
Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show
-Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Playlists, :index
Invidious::Routing.get "/create_playlist", Invidious::Routes::Playlists, :new
Invidious::Routing.post "/create_playlist", Invidious::Routes::Playlists, :create
Invidious::Routing.get "/subscribe_playlist", Invidious::Routes::Playlists, :subscribe
@@ -363,7 +373,28 @@ Invidious::Routing.get "/preferences", Invidious::Routes::PreferencesRoute, :sho
Invidious::Routing.post "/preferences", Invidious::Routes::PreferencesRoute, :update
Invidious::Routing.get "/toggle_theme", Invidious::Routes::PreferencesRoute, :toggle_theme
+# Feeds
+Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Feeds, :view_all_playlists_redirect
+Invidious::Routing.get "/feed/playlists", Invidious::Routes::Feeds, :playlists
+Invidious::Routing.get "/feed/popular", Invidious::Routes::Feeds, :popular
+Invidious::Routing.get "/feed/trending", Invidious::Routes::Feeds, :trending
+Invidious::Routing.get "/feed/subscriptions", Invidious::Routes::Feeds, :subscriptions
+Invidious::Routing.get "/feed/history", Invidious::Routes::Feeds, :history
+
+# RSS Feeds
+Invidious::Routing.get "/feed/channel/:ucid", Invidious::Routes::Feeds, :rss_channel
+Invidious::Routing.get "/feed/private", Invidious::Routes::Feeds, :rss_private
+Invidious::Routing.get "/feed/playlist/:plid", Invidious::Routes::Feeds, :rss_playlist
+Invidious::Routing.get "/feeds/videos.xml", Invidious::Routes::Feeds, :rss_videos
+
+# Support push notifications via PubSubHubbub
+Invidious::Routing.get "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_get
+Invidious::Routing.post "/feed/webhook/:token", Invidious::Routes::Feeds, :push_notifications_post
+
+# API routes (macro)
define_v1_api_routes()
+
+# Video playback (macros)
define_api_manifest_routes()
define_video_playback_routes()
@@ -1183,425 +1214,6 @@ post "/token_ajax" do |env|
end
end
-# Feeds
-
-get "/feed/playlists" do |env|
- env.redirect "/view_all_playlists"
-end
-
-get "/feed/top" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- message = translate(locale, "The Top feed has been removed from Invidious.")
- templated "message"
-end
-
-get "/feed/popular" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- if CONFIG.popular_enabled
- templated "popular"
- else
- message = translate(locale, "The Popular feed has been disabled by the administrator.")
- templated "message"
- end
-end
-
-get "/feed/trending" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- trending_type = env.params.query["type"]?
- trending_type ||= "Default"
-
- region = env.params.query["region"]?
- region ||= "US"
-
- begin
- trending, plid = fetch_trending(trending_type, region, locale)
- rescue ex
- next error_template(500, ex)
- end
-
- templated "trending"
-end
-
-get "/feed/subscriptions" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- user = env.get? "user"
- sid = env.get? "sid"
- referer = get_referer(env)
-
- if !user
- next env.redirect referer
- end
-
- user = user.as(User)
- sid = sid.as(String)
- token = user.token
-
- if user.preferences.unseen_only
- env.set "show_watched", true
- end
-
- # Refresh account
- headers = HTTP::Headers.new
- headers["Cookie"] = env.request.headers["Cookie"]
-
- if !user.password
- user, sid = get_user(sid, headers, PG_DB)
- end
-
- max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
- max_results ||= user.preferences.max_results
- max_results ||= CONFIG.default_user_preferences.max_results
-
- page = env.params.query["page"]?.try &.to_i?
- page ||= 1
-
- videos, notifications = get_subscription_feed(PG_DB, user, max_results, page)
-
- # "updated" here is used for delivering new notifications, so if
- # we know a user has looked at their feed e.g. in the past 10 minutes,
- # they've already seen a video posted 20 minutes ago, and don't need
- # to be notified.
- PG_DB.exec("UPDATE users SET notifications = $1, updated = $2 WHERE email = $3", [] of String, Time.utc,
- user.email)
- user.notifications = [] of String
- env.set "user", user
-
- templated "subscriptions"
-end
-
-get "/feed/history" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- user = env.get? "user"
- referer = get_referer(env)
-
- page = env.params.query["page"]?.try &.to_i?
- page ||= 1
-
- if !user
- next env.redirect referer
- end
-
- user = user.as(User)
-
- max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
- max_results ||= user.preferences.max_results
- max_results ||= CONFIG.default_user_preferences.max_results
-
- if user.watched[(page - 1) * max_results]?
- watched = user.watched.reverse[(page - 1) * max_results, max_results]
- end
- watched ||= [] of String
-
- templated "history"
-end
-
-get "/feed/channel/:ucid" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- env.response.content_type = "application/atom+xml"
-
- ucid = env.params.url["ucid"]
-
- params = HTTP::Params.parse(env.params.query["params"]? || "")
-
- begin
- channel = get_about_info(ucid, locale)
- rescue ex : ChannelRedirect
- next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
- rescue ex
- next error_atom(500, ex)
- end
-
- response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
- rss = XML.parse_html(response.body)
-
- videos = rss.xpath_nodes("//feed/entry").map do |entry|
- video_id = entry.xpath_node("videoid").not_nil!.content
- title = entry.xpath_node("title").not_nil!.content
-
- published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content)
- updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content)
-
- author = entry.xpath_node("author/name").not_nil!.content
- ucid = entry.xpath_node("channelid").not_nil!.content
- description_html = entry.xpath_node("group/description").not_nil!.to_s
- views = entry.xpath_node("group/community/statistics").not_nil!.["views"].to_i64
-
- SearchVideo.new({
- title: title,
- id: video_id,
- author: author,
- ucid: ucid,
- published: published,
- views: views,
- description_html: description_html,
- length_seconds: 0,
- live_now: false,
- premium: false,
- premiere_timestamp: nil,
- })
- end
-
- XML.build(indent: " ", encoding: "UTF-8") do |xml|
- xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
- "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
- "xml:lang": "en-US") do
- xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
- xml.element("id") { xml.text "yt:channel:#{channel.ucid}" }
- xml.element("yt:channelId") { xml.text channel.ucid }
- xml.element("icon") { xml.text channel.author_thumbnail }
- xml.element("title") { xml.text channel.author }
- xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{channel.ucid}")
-
- xml.element("author") do
- xml.element("name") { xml.text channel.author }
- xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
- end
-
- videos.each do |video|
- video.to_xml(channel.auto_generated, params, xml)
- end
- end
- end
-end
-
-get "/feed/private" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- env.response.content_type = "application/atom+xml"
-
- token = env.params.query["token"]?
-
- if !token
- env.response.status_code = 403
- next
- end
-
- user = PG_DB.query_one?("SELECT * FROM users WHERE token = $1", token.strip, as: User)
- if !user
- env.response.status_code = 403
- next
- end
-
- max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
- max_results ||= user.preferences.max_results
- max_results ||= CONFIG.default_user_preferences.max_results
-
- page = env.params.query["page"]?.try &.to_i?
- page ||= 1
-
- params = HTTP::Params.parse(env.params.query["params"]? || "")
-
- videos, notifications = get_subscription_feed(PG_DB, user, max_results, page)
-
- XML.build(indent: " ", encoding: "UTF-8") do |xml|
- xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
- "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
- "xml:lang": "en-US") do
- xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions")
- xml.element("link", "type": "application/atom+xml", rel: "self",
- href: "#{HOST_URL}#{env.request.resource}")
- xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
-
- (notifications + videos).each do |video|
- video.to_xml(locale, params, xml)
- end
- end
- end
-end
-
-get "/feed/playlist/:plid" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- env.response.content_type = "application/atom+xml"
-
- plid = env.params.url["plid"]
-
- params = HTTP::Params.parse(env.params.query["params"]? || "")
- path = env.request.path
-
- if plid.starts_with? "IV"
- if playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
- videos = get_playlist_videos(PG_DB, playlist, offset: 0, locale: locale)
-
- next XML.build(indent: " ", encoding: "UTF-8") do |xml|
- xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
- "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
- "xml:lang": "en-US") do
- xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
- xml.element("id") { xml.text "iv:playlist:#{plid}" }
- xml.element("iv:playlistId") { xml.text plid }
- xml.element("title") { xml.text playlist.title }
- xml.element("link", rel: "alternate", href: "#{HOST_URL}/playlist?list=#{plid}")
-
- xml.element("author") do
- xml.element("name") { xml.text playlist.author }
- end
-
- videos.each do |video|
- video.to_xml(false, xml)
- end
- end
- end
- else
- env.response.status_code = 404
- next
- end
- end
-
- response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
- document = XML.parse(response.body)
-
- document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|
- node.attributes.each do |attribute|
- case attribute.name
- when "url", "href"
- request_target = URI.parse(node[attribute.name]).request_target
- query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : ""
- node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}"
- else nil # Skip
- end
- end
- end
-
- document = document.to_xml(options: XML::SaveOptions::NO_DECL)
-
- document.scan(/<uri>(?<url>[^<]+)<\/uri>/).each do |match|
- content = "#{HOST_URL}#{URI.parse(match["url"]).request_target}"
- document = document.gsub(match[0], "<uri>#{content}</uri>")
- end
-
- document
-end
-
-get "/feeds/videos.xml" do |env|
- if ucid = env.params.query["channel_id"]?
- env.redirect "/feed/channel/#{ucid}"
- elsif user = env.params.query["user"]?
- env.redirect "/feed/channel/#{user}"
- elsif plid = env.params.query["playlist_id"]?
- env.redirect "/feed/playlist/#{plid}"
- end
-end
-
-# Support push notifications via PubSubHubbub
-
-get "/feed/webhook/:token" do |env|
- verify_token = env.params.url["token"]
-
- mode = env.params.query["hub.mode"]?
- topic = env.params.query["hub.topic"]?
- challenge = env.params.query["hub.challenge"]?
-
- if !mode || !topic || !challenge
- env.response.status_code = 400
- next
- else
- mode = mode.not_nil!
- topic = topic.not_nil!
- challenge = challenge.not_nil!
- end
-
- case verify_token
- when .starts_with? "v1"
- _, time, nonce, signature = verify_token.split(":")
- data = "#{time}:#{nonce}"
- when .starts_with? "v2"
- time, signature = verify_token.split(":")
- data = "#{time}"
- else
- env.response.status_code = 400
- next
- end
-
- # The hub will sometimes check if we're still subscribed after delivery errors,
- # so we reply with a 200 as long as the request hasn't expired
- if Time.utc.to_unix - time.to_i > 432000
- env.response.status_code = 400
- next
- end
-
- if OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, data) != signature
- env.response.status_code = 400
- next
- end
-
- if ucid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["channel_id"]?
- PG_DB.exec("UPDATE channels SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
- elsif plid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["playlist_id"]?
- PG_DB.exec("UPDATE playlists SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
- else
- env.response.status_code = 400
- next
- end
-
- env.response.status_code = 200
- challenge
-end
-
-post "/feed/webhook/:token" do |env|
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- token = env.params.url["token"]
- body = env.request.body.not_nil!.gets_to_end
- signature = env.request.headers["X-Hub-Signature"].lchop("sha1=")
-
- if signature != OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, body)
- LOGGER.error("/feed/webhook/#{token} : Invalid signature")
- env.response.status_code = 200
- next
- end
-
- spawn do
- rss = XML.parse_html(body)
- rss.xpath_nodes("//feed/entry").each do |entry|
- id = entry.xpath_node("videoid").not_nil!.content
- author = entry.xpath_node("author/name").not_nil!.content
- published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content)
- updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content)
-
- video = get_video(id, PG_DB, force_refresh: true)
-
- # Deliver notifications to `/api/v1/auth/notifications`
- payload = {
- "topic" => video.ucid,
- "videoId" => video.id,
- "published" => published.to_unix,
- }.to_json
- PG_DB.exec("NOTIFY notifications, E'#{payload}'")
-
- video = ChannelVideo.new({
- id: id,
- title: video.title,
- published: published,
- updated: updated,
- ucid: video.ucid,
- author: author,
- length_seconds: video.length_seconds,
- live_now: video.live_now,
- premiere_timestamp: video.premiere_timestamp,
- views: video.views,
- })
-
- 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 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
-
- env.response.status_code = 200
- next
-end
-
# Channels
{"/channel/:ucid/live", "/user/:user/live", "/c/:user/live"}.each do |route|
diff --git a/src/invidious/helpers/youtube_api.cr b/src/invidious/helpers/youtube_api.cr
index 4ed707f6..b3815f6a 100644
--- a/src/invidious/helpers/youtube_api.cr
+++ b/src/invidious/helpers/youtube_api.cr
@@ -8,12 +8,12 @@ module YoutubeAPI
# Enumerate used to select one of the clients supported by the API
enum ClientType
Web
- WebEmbed
+ WebEmbeddedPlayer
WebMobile
- WebAgeBypass
+ WebScreenEmbed
Android
- AndroidEmbed
- AndroidAgeBypass
+ AndroidEmbeddedPlayer
+ AndroidScreenEmbed
end
# List of hard-coded values used by the different clients
@@ -24,7 +24,7 @@ module YoutubeAPI
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "WATCH_FULL_SCREEN",
},
- ClientType::WebEmbed => {
+ ClientType::WebEmbeddedPlayer => {
name: "WEB_EMBEDDED_PLAYER", # 56
version: "1.20210721.1.0",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
@@ -36,7 +36,7 @@ module YoutubeAPI
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "", # None
},
- ClientType::WebAgeBypass => {
+ ClientType::WebScreenEmbed => {
name: "WEB",
version: "2.20210721.00.00",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
@@ -48,13 +48,13 @@ module YoutubeAPI
api_key: "AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w",
screen: "", # ??
},
- ClientType::AndroidEmbed => {
+ ClientType::AndroidEmbeddedPlayer => {
name: "ANDROID_EMBEDDED_PLAYER", # 55
version: "16.20",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
screen: "", # None?
},
- ClientType::AndroidAgeBypass => {
+ ClientType::AndroidScreenEmbed => {
name: "ANDROID", # 3
version: "16.20",
api_key: "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8",
@@ -156,9 +156,6 @@ module YoutubeAPI
"gl" => client_config.region || "US", # Can't be empty!
"clientName" => client_config.name,
"clientVersion" => client_config.version,
- "thirdParty" => {
- "embedUrl" => "", # Placeholder
- },
},
}
@@ -167,14 +164,10 @@ module YoutubeAPI
client_context["client"]["clientScreen"] = client_config.screen
end
- # Replacing/removing the placeholder is easier than trying to
- # merge two different Hash structures.
if client_config.screen == "EMBED"
- client_context["client"]["thirdParty"] = {
+ client_context["thirdParty"] = {
"embedUrl" => "https://www.youtube.com/embed/dQw4w9WgXcQ",
}
- else
- client_context["client"].delete("thirdParty")
end
return client_context
diff --git a/src/invidious/routes/feeds.cr b/src/invidious/routes/feeds.cr
new file mode 100644
index 00000000..c88e96cf
--- /dev/null
+++ b/src/invidious/routes/feeds.cr
@@ -0,0 +1,431 @@
+module Invidious::Routes::Feeds
+ def self.view_all_playlists_redirect(env)
+ env.redirect "/feed/playlists"
+ end
+
+ def self.playlists(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ user = env.get? "user"
+ referer = get_referer(env)
+
+ return env.redirect "/" if user.nil?
+
+ user = user.as(User)
+
+ items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
+ items_created.map! do |item|
+ item.author = ""
+ item
+ end
+
+ items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
+ items_saved.map! do |item|
+ item.author = ""
+ item
+ end
+
+ templated "feeds/playlists"
+ end
+
+ def self.popular(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ if CONFIG.popular_enabled
+ templated "feeds/popular"
+ else
+ message = translate(locale, "The Popular feed has been disabled by the administrator.")
+ templated "message"
+ end
+ end
+
+ def self.trending(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ trending_type = env.params.query["type"]?
+ trending_type ||= "Default"
+
+ region = env.params.query["region"]?
+ region ||= "US"
+
+ begin
+ trending, plid = fetch_trending(trending_type, region, locale)
+ rescue ex
+ return error_template(500, ex)
+ end
+
+ templated "feeds/trending"
+ end
+
+ def self.subscriptions(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ user = env.get? "user"
+ sid = env.get? "sid"
+ referer = get_referer(env)
+
+ if !user
+ return env.redirect referer
+ end
+
+ user = user.as(User)
+ sid = sid.as(String)
+ token = user.token
+
+ if user.preferences.unseen_only
+ env.set "show_watched", true
+ end
+
+ # Refresh account
+ headers = HTTP::Headers.new
+ headers["Cookie"] = env.request.headers["Cookie"]
+
+ if !user.password
+ user, sid = get_user(sid, headers, PG_DB)
+ end
+
+ max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
+ max_results ||= user.preferences.max_results
+ max_results ||= CONFIG.default_user_preferences.max_results
+
+ page = env.params.query["page"]?.try &.to_i?
+ page ||= 1
+
+ videos, notifications = get_subscription_feed(PG_DB, user, max_results, page)
+
+ # "updated" here is used for delivering new notifications, so if
+ # we know a user has looked at their feed e.g. in the past 10 minutes,
+ # they've already seen a video posted 20 minutes ago, and don't need
+ # to be notified.
+ PG_DB.exec("UPDATE users SET notifications = $1, updated = $2 WHERE email = $3", [] of String, Time.utc,
+ user.email)
+ user.notifications = [] of String
+ env.set "user", user
+
+ templated "feeds/subscriptions"
+ end
+
+ def self.history(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ user = env.get? "user"
+ referer = get_referer(env)
+
+ page = env.params.query["page"]?.try &.to_i?
+ page ||= 1
+
+ if !user
+ return env.redirect referer
+ end
+
+ user = user.as(User)
+
+ max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
+ max_results ||= user.preferences.max_results
+ max_results ||= CONFIG.default_user_preferences.max_results
+
+ if user.watched[(page - 1) * max_results]?
+ watched = user.watched.reverse[(page - 1) * max_results, max_results]
+ end
+ watched ||= [] of String
+
+ templated "feeds/history"
+ end
+
+ # RSS feeds
+
+ def self.rss_channel(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ env.response.headers["Content-Type"] = "application/atom+xml"
+ env.response.content_type = "application/atom+xml"
+
+ ucid = env.params.url["ucid"]
+
+ params = HTTP::Params.parse(env.params.query["params"]? || "")
+
+ begin
+ channel = get_about_info(ucid, locale)
+ rescue ex : ChannelRedirect
+ return env.redirect env.request.resource.gsub(ucid, ex.channel_id)
+ rescue ex
+ return error_atom(500, ex)
+ end
+
+ response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
+ rss = XML.parse_html(response.body)
+
+ videos = rss.xpath_nodes("//feed/entry").map do |entry|
+ video_id = entry.xpath_node("videoid").not_nil!.content
+ title = entry.xpath_node("title").not_nil!.content
+
+ published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content)
+ updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content)
+
+ author = entry.xpath_node("author/name").not_nil!.content
+ ucid = entry.xpath_node("channelid").not_nil!.content
+ description_html = entry.xpath_node("group/description").not_nil!.to_s
+ views = entry.xpath_node("group/community/statistics").not_nil!.["views"].to_i64
+
+ SearchVideo.new({
+ title: title,
+ id: video_id,
+ author: author,
+ ucid: ucid,
+ published: published,
+ views: views,
+ description_html: description_html,
+ length_seconds: 0,
+ live_now: false,
+ paid: false,
+ premium: false,
+ premiere_timestamp: nil,
+ })
+ end
+
+ XML.build(indent: " ", encoding: "UTF-8") do |xml|
+ xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
+ "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
+ "xml:lang": "en-US") do
+ xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
+ xml.element("id") { xml.text "yt:channel:#{channel.ucid}" }
+ xml.element("yt:channelId") { xml.text channel.ucid }
+ xml.element("icon") { xml.text channel.author_thumbnail }
+ xml.element("title") { xml.text channel.author }
+ xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{channel.ucid}")
+
+ xml.element("author") do
+ xml.element("name") { xml.text channel.author }
+ xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
+ end
+
+ videos.each do |video|
+ video.to_xml(channel.auto_generated, params, xml)
+ end
+ end
+ end
+ end
+
+ def self.rss_private(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ env.response.headers["Content-Type"] = "application/atom+xml"
+ env.response.content_type = "application/atom+xml"
+
+ token = env.params.query["token"]?
+
+ if !token
+ haltf env, status_code: 403
+ end
+
+ user = PG_DB.query_one?("SELECT * FROM users WHERE token = $1", token.strip, as: User)
+ if !user
+ haltf env, status_code: 403
+ end
+
+ max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
+ max_results ||= user.preferences.max_results
+ max_results ||= CONFIG.default_user_preferences.max_results
+
+ page = env.params.query["page"]?.try &.to_i?
+ page ||= 1
+
+ params = HTTP::Params.parse(env.params.query["params"]? || "")
+
+ videos, notifications = get_subscription_feed(PG_DB, user, max_results, page)
+
+ XML.build(indent: " ", encoding: "UTF-8") do |xml|
+ xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
+ "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
+ "xml:lang": "en-US") do
+ xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions")
+ xml.element("link", "type": "application/atom+xml", rel: "self",
+ href: "#{HOST_URL}#{env.request.resource}")
+ xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
+
+ (notifications + videos).each do |video|
+ video.to_xml(locale, params, xml)
+ end
+ end
+ end
+ end
+
+ def self.rss_playlist(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ env.response.headers["Content-Type"] = "application/atom+xml"
+ env.response.content_type = "application/atom+xml"
+
+ plid = env.params.url["plid"]
+
+ params = HTTP::Params.parse(env.params.query["params"]? || "")
+ path = env.request.path
+
+ if plid.starts_with? "IV"
+ if playlist = PG_DB.query_one?("SELECT * FROM playlists WHERE id = $1", plid, as: InvidiousPlaylist)
+ videos = get_playlist_videos(PG_DB, playlist, offset: 0, locale: locale)
+
+ return XML.build(indent: " ", encoding: "UTF-8") do |xml|
+ xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
+ "xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
+ "xml:lang": "en-US") do
+ xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
+ xml.element("id") { xml.text "iv:playlist:#{plid}" }
+ xml.element("iv:playlistId") { xml.text plid }
+ xml.element("title") { xml.text playlist.title }
+ xml.element("link", rel: "alternate", href: "#{HOST_URL}/playlist?list=#{plid}")
+
+ xml.element("author") do
+ xml.element("name") { xml.text playlist.author }
+ end
+
+ videos.each do |video|
+ video.to_xml(false, xml)
+ end
+ end
+ end
+ else
+ haltf env, status_code: 404
+ end
+ end
+
+ response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
+ document = XML.parse(response.body)
+
+ document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|
+ node.attributes.each do |attribute|
+ case attribute.name
+ when "url", "href"
+ request_target = URI.parse(node[attribute.name]).request_target
+ query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : ""
+ node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}"
+ else nil # Skip
+ end
+ end
+ end
+
+ document = document.to_xml(options: XML::SaveOptions::NO_DECL)
+
+ document.scan(/<uri>(?<url>[^<]+)<\/uri>/).each do |match|
+ content = "#{HOST_URL}#{URI.parse(match["url"]).request_target}"
+ document = document.gsub(match[0], "<uri>#{content}</uri>")
+ end
+ document
+ end
+
+ def self.rss_videos(env)
+ if ucid = env.params.query["channel_id"]?
+ env.redirect "/feed/channel/#{ucid}"
+ elsif user = env.params.query["user"]?
+ env.redirect "/feed/channel/#{user}"
+ elsif plid = env.params.query["playlist_id"]?
+ env.redirect "/feed/playlist/#{plid}"
+ end
+ end
+
+ # Push notifications via PubSub
+
+ def self.push_notifications_get(env)
+ verify_token = env.params.url["token"]
+
+ mode = env.params.query["hub.mode"]?
+ topic = env.params.query["hub.topic"]?
+ challenge = env.params.query["hub.challenge"]?
+
+ if !mode || !topic || !challenge
+ haltf env, status_code: 400
+ else
+ mode = mode.not_nil!
+ topic = topic.not_nil!
+ challenge = challenge.not_nil!
+ end
+
+ case verify_token
+ when .starts_with? "v1"
+ _, time, nonce, signature = verify_token.split(":")
+ data = "#{time}:#{nonce}"
+ when .starts_with? "v2"
+ time, signature = verify_token.split(":")
+ data = "#{time}"
+ else
+ haltf env, status_code: 400
+ end
+
+ # The hub will sometimes check if we're still subscribed after delivery errors,
+ # so we reply with a 200 as long as the request hasn't expired
+ if Time.utc.to_unix - time.to_i > 432000
+ haltf env, status_code: 400
+ end
+
+ if OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, data) != signature
+ haltf env, status_code: 400
+ end
+
+ if ucid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["channel_id"]?
+ PG_DB.exec("UPDATE channels SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
+ elsif plid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["playlist_id"]?
+ PG_DB.exec("UPDATE playlists SET subscribed = $1 WHERE id = $2", Time.utc, ucid)
+ else
+ haltf env, status_code: 400
+ end
+
+ env.response.status_code = 200
+ challenge
+ end
+
+ def self.push_notifications_post(env)
+ locale = LOCALES[env.get("preferences").as(Preferences).locale]?
+
+ token = env.params.url["token"]
+ body = env.request.body.not_nil!.gets_to_end
+ signature = env.request.headers["X-Hub-Signature"].lchop("sha1=")
+
+ if signature != OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, body)
+ LOGGER.error("/feed/webhook/#{token} : Invalid signature")
+ haltf env, status_code: 200
+ end
+
+ spawn do
+ rss = XML.parse_html(body)
+ rss.xpath_nodes("//feed/entry").each do |entry|
+ id = entry.xpath_node("videoid").not_nil!.content
+ author = entry.xpath_node("author/name").not_nil!.content
+ published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content)
+ updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content)
+
+ video = get_video(id, PG_DB, force_refresh: true)
+
+ # Deliver notifications to `/api/v1/auth/notifications`
+ payload = {
+ "topic" => video.ucid,
+ "videoId" => video.id,
+ "published" => published.to_unix,
+ }.to_json
+ PG_DB.exec("NOTIFY notifications, E'#{payload}'")
+
+ video = ChannelVideo.new({
+ id: id,
+ title: video.title,
+ published: published,
+ updated: updated,
+ ucid: video.ucid,
+ author: author,
+ length_seconds: video.length_seconds,
+ live_now: video.live_now,
+ premiere_timestamp: video.premiere_timestamp,
+ views: video.views,
+ })
+
+ 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 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
+
+ env.response.status_code = 200
+ end
+end
diff --git a/src/invidious/routes/login.cr b/src/invidious/routes/login.cr
index 21d3fafd..f052d3f4 100644
--- a/src/invidious/routes/login.cr
+++ b/src/invidious/routes/login.cr
@@ -434,6 +434,13 @@ module Invidious::Routes::Login
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
user, sid = create_user(sid, email, password)
+
+ if language_header = env.request.headers["Accept-Language"]?
+ if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
+ user.preferences.locale = language.header
+ end
+ end
+
user_array = user.to_a
user_array[4] = user_array[4].to_json # User preferences
diff --git a/src/invidious/routes/misc.cr b/src/invidious/routes/misc.cr
index fa548f53..82c40a95 100644
--- a/src/invidious/routes/misc.cr
+++ b/src/invidious/routes/misc.cr
@@ -17,7 +17,7 @@ module Invidious::Routes::Misc
end
when "Playlists"
if user
- env.redirect "/view_all_playlists"
+ env.redirect "/feed/playlists"
else
env.redirect "/feed/popular"
end
diff --git a/src/invidious/routes/playlists.cr b/src/invidious/routes/playlists.cr
index a2166bdd..05a198d8 100644
--- a/src/invidious/routes/playlists.cr
+++ b/src/invidious/routes/playlists.cr
@@ -1,29 +1,4 @@
module Invidious::Routes::Playlists
- def self.index(env)
- locale = LOCALES[env.get("preferences").as(Preferences).locale]?
-
- user = env.get? "user"
- referer = get_referer(env)
-
- return env.redirect "/" if user.nil?
-
- user = user.as(User)
-
- items_created = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
- items_created.map! do |item|
- item.author = ""
- item
- end
-
- items_saved = PG_DB.query_all("SELECT * FROM playlists WHERE author = $1 AND id NOT LIKE 'IV%' ORDER BY created", user.email, as: InvidiousPlaylist)
- items_saved.map! do |item|
- item.author = ""
- item
- end
-
- templated "view_all_playlists"
- end
-
def self.new(env)
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
@@ -148,7 +123,7 @@ module Invidious::Routes::Playlists
PG_DB.exec("DELETE FROM playlist_videos * WHERE plid = $1", plid)
PG_DB.exec("DELETE FROM playlists * WHERE id = $1", plid)
- env.redirect "/view_all_playlists"
+ env.redirect "/feed/playlists"
end
def self.edit(env)
diff --git a/src/invidious/videos.cr b/src/invidious/videos.cr
index 79897985..27d85b92 100644
--- a/src/invidious/videos.cr
+++ b/src/invidious/videos.cr
@@ -301,6 +301,7 @@ struct Video
json.field "likeCount", self.likes
json.field "dislikeCount", self.dislikes
+ json.field "paid", self.paid
json.field "premium", self.premium
json.field "isFamilyFriendly", self.is_family_friendly
json.field "allowedRegions", self.allowed_regions
@@ -525,10 +526,6 @@ struct Video
info["microformat"].as_h["playerMicroformatRenderer"].as_h["publishDate"] = JSON::Any.new(other.to_s("%Y-%m-%d"))
end
- def cookie
- info["cookie"]?.try &.as_h.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
- end
-
def allow_ratings
r = info["videoDetails"]["allowRatings"]?.try &.as_bool
r.nil? ? false : r
@@ -692,6 +689,12 @@ struct Video
items
end
+ def paid
+ reason = info["playabilityStatus"]?.try &.["reason"]?
+ paid = reason == "This video requires payment to watch." ? true : false
+ paid
+ end
+
def premium
keywords.includes? "YouTube Red"
end
@@ -773,10 +776,6 @@ struct Video
def reason : String?
info["reason"]?.try &.as_s
end
-
- def session_token : String?
- info["sessionToken"]?.try &.as_s?
- end
end
struct Caption
@@ -820,44 +819,61 @@ def parse_related(r : JSON::Any) : JSON::Any?
JSON::Any.new(rv)
end
-def extract_polymer_config(body)
+def extract_video_info(video_id : String, proxy_region : String? = nil, context_screen : String? = nil)
params = {} of String => JSON::Any
- player_response = body.match(/(window\["ytInitialPlayerResponse"\]|var\sytInitialPlayerResponse)\s*=\s*(?<info>{.*?});\s*var\s*meta/m)
- .try { |r| JSON.parse(r["info"]).as_h }
-
- if body.includes?("To continue with your YouTube experience, please fill out the form below.") ||
- body.includes?("https://www.google.com/sorry/index")
- params["reason"] = JSON::Any.new("Could not extract video info. Instance is likely blocked.")
- elsif !player_response
- params["reason"] = JSON::Any.new("Video unavailable.")
- elsif player_response["playabilityStatus"]?.try &.["status"]?.try &.as_s != "OK"
- reason = player_response["playabilityStatus"]["errorScreen"]?.try &.["playerErrorMessageRenderer"]?.try &.["subreason"]?.try { |s| s["simpleText"]?.try &.as_s || s["runs"].as_a.map { |r| r["text"] }.join("") } ||
- player_response["playabilityStatus"]["reason"].as_s
+
+ client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region)
+ if context_screen == "embed"
+ client_config.client_type = YoutubeAPI::ClientType::WebScreenEmbed
+ end
+
+ player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config)
+
+ if player_response["playabilityStatus"]?.try &.["status"]?.try &.as_s != "OK"
+ reason = player_response["playabilityStatus"]["errorScreen"]?.try &.["playerErrorMessageRenderer"]?.try &.["subreason"]?.try { |s|
+ s["simpleText"]?.try &.as_s || s["runs"].as_a.map { |r| r["text"] }.join("")
+ } || player_response["playabilityStatus"]["reason"].as_s
params["reason"] = JSON::Any.new(reason)
end
- session_token_json_encoded = body.match(/"XSRF_TOKEN":"(?<session_token>[^"]+)"/).try &.["session_token"]? || ""
- params["sessionToken"] = JSON.parse(%({"key": "#{session_token_json_encoded}"}))["key"]
- params["shortDescription"] = JSON::Any.new(body.match(/"og:description" content="(?<description>[^"]+)"/).try &.["description"]?)
+ params["shortDescription"] = player_response.dig?("videoDetails", "shortDescription") || JSON::Any.new(nil)
+
+ # Don't fetch the next endpoint if the video is unavailable.
+ if !params["reason"]?
+ next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
+ player_response = player_response.merge(next_response)
+ end
- return params if !player_response
+ # Fetch the video streams using an Android client in order to get the decrypted URLs and
+ # maybe fix throttling issues (#2194).See for the explanation about the decrypted URLs:
+ # https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
+ if !params["reason"]?
+ if context_screen == "embed"
+ client_config.client_type = YoutubeAPI::ClientType::AndroidScreenEmbed
+ else
+ client_config.client_type = YoutubeAPI::ClientType::Android
+ end
+ stream_data = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config)
+ params["streamingData"] = stream_data["streamingData"]? || JSON::Any.new("")
+ end
{"captions", "microformat", "playabilityStatus", "storyboards", "videoDetails"}.each do |f|
params[f] = player_response[f] if player_response[f]?
end
- yt_initial_data = extract_initial_data(body)
+ params["relatedVideos"] = (
+ player_response
+ .dig?("playerOverlays", "playerOverlayRenderer", "endScreen", "watchNextEndScreenRenderer", "results")
+ .try &.as_a.compact_map { |r| parse_related r } || \
+ player_response
+ .dig?("webWatchNextResponseExtensionData", "relatedVideoArgs")
+ .try &.as_s.split(",").map { |r|
+ r = HTTP::Params.parse(r).to_h
+ JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) }))
+ }
+ ).try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any)
- params["relatedVideos"] = yt_initial_data.try &.["playerOverlays"]?.try &.["playerOverlayRenderer"]?
- .try &.["endScreen"]?.try &.["watchNextEndScreenRenderer"]?.try &.["results"]?.try &.as_a.compact_map { |r|
- parse_related r
- }.try { |a| JSON::Any.new(a) } || yt_initial_data.try &.["webWatchNextResponseExtensionData"]?.try &.["relatedVideoArgs"]?
- .try &.as_s.split(",").map { |r|
- r = HTTP::Params.parse(r).to_h
- JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) }))
- }.try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any)
-
- primary_results = yt_initial_data.try &.["contents"]?.try &.["twoColumnWatchNextResults"]?.try &.["results"]?
+ primary_results = player_response.try &.["contents"]?.try &.["twoColumnWatchNextResults"]?.try &.["results"]?
.try &.["results"]?.try &.["contents"]?
sentiment_bar = primary_results.try &.as_a.select { |object| object["videoPrimaryInfoRenderer"]? }[0]?
.try &.["videoPrimaryInfoRenderer"]?
@@ -917,20 +933,6 @@ def extract_polymer_config(body)
params["subCountText"] = JSON::Any.new(author_info.try &.["subscriberCountText"]?
.try { |t| t["simpleText"]? || t["runs"]?.try &.[0]?.try &.["text"]? }.try &.as_s.split(" ", 2)[0] || "-")
- initial_data = body.match(/ytplayer\.config\s*=\s*(?<info>.*?);ytplayer\.web_player_context_config/)
- .try { |r| JSON.parse(r["info"]) }.try &.["args"]["player_response"]?
- .try &.as_s?.try &.try { |r| JSON.parse(r).as_h }
-
- if initial_data
- {"playabilityStatus", "streamingData"}.each do |f|
- params[f] = initial_data[f] if initial_data[f]?
- end
- else
- {"playabilityStatus", "streamingData"}.each do |f|
- params[f] = player_response[f] if player_response[f]?
- end
- end
-
params
end
@@ -961,76 +963,27 @@ def get_video(id, db, refresh = true, region = nil, force_refresh = false)
end
def fetch_video(id, region)
- response = YT_POOL.client(region, &.get("/watch?v=#{id}&gl=US&hl=en&has_verified=1&bpctr=9999999999"))
-
- if md = response.headers["location"]?.try &.match(/v=(?<id>[a-zA-Z0-9_-]{11})/)
- raise VideoRedirect.new(video_id: md["id"])
- end
+ info = extract_video_info(video_id: id)
- info = extract_polymer_config(response.body)
- info["cookie"] = JSON::Any.new(response.cookies.to_h.transform_values { |v| JSON::Any.new(v.value) })
- allowed_regions = info["microformat"]?.try &.["playerMicroformatRenderer"]["availableCountries"]?.try &.as_a.map &.as_s || [] of String
+ allowed_regions = info
+ .dig?("microformat", "playerMicroformatRenderer", "availableCountries")
+ .try &.as_a.map &.as_s || [] of String
# Check for region-blocks
if info["reason"]?.try &.as_s.includes?("your country")
bypass_regions = PROXY_LIST.keys & allowed_regions
if !bypass_regions.empty?
region = bypass_regions[rand(bypass_regions.size)]
- response = YT_POOL.client(region, &.get("/watch?v=#{id}&gl=US&hl=en&has_verified=1&bpctr=9999999999"))
-
- region_info = extract_polymer_config(response.body)
+ region_info = extract_video_info(video_id: id, proxy_region: region)
region_info["region"] = JSON::Any.new(region) if region
- region_info["cookie"] = JSON::Any.new(response.cookies.to_h.transform_values { |v| JSON::Any.new(v.value) })
info = region_info if !region_info["reason"]?
end
end
- # Try to pull streams from embed URL
+ # Try to fetch video info using an embedded client
if info["reason"]?
- required_parameters = {
- "video_id" => id,
- "eurl" => "https://youtube.googleapis.com/v/#{id}",
- "html5" => "1",
- "gl" => "US",
- "hl" => "en",
- }
- if info["reason"].as_s.includes?("inappropriate")
- # The html5, c and cver parameters are required in order to extract age-restricted videos
- # See https://github.com/yt-dlp/yt-dlp/commit/4e6767b5f2e2523ebd3dd1240584ead53e8c8905
- required_parameters.merge!({
- "c" => "TVHTML5",
- "cver" => "6.20180913",
- })
-
- # In order to actually extract video info without error, the `x-youtube-client-version`
- # has to be set to the same version as `cver` above.
- additional_headers = HTTP::Headers{"x-youtube-client-version" => "6.20180913"}
- else
- embed_page = YT_POOL.client &.get("/embed/#{id}").body
- sts = embed_page.match(/"sts"\s*:\s*(?<sts>\d+)/).try &.["sts"]? || ""
- required_parameters["sts"] = sts
- additional_headers = HTTP::Headers{} of String => String
- end
-
- embed_info = HTTP::Params.parse(YT_POOL.client &.get("/get_video_info?#{URI::Params.encode(required_parameters)}",
- headers: additional_headers).body)
-
- if embed_info["player_response"]?
- player_response = JSON.parse(embed_info["player_response"])
- {"captions", "microformat", "playabilityStatus", "streamingData", "videoDetails", "storyboards"}.each do |f|
- info[f] = player_response[f] if player_response[f]?
- end
- end
-
- initial_data = JSON.parse(embed_info["watch_next_response"]) if embed_info["watch_next_response"]?
-
- info["relatedVideos"] = initial_data.try &.["playerOverlays"]?.try &.["playerOverlayRenderer"]?
- .try &.["endScreen"]?.try &.["watchNextEndScreenRenderer"]?.try &.["results"]?.try &.as_a.compact_map { |r|
- parse_related r
- }.try { |a| JSON::Any.new(a) } || embed_info["rvs"]?.try &.split(",").map { |r|
- r = HTTP::Params.parse(r).to_h
- JSON::Any.new(Hash.zip(r.keys, r.values.map { |v| JSON::Any.new(v) }))
- }.try { |a| JSON::Any.new(a) } || JSON::Any.new([] of JSON::Any)
+ embed_info = extract_video_info(video_id: id, context_screen: "embed")
+ info = embed_info if !embed_info["reason"]?
end
raise InfoException.new(info["reason"]?.try &.as_s || "") if !info["videoDetails"]?
diff --git a/src/invidious/views/history.ecr b/src/invidious/views/feeds/history.ecr
index 40584979..40584979 100644
--- a/src/invidious/views/history.ecr
+++ b/src/invidious/views/feeds/history.ecr
diff --git a/src/invidious/views/view_all_playlists.ecr b/src/invidious/views/feeds/playlists.ecr
index 868cfeda..868cfeda 100644
--- a/src/invidious/views/view_all_playlists.ecr
+++ b/src/invidious/views/feeds/playlists.ecr
diff --git a/src/invidious/views/popular.ecr b/src/invidious/views/feeds/popular.ecr
index e77f35b9..e77f35b9 100644
--- a/src/invidious/views/popular.ecr
+++ b/src/invidious/views/feeds/popular.ecr
diff --git a/src/invidious/views/subscriptions.ecr b/src/invidious/views/feeds/subscriptions.ecr
index 97184e2b..97184e2b 100644
--- a/src/invidious/views/subscriptions.ecr
+++ b/src/invidious/views/feeds/subscriptions.ecr
diff --git a/src/invidious/views/trending.ecr b/src/invidious/views/feeds/trending.ecr
index a35c4ee3..a35c4ee3 100644
--- a/src/invidious/views/trending.ecr
+++ b/src/invidious/views/feeds/trending.ecr
diff --git a/src/invidious/views/playlist.ecr b/src/invidious/views/playlist.ecr
index b1fee211..12f93a72 100644
--- a/src/invidious/views/playlist.ecr
+++ b/src/invidious/views/playlist.ecr
@@ -12,7 +12,7 @@
<% if playlist.is_a? InvidiousPlaylist %>
<b>
<% if playlist.author == user.try &.email %>
- <a href="/view_all_playlists"><%= author %></a> |
+ <a href="/feed/playlists"><%= author %></a> |
<% else %>
<%= author %> |
<% end %>
diff --git a/src/invidious/views/preferences.ecr b/src/invidious/views/preferences.ecr
index d98c3bb5..be021c59 100644
--- a/src/invidious/views/preferences.ecr
+++ b/src/invidious/views/preferences.ecr
@@ -312,7 +312,7 @@
</div>
<div class="pure-control-group">
- <a href="/view_all_playlists"><%= translate(locale, "View all playlists") %></a>
+ <a href="/feed/playlists"><%= translate(locale, "View all playlists") %></a>
</div>
<div class="pure-control-group">