mirror of
https://github.com/iv-org/invidious.git
synced 2025-08-06 04:38:31 +00:00
Merge branch 'master' into api-only
This commit is contained in:
commit
507f924c5b
28
.travis.yml
Normal file
28
.travis.yml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
dist: bionic
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
include:
|
||||||
|
- stage: build
|
||||||
|
language: crystal
|
||||||
|
crystal: latest
|
||||||
|
before_install:
|
||||||
|
- shards update
|
||||||
|
- shards install
|
||||||
|
install:
|
||||||
|
- crystal build --error-on-warnings src/invidious.cr
|
||||||
|
script:
|
||||||
|
- crystal tool format --check
|
||||||
|
- crystal spec
|
||||||
|
|
||||||
|
- stage: build_docker
|
||||||
|
language: minimal
|
||||||
|
services:
|
||||||
|
- docker
|
||||||
|
install:
|
||||||
|
- docker-compose build
|
||||||
|
script:
|
||||||
|
- docker-compose up -d
|
||||||
|
- sleep 15 # Wait for cluster to become ready, TODO: do not sleep
|
||||||
|
- HEADERS="$(curl -I -s http://localhost:3000/)"
|
||||||
|
- STATUS="$(echo $HEADERS | head -n1)"
|
||||||
|
- if [[ "$STATUS" != *"200 OK"* ]]; then echo "$HEADERS"; exit 1; fi
|
@ -1,5 +1,7 @@
|
|||||||
# Invidious
|
# Invidious
|
||||||
|
|
||||||
|
[](https://travis-ci.org/omarroth/invidious)
|
||||||
|
|
||||||
## Invidious is an alternative front-end to YouTube
|
## Invidious is an alternative front-end to YouTube
|
||||||
|
|
||||||
- Audio-only mode (and no need to keep window open on mobile)
|
- Audio-only mode (and no need to keep window open on mobile)
|
||||||
|
@ -1,15 +1,28 @@
|
|||||||
FROM archlinux/base
|
FROM alpine:edge AS builder
|
||||||
|
RUN apk add -u crystal shards libc-dev \
|
||||||
RUN pacman -Sy --noconfirm shards crystal imagemagick librsvg \
|
yaml-dev libxml2-dev sqlite-dev sqlite-static zlib-dev openssl-dev
|
||||||
which pkgconf gcc ttf-liberation glibc
|
|
||||||
# base-devel contains many other basic packages, that are normally assumed to already exist on a clean arch system
|
|
||||||
|
|
||||||
ADD . /invidious
|
|
||||||
|
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
|
COPY ./shard.yml ./shard.yml
|
||||||
|
RUN shards update && shards install
|
||||||
|
COPY ./src/ ./src/
|
||||||
|
# TODO: .git folder is required for building – this is destructive.
|
||||||
|
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
||||||
|
COPY ./.git/ ./.git/
|
||||||
|
RUN crystal build --static --release \
|
||||||
|
# TODO: Remove next line, see https://github.com/crystal-lang/crystal/issues/7946
|
||||||
|
-Dmusl \
|
||||||
|
./src/invidious.cr
|
||||||
|
|
||||||
RUN sed -i 's/host: localhost/host: postgres/' config/config.yml && \
|
FROM alpine:latest
|
||||||
shards update && shards install && \
|
RUN apk add -u imagemagick ttf-opensans
|
||||||
crystal build src/invidious.cr
|
WORKDIR /invidious
|
||||||
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
|
adduser -u 1000 -S invidious -G invidious
|
||||||
|
COPY ./assets/ ./assets/
|
||||||
|
COPY ./config/config.yml ./config/config.yml
|
||||||
|
COPY ./config/sql/ ./config/sql/
|
||||||
|
COPY ./locales/ ./locales/
|
||||||
|
RUN sed -i 's/host: localhost/host: postgres/' config/config.yml
|
||||||
|
COPY --from=builder /invidious/invidious .
|
||||||
|
USER invidious
|
||||||
CMD [ "/invidious/invidious" ]
|
CMD [ "/invidious/invidious" ]
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "عرض مقاطع الفيديو ذات الصلة؟",
|
"Show related videos: ": "عرض مقاطع الفيديو ذات الصلة؟",
|
||||||
"Show annotations by default: ": "عرض الملاحظات فى الفيديو تلقائيا ؟",
|
"Show annotations by default: ": "عرض الملاحظات فى الفيديو تلقائيا ؟",
|
||||||
"Visual preferences": "التفضيلات المرئية",
|
"Visual preferences": "التفضيلات المرئية",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "الوضع الليلى: ",
|
"Dark mode: ": "الوضع الليلى: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "الوضع الخفيف: ",
|
"Thin mode: ": "الوضع الخفيف: ",
|
||||||
"Subscription preferences": "تفضيلات الإشتراك",
|
"Subscription preferences": "تفضيلات الإشتراك",
|
||||||
"Show annotations by default for subscribed channels: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟",
|
"Show annotations by default for subscribed channels: ": "عرض الملاحظات فى الفيديوهات تلقائيا فى القنوات المشترك بها فقط ؟",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Ähnliche Videos anzeigen? ",
|
"Show related videos: ": "Ähnliche Videos anzeigen? ",
|
||||||
"Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ",
|
"Show annotations by default: ": "Standardmäßig Anmerkungen anzeigen? ",
|
||||||
"Visual preferences": "Anzeigeeinstellungen",
|
"Visual preferences": "Anzeigeeinstellungen",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Nachtmodus: ",
|
"Dark mode: ": "Nachtmodus: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Schlanker Modus: ",
|
"Thin mode: ": "Schlanker Modus: ",
|
||||||
"Subscription preferences": "Abonnementeinstellungen",
|
"Subscription preferences": "Abonnementeinstellungen",
|
||||||
"Show annotations by default for subscribed channels: ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ",
|
"Show annotations by default for subscribed channels: ": "Anmerkungen für abonnierte Kanäle standardmäßig anzeigen? ",
|
||||||
|
@ -74,7 +74,11 @@
|
|||||||
"Show related videos: ": "Προβολή σχετικών βίντεο; ",
|
"Show related videos: ": "Προβολή σχετικών βίντεο; ",
|
||||||
"Show annotations by default: ": "Αυτόματη προβολή σημειώσεων; :",
|
"Show annotations by default: ": "Αυτόματη προβολή σημειώσεων; :",
|
||||||
"Visual preferences": "Προτιμήσεις εμφάνισης",
|
"Visual preferences": "Προτιμήσεις εμφάνισης",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Σκοτεινή λειτουργία: ",
|
"Dark mode: ": "Σκοτεινή λειτουργία: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Ελαφριά λειτουργία: ",
|
"Thin mode: ": "Ελαφριά λειτουργία: ",
|
||||||
"Subscription preferences": "Προτιμήσεις συνδρομών",
|
"Subscription preferences": "Προτιμήσεις συνδρομών",
|
||||||
"Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ",
|
"Show annotations by default for subscribed channels: ": "Προβολή σημειώσεων μόνο για κανάλια στα οποία είστε συνδρομητής; ",
|
||||||
|
@ -74,7 +74,11 @@
|
|||||||
"Show related videos: ": "Show related videos: ",
|
"Show related videos: ": "Show related videos: ",
|
||||||
"Show annotations by default: ": "Show annotations by default: ",
|
"Show annotations by default: ": "Show annotations by default: ",
|
||||||
"Visual preferences": "Visual preferences",
|
"Visual preferences": "Visual preferences",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Dark mode: ",
|
"Dark mode: ": "Dark mode: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Thin mode: ",
|
"Thin mode: ": "Thin mode: ",
|
||||||
"Subscription preferences": "Subscription preferences",
|
"Subscription preferences": "Subscription preferences",
|
||||||
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
|
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Ĉu montri rilatajn videojn? ",
|
"Show related videos: ": "Ĉu montri rilatajn videojn? ",
|
||||||
"Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ",
|
"Show annotations by default: ": "Ĉu montri prinotojn defaŭlte? ",
|
||||||
"Visual preferences": "Vidaj preferoj",
|
"Visual preferences": "Vidaj preferoj",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Malhela reĝimo: ",
|
"Dark mode: ": "Malhela reĝimo: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Maldika reĝimo: ",
|
"Thin mode: ": "Maldika reĝimo: ",
|
||||||
"Subscription preferences": "Abonaj agordoj",
|
"Subscription preferences": "Abonaj agordoj",
|
||||||
"Show annotations by default for subscribed channels: ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
|
"Show annotations by default for subscribed channels: ": "Ĉu montri prinotojn defaŭlte por abonitaj kanaloj? ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "¿Mostrar vídeos relacionados? ",
|
"Show related videos: ": "¿Mostrar vídeos relacionados? ",
|
||||||
"Show annotations by default: ": "¿Mostrar anotaciones por defecto? ",
|
"Show annotations by default: ": "¿Mostrar anotaciones por defecto? ",
|
||||||
"Visual preferences": "Preferencias visuales",
|
"Visual preferences": "Preferencias visuales",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Modo oscuro: ",
|
"Dark mode: ": "Modo oscuro: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Modo compacto: ",
|
"Thin mode: ": "Modo compacto: ",
|
||||||
"Subscription preferences": "Preferencias de la suscripción",
|
"Subscription preferences": "Preferencias de la suscripción",
|
||||||
"Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ",
|
"Show annotations by default for subscribed channels: ": "¿Mostrar anotaciones por defecto para los canales suscritos? ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "",
|
"Show related videos: ": "",
|
||||||
"Show annotations by default: ": "",
|
"Show annotations by default: ": "",
|
||||||
"Visual preferences": "",
|
"Visual preferences": "",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "",
|
"Dark mode: ": "",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "",
|
"Thin mode: ": "",
|
||||||
"Subscription preferences": "",
|
"Subscription preferences": "",
|
||||||
"Show annotations by default for subscribed channels: ": "",
|
"Show annotations by default for subscribed channels: ": "",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Voir les vidéos liées : ",
|
"Show related videos: ": "Voir les vidéos liées : ",
|
||||||
"Show annotations by default: ": "Voir les annotations par défaut : ",
|
"Show annotations by default: ": "Voir les annotations par défaut : ",
|
||||||
"Visual preferences": "Préférences du site",
|
"Visual preferences": "Préférences du site",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Mode Sombre : ",
|
"Dark mode: ": "Mode Sombre : ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Mode Simplifié : ",
|
"Thin mode: ": "Mode Simplifié : ",
|
||||||
"Subscription preferences": "Préférences de la page d'abonnements",
|
"Subscription preferences": "Préférences de la page d'abonnements",
|
||||||
"Show annotations by default for subscribed channels: ": "Voir les annotations par défaut sur les chaînes suivies : ",
|
"Show annotations by default for subscribed channels: ": "Voir les annotations par défaut sur les chaînes suivies : ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Sýna tengd myndbönd? ",
|
"Show related videos: ": "Sýna tengd myndbönd? ",
|
||||||
"Show annotations by default: ": "Á að sýna glósur sjálfgefið? ",
|
"Show annotations by default: ": "Á að sýna glósur sjálfgefið? ",
|
||||||
"Visual preferences": "Sjónrænar stillingar",
|
"Visual preferences": "Sjónrænar stillingar",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Myrkur ham: ",
|
"Dark mode: ": "Myrkur ham: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Þunnt ham: ",
|
"Thin mode: ": "Þunnt ham: ",
|
||||||
"Subscription preferences": "Áskriftarstillingar",
|
"Subscription preferences": "Áskriftarstillingar",
|
||||||
"Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ",
|
"Show annotations by default for subscribed channels: ": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Mostra video correlati? ",
|
"Show related videos: ": "Mostra video correlati? ",
|
||||||
"Show annotations by default: ": "Mostra le annotazioni per impostazione predefinita? ",
|
"Show annotations by default: ": "Mostra le annotazioni per impostazione predefinita? ",
|
||||||
"Visual preferences": "Preferenze grafiche",
|
"Visual preferences": "Preferenze grafiche",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Tema scuro: ",
|
"Dark mode: ": "Tema scuro: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Modalità per connessioni lente: ",
|
"Thin mode: ": "Modalità per connessioni lente: ",
|
||||||
"Subscription preferences": "Preferenze iscrizioni",
|
"Subscription preferences": "Preferenze iscrizioni",
|
||||||
"Show annotations by default for subscribed channels: ": "",
|
"Show annotations by default for subscribed channels: ": "",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Vis relaterte videoer? ",
|
"Show related videos: ": "Vis relaterte videoer? ",
|
||||||
"Show annotations by default: ": "Vis merknader som forvalg? ",
|
"Show annotations by default: ": "Vis merknader som forvalg? ",
|
||||||
"Visual preferences": "Visuelle innstillinger",
|
"Visual preferences": "Visuelle innstillinger",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Mørk drakt: ",
|
"Dark mode: ": "Mørk drakt: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Tynt modus: ",
|
"Thin mode: ": "Tynt modus: ",
|
||||||
"Subscription preferences": "Abonnementsinnstillinger",
|
"Subscription preferences": "Abonnementsinnstillinger",
|
||||||
"Show annotations by default for subscribed channels: ": "Vis merknader som forvalg for kanaler det abonneres på? ",
|
"Show annotations by default for subscribed channels: ": "Vis merknader som forvalg for kanaler det abonneres på? ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Gerelateerde video's tonen? ",
|
"Show related videos: ": "Gerelateerde video's tonen? ",
|
||||||
"Show annotations by default: ": "Standaard annotaties tonen? ",
|
"Show annotations by default: ": "Standaard annotaties tonen? ",
|
||||||
"Visual preferences": "Visuele instellingen",
|
"Visual preferences": "Visuele instellingen",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Donkere modus: ",
|
"Dark mode: ": "Donkere modus: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Smalle modus: ",
|
"Thin mode: ": "Smalle modus: ",
|
||||||
"Subscription preferences": "Abonnementsinstellingen",
|
"Subscription preferences": "Abonnementsinstellingen",
|
||||||
"Show annotations by default for subscribed channels: ": "Standaard annotaties tonen voor geabonneerde kanalen? ",
|
"Show annotations by default for subscribed channels: ": "Standaard annotaties tonen voor geabonneerde kanalen? ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Pokaż powiązane filmy? ",
|
"Show related videos: ": "Pokaż powiązane filmy? ",
|
||||||
"Show annotations by default: ": "",
|
"Show annotations by default: ": "",
|
||||||
"Visual preferences": "Preferencje Wizualne",
|
"Visual preferences": "Preferencje Wizualne",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Ciemny motyw: ",
|
"Dark mode: ": "Ciemny motyw: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Tryb minimalny: ",
|
"Thin mode: ": "Tryb minimalny: ",
|
||||||
"Subscription preferences": "Preferencje subskrybcji",
|
"Subscription preferences": "Preferencje subskrybcji",
|
||||||
"Show annotations by default for subscribed channels: ": "",
|
"Show annotations by default for subscribed channels: ": "",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Показывать похожие видео? ",
|
"Show related videos: ": "Показывать похожие видео? ",
|
||||||
"Show annotations by default: ": "Всегда показывать аннотации? ",
|
"Show annotations by default: ": "Всегда показывать аннотации? ",
|
||||||
"Visual preferences": "Настройки сайта",
|
"Visual preferences": "Настройки сайта",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Тёмное оформление: ",
|
"Dark mode: ": "Тёмное оформление: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Облегчённое оформление: ",
|
"Thin mode: ": "Облегчённое оформление: ",
|
||||||
"Subscription preferences": "Настройки подписок",
|
"Subscription preferences": "Настройки подписок",
|
||||||
"Show annotations by default for subscribed channels: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ",
|
"Show annotations by default for subscribed channels: ": "Всегда показывать аннотации в видео каналов, на которые вы подписаны? ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "Показувати схожі відео? ",
|
"Show related videos: ": "Показувати схожі відео? ",
|
||||||
"Show annotations by default: ": "Завжди показувати анотації? ",
|
"Show annotations by default: ": "Завжди показувати анотації? ",
|
||||||
"Visual preferences": "Налаштування сайту",
|
"Visual preferences": "Налаштування сайту",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "Темне оформлення: ",
|
"Dark mode: ": "Темне оформлення: ",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "Полегшене оформлення: ",
|
"Thin mode: ": "Полегшене оформлення: ",
|
||||||
"Subscription preferences": "Налаштування підписок",
|
"Subscription preferences": "Налаштування підписок",
|
||||||
"Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ",
|
"Show annotations by default for subscribed channels: ": "Завжди показувати анотації у відео каналів, на які ви підписані? ",
|
||||||
|
@ -68,7 +68,11 @@
|
|||||||
"Show related videos: ": "显示相关视频?",
|
"Show related videos: ": "显示相关视频?",
|
||||||
"Show annotations by default: ": "默认显示视频注释?",
|
"Show annotations by default: ": "默认显示视频注释?",
|
||||||
"Visual preferences": "视觉选项",
|
"Visual preferences": "视觉选项",
|
||||||
|
"Player style: ": "",
|
||||||
"Dark mode: ": "暗色模式:",
|
"Dark mode: ": "暗色模式:",
|
||||||
|
"Theme: ": "",
|
||||||
|
"dark": "",
|
||||||
|
"light": "",
|
||||||
"Thin mode: ": "窄页模式:",
|
"Thin mode: ": "窄页模式:",
|
||||||
"Subscription preferences": "订阅设置",
|
"Subscription preferences": "订阅设置",
|
||||||
"Show annotations by default for subscribed channels: ": "在订阅频道的视频默认显示注释?",
|
"Show annotations by default for subscribed channels: ": "在订阅频道的视频默认显示注释?",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
name: invidious
|
name: invidious
|
||||||
version: 0.19.0
|
version: 0.19.1
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Omar Roth <omarroth@protonmail.com>
|
- Omar Roth <omarroth@protonmail.com>
|
||||||
@ -11,11 +11,14 @@ targets:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pg:
|
pg:
|
||||||
github: will/crystal-pg
|
github: will/crystal-pg
|
||||||
|
version: ~> 0.18.1
|
||||||
sqlite3:
|
sqlite3:
|
||||||
github: crystal-lang/crystal-sqlite3
|
github: crystal-lang/crystal-sqlite3
|
||||||
|
version: ~> 0.13.0
|
||||||
kemal:
|
kemal:
|
||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
|
version: ~> 0.26.0
|
||||||
|
|
||||||
crystal: 0.29.0
|
crystal: 0.30.1
|
||||||
|
|
||||||
license: AGPLv3
|
license: AGPLv3
|
||||||
|
@ -1025,8 +1025,10 @@ get "/api/v1/playlists/:plid" do |env|
|
|||||||
|
|
||||||
response = JSON.build do |json|
|
response = JSON.build do |json|
|
||||||
json.object do
|
json.object do
|
||||||
|
json.field "type", "playlist"
|
||||||
json.field "title", playlist.title
|
json.field "title", playlist.title
|
||||||
json.field "playlistId", playlist.id
|
json.field "playlistId", playlist.id
|
||||||
|
json.field "playlistThumbnail", playlist.thumbnail
|
||||||
|
|
||||||
json.field "author", playlist.author
|
json.field "author", playlist.author
|
||||||
json.field "authorId", playlist.ucid
|
json.field "authorId", playlist.ucid
|
||||||
@ -1216,7 +1218,7 @@ get "/api/manifest/dash/id/:id" do |env|
|
|||||||
end
|
end
|
||||||
|
|
||||||
audio_streams = video.audio_streams(adaptive_fmts)
|
audio_streams = video.audio_streams(adaptive_fmts)
|
||||||
video_streams = video.video_streams(adaptive_fmts)
|
video_streams = video.video_streams(adaptive_fmts).sort_by { |stream| stream["fps"].to_i }.reverse
|
||||||
|
|
||||||
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||||
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
|
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
|
||||||
@ -1772,6 +1774,43 @@ get "/sb/:id/:storyboard/:index" do |env|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/s_p/:id/:name" do |env|
|
||||||
|
id = env.params.url["id"]
|
||||||
|
name = env.params.url["name"]
|
||||||
|
|
||||||
|
host = "https://i9.ytimg.com"
|
||||||
|
client = make_client(URI.parse(host))
|
||||||
|
url = env.request.resource
|
||||||
|
|
||||||
|
headers = HTTP::Headers.new
|
||||||
|
REQUEST_HEADERS_WHITELIST.each do |header|
|
||||||
|
if env.request.headers[header]?
|
||||||
|
headers[header] = env.request.headers[header]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
client.get(url, headers) do |response|
|
||||||
|
env.response.status_code = response.status_code
|
||||||
|
response.headers.each do |key, value|
|
||||||
|
if !RESPONSE_HEADERS_BLACKLIST.includes? key
|
||||||
|
env.response.headers[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
|
||||||
|
if response.status_code >= 300 && response.status_code != 404
|
||||||
|
env.response.headers.delete("Transfer-Encoding")
|
||||||
|
break
|
||||||
|
end
|
||||||
|
|
||||||
|
proxy_file(response, env)
|
||||||
|
end
|
||||||
|
rescue ex
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
get "/vi/:id/:name" do |env|
|
get "/vi/:id/:name" do |env|
|
||||||
id = env.params.url["id"]
|
id = env.params.url["id"]
|
||||||
name = env.params.url["name"]
|
name = env.params.url["name"]
|
||||||
|
@ -387,14 +387,15 @@ def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
|
|||||||
|
|
||||||
html = XML.parse_html(json["content_html"].as_s)
|
html = XML.parse_html(json["content_html"].as_s)
|
||||||
nodeset = html.xpath_nodes(%q(//li[contains(@class, "feed-item-container")]))
|
nodeset = html.xpath_nodes(%q(//li[contains(@class, "feed-item-container")]))
|
||||||
else
|
elsif auto_generated
|
||||||
url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list"
|
url = "/channel/#{ucid}"
|
||||||
|
|
||||||
if auto_generated
|
response = client.get(url)
|
||||||
url += "&view=50"
|
html = XML.parse_html(response.body)
|
||||||
else
|
|
||||||
url += "&view=1"
|
nodeset = html.xpath_nodes(%q(//ul[@id="browse-items-primary"]/li[contains(@class, "feed-item-container")]))
|
||||||
end
|
else
|
||||||
|
url = "/channel/#{ucid}/playlists?disable_polymer=1&flow=list&view=1"
|
||||||
|
|
||||||
case sort_by
|
case sort_by
|
||||||
when "last", "last_added"
|
when "last", "last_added"
|
||||||
|
@ -69,20 +69,20 @@ class FilteredCompressHandler < Kemal::Handler
|
|||||||
return call_next env if exclude_match? env
|
return call_next env if exclude_match? env
|
||||||
|
|
||||||
{% if flag?(:without_zlib) %}
|
{% if flag?(:without_zlib) %}
|
||||||
call_next env
|
call_next env
|
||||||
{% else %}
|
{% else %}
|
||||||
request_headers = env.request.headers
|
request_headers = env.request.headers
|
||||||
|
|
||||||
if request_headers.includes_word?("Accept-Encoding", "gzip")
|
if request_headers.includes_word?("Accept-Encoding", "gzip")
|
||||||
env.response.headers["Content-Encoding"] = "gzip"
|
env.response.headers["Content-Encoding"] = "gzip"
|
||||||
env.response.output = Gzip::Writer.new(env.response.output, sync_close: true)
|
env.response.output = Gzip::Writer.new(env.response.output, sync_close: true)
|
||||||
elsif request_headers.includes_word?("Accept-Encoding", "deflate")
|
elsif request_headers.includes_word?("Accept-Encoding", "deflate")
|
||||||
env.response.headers["Content-Encoding"] = "deflate"
|
env.response.headers["Content-Encoding"] = "deflate"
|
||||||
env.response.output = Flate::Writer.new(env.response.output, sync_close: true)
|
env.response.output = Flate::Writer.new(env.response.output, sync_close: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
call_next env
|
call_next env
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -24,6 +24,27 @@ end
|
|||||||
|
|
||||||
struct ConfigPreferences
|
struct ConfigPreferences
|
||||||
module StringToArray
|
module StringToArray
|
||||||
|
def self.to_json(value : Array(String), json : JSON::Builder)
|
||||||
|
json.array do
|
||||||
|
value.each do |element|
|
||||||
|
json.string element
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_json(value : JSON::PullParser) : Array(String)
|
||||||
|
begin
|
||||||
|
result = [] of String
|
||||||
|
value.read_array do
|
||||||
|
result << HTML.escape(value.read_string[0, 100])
|
||||||
|
end
|
||||||
|
rescue ex
|
||||||
|
result = [HTML.escape(value.read_string[0, 100]), ""]
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
|
||||||
def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder)
|
def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder)
|
||||||
yaml.sequence do
|
yaml.sequence do
|
||||||
value.each do |element|
|
value.each do |element|
|
||||||
@ -44,11 +65,11 @@ struct ConfigPreferences
|
|||||||
node.raise "Expected scalar, not #{item.class}"
|
node.raise "Expected scalar, not #{item.class}"
|
||||||
end
|
end
|
||||||
|
|
||||||
result << item.value
|
result << HTML.escape(item.value[0, 100])
|
||||||
end
|
end
|
||||||
rescue ex
|
rescue ex
|
||||||
if node.is_a?(YAML::Nodes::Scalar)
|
if node.is_a?(YAML::Nodes::Scalar)
|
||||||
result = [node.value, ""]
|
result = [HTML.escape(node.value[0, 100]), ""]
|
||||||
else
|
else
|
||||||
result = ["", ""]
|
result = ["", ""]
|
||||||
end
|
end
|
||||||
@ -58,6 +79,53 @@ struct ConfigPreferences
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
module BoolToString
|
||||||
|
def self.to_json(value : String, json : JSON::Builder)
|
||||||
|
json.string value
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_json(value : JSON::PullParser) : String
|
||||||
|
begin
|
||||||
|
result = value.read_string
|
||||||
|
|
||||||
|
if result.empty?
|
||||||
|
CONFIG.default_user_preferences.dark_mode
|
||||||
|
else
|
||||||
|
result
|
||||||
|
end
|
||||||
|
rescue ex
|
||||||
|
result = value.read_bool
|
||||||
|
|
||||||
|
if result
|
||||||
|
"dark"
|
||||||
|
else
|
||||||
|
"light"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.to_yaml(value : String, yaml : YAML::Nodes::Builder)
|
||||||
|
yaml.scalar value
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : String
|
||||||
|
unless node.is_a?(YAML::Nodes::Scalar)
|
||||||
|
node.raise "Expected sequence, not #{node.class}"
|
||||||
|
end
|
||||||
|
|
||||||
|
case node.value
|
||||||
|
when "true"
|
||||||
|
"dark"
|
||||||
|
when "false"
|
||||||
|
"light"
|
||||||
|
when ""
|
||||||
|
CONFIG.default_user_preferences.dark_mode
|
||||||
|
else
|
||||||
|
node.value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
yaml_mapping({
|
yaml_mapping({
|
||||||
annotations: {type: Bool, default: false},
|
annotations: {type: Bool, default: false},
|
||||||
annotations_subscribed: {type: Bool, default: false},
|
annotations_subscribed: {type: Bool, default: false},
|
||||||
@ -66,13 +134,14 @@ struct ConfigPreferences
|
|||||||
comments: {type: Array(String), default: ["youtube", ""], converter: StringToArray},
|
comments: {type: Array(String), default: ["youtube", ""], converter: StringToArray},
|
||||||
continue: {type: Bool, default: false},
|
continue: {type: Bool, default: false},
|
||||||
continue_autoplay: {type: Bool, default: true},
|
continue_autoplay: {type: Bool, default: true},
|
||||||
dark_mode: {type: Bool, default: false},
|
dark_mode: {type: String, default: "", converter: BoolToString},
|
||||||
latest_only: {type: Bool, default: false},
|
latest_only: {type: Bool, default: false},
|
||||||
listen: {type: Bool, default: false},
|
listen: {type: Bool, default: false},
|
||||||
local: {type: Bool, default: false},
|
local: {type: Bool, default: false},
|
||||||
locale: {type: String, default: "en-US"},
|
locale: {type: String, default: "en-US"},
|
||||||
max_results: {type: Int32, default: 40},
|
max_results: {type: Int32, default: 40},
|
||||||
notifications_only: {type: Bool, default: false},
|
notifications_only: {type: Bool, default: false},
|
||||||
|
player_style: {type: String, default: "invidious"},
|
||||||
quality: {type: String, default: "hd720"},
|
quality: {type: String, default: "hd720"},
|
||||||
redirect_feed: {type: Bool, default: false},
|
redirect_feed: {type: Bool, default: false},
|
||||||
related_videos: {type: Bool, default: true},
|
related_videos: {type: Bool, default: true},
|
||||||
@ -243,8 +312,7 @@ end
|
|||||||
|
|
||||||
def extract_videos(nodeset, ucid = nil, author_name = nil)
|
def extract_videos(nodeset, ucid = nil, author_name = nil)
|
||||||
videos = extract_items(nodeset, ucid, author_name)
|
videos = extract_items(nodeset, ucid, author_name)
|
||||||
videos.select! { |item| !item.is_a?(SearchChannel | SearchPlaylist) }
|
videos.select { |item| item.is_a?(SearchVideo) }.map { |video| video.as(SearchVideo) }
|
||||||
videos.map { |video| video.as(SearchVideo) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_items(nodeset, ucid = nil, author_name = nil)
|
def extract_items(nodeset, ucid = nil, author_name = nil)
|
||||||
@ -263,18 +331,8 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
|||||||
next
|
next
|
||||||
end
|
end
|
||||||
|
|
||||||
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a))
|
author_id = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).try &.["href"].split("/")[-1] || ucid || ""
|
||||||
if anchor
|
author = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)).try &.content.strip || author_name || ""
|
||||||
author = anchor.content.strip
|
|
||||||
author_id = anchor["href"].split("/")[-1]
|
|
||||||
end
|
|
||||||
|
|
||||||
author ||= author_name
|
|
||||||
author_id ||= ucid
|
|
||||||
|
|
||||||
author ||= ""
|
|
||||||
author_id ||= ""
|
|
||||||
|
|
||||||
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")])).try &.to_s || ""
|
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")])).try &.to_s || ""
|
||||||
|
|
||||||
tile = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-tile")]))
|
tile = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-tile")]))
|
||||||
@ -292,14 +350,14 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
|||||||
anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a))
|
anchor = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li/a))
|
||||||
end
|
end
|
||||||
|
|
||||||
video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b))
|
video_count = node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) ||
|
||||||
|
node.xpath_node(%q(.//span[@class="formatted-video-count-label"]))
|
||||||
if video_count
|
if video_count
|
||||||
video_count = video_count.content
|
video_count = video_count.content
|
||||||
|
|
||||||
if video_count == "50+"
|
if video_count == "50+"
|
||||||
author = "YouTube"
|
author = "YouTube"
|
||||||
author_id = "UC-9-kyTW8ZkZNDHQJ6FgpwQ"
|
author_id = "UC-9-kyTW8ZkZNDHQJ6FgpwQ"
|
||||||
video_count = video_count.rchop("+")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
video_count = video_count.gsub(/\D/, "").to_i?
|
video_count = video_count.gsub(/\D/, "").to_i?
|
||||||
@ -329,22 +387,17 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
playlist_thumbnail = node.xpath_node(%q(.//div/span/img)).try &.["data-thumb"]?
|
playlist_thumbnail = node.xpath_node(%q(.//span/img)).try &.["data-thumb"]?
|
||||||
playlist_thumbnail ||= node.xpath_node(%q(.//div/span/img)).try &.["src"]
|
playlist_thumbnail ||= node.xpath_node(%q(.//span/img)).try &.["src"]
|
||||||
if !playlist_thumbnail || playlist_thumbnail.empty?
|
|
||||||
thumbnail_id = videos[0]?.try &.id
|
|
||||||
else
|
|
||||||
thumbnail_id = playlist_thumbnail.match(/\/vi\/(?<video_id>[a-zA-Z0-9_-]{11})\/\w+\.jpg/).try &.["video_id"]
|
|
||||||
end
|
|
||||||
|
|
||||||
items << SearchPlaylist.new(
|
items << SearchPlaylist.new(
|
||||||
title,
|
title: title,
|
||||||
plid,
|
id: plid,
|
||||||
author,
|
author: author,
|
||||||
author_id,
|
ucid: author_id,
|
||||||
video_count,
|
video_count: video_count,
|
||||||
videos,
|
videos: videos,
|
||||||
thumbnail_id
|
thumbnail: playlist_thumbnail
|
||||||
)
|
)
|
||||||
when .includes? "yt-lockup-channel"
|
when .includes? "yt-lockup-channel"
|
||||||
author = title.strip
|
author = title.strip
|
||||||
@ -379,47 +432,20 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
|
|||||||
else
|
else
|
||||||
id = id.lchop("/watch?v=")
|
id = id.lchop("/watch?v=")
|
||||||
|
|
||||||
metadata = node.xpath_nodes(%q(.//div[contains(@class,"yt-lockup-meta")]/ul/li))
|
metadata = node.xpath_node(%q(.//div[contains(@class,"yt-lockup-meta")]/ul))
|
||||||
|
|
||||||
begin
|
published = metadata.try &.xpath_node(%q(.//li[contains(text(), " ago")])).try { |node| decode_date(node.content.sub(/^[a-zA-Z]+ /, "")) }
|
||||||
published = decode_date(metadata[0].content.lchop("Streamed ").lchop("Starts "))
|
published ||= metadata.try &.xpath_node(%q(.//span[@data-timestamp])).try { |node| Time.unix(node["data-timestamp"].to_i64) }
|
||||||
rescue ex
|
|
||||||
end
|
|
||||||
begin
|
|
||||||
published ||= Time.unix(metadata[0].xpath_node(%q(.//span)).not_nil!["data-timestamp"].to_i64)
|
|
||||||
rescue ex
|
|
||||||
end
|
|
||||||
published ||= Time.utc
|
published ||= Time.utc
|
||||||
|
|
||||||
begin
|
view_count = metadata.try &.xpath_node(%q(.//li[contains(text(), " views")])).try &.content.gsub(/\D/, "").to_i64?
|
||||||
view_count = metadata[0].content.rchop(" watching").delete(",").try &.to_i64?
|
|
||||||
rescue ex
|
|
||||||
end
|
|
||||||
begin
|
|
||||||
view_count ||= metadata.try &.[1].content.delete("No views,").try &.to_i64?
|
|
||||||
rescue ex
|
|
||||||
end
|
|
||||||
view_count ||= 0_i64
|
view_count ||= 0_i64
|
||||||
|
|
||||||
length_seconds = node.xpath_node(%q(.//span[@class="video-time"]))
|
length_seconds = node.xpath_node(%q(.//span[@class="video-time"])).try { |node| decode_length_seconds(node.content) }
|
||||||
if length_seconds
|
length_seconds ||= -1
|
||||||
length_seconds = decode_length_seconds(length_seconds.content)
|
|
||||||
else
|
|
||||||
length_seconds = -1
|
|
||||||
end
|
|
||||||
|
|
||||||
live_now = node.xpath_node(%q(.//span[contains(@class, "yt-badge-live")]))
|
live_now = node.xpath_node(%q(.//span[contains(@class, "yt-badge-live")])) ? true : false
|
||||||
if live_now
|
premium = node.xpath_node(%q(.//span[text()="Premium"])) ? true : false
|
||||||
live_now = true
|
|
||||||
else
|
|
||||||
live_now = false
|
|
||||||
end
|
|
||||||
|
|
||||||
if node.xpath_node(%q(.//span[text()="Premium"]))
|
|
||||||
premium = true
|
|
||||||
else
|
|
||||||
premium = false
|
|
||||||
end
|
|
||||||
|
|
||||||
if !premium || node.xpath_node(%q(.//span[contains(text(), "Free episode")]))
|
if !premium || node.xpath_node(%q(.//span[contains(text(), "Free episode")]))
|
||||||
paid = false
|
paid = false
|
||||||
@ -457,26 +483,18 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil)
|
|||||||
|
|
||||||
nodeset.each do |shelf|
|
nodeset.each do |shelf|
|
||||||
shelf_anchor = shelf.xpath_node(%q(.//h2[contains(@class, "branded-page-module-title")]))
|
shelf_anchor = shelf.xpath_node(%q(.//h2[contains(@class, "branded-page-module-title")]))
|
||||||
|
next if !shelf_anchor
|
||||||
|
|
||||||
if !shelf_anchor
|
title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")])).try &.content.strip
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")]))
|
|
||||||
if title
|
|
||||||
title = title.content.strip
|
|
||||||
end
|
|
||||||
title ||= ""
|
title ||= ""
|
||||||
|
|
||||||
id = shelf_anchor.xpath_node(%q(.//a)).try &.["href"]
|
id = shelf_anchor.xpath_node(%q(.//a)).try &.["href"]
|
||||||
if !id
|
next if !id
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
is_playlist = false
|
shelf_is_playlist = false
|
||||||
videos = [] of SearchPlaylistVideo
|
videos = [] of SearchPlaylistVideo
|
||||||
|
|
||||||
shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list")]/li)).each do |child_node|
|
shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list") or contains(@class, "expanded-shelf-content-list")]/li)).each do |child_node|
|
||||||
type = child_node.xpath_node(%q(./div))
|
type = child_node.xpath_node(%q(./div))
|
||||||
if !type
|
if !type
|
||||||
next
|
next
|
||||||
@ -484,7 +502,7 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil)
|
|||||||
|
|
||||||
case type["class"]
|
case type["class"]
|
||||||
when .includes? "yt-lockup-video"
|
when .includes? "yt-lockup-video"
|
||||||
is_playlist = true
|
shelf_is_playlist = true
|
||||||
|
|
||||||
anchor = child_node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a))
|
anchor = child_node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a))
|
||||||
if anchor
|
if anchor
|
||||||
@ -517,41 +535,60 @@ def extract_shelf_items(nodeset, ucid = nil, author_name = nil)
|
|||||||
|
|
||||||
playlist_thumbnail = child_node.xpath_node(%q(.//span/img)).try &.["data-thumb"]?
|
playlist_thumbnail = child_node.xpath_node(%q(.//span/img)).try &.["data-thumb"]?
|
||||||
playlist_thumbnail ||= child_node.xpath_node(%q(.//span/img)).try &.["src"]
|
playlist_thumbnail ||= child_node.xpath_node(%q(.//span/img)).try &.["src"]
|
||||||
if !playlist_thumbnail || playlist_thumbnail.empty?
|
|
||||||
thumbnail_id = videos[0]?.try &.id
|
|
||||||
else
|
|
||||||
thumbnail_id = playlist_thumbnail.match(/\/vi\/(?<video_id>[a-zA-Z0-9_-]{11})\/\w+\.jpg/).try &.["video_id"]
|
|
||||||
end
|
|
||||||
|
|
||||||
video_count_label = child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"]))
|
video_count = child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"]/b)) ||
|
||||||
if video_count_label
|
child_node.xpath_node(%q(.//span[@class="formatted-video-count-label"]))
|
||||||
video_count = video_count_label.content.gsub(/\D/, "").to_i?
|
if video_count
|
||||||
|
video_count = video_count.content.gsub(/\D/, "").to_i?
|
||||||
end
|
end
|
||||||
video_count ||= 50
|
video_count ||= 50
|
||||||
|
|
||||||
|
videos = [] of SearchPlaylistVideo
|
||||||
|
child_node.xpath_nodes(%q(.//*[contains(@class, "yt-lockup-playlist-items")]/li)).each do |video|
|
||||||
|
anchor = video.xpath_node(%q(.//a))
|
||||||
|
if anchor
|
||||||
|
video_title = anchor.content.strip
|
||||||
|
id = HTTP::Params.parse(URI.parse(anchor["href"]).query.not_nil!)["v"]
|
||||||
|
end
|
||||||
|
video_title ||= ""
|
||||||
|
id ||= ""
|
||||||
|
|
||||||
|
anchor = video.xpath_node(%q(.//span/span))
|
||||||
|
if anchor
|
||||||
|
length_seconds = decode_length_seconds(anchor.content)
|
||||||
|
end
|
||||||
|
length_seconds ||= 0
|
||||||
|
|
||||||
|
videos << SearchPlaylistVideo.new(
|
||||||
|
video_title,
|
||||||
|
id,
|
||||||
|
length_seconds
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
items << SearchPlaylist.new(
|
items << SearchPlaylist.new(
|
||||||
playlist_title,
|
title: playlist_title,
|
||||||
plid,
|
id: plid,
|
||||||
author_name,
|
author: author_name,
|
||||||
ucid,
|
ucid: ucid,
|
||||||
video_count,
|
video_count: video_count,
|
||||||
Array(SearchPlaylistVideo).new,
|
videos: videos,
|
||||||
thumbnail_id
|
thumbnail: playlist_thumbnail
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_playlist
|
if shelf_is_playlist
|
||||||
plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"]
|
plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"]
|
||||||
|
|
||||||
items << SearchPlaylist.new(
|
items << SearchPlaylist.new(
|
||||||
title,
|
title: title,
|
||||||
plid,
|
id: plid,
|
||||||
author_name,
|
author: author_name,
|
||||||
ucid,
|
ucid: ucid,
|
||||||
videos.size,
|
video_count: videos.size,
|
||||||
videos,
|
videos: videos,
|
||||||
videos[0].try &.id
|
thumbnail: "https://i.ytimg.com/vi/#{videos[0].id}/mqdefault.jpg"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,7 +4,7 @@ def Object.from_json(string_or_io, default) : self
|
|||||||
new parser, default
|
new parser, default
|
||||||
end
|
end
|
||||||
|
|
||||||
# Adds configurable 'default' to
|
# Adds configurable 'default'
|
||||||
macro patched_json_mapping(_properties_, strict = false)
|
macro patched_json_mapping(_properties_, strict = false)
|
||||||
{% for key, value in _properties_ %}
|
{% for key, value in _properties_ %}
|
||||||
{% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %}
|
{% _properties_[key] = {type: value} unless value.is_a?(HashLiteral) || value.is_a?(NamedTupleLiteral) %}
|
||||||
|
@ -31,10 +31,10 @@ class HTTPProxy
|
|||||||
|
|
||||||
if resp[:code]? == 200
|
if resp[:code]? == 200
|
||||||
{% if !flag?(:without_openssl) %}
|
{% if !flag?(:without_openssl) %}
|
||||||
if tls
|
if tls
|
||||||
tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
|
tls_socket = OpenSSL::SSL::Socket::Client.new(socket, context: tls, sync_close: true, hostname: host)
|
||||||
socket = tls_socket
|
socket = tls_socket
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
return socket
|
return socket
|
||||||
|
@ -356,3 +356,16 @@ def parse_range(range)
|
|||||||
|
|
||||||
return 0_i64, nil
|
return 0_i64, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def convert_theme(theme)
|
||||||
|
case theme
|
||||||
|
when "true"
|
||||||
|
"dark"
|
||||||
|
when "false"
|
||||||
|
"light"
|
||||||
|
when "", nil
|
||||||
|
nil
|
||||||
|
else
|
||||||
|
theme
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@ -51,6 +51,7 @@ struct Playlist
|
|||||||
video_count: Int32,
|
video_count: Int32,
|
||||||
views: Int64,
|
views: Int64,
|
||||||
updated: Time,
|
updated: Time,
|
||||||
|
thumbnail: String?,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -223,6 +224,9 @@ def fetch_playlist(plid, locale)
|
|||||||
description_html = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1])).try &.to_s ||
|
description_html = document.xpath_node(%q(//span[@class="pl-header-description-text"]/div/div[1])).try &.to_s ||
|
||||||
document.xpath_node(%q(//span[@class="pl-header-description-text"])).try &.to_s || ""
|
document.xpath_node(%q(//span[@class="pl-header-description-text"])).try &.to_s || ""
|
||||||
|
|
||||||
|
playlist_thumbnail = document.xpath_node(%q(//div[@class="pl-header-thumb"]/img)).try &.["data-thumb"]? ||
|
||||||
|
document.xpath_node(%q(//div[@class="pl-header-thumb"]/img)).try &.["src"]
|
||||||
|
|
||||||
# YouTube allows anonymous playlists, so most of this can be empty or optional
|
# YouTube allows anonymous playlists, so most of this can be empty or optional
|
||||||
anchor = document.xpath_node(%q(//ul[@class="pl-header-details"]))
|
anchor = document.xpath_node(%q(//ul[@class="pl-header-details"]))
|
||||||
author = anchor.try &.xpath_node(%q(.//li[1]/a)).try &.content
|
author = anchor.try &.xpath_node(%q(.//li[1]/a)).try &.content
|
||||||
@ -234,15 +238,12 @@ def fetch_playlist(plid, locale)
|
|||||||
|
|
||||||
video_count = anchor.try &.xpath_node(%q(.//li[2])).try &.content.gsub(/\D/, "").to_i?
|
video_count = anchor.try &.xpath_node(%q(.//li[2])).try &.content.gsub(/\D/, "").to_i?
|
||||||
video_count ||= 0
|
video_count ||= 0
|
||||||
views = anchor.try &.xpath_node(%q(.//li[3])).try &.content.delete("No views, ").to_i64?
|
|
||||||
|
views = anchor.try &.xpath_node(%q(.//li[3])).try &.content.gsub(/\D/, "").to_i64?
|
||||||
views ||= 0_i64
|
views ||= 0_i64
|
||||||
|
|
||||||
updated = anchor.try &.xpath_node(%q(.//li[4])).try &.content.lchop("Last updated on ").lchop("Updated ")
|
updated = anchor.try &.xpath_node(%q(.//li[4])).try &.content.lchop("Last updated on ").lchop("Updated ").try { |date| decode_date(date) }
|
||||||
if updated
|
updated ||= Time.utc
|
||||||
updated = decode_date(updated)
|
|
||||||
else
|
|
||||||
updated = Time.utc
|
|
||||||
end
|
|
||||||
|
|
||||||
playlist = Playlist.new(
|
playlist = Playlist.new(
|
||||||
title: title,
|
title: title,
|
||||||
@ -253,7 +254,8 @@ def fetch_playlist(plid, locale)
|
|||||||
description_html: description_html,
|
description_html: description_html,
|
||||||
video_count: video_count,
|
video_count: video_count,
|
||||||
views: views,
|
views: views,
|
||||||
updated: updated
|
updated: updated,
|
||||||
|
thumbnail: playlist_thumbnail,
|
||||||
)
|
)
|
||||||
|
|
||||||
return playlist
|
return playlist
|
||||||
|
@ -117,6 +117,7 @@ struct SearchPlaylist
|
|||||||
json.field "type", "playlist"
|
json.field "type", "playlist"
|
||||||
json.field "title", self.title
|
json.field "title", self.title
|
||||||
json.field "playlistId", self.id
|
json.field "playlistId", self.id
|
||||||
|
json.field "playlistThumbnail", self.thumbnail
|
||||||
|
|
||||||
json.field "author", self.author
|
json.field "author", self.author
|
||||||
json.field "authorId", self.ucid
|
json.field "authorId", self.ucid
|
||||||
@ -152,13 +153,13 @@ struct SearchPlaylist
|
|||||||
end
|
end
|
||||||
|
|
||||||
db_mapping({
|
db_mapping({
|
||||||
title: String,
|
title: String,
|
||||||
id: String,
|
id: String,
|
||||||
author: String,
|
author: String,
|
||||||
ucid: String,
|
ucid: String,
|
||||||
video_count: Int32,
|
video_count: Int32,
|
||||||
videos: Array(SearchPlaylistVideo),
|
videos: Array(SearchPlaylistVideo),
|
||||||
thumbnail_id: String?,
|
thumbnail: String?,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -31,62 +31,6 @@ struct User
|
|||||||
end
|
end
|
||||||
|
|
||||||
struct Preferences
|
struct Preferences
|
||||||
module StringToArray
|
|
||||||
def self.to_json(value : Array(String), json : JSON::Builder)
|
|
||||||
json.array do
|
|
||||||
value.each do |element|
|
|
||||||
json.string element
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.from_json(value : JSON::PullParser) : Array(String)
|
|
||||||
begin
|
|
||||||
result = [] of String
|
|
||||||
value.read_array do
|
|
||||||
result << HTML.escape(value.read_string[0, 100])
|
|
||||||
end
|
|
||||||
rescue ex
|
|
||||||
result = [HTML.escape(value.read_string[0, 100]), ""]
|
|
||||||
end
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.to_yaml(value : Array(String), yaml : YAML::Nodes::Builder)
|
|
||||||
yaml.sequence do
|
|
||||||
value.each do |element|
|
|
||||||
yaml.scalar element
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.from_yaml(ctx : YAML::ParseContext, node : YAML::Nodes::Node) : Array(String)
|
|
||||||
begin
|
|
||||||
unless node.is_a?(YAML::Nodes::Sequence)
|
|
||||||
node.raise "Expected sequence, not #{node.class}"
|
|
||||||
end
|
|
||||||
|
|
||||||
result = [] of String
|
|
||||||
node.nodes.each do |item|
|
|
||||||
unless item.is_a?(YAML::Nodes::Scalar)
|
|
||||||
node.raise "Expected scalar, not #{item.class}"
|
|
||||||
end
|
|
||||||
|
|
||||||
result << HTML.escape(item.value[0, 100])
|
|
||||||
end
|
|
||||||
rescue ex
|
|
||||||
if node.is_a?(YAML::Nodes::Scalar)
|
|
||||||
result = [HTML.escape(node.value[0, 100]), ""]
|
|
||||||
else
|
|
||||||
result = ["", ""]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
module ProcessString
|
module ProcessString
|
||||||
def self.to_json(value : String, json : JSON::Builder)
|
def self.to_json(value : String, json : JSON::Builder)
|
||||||
json.string value
|
json.string value
|
||||||
@ -127,17 +71,18 @@ struct Preferences
|
|||||||
annotations: {type: Bool, default: CONFIG.default_user_preferences.annotations},
|
annotations: {type: Bool, default: CONFIG.default_user_preferences.annotations},
|
||||||
annotations_subscribed: {type: Bool, default: CONFIG.default_user_preferences.annotations_subscribed},
|
annotations_subscribed: {type: Bool, default: CONFIG.default_user_preferences.annotations_subscribed},
|
||||||
autoplay: {type: Bool, default: CONFIG.default_user_preferences.autoplay},
|
autoplay: {type: Bool, default: CONFIG.default_user_preferences.autoplay},
|
||||||
captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: StringToArray},
|
captions: {type: Array(String), default: CONFIG.default_user_preferences.captions, converter: ConfigPreferences::StringToArray},
|
||||||
comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: StringToArray},
|
comments: {type: Array(String), default: CONFIG.default_user_preferences.comments, converter: ConfigPreferences::StringToArray},
|
||||||
continue: {type: Bool, default: CONFIG.default_user_preferences.continue},
|
continue: {type: Bool, default: CONFIG.default_user_preferences.continue},
|
||||||
continue_autoplay: {type: Bool, default: CONFIG.default_user_preferences.continue_autoplay},
|
continue_autoplay: {type: Bool, default: CONFIG.default_user_preferences.continue_autoplay},
|
||||||
dark_mode: {type: Bool, default: CONFIG.default_user_preferences.dark_mode},
|
dark_mode: {type: String, default: CONFIG.default_user_preferences.dark_mode, converter: ConfigPreferences::BoolToString},
|
||||||
latest_only: {type: Bool, default: CONFIG.default_user_preferences.latest_only},
|
latest_only: {type: Bool, default: CONFIG.default_user_preferences.latest_only},
|
||||||
listen: {type: Bool, default: CONFIG.default_user_preferences.listen},
|
listen: {type: Bool, default: CONFIG.default_user_preferences.listen},
|
||||||
local: {type: Bool, default: CONFIG.default_user_preferences.local},
|
local: {type: Bool, default: CONFIG.default_user_preferences.local},
|
||||||
locale: {type: String, default: CONFIG.default_user_preferences.locale, converter: ProcessString},
|
locale: {type: String, default: CONFIG.default_user_preferences.locale, converter: ProcessString},
|
||||||
max_results: {type: Int32, default: CONFIG.default_user_preferences.max_results, converter: ClampInt},
|
max_results: {type: Int32, default: CONFIG.default_user_preferences.max_results, converter: ClampInt},
|
||||||
notifications_only: {type: Bool, default: CONFIG.default_user_preferences.notifications_only},
|
notifications_only: {type: Bool, default: CONFIG.default_user_preferences.notifications_only},
|
||||||
|
player_style: {type: String, default: CONFIG.default_user_preferences.player_style, converter: ProcessString},
|
||||||
quality: {type: String, default: CONFIG.default_user_preferences.quality, converter: ProcessString},
|
quality: {type: String, default: CONFIG.default_user_preferences.quality, converter: ProcessString},
|
||||||
redirect_feed: {type: Bool, default: CONFIG.default_user_preferences.redirect_feed},
|
redirect_feed: {type: Bool, default: CONFIG.default_user_preferences.redirect_feed},
|
||||||
related_videos: {type: Bool, default: CONFIG.default_user_preferences.related_videos},
|
related_videos: {type: Bool, default: CONFIG.default_user_preferences.related_videos},
|
||||||
|
@ -108,33 +108,7 @@ CAPTION_LANGUAGES = {
|
|||||||
"Zulu",
|
"Zulu",
|
||||||
}
|
}
|
||||||
|
|
||||||
REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"}
|
REGIONS = {"AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW"}
|
||||||
BYPASS_REGIONS = {
|
|
||||||
"GB",
|
|
||||||
"DE",
|
|
||||||
"FR",
|
|
||||||
"IN",
|
|
||||||
"CN",
|
|
||||||
"RU",
|
|
||||||
"CA",
|
|
||||||
"JP",
|
|
||||||
"IT",
|
|
||||||
"TH",
|
|
||||||
"ES",
|
|
||||||
"AE",
|
|
||||||
"KR",
|
|
||||||
"IR",
|
|
||||||
"BR",
|
|
||||||
"PK",
|
|
||||||
"ID",
|
|
||||||
"BD",
|
|
||||||
"MX",
|
|
||||||
"PH",
|
|
||||||
"EG",
|
|
||||||
"VN",
|
|
||||||
"CD",
|
|
||||||
"TR",
|
|
||||||
}
|
|
||||||
|
|
||||||
# See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L380-#L476
|
# See https://github.com/rg3/youtube-dl/blob/master/youtube_dl/extractor/youtube.py#L380-#L476
|
||||||
VIDEO_FORMATS = {
|
VIDEO_FORMATS = {
|
||||||
@ -258,6 +232,7 @@ struct VideoPreferences
|
|||||||
listen: Bool,
|
listen: Bool,
|
||||||
local: Bool,
|
local: Bool,
|
||||||
preferred_captions: Array(String),
|
preferred_captions: Array(String),
|
||||||
|
player_style: String,
|
||||||
quality: String,
|
quality: String,
|
||||||
raw: Bool,
|
raw: Bool,
|
||||||
region: String?,
|
region: String?,
|
||||||
@ -803,8 +778,11 @@ struct Video
|
|||||||
end
|
end
|
||||||
|
|
||||||
def premium
|
def premium
|
||||||
premium = self.player_response.to_s.includes? "Get YouTube without the ads."
|
if info["premium"]?
|
||||||
return premium
|
self.info["premium"] == "true"
|
||||||
|
else
|
||||||
|
false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def captions
|
def captions
|
||||||
@ -1128,34 +1106,24 @@ def fetch_video(id, region)
|
|||||||
info = extract_player_config(response.body, html)
|
info = extract_player_config(response.body, html)
|
||||||
info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
|
info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
|
||||||
|
|
||||||
# Try to use proxies for region-blocked videos
|
allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",")
|
||||||
if info["reason"]? && info["reason"].includes? "your country"
|
if !allowed_regions || allowed_regions == [""]
|
||||||
bypass_channel = Channel({XML::Node, HTTP::Params} | Nil).new
|
allowed_regions = [] of String
|
||||||
|
end
|
||||||
|
|
||||||
PROXY_LIST.each do |proxy_region, list|
|
# Check for region-blocks
|
||||||
spawn do
|
if info["reason"]? && info["reason"].includes?("your country")
|
||||||
client = make_client(YT_URL, proxy_region)
|
bypass_regions = PROXY_LIST.keys & allowed_regions
|
||||||
proxy_response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
|
if !bypass_regions.empty?
|
||||||
|
region = bypass_regions[rand(bypass_regions.size)]
|
||||||
|
client = make_client(YT_URL, region)
|
||||||
|
response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
|
||||||
|
|
||||||
proxy_html = XML.parse_html(proxy_response.body)
|
html = XML.parse_html(response.body)
|
||||||
proxy_info = extract_player_config(proxy_response.body, proxy_html)
|
info = extract_player_config(response.body, html)
|
||||||
|
|
||||||
if !proxy_info["reason"]?
|
info["region"] = region if region
|
||||||
proxy_info["region"] = proxy_region
|
info["cookie"] = response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
|
||||||
proxy_info["cookie"] = proxy_response.cookies.to_h.map { |name, cookie| "#{name}=#{cookie.value}" }.join("; ")
|
|
||||||
bypass_channel.send({proxy_html, proxy_info})
|
|
||||||
else
|
|
||||||
bypass_channel.send(nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
PROXY_LIST.size.times do
|
|
||||||
response = bypass_channel.receive
|
|
||||||
if response
|
|
||||||
html, info = response
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1175,7 +1143,7 @@ def fetch_video(id, region)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if info["errorcode"]?.try &.== "2" || !info["player_response"]
|
if !info["player_response"]? || info["errorcode"]?.try &.== "2"
|
||||||
raise "Video unavailable."
|
raise "Video unavailable."
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -1189,6 +1157,8 @@ def fetch_video(id, region)
|
|||||||
author = player_json["videoDetails"]["author"]?.try &.as_s || ""
|
author = player_json["videoDetails"]["author"]?.try &.as_s || ""
|
||||||
ucid = player_json["videoDetails"]["channelId"]?.try &.as_s || ""
|
ucid = player_json["videoDetails"]["channelId"]?.try &.as_s || ""
|
||||||
|
|
||||||
|
info["premium"] = html.xpath_node(%q(.//span[text()="Premium"])) ? "true" : "false"
|
||||||
|
|
||||||
views = html.xpath_node(%q(//meta[@itemprop="interactionCount"]))
|
views = html.xpath_node(%q(//meta[@itemprop="interactionCount"]))
|
||||||
.try &.["content"].to_i64? || 0_i64
|
.try &.["content"].to_i64? || 0_i64
|
||||||
|
|
||||||
@ -1209,9 +1179,6 @@ def fetch_video(id, region)
|
|||||||
published ||= Time.utc.to_s("%Y-%m-%d")
|
published ||= Time.utc.to_s("%Y-%m-%d")
|
||||||
published = Time.parse(published, "%Y-%m-%d", Time::Location.local)
|
published = Time.parse(published, "%Y-%m-%d", Time::Location.local)
|
||||||
|
|
||||||
allowed_regions = html.xpath_node(%q(//meta[@itemprop="regionsAllowed"])).try &.["content"].split(",")
|
|
||||||
allowed_regions ||= [] of String
|
|
||||||
|
|
||||||
is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).try &.["content"] == "True"
|
is_family_friendly = html.xpath_node(%q(//meta[@itemprop="isFamilyFriendly"])).try &.["content"] == "True"
|
||||||
is_family_friendly ||= true
|
is_family_friendly ||= true
|
||||||
|
|
||||||
@ -1259,6 +1226,7 @@ def process_video_params(query, preferences)
|
|||||||
continue_autoplay = query["continue_autoplay"]?.try &.to_i?
|
continue_autoplay = query["continue_autoplay"]?.try &.to_i?
|
||||||
listen = query["listen"]? && (query["listen"] == "true" || query["listen"] == "1").to_unsafe
|
listen = query["listen"]? && (query["listen"] == "true" || query["listen"] == "1").to_unsafe
|
||||||
local = query["local"]? && (query["local"] == "true" || query["local"] == "1").to_unsafe
|
local = query["local"]? && (query["local"] == "true" || query["local"] == "1").to_unsafe
|
||||||
|
player_style = query["player_style"]?
|
||||||
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
||||||
quality = query["quality"]?
|
quality = query["quality"]?
|
||||||
region = query["region"]?
|
region = query["region"]?
|
||||||
@ -1276,6 +1244,7 @@ def process_video_params(query, preferences)
|
|||||||
continue_autoplay ||= preferences.continue_autoplay.to_unsafe
|
continue_autoplay ||= preferences.continue_autoplay.to_unsafe
|
||||||
listen ||= preferences.listen.to_unsafe
|
listen ||= preferences.listen.to_unsafe
|
||||||
local ||= preferences.local.to_unsafe
|
local ||= preferences.local.to_unsafe
|
||||||
|
player_style ||= preferences.player_style
|
||||||
preferred_captions ||= preferences.captions
|
preferred_captions ||= preferences.captions
|
||||||
quality ||= preferences.quality
|
quality ||= preferences.quality
|
||||||
related_videos ||= preferences.related_videos.to_unsafe
|
related_videos ||= preferences.related_videos.to_unsafe
|
||||||
@ -1291,6 +1260,7 @@ def process_video_params(query, preferences)
|
|||||||
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
|
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
|
||||||
listen ||= CONFIG.default_user_preferences.listen.to_unsafe
|
listen ||= CONFIG.default_user_preferences.listen.to_unsafe
|
||||||
local ||= CONFIG.default_user_preferences.local.to_unsafe
|
local ||= CONFIG.default_user_preferences.local.to_unsafe
|
||||||
|
player_style ||= CONFIG.default_user_preferences.player_style
|
||||||
preferred_captions ||= CONFIG.default_user_preferences.captions
|
preferred_captions ||= CONFIG.default_user_preferences.captions
|
||||||
quality ||= CONFIG.default_user_preferences.quality
|
quality ||= CONFIG.default_user_preferences.quality
|
||||||
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
|
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
|
||||||
@ -1349,6 +1319,7 @@ def process_video_params(query, preferences)
|
|||||||
controls: controls,
|
controls: controls,
|
||||||
listen: listen,
|
listen: listen,
|
||||||
local: local,
|
local: local,
|
||||||
|
player_style: player_style,
|
||||||
preferred_captions: preferred_captions,
|
preferred_captions: preferred_captions,
|
||||||
quality: quality,
|
quality: quality,
|
||||||
raw: raw,
|
raw: raw,
|
||||||
|
Loading…
Reference in New Issue
Block a user