mirror of
https://github.com/iv-org/invidious.git
synced 2025-08-09 20:24:03 +00:00
Compare commits
57 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a65998274f | ||
![]() |
b2f4a0276a | ||
![]() |
99d9c3a900 | ||
![]() |
e4dc430c74 | ||
![]() |
1435516a9c | ||
![]() |
2a1befb41a | ||
![]() |
2840d98fd4 | ||
![]() |
32b9c0c840 | ||
![]() |
6375a62465 | ||
![]() |
aa63c3f70e | ||
![]() |
004fb96b2f | ||
![]() |
5895604282 | ||
![]() |
a1af75a87f | ||
![]() |
732bd28c92 | ||
![]() |
90715467a2 | ||
![]() |
7425700009 | ||
![]() |
8e884fe115 | ||
![]() |
96c09450b8 | ||
![]() |
64cfd2296c | ||
![]() |
17cf0772fb | ||
![]() |
66605196ad | ||
![]() |
2c9b148627 | ||
![]() |
07ef48a07a | ||
![]() |
03f94db5e2 | ||
![]() |
9b202adebd | ||
![]() |
daf8e5b8b6 | ||
![]() |
25bd27ef95 | ||
![]() |
dd41e4906c | ||
![]() |
20660b92f8 | ||
![]() |
f0cc7a925c | ||
![]() |
057e69fe70 | ||
![]() |
4be82c5ca6 | ||
![]() |
0eaf8f38a1 | ||
![]() |
f31af18aa9 | ||
![]() |
5859cd290c | ||
![]() |
a39b1583da | ||
![]() |
ac0eb9acaf | ||
![]() |
a0d9e46c33 | ||
![]() |
573404d3ac | ||
![]() |
2fe545e19a | ||
![]() |
ea52c05f05 | ||
![]() |
2a643e86bc | ||
![]() |
cc76428cd2 | ||
![]() |
7ffc3a0652 | ||
![]() |
51df0860cc | ||
![]() |
e4f397d049 | ||
![]() |
0c8dff162d | ||
![]() |
4865529fed | ||
![]() |
0a404cc9a6 | ||
![]() |
17b84f32df | ||
![]() |
a03958d937 | ||
![]() |
27cd1e73f3 | ||
![]() |
d6bd893573 | ||
![]() |
7a7049b25b | ||
![]() |
62ff9605ce | ||
![]() |
2847c34f58 | ||
![]() |
b5a00f3c47 |
@@ -117,7 +117,7 @@ $ exit
|
|||||||
```bash
|
```bash
|
||||||
$ sudo -i -u invidious
|
$ sudo -i -u invidious
|
||||||
$ cd invidious
|
$ cd invidious
|
||||||
$ shards
|
$ shards update && shards install
|
||||||
$ crystal build src/invidious.cr --release
|
$ crystal build src/invidious.cr --release
|
||||||
# test compiled binary
|
# test compiled binary
|
||||||
$ ./invidious # stop with ctrl c
|
$ ./invidious # stop with ctrl c
|
||||||
@@ -153,7 +153,7 @@ $ psql invidious < config/sql/session_ids.sql
|
|||||||
$ psql invidious < config/sql/nonces.sql
|
$ psql invidious < config/sql/nonces.sql
|
||||||
|
|
||||||
# Setup Invidious
|
# Setup Invidious
|
||||||
$ shards
|
$ shards update && shards install
|
||||||
$ crystal build src/invidious.cr --release
|
$ crystal build src/invidious.cr --release
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@@ -150,6 +150,7 @@ img.thumbnail {
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
|
||||||
transition: 0.1s border-bottom;
|
transition: 0.1s border-bottom;
|
||||||
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar > .searchbar .pure-form fieldset {
|
.navbar > .searchbar .pure-form fieldset {
|
||||||
|
@@ -10,4 +10,4 @@ db:
|
|||||||
dbname: invidious
|
dbname: invidious
|
||||||
full_refresh: false
|
full_refresh: false
|
||||||
https_only: false
|
https_only: false
|
||||||
domain: invidio.us
|
domain:
|
||||||
|
4
config/migrate-scripts/migrate-db-17cf077.sh
Executable file
4
config/migrate-scripts/migrate-db-17cf077.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
psql invidious -c "ALTER TABLE channels ADD COLUMN subscribed bool;"
|
||||||
|
psql invidious -c "UPDATE channels SET subscribed = false;"
|
5
config/migrate-scripts/migrate-db-8e884fe1.sh
Executable file
5
config/migrate-scripts/migrate-db-8e884fe1.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
psql invidious -c "ALTER TABLE channels DROP COLUMN subscribed"
|
||||||
|
psql invidious -c "ALTER TABLE channels ADD COLUMN subscribed timestamptz"
|
||||||
|
psql invidious -c "UPDATE channels SET subscribed = '2019-01-01 00:00:00+00'"
|
@@ -8,6 +8,7 @@ CREATE TABLE public.channels
|
|||||||
author text,
|
author text,
|
||||||
updated timestamp with time zone,
|
updated timestamp with time zone,
|
||||||
deleted boolean,
|
deleted boolean,
|
||||||
|
subscribed timestamp with time zone,
|
||||||
CONSTRAINT channels_id_key UNIQUE (id)
|
CONSTRAINT channels_id_key UNIQUE (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@@ -9,7 +9,7 @@ ADD . /invidious
|
|||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
|
|
||||||
RUN sed -i 's/host: localhost/host: postgres/' config/config.yml && \
|
RUN sed -i 's/host: localhost/host: postgres/' config/config.yml && \
|
||||||
shards && \
|
shards update && shards install && \
|
||||||
crystal build src/invidious.cr
|
crystal build src/invidious.cr
|
||||||
|
|
||||||
CMD [ "/invidious/invidious" ]
|
CMD [ "/invidious/invidious" ]
|
||||||
|
@@ -82,6 +82,14 @@
|
|||||||
"Manage subscriptions": "إدارة المشتركين",
|
"Manage subscriptions": "إدارة المشتركين",
|
||||||
"Watch history": "سجل المشاهدة",
|
"Watch history": "سجل المشاهدة",
|
||||||
"Delete account": "حذف الحساب",
|
"Delete account": "حذف الحساب",
|
||||||
|
"Administrator preferences": "",
|
||||||
|
"Default homepage: ": "",
|
||||||
|
"Feed menu: ": "",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "",
|
||||||
|
"Login enabled? ": "",
|
||||||
|
"Registration enabled? ": "",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "حفظ التفضيلات",
|
"Save preferences": "حفظ التفضيلات",
|
||||||
"Subscription manager": "مدير الإشتراكات",
|
"Subscription manager": "مدير الإشتراكات",
|
||||||
"`x` subscriptions": "`x` مشتركين",
|
"`x` subscriptions": "`x` مشتركين",
|
||||||
|
@@ -82,6 +82,14 @@
|
|||||||
"Manage subscriptions": "Abonnements verwalten",
|
"Manage subscriptions": "Abonnements verwalten",
|
||||||
"Watch history": "Verlauf",
|
"Watch history": "Verlauf",
|
||||||
"Delete account": "Account löschen",
|
"Delete account": "Account löschen",
|
||||||
|
"Administrator preferences": "",
|
||||||
|
"Default homepage: ": "",
|
||||||
|
"Feed menu: ": "",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "",
|
||||||
|
"Login enabled? ": "",
|
||||||
|
"Registration enabled? ": "",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "Einstellungen speichern",
|
"Save preferences": "Einstellungen speichern",
|
||||||
"Subscription manager": "Abonnementverwaltung",
|
"Subscription manager": "Abonnementverwaltung",
|
||||||
"`x` subscriptions": "`x` Abonnements",
|
"`x` subscriptions": "`x` Abonnements",
|
||||||
|
@@ -80,6 +80,14 @@
|
|||||||
"Manage subscriptions": "Manage subscriptions",
|
"Manage subscriptions": "Manage subscriptions",
|
||||||
"Watch history": "Watch history",
|
"Watch history": "Watch history",
|
||||||
"Delete account": "Delete account",
|
"Delete account": "Delete account",
|
||||||
|
"Administrator preferences": "Administrator preferences",
|
||||||
|
"Default homepage: ": "Default homepage: ",
|
||||||
|
"Feed menu: ": "Feed menu: ",
|
||||||
|
"Top enabled? ": "Top enabled? ",
|
||||||
|
"CAPTCHA enabled? ": "CAPTCHA enabled? ",
|
||||||
|
"Login enabled? ": "Login enabled? ",
|
||||||
|
"Registration enabled? ": "Registration enabled? ",
|
||||||
|
"Report statistics? ": "Report statistics? ",
|
||||||
"Save preferences": "Save preferences",
|
"Save preferences": "Save preferences",
|
||||||
"Subscription manager": "Subscription manager",
|
"Subscription manager": "Subscription manager",
|
||||||
"`x` subscriptions": "`x` subscriptions",
|
"`x` subscriptions": "`x` subscriptions",
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"`x` subscribers": "",
|
"`x` subscribers": "`x` harpidedun",
|
||||||
"`x` videos": "",
|
"`x` videos": "`x` bideo",
|
||||||
"LIVE": "",
|
"LIVE": "ZUZENEAN",
|
||||||
"Shared `x` ago": "",
|
"Shared `x` ago": "Duela `x` partekatua",
|
||||||
"Unsubscribe": "",
|
"Unsubscribe": "Harpidetza kendu",
|
||||||
"Subscribe": "Harpidetu",
|
"Subscribe": "Harpidetu",
|
||||||
"Login to subscribe to `x`": "",
|
"Login to subscribe to `x`": "Saioa hasi `x`(e)ra harpidetzeko",
|
||||||
"View channel on YouTube": "Ikusi kanala YouTuben",
|
"View channel on YouTube": "Ikusi kanala YouTuben",
|
||||||
"newest": "berrienak",
|
"newest": "berrienak",
|
||||||
"oldest": "zaharrenak",
|
"oldest": "zaharrenak",
|
||||||
@@ -24,22 +24,22 @@
|
|||||||
"Import NewPipe data (.zip)": "NewPipeko datuak inportatu (.zip)",
|
"Import NewPipe data (.zip)": "NewPipeko datuak inportatu (.zip)",
|
||||||
"Export": "Esportatu",
|
"Export": "Esportatu",
|
||||||
"Export subscriptions as OPML": "Esportatu harpidetzak OPML bezala",
|
"Export subscriptions as OPML": "Esportatu harpidetzak OPML bezala",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Harpidetzak OPML bezala esportatu (NewPipe eta FreeTuberako)",
|
||||||
"Export data as JSON": "",
|
"Export data as JSON": "Datuak JSON bezala esportatu",
|
||||||
"Delete account?": "Kontua ezabatu?",
|
"Delete account?": "Kontua ezabatu?",
|
||||||
"History": "Historia",
|
"History": "Historia",
|
||||||
"Previous page": "Aurreko orria",
|
"Previous page": "Aurreko orria",
|
||||||
"An alternative front-end to YouTube": "",
|
"An alternative front-end to YouTube": "YouTuberako interfaze alternatibo bat",
|
||||||
"JavaScript license information": "",
|
"JavaScript license information": "JavaScript lizentzia informazioa",
|
||||||
"source": "",
|
"source": "iturburua",
|
||||||
"Login": "",
|
"Login": "Saioa hasi",
|
||||||
"Login/Register": "",
|
"Login/Register": "Saioa hasi/Izena eman",
|
||||||
"Login to Google": "",
|
"Login to Google": "Googlekin hasi saioa",
|
||||||
"User ID:": "",
|
"User ID:": "Erabiltzaile IDa:",
|
||||||
"Password:": "",
|
"Password:": "Pasahitza:",
|
||||||
"Time (h:mm:ss):": "",
|
"Time (h:mm:ss):": "Denbora (o:mm:ss):",
|
||||||
"Text CAPTCHA": "",
|
"Text CAPTCHA": "Testu CAPTCHA",
|
||||||
"Image CAPTCHA": "",
|
"Image CAPTCHA": "Irudi CAPTCHA",
|
||||||
"Sign In": "",
|
"Sign In": "",
|
||||||
"Register": "",
|
"Register": "",
|
||||||
"Email:": "",
|
"Email:": "",
|
||||||
@@ -80,6 +80,14 @@
|
|||||||
"Manage subscriptions": "",
|
"Manage subscriptions": "",
|
||||||
"Watch history": "",
|
"Watch history": "",
|
||||||
"Delete account": "",
|
"Delete account": "",
|
||||||
|
"Administrator preferences": "",
|
||||||
|
"Default homepage: ": "",
|
||||||
|
"Feed menu: ": "",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "",
|
||||||
|
"Login enabled? ": "",
|
||||||
|
"Registration enabled? ": "",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "",
|
"Save preferences": "",
|
||||||
"Subscription manager": "",
|
"Subscription manager": "",
|
||||||
"`x` subscriptions": "",
|
"`x` subscriptions": "",
|
||||||
|
@@ -79,6 +79,14 @@
|
|||||||
"Manage subscriptions": "Gérer les abonnements",
|
"Manage subscriptions": "Gérer les abonnements",
|
||||||
"Watch history": "Historique de visionnage",
|
"Watch history": "Historique de visionnage",
|
||||||
"Delete account": "Supprimer votre compte",
|
"Delete account": "Supprimer votre compte",
|
||||||
|
"Administrator preferences": "",
|
||||||
|
"Default homepage: ": "",
|
||||||
|
"Feed menu: ": "",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "",
|
||||||
|
"Login enabled? ": "",
|
||||||
|
"Registration enabled? ": "",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "Enregistrer les préférences",
|
"Save preferences": "Enregistrer les préférences",
|
||||||
"Subscription manager": "Gestionnaire d'abonnement",
|
"Subscription manager": "Gestionnaire d'abonnement",
|
||||||
"`x` subscriptions": "`x` abonnements",
|
"`x` subscriptions": "`x` abonnements",
|
||||||
|
@@ -79,6 +79,14 @@
|
|||||||
"Manage subscriptions": "Gestisci le iscrizioni",
|
"Manage subscriptions": "Gestisci le iscrizioni",
|
||||||
"Watch history": "Cronologia dei video",
|
"Watch history": "Cronologia dei video",
|
||||||
"Delete account": "Elimina l'account",
|
"Delete account": "Elimina l'account",
|
||||||
|
"Administrator preferences": "",
|
||||||
|
"Default homepage: ": "",
|
||||||
|
"Feed menu: ": "",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "",
|
||||||
|
"Login enabled? ": "",
|
||||||
|
"Registration enabled? ": "",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "Salva le preferenze",
|
"Save preferences": "Salva le preferenze",
|
||||||
"Subscription manager": "Gestisci le iscrizioni",
|
"Subscription manager": "Gestisci le iscrizioni",
|
||||||
"`x` subscriptions": "`x` iscrizioni",
|
"`x` subscriptions": "`x` iscrizioni",
|
||||||
|
@@ -80,6 +80,14 @@
|
|||||||
"Manage subscriptions": "Behandle abonnementer",
|
"Manage subscriptions": "Behandle abonnementer",
|
||||||
"Watch history": "Visningshistorikk",
|
"Watch history": "Visningshistorikk",
|
||||||
"Delete account": "Slett konto",
|
"Delete account": "Slett konto",
|
||||||
|
"Administrator preferences": "Administratorinnstillinger",
|
||||||
|
"Default homepage: ": "Forvalgt hjemmeside: ",
|
||||||
|
"Feed menu: ": "Flyt-meny: ",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "CAPTCHA påskrudd? ",
|
||||||
|
"Login enabled? ": "Innlogging påskrudd? ",
|
||||||
|
"Registration enabled? ": "Registrering påskrudd? ",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "Lagre innstillinger",
|
"Save preferences": "Lagre innstillinger",
|
||||||
"Subscription manager": "Abonnementsbehandler",
|
"Subscription manager": "Abonnementsbehandler",
|
||||||
"`x` subscriptions": "`x` abonnementer",
|
"`x` subscriptions": "`x` abonnementer",
|
||||||
|
@@ -80,6 +80,14 @@
|
|||||||
"Manage subscriptions": "Abonnees beheren",
|
"Manage subscriptions": "Abonnees beheren",
|
||||||
"Watch history": "Kijkgeschiedenis",
|
"Watch history": "Kijkgeschiedenis",
|
||||||
"Delete account": "Account verwijderen",
|
"Delete account": "Account verwijderen",
|
||||||
|
"Administrator preferences": "",
|
||||||
|
"Default homepage: ": "",
|
||||||
|
"Feed menu: ": "",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "",
|
||||||
|
"Login enabled? ": "",
|
||||||
|
"Registration enabled? ": "",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "Opslaan voorkeuren",
|
"Save preferences": "Opslaan voorkeuren",
|
||||||
"Subscription manager": "Abonnees beheerder",
|
"Subscription manager": "Abonnees beheerder",
|
||||||
"`x` subscriptions": "`x` abonnees",
|
"`x` subscriptions": "`x` abonnees",
|
||||||
|
178
locales/pl.json
178
locales/pl.json
@@ -29,7 +29,7 @@
|
|||||||
"Delete account?": "Usunąć konto?",
|
"Delete account?": "Usunąć konto?",
|
||||||
"History": "Historia",
|
"History": "Historia",
|
||||||
"Previous page": "Poprzednia strona",
|
"Previous page": "Poprzednia strona",
|
||||||
"An alternative front-end to YouTube": "",
|
"An alternative front-end to YouTube": "Alternatywny front-end dla YouTube",
|
||||||
"JavaScript license information": "Informacja o licencji JavaScript",
|
"JavaScript license information": "Informacja o licencji JavaScript",
|
||||||
"source": "źródło",
|
"source": "źródło",
|
||||||
"Login": "Zaloguj",
|
"Login": "Zaloguj",
|
||||||
@@ -80,6 +80,14 @@
|
|||||||
"Manage subscriptions": "Organizuj subskrybcje",
|
"Manage subscriptions": "Organizuj subskrybcje",
|
||||||
"Watch history": "Historia",
|
"Watch history": "Historia",
|
||||||
"Delete account": "Usuń konto",
|
"Delete account": "Usuń konto",
|
||||||
|
"Administrator preferences": "",
|
||||||
|
"Default homepage: ": "",
|
||||||
|
"Feed menu: ": "",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "",
|
||||||
|
"Login enabled? ": "",
|
||||||
|
"Registration enabled? ": "",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "Zapisz preferencje",
|
"Save preferences": "Zapisz preferencje",
|
||||||
"Subscription manager": "Manager subskrybcji",
|
"Subscription manager": "Manager subskrybcji",
|
||||||
"`x` subscriptions": "`x` subskrybcji",
|
"`x` subscriptions": "`x` subskrybcji",
|
||||||
@@ -145,107 +153,107 @@
|
|||||||
"Invalid token": "Niepoprawny token",
|
"Invalid token": "Niepoprawny token",
|
||||||
"Invalid user": "Niepoprawny użytkownik",
|
"Invalid user": "Niepoprawny użytkownik",
|
||||||
"Token is expired, please try again": "Token wygasł, spróbuj ponownie",
|
"Token is expired, please try again": "Token wygasł, spróbuj ponownie",
|
||||||
"English": "",
|
"English": "angielski",
|
||||||
"English (auto-generated)": "",
|
"English (auto-generated)": "angielski (automatycznie generowane)",
|
||||||
"Afrikaans": "",
|
"Afrikaans": "",
|
||||||
"Albanian": "",
|
"Albanian": "albański",
|
||||||
"Amharic": "",
|
"Amharic": "",
|
||||||
"Arabic": "",
|
"Arabic": "arabski",
|
||||||
"Armenian": "",
|
"Armenian": "",
|
||||||
"Azerbaijani": "",
|
"Azerbaijani": "",
|
||||||
"Bangla": "",
|
"Bangla": "",
|
||||||
"Basque": "",
|
"Basque": "",
|
||||||
"Belarusian": "",
|
"Belarusian": "białoruski",
|
||||||
"Bosnian": "",
|
"Bosnian": "bośniacki",
|
||||||
"Bulgarian": "",
|
"Bulgarian": "bułgarski",
|
||||||
"Burmese": "",
|
"Burmese": "birmański",
|
||||||
"Catalan": "",
|
"Catalan": "kataloński",
|
||||||
"Cebuano": "",
|
"Cebuano": "",
|
||||||
"Chinese (Simplified)": "",
|
"Chinese (Simplified)": "chiński (uproszczony)",
|
||||||
"Chinese (Traditional)": "",
|
"Chinese (Traditional)": "chiński (tradycyjny)",
|
||||||
"Corsican": "",
|
"Corsican": "korsykański",
|
||||||
"Croatian": "",
|
"Croatian": "chorwacki",
|
||||||
"Czech": "",
|
"Czech": "czeski",
|
||||||
"Danish": "",
|
"Danish": "duński",
|
||||||
"Dutch": "",
|
"Dutch": "holenderski",
|
||||||
"Esperanto": "",
|
"Esperanto": "esperanto",
|
||||||
"Estonian": "",
|
"Estonian": "estoński",
|
||||||
"Filipino": "",
|
"Filipino": "filipiński",
|
||||||
"Finnish": "",
|
"Finnish": "fiński",
|
||||||
"French": "",
|
"French": "francuski",
|
||||||
"Galician": "",
|
"Galician": "galicyjski",
|
||||||
"Georgian": "",
|
"Georgian": "gruziński",
|
||||||
"German": "",
|
"German": "niemiecki",
|
||||||
"Greek": "",
|
"Greek": "grecki",
|
||||||
"Gujarati": "",
|
"Gujarati": "",
|
||||||
"Haitian Creole": "",
|
"Haitian Creole": "",
|
||||||
"Hausa": "",
|
"Hausa": "",
|
||||||
"Hawaiian": "",
|
"Hawaiian": "hawajski",
|
||||||
"Hebrew": "",
|
"Hebrew": "hebrajski",
|
||||||
"Hindi": "",
|
"Hindi": "hindi",
|
||||||
"Hmong": "",
|
"Hmong": "",
|
||||||
"Hungarian": "",
|
"Hungarian": "węgierski",
|
||||||
"Icelandic": "",
|
"Icelandic": "islandzki",
|
||||||
"Igbo": "",
|
"Igbo": "",
|
||||||
"Indonesian": "",
|
"Indonesian": "indonezyjski",
|
||||||
"Irish": "",
|
"Irish": "irlandzki",
|
||||||
"Italian": "",
|
"Italian": "włoski",
|
||||||
"Japanese": "",
|
"Japanese": "japoński",
|
||||||
"Javanese": "",
|
"Javanese": "jawajski",
|
||||||
"Kannada": "",
|
"Kannada": "",
|
||||||
"Kazakh": "",
|
"Kazakh": "kazachski",
|
||||||
"Khmer": "",
|
"Khmer": "",
|
||||||
"Korean": "",
|
"Korean": "koreański",
|
||||||
"Kurdish": "",
|
"Kurdish": "kurdyjski",
|
||||||
"Kyrgyz": "",
|
"Kyrgyz": "kirgiski",
|
||||||
"Lao": "",
|
"Lao": "",
|
||||||
"Latin": "",
|
"Latin": "łaciński",
|
||||||
"Latvian": "",
|
"Latvian": "łotewski",
|
||||||
"Lithuanian": "",
|
"Lithuanian": "litewski",
|
||||||
"Luxembourgish": "",
|
"Luxembourgish": "luksemburski",
|
||||||
"Macedonian": "",
|
"Macedonian": "macedoński",
|
||||||
"Malagasy": "",
|
"Malagasy": "malgaski",
|
||||||
"Malay": "",
|
"Malay": "malajski",
|
||||||
"Malayalam": "",
|
"Malayalam": "",
|
||||||
"Maltese": "",
|
"Maltese": "maltański",
|
||||||
"Maori": "",
|
"Maori": "",
|
||||||
"Marathi": "",
|
"Marathi": "",
|
||||||
"Mongolian": "",
|
"Mongolian": "mongolski",
|
||||||
"Nepali": "",
|
"Nepali": "nepalski",
|
||||||
"Norwegian": "",
|
"Norwegian": "norweski",
|
||||||
"Nyanja": "",
|
"Nyanja": "",
|
||||||
"Pashto": "",
|
"Pashto": "",
|
||||||
"Persian": "",
|
"Persian": "perski",
|
||||||
"Polish": "",
|
"Polish": "polski",
|
||||||
"Portuguese": "",
|
"Portuguese": "portugalski",
|
||||||
"Punjabi": "",
|
"Punjabi": "",
|
||||||
"Romanian": "",
|
"Romanian": "rumuński",
|
||||||
"Russian": "",
|
"Russian": "rosyjski",
|
||||||
"Samoan": "",
|
"Samoan": "",
|
||||||
"Scottish Gaelic": "",
|
"Scottish Gaelic": "",
|
||||||
"Serbian": "",
|
"Serbian": "serbski",
|
||||||
"Shona": "",
|
"Shona": "",
|
||||||
"Sindhi": "",
|
"Sindhi": "",
|
||||||
"Sinhala": "",
|
"Sinhala": "",
|
||||||
"Slovak": "",
|
"Slovak": "słowacki",
|
||||||
"Slovenian": "",
|
"Slovenian": "słoweński",
|
||||||
"Somali": "",
|
"Somali": "somalijski",
|
||||||
"Southern Sotho": "",
|
"Southern Sotho": "",
|
||||||
"Spanish": "",
|
"Spanish": "hiszpański",
|
||||||
"Spanish (Latin America)": "",
|
"Spanish (Latin America)": "hiszpański (ameryka łacińska)",
|
||||||
"Sundanese": "",
|
"Sundanese": "",
|
||||||
"Swahili": "",
|
"Swahili": "",
|
||||||
"Swedish": "",
|
"Swedish": "szwedzki",
|
||||||
"Tajik": "",
|
"Tajik": "",
|
||||||
"Tamil": "",
|
"Tamil": "",
|
||||||
"Telugu": "",
|
"Telugu": "",
|
||||||
"Thai": "",
|
"Thai": "tajski",
|
||||||
"Turkish": "",
|
"Turkish": "turecki",
|
||||||
"Ukrainian": "",
|
"Ukrainian": "ukraiński",
|
||||||
"Urdu": "",
|
"Urdu": "",
|
||||||
"Uzbek": "",
|
"Uzbek": "uzbecki",
|
||||||
"Vietnamese": "",
|
"Vietnamese": "wietnamski",
|
||||||
"Welsh": "",
|
"Welsh": "walijski",
|
||||||
"Western Frisian": "",
|
"Western Frisian": "",
|
||||||
"Xhosa": "",
|
"Xhosa": "",
|
||||||
"Yiddish": "",
|
"Yiddish": "",
|
||||||
@@ -258,23 +266,23 @@
|
|||||||
"`x` hours": "`x` godzin",
|
"`x` hours": "`x` godzin",
|
||||||
"`x` minutes": "`x` minut",
|
"`x` minutes": "`x` minut",
|
||||||
"`x` seconds": "`x` sekund",
|
"`x` seconds": "`x` sekund",
|
||||||
"Fallback comments: ": "",
|
"Fallback comments: ": "Zastępcze komentarze: ",
|
||||||
"Popular": "",
|
"Popular": "Popularne",
|
||||||
"Top": "",
|
"Top": "Na czasie",
|
||||||
"About": "",
|
"About": "Informacje",
|
||||||
"Rating: ": "",
|
"Rating: ": "Ocena: ",
|
||||||
"Language: ": "",
|
"Language: ": "Język: ",
|
||||||
"Default": "",
|
"Default": "",
|
||||||
"Music": "",
|
"Music": "Muzyka",
|
||||||
"Gaming": "",
|
"Gaming": "Gry",
|
||||||
"News": "",
|
"News": "Wiadomości",
|
||||||
"Movies": "",
|
"Movies": "Filmy",
|
||||||
"Download": "",
|
"Download": "Pobierz",
|
||||||
"Download as: ": "",
|
"Download as: ": "Pobierz jako: ",
|
||||||
"%A %B %-d, %Y": "",
|
"%A %B %-d, %Y": "",
|
||||||
"(edited)": "",
|
"(edited)": "(edytowany)",
|
||||||
"Youtube permalink of the comment": "",
|
"Youtube permalink of the comment": "Odnośnik bezpośredni do komentarza na YouTube",
|
||||||
"`x` marked it with a ❤": "",
|
"`x` marked it with a ❤": "",
|
||||||
"Audio mode": "",
|
"Audio mode": "Tryb audio",
|
||||||
"Video mode": ""
|
"Video mode": "Tryb wideo"
|
||||||
}
|
}
|
||||||
|
206
locales/ru.json
206
locales/ru.json
@@ -82,6 +82,14 @@
|
|||||||
"Manage subscriptions": "Управление подписками",
|
"Manage subscriptions": "Управление подписками",
|
||||||
"Watch history": "История просмотров",
|
"Watch history": "История просмотров",
|
||||||
"Delete account": "Удалить аккаунт",
|
"Delete account": "Удалить аккаунт",
|
||||||
|
"Administrator preferences": "",
|
||||||
|
"Default homepage: ": "",
|
||||||
|
"Feed menu: ": "",
|
||||||
|
"Top enabled? ": "",
|
||||||
|
"CAPTCHA enabled? ": "",
|
||||||
|
"Login enabled? ": "",
|
||||||
|
"Registration enabled? ": "",
|
||||||
|
"Report statistics? ": "",
|
||||||
"Save preferences": "Сохранить настройки",
|
"Save preferences": "Сохранить настройки",
|
||||||
"Subscription manager": "Менеджер подписок",
|
"Subscription manager": "Менеджер подписок",
|
||||||
"`x` subscriptions": "`x` подписок",
|
"`x` subscriptions": "`x` подписок",
|
||||||
@@ -159,103 +167,103 @@
|
|||||||
"Arabic": "Арабский",
|
"Arabic": "Арабский",
|
||||||
"Armenian": "Армянский",
|
"Armenian": "Армянский",
|
||||||
"Azerbaijani": "Азербайджанский",
|
"Azerbaijani": "Азербайджанский",
|
||||||
"Bangla": "",
|
"Bangla": "Бенгальский",
|
||||||
"Basque": "",
|
"Basque": "Баскский",
|
||||||
"Belarusian": "",
|
"Belarusian": "Белорусский",
|
||||||
"Bosnian": "",
|
"Bosnian": "Боснийский",
|
||||||
"Bulgarian": "",
|
"Bulgarian": "Болгарский",
|
||||||
"Burmese": "",
|
"Burmese": "Бирманский",
|
||||||
"Catalan": "",
|
"Catalan": "Каталонский",
|
||||||
"Cebuano": "",
|
"Cebuano": "Себуанский",
|
||||||
"Chinese (Simplified)": "",
|
"Chinese (Simplified)": "Китайский (упрощенный)",
|
||||||
"Chinese (Traditional)": "",
|
"Chinese (Traditional)": "Китайский (традиционный)",
|
||||||
"Corsican": "",
|
"Corsican": "Корсиканский",
|
||||||
"Croatian": "",
|
"Croatian": "Хорватский",
|
||||||
"Czech": "",
|
"Czech": "Чешский",
|
||||||
"Danish": "",
|
"Danish": "Датский",
|
||||||
"Dutch": "",
|
"Dutch": "Нидерландский",
|
||||||
"Esperanto": "",
|
"Esperanto": "Эсперанто",
|
||||||
"Estonian": "",
|
"Estonian": "Эстонский",
|
||||||
"Filipino": "",
|
"Filipino": "Филиппинский",
|
||||||
"Finnish": "",
|
"Finnish": "Финский",
|
||||||
"French": "",
|
"French": "Французский",
|
||||||
"Galician": "",
|
"Galician": "Галисийский",
|
||||||
"Georgian": "",
|
"Georgian": "Грузинский",
|
||||||
"German": "",
|
"German": "Немецкий",
|
||||||
"Greek": "",
|
"Greek": "Греческий",
|
||||||
"Gujarati": "",
|
"Gujarati": "Гуджаратский",
|
||||||
"Haitian Creole": "",
|
"Haitian Creole": "Гаит. креольский",
|
||||||
"Hausa": "",
|
"Hausa": "Хауса",
|
||||||
"Hawaiian": "",
|
"Hawaiian": "Гавайский",
|
||||||
"Hebrew": "",
|
"Hebrew": "Иврит",
|
||||||
"Hindi": "",
|
"Hindi": "Хинди",
|
||||||
"Hmong": "",
|
"Hmong": "Хмонг (мяо)",
|
||||||
"Hungarian": "",
|
"Hungarian": "Венгерский",
|
||||||
"Icelandic": "",
|
"Icelandic": "Исландский",
|
||||||
"Igbo": "",
|
"Igbo": "Игбо",
|
||||||
"Indonesian": "",
|
"Indonesian": "Индонезийский",
|
||||||
"Irish": "",
|
"Irish": "Ирландский",
|
||||||
"Italian": "",
|
"Italian": "Итальянский",
|
||||||
"Japanese": "",
|
"Japanese": "Японский",
|
||||||
"Javanese": "",
|
"Javanese": "Яванский",
|
||||||
"Kannada": "",
|
"Kannada": "Каннада",
|
||||||
"Kazakh": "",
|
"Kazakh": "Казахский",
|
||||||
"Khmer": "",
|
"Khmer": "Кхмерский",
|
||||||
"Korean": "",
|
"Korean": "Корейский",
|
||||||
"Kurdish": "",
|
"Kurdish": "Курдский",
|
||||||
"Kyrgyz": "",
|
"Kyrgyz": "Киргизский",
|
||||||
"Lao": "",
|
"Lao": "Лаосский",
|
||||||
"Latin": "",
|
"Latin": "Латинский",
|
||||||
"Latvian": "",
|
"Latvian": "Латышский",
|
||||||
"Lithuanian": "",
|
"Lithuanian": "Литовский",
|
||||||
"Luxembourgish": "",
|
"Luxembourgish": "Люксембургский",
|
||||||
"Macedonian": "",
|
"Macedonian": "Македонский",
|
||||||
"Malagasy": "",
|
"Malagasy": "Малагасийский",
|
||||||
"Malay": "",
|
"Malay": "Малайский",
|
||||||
"Malayalam": "",
|
"Malayalam": "Малаялам",
|
||||||
"Maltese": "",
|
"Maltese": "Мальтийский",
|
||||||
"Maori": "",
|
"Maori": "Маори",
|
||||||
"Marathi": "",
|
"Marathi": "Маратхи",
|
||||||
"Mongolian": "",
|
"Mongolian": "Монгольская",
|
||||||
"Nepali": "",
|
"Nepali": "Непальский",
|
||||||
"Norwegian": "",
|
"Norwegian": "Норвежский",
|
||||||
"Nyanja": "",
|
"Nyanja": "Ньянджа",
|
||||||
"Pashto": "",
|
"Pashto": "Пушту",
|
||||||
"Persian": "",
|
"Persian": "Персидский",
|
||||||
"Polish": "",
|
"Polish": "Польский",
|
||||||
"Portuguese": "",
|
"Portuguese": "Португальский",
|
||||||
"Punjabi": "",
|
"Punjabi": "Панджаби",
|
||||||
"Romanian": "",
|
"Romanian": "Румынский",
|
||||||
"Russian": "",
|
"Russian": "Русский",
|
||||||
"Samoan": "",
|
"Samoan": "Самоанский",
|
||||||
"Scottish Gaelic": "",
|
"Scottish Gaelic": "Шотландский (гэльский)",
|
||||||
"Serbian": "",
|
"Serbian": "Сербский",
|
||||||
"Shona": "",
|
"Shona": "Шона",
|
||||||
"Sindhi": "",
|
"Sindhi": "Синдхи",
|
||||||
"Sinhala": "",
|
"Sinhala": "Сингальский",
|
||||||
"Slovak": "",
|
"Slovak": "Словацкий",
|
||||||
"Slovenian": "",
|
"Slovenian": "Словенский",
|
||||||
"Somali": "",
|
"Somali": "Сомалийский",
|
||||||
"Southern Sotho": "",
|
"Southern Sotho": "Сесото (южный сото)",
|
||||||
"Spanish": "",
|
"Spanish": "Испанский",
|
||||||
"Spanish (Latin America)": "",
|
"Spanish (Latin America)": "Испанский (Латинская Америка)",
|
||||||
"Sundanese": "",
|
"Sundanese": "Сунданский",
|
||||||
"Swahili": "",
|
"Swahili": "Суахили",
|
||||||
"Swedish": "",
|
"Swedish": "Шведский",
|
||||||
"Tajik": "",
|
"Tajik": "Таджикский",
|
||||||
"Tamil": "",
|
"Tamil": "Тамильский",
|
||||||
"Telugu": "",
|
"Telugu": "Телугу",
|
||||||
"Thai": "",
|
"Thai": "Тайский",
|
||||||
"Turkish": "",
|
"Turkish": "Турецкий",
|
||||||
"Ukrainian": "",
|
"Ukrainian": "Украинский",
|
||||||
"Urdu": "",
|
"Urdu": "Урду",
|
||||||
"Uzbek": "",
|
"Uzbek": "Узбекский",
|
||||||
"Vietnamese": "",
|
"Vietnamese": "Вьетнамский",
|
||||||
"Welsh": "",
|
"Welsh": "Валлийский",
|
||||||
"Western Frisian": "",
|
"Western Frisian": "Западнофризский",
|
||||||
"Xhosa": "",
|
"Xhosa": "Коса",
|
||||||
"Yiddish": "",
|
"Yiddish": "Идиш",
|
||||||
"Yoruba": "",
|
"Yoruba": "Йоруба",
|
||||||
"Zulu": "Зулусский",
|
"Zulu": "Зулусский",
|
||||||
"`x` years": "`x` лет",
|
"`x` years": "`x` лет",
|
||||||
"`x` months": "`x` месяцев",
|
"`x` months": "`x` месяцев",
|
||||||
@@ -281,6 +289,6 @@
|
|||||||
"(edited)": "(изменено)",
|
"(edited)": "(изменено)",
|
||||||
"Youtube permalink of the comment": "Прямая ссылка на YouTube",
|
"Youtube permalink of the comment": "Прямая ссылка на YouTube",
|
||||||
"`x` marked it with a ❤": "❤ от автора канала \"`x`\"",
|
"`x` marked it with a ❤": "❤ от автора канала \"`x`\"",
|
||||||
"Audio mode": "",
|
"Audio mode": "Аудио режим",
|
||||||
"Video mode": ""
|
"Video mode": "Видео режим"
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
name: invidious
|
name: invidious
|
||||||
version: 0.14.0
|
version: 0.14.1
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Omar Roth <omarroth@hotmail.com>
|
- Omar Roth <omarroth@hotmail.com>
|
||||||
@@ -9,16 +9,13 @@ targets:
|
|||||||
main: src/invidious.cr
|
main: src/invidious.cr
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
detect_language:
|
|
||||||
github: detectlanguage/detectlanguage-crystal
|
|
||||||
kemal:
|
kemal:
|
||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
commit: afd17fc
|
|
||||||
pg:
|
pg:
|
||||||
github: will/crystal-pg
|
github: will/crystal-pg
|
||||||
sqlite3:
|
sqlite3:
|
||||||
github: crystal-lang/crystal-sqlite3
|
github: crystal-lang/crystal-sqlite3
|
||||||
|
|
||||||
crystal: 0.27.1
|
crystal: 0.27.2
|
||||||
|
|
||||||
license: AGPLv3
|
license: AGPLv3
|
||||||
|
393
src/invidious.cr
393
src/invidious.cr
@@ -14,7 +14,6 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
require "detect_language"
|
|
||||||
require "digest/md5"
|
require "digest/md5"
|
||||||
require "file_utils"
|
require "file_utils"
|
||||||
require "kemal"
|
require "kemal"
|
||||||
@@ -29,44 +28,40 @@ require "./invidious/helpers/*"
|
|||||||
require "./invidious/*"
|
require "./invidious/*"
|
||||||
|
|
||||||
CONFIG = Config.from_yaml(File.read("config/config.yml"))
|
CONFIG = Config.from_yaml(File.read("config/config.yml"))
|
||||||
HMAC_KEY = CONFIG.hmac_key || Random::Secure.random_bytes(32)
|
HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32)
|
||||||
|
|
||||||
crawl_threads = CONFIG.crawl_threads
|
|
||||||
channel_threads = CONFIG.channel_threads
|
|
||||||
feed_threads = CONFIG.feed_threads
|
|
||||||
video_threads = CONFIG.video_threads
|
|
||||||
|
|
||||||
|
config = CONFIG
|
||||||
logger = Invidious::LogHandler.new
|
logger = Invidious::LogHandler.new
|
||||||
|
|
||||||
Kemal.config.extra_options do |parser|
|
Kemal.config.extra_options do |parser|
|
||||||
parser.banner = "Usage: invidious [arguments]"
|
parser.banner = "Usage: invidious [arguments]"
|
||||||
parser.on("-t THREADS", "--crawl-threads=THREADS", "Number of threads for crawling YouTube (default: #{crawl_threads})") do |number|
|
parser.on("-t THREADS", "--crawl-threads=THREADS", "Number of threads for crawling YouTube (default: #{config.crawl_threads})") do |number|
|
||||||
begin
|
begin
|
||||||
crawl_threads = number.to_i
|
config.crawl_threads = number.to_i
|
||||||
rescue ex
|
rescue ex
|
||||||
puts "THREADS must be integer"
|
puts "THREADS must be integer"
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
parser.on("-c THREADS", "--channel-threads=THREADS", "Number of threads for refreshing channels (default: #{channel_threads})") do |number|
|
parser.on("-c THREADS", "--channel-threads=THREADS", "Number of threads for refreshing channels (default: #{config.channel_threads})") do |number|
|
||||||
begin
|
begin
|
||||||
channel_threads = number.to_i
|
config.channel_threads = number.to_i
|
||||||
rescue ex
|
rescue ex
|
||||||
puts "THREADS must be integer"
|
puts "THREADS must be integer"
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
parser.on("-f THREADS", "--feed-threads=THREADS", "Number of threads for refreshing feeds (default: #{feed_threads})") do |number|
|
parser.on("-f THREADS", "--feed-threads=THREADS", "Number of threads for refreshing feeds (default: #{config.feed_threads})") do |number|
|
||||||
begin
|
begin
|
||||||
feed_threads = number.to_i
|
config.feed_threads = number.to_i
|
||||||
rescue ex
|
rescue ex
|
||||||
puts "THREADS must be integer"
|
puts "THREADS must be integer"
|
||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
parser.on("-v THREADS", "--video-threads=THREADS", "Number of threads for refreshing videos (default: #{video_threads})") do |number|
|
parser.on("-v THREADS", "--video-threads=THREADS", "Number of threads for refreshing videos (default: #{config.video_threads})") do |number|
|
||||||
begin
|
begin
|
||||||
video_threads = number.to_i
|
config.video_threads = number.to_i
|
||||||
rescue ex
|
rescue ex
|
||||||
puts "THREADS must be integer"
|
puts "THREADS must be integer"
|
||||||
exit
|
exit
|
||||||
@@ -78,7 +73,7 @@ Kemal.config.extra_options do |parser|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Kemal::CLI.new
|
Kemal::CLI.new ARGV
|
||||||
|
|
||||||
PG_URL = URI.new(
|
PG_URL = URI.new(
|
||||||
scheme: "postgres",
|
scheme: "postgres",
|
||||||
@@ -93,12 +88,17 @@ PG_DB = DB.open PG_URL
|
|||||||
YT_URL = URI.parse("https://www.youtube.com")
|
YT_URL = URI.parse("https://www.youtube.com")
|
||||||
REDDIT_URL = URI.parse("https://www.reddit.com")
|
REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||||
LOGIN_URL = URI.parse("https://accounts.google.com")
|
LOGIN_URL = URI.parse("https://accounts.google.com")
|
||||||
|
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
||||||
TEXTCAPTCHA_URL = URI.parse("http://textcaptcha.com/omarroth@hotmail.com.json")
|
TEXTCAPTCHA_URL = URI.parse("http://textcaptcha.com/omarroth@hotmail.com.json")
|
||||||
|
CURRENT_BRANCH = `git branch | sed -n '/\* /s///p'`.strip
|
||||||
|
CURRENT_COMMIT = `git rev-list HEAD --max-count=1 --abbrev-commit`.strip
|
||||||
|
CURRENT_VERSION = `git describe --tags --abbrev=0`.strip
|
||||||
|
|
||||||
LOCALES = {
|
LOCALES = {
|
||||||
"ar" => load_locale("ar"),
|
"ar" => load_locale("ar"),
|
||||||
"de" => load_locale("de"),
|
"de" => load_locale("de"),
|
||||||
"en-US" => load_locale("en-US"),
|
"en-US" => load_locale("en-US"),
|
||||||
|
"eu" => load_locale("eu"),
|
||||||
"fr" => load_locale("fr"),
|
"fr" => load_locale("fr"),
|
||||||
"it" => load_locale("it"),
|
"it" => load_locale("it"),
|
||||||
"nb_NO" => load_locale("nb_NO"),
|
"nb_NO" => load_locale("nb_NO"),
|
||||||
@@ -107,29 +107,66 @@ LOCALES = {
|
|||||||
"ru" => load_locale("ru"),
|
"ru" => load_locale("ru"),
|
||||||
}
|
}
|
||||||
|
|
||||||
crawl_threads.times do
|
config.crawl_threads.times do
|
||||||
spawn do
|
spawn do
|
||||||
crawl_videos(PG_DB, logger)
|
crawl_videos(PG_DB, logger)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
refresh_channels(PG_DB, logger, channel_threads, CONFIG.full_refresh)
|
refresh_channels(PG_DB, logger, config.channel_threads, config.full_refresh)
|
||||||
|
|
||||||
refresh_feeds(PG_DB, logger, feed_threads)
|
refresh_feeds(PG_DB, logger, config.feed_threads)
|
||||||
|
|
||||||
video_threads.times do |i|
|
subscribe_to_feeds(PG_DB, logger, HMAC_KEY, config)
|
||||||
|
|
||||||
|
config.video_threads.times do |i|
|
||||||
spawn do
|
spawn do
|
||||||
refresh_videos(PG_DB, logger)
|
refresh_videos(PG_DB, logger)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
statistics = {
|
||||||
|
"error" => "Statistics are not availabile.",
|
||||||
|
}
|
||||||
|
if config.statistics_enabled
|
||||||
|
spawn do
|
||||||
|
loop do
|
||||||
|
statistics = {
|
||||||
|
"version" => "2.0",
|
||||||
|
"software" => {
|
||||||
|
"name" => "invidious",
|
||||||
|
"version" => "#{CURRENT_VERSION}-#{CURRENT_COMMIT}",
|
||||||
|
"branch" => "#{CURRENT_BRANCH}",
|
||||||
|
},
|
||||||
|
"openRegistrations" => config.registration_enabled,
|
||||||
|
"usage" => {
|
||||||
|
"users" => {
|
||||||
|
"total" => PG_DB.query_one("SELECT count(*) FROM users", as: Int64),
|
||||||
|
"activeHalfyear" => PG_DB.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '6 months'", as: Int64),
|
||||||
|
"activeMonth" => PG_DB.query_one("SELECT count(*) FROM users WHERE CURRENT_TIMESTAMP - updated < '1 month'", as: Int64),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"metadata" => {
|
||||||
|
"updatedAt" => Time.now.to_unix,
|
||||||
|
"lastChannelRefreshedAt" => PG_DB.query_one?("SELECT updated FROM channels ORDER BY updated DESC LIMIT 1", as: Time).try &.to_unix || 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep 1.minute
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
top_videos = [] of Video
|
top_videos = [] of Video
|
||||||
spawn do
|
if config.top_enabled
|
||||||
pull_top_videos(CONFIG, PG_DB) do |videos|
|
spawn do
|
||||||
|
pull_top_videos(config, PG_DB) do |videos|
|
||||||
top_videos = videos
|
top_videos = videos
|
||||||
sleep 1.minutes
|
sleep 1.minutes
|
||||||
Fiber.yield
|
Fiber.yield
|
||||||
end
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
popular_videos = [] of ChannelVideo
|
popular_videos = [] of ChannelVideo
|
||||||
@@ -231,7 +268,20 @@ get "/" do |env|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
templated "index"
|
case config.default_home
|
||||||
|
when "Popular"
|
||||||
|
templated "popular"
|
||||||
|
when "Top"
|
||||||
|
templated "top"
|
||||||
|
when "Trending"
|
||||||
|
env.redirect "/feed/trending"
|
||||||
|
when "Subscriptions"
|
||||||
|
if user
|
||||||
|
env.redirect "/feed/subscriptions"
|
||||||
|
else
|
||||||
|
templated "popular"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/licenses" do |env|
|
get "/licenses" do |env|
|
||||||
@@ -367,7 +417,7 @@ get "/watch" do |env|
|
|||||||
video.description = replace_links(video.description)
|
video.description = replace_links(video.description)
|
||||||
description = video.short_description
|
description = video.short_description
|
||||||
|
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
host_params = env.request.query_params
|
host_params = env.request.query_params
|
||||||
host_params.delete_all("v")
|
host_params.delete_all("v")
|
||||||
|
|
||||||
@@ -467,7 +517,7 @@ get "/embed/:id" do |env|
|
|||||||
video.description = replace_links(video.description)
|
video.description = replace_links(video.description)
|
||||||
description = video.short_description
|
description = video.short_description
|
||||||
|
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
host_params = env.request.query_params
|
host_params = env.request.query_params
|
||||||
host_params.delete_all("v")
|
host_params.delete_all("v")
|
||||||
|
|
||||||
@@ -553,7 +603,7 @@ get "/opensearch.xml" do |env|
|
|||||||
locale = LOCALES[env.get("locale").as(String)]?
|
locale = LOCALES[env.get("locale").as(String)]?
|
||||||
env.response.content_type = "application/opensearchdescription+xml"
|
env.response.content_type = "application/opensearchdescription+xml"
|
||||||
|
|
||||||
host = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host = make_host_url(config, Kemal.config)
|
||||||
|
|
||||||
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||||
xml.element("OpenSearchDescription", xmlns: "http://a9.com/-/spec/opensearch/1.1/") do
|
xml.element("OpenSearchDescription", xmlns: "http://a9.com/-/spec/opensearch/1.1/") do
|
||||||
@@ -678,6 +728,11 @@ get "/login" do |env|
|
|||||||
next env.redirect "/feed/subscriptions"
|
next env.redirect "/feed/subscriptions"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if !config.login_enabled
|
||||||
|
error_message = "Login has been disabled by administrator."
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
referer = get_referer(env, "/feed/subscriptions")
|
referer = get_referer(env, "/feed/subscriptions")
|
||||||
|
|
||||||
account_type = env.params.query["type"]?
|
account_type = env.params.query["type"]?
|
||||||
@@ -716,6 +771,11 @@ post "/login" do |env|
|
|||||||
|
|
||||||
referer = get_referer(env, "/feed/subscriptions")
|
referer = get_referer(env, "/feed/subscriptions")
|
||||||
|
|
||||||
|
if !config.login_enabled
|
||||||
|
error_message = "Login has been disabled by administrator."
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
email = env.params.body["email"]?
|
email = env.params.body["email"]?
|
||||||
password = env.params.body["password"]?
|
password = env.params.body["password"]?
|
||||||
|
|
||||||
@@ -876,14 +936,14 @@ post "/login" do |env|
|
|||||||
|
|
||||||
host = URI.parse(env.request.headers["Host"]).host
|
host = URI.parse(env.request.headers["Host"]).host
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
if Kemal.config.ssl || config.https_only
|
||||||
secure = true
|
secure = true
|
||||||
else
|
else
|
||||||
secure = false
|
secure = false
|
||||||
end
|
end
|
||||||
|
|
||||||
login.cookies.each do |cookie|
|
login.cookies.each do |cookie|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
if Kemal.config.ssl || config.https_only
|
||||||
cookie.secure = secure
|
cookie.secure = secure
|
||||||
else
|
else
|
||||||
cookie.secure = secure
|
cookie.secure = secure
|
||||||
@@ -912,6 +972,7 @@ post "/login" do |env|
|
|||||||
answer = env.params.body["answer"]?
|
answer = env.params.body["answer"]?
|
||||||
text_answer = env.params.body["text_answer"]?
|
text_answer = env.params.body["text_answer"]?
|
||||||
|
|
||||||
|
if config.captcha_enabled
|
||||||
if answer
|
if answer
|
||||||
answer = answer.lstrip('0')
|
answer = answer.lstrip('0')
|
||||||
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
||||||
@@ -961,6 +1022,7 @@ post "/login" do |env|
|
|||||||
error_message = translate(locale, "CAPTCHA is a required field")
|
error_message = translate(locale, "CAPTCHA is a required field")
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
action = env.params.body["action"]?
|
action = env.params.body["action"]?
|
||||||
action ||= "signin"
|
action ||= "signin"
|
||||||
@@ -992,14 +1054,14 @@ post "/login" do |env|
|
|||||||
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||||
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.now)
|
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.now)
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
if Kemal.config.ssl || config.https_only
|
||||||
secure = true
|
secure = true
|
||||||
else
|
else
|
||||||
secure = false
|
secure = false
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.domain
|
if config.domain
|
||||||
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{CONFIG.domain}", value: sid, expires: Time.now + 2.years,
|
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{config.domain}", value: sid, expires: Time.now + 2.years,
|
||||||
secure: secure, http_only: true)
|
secure: secure, http_only: true)
|
||||||
else
|
else
|
||||||
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years,
|
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years,
|
||||||
@@ -1016,6 +1078,11 @@ post "/login" do |env|
|
|||||||
secure: secure, http_only: true)
|
secure: secure, http_only: true)
|
||||||
end
|
end
|
||||||
elsif action == "register"
|
elsif action == "register"
|
||||||
|
if !config.registration_enabled
|
||||||
|
error_message = "Registration has been disabled by administrator."
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
if password.empty?
|
if password.empty?
|
||||||
error_message = translate(locale, "Password cannot be empty")
|
error_message = translate(locale, "Password cannot be empty")
|
||||||
next templated "error"
|
next templated "error"
|
||||||
@@ -1049,14 +1116,14 @@ post "/login" do |env|
|
|||||||
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
|
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
|
||||||
ORDER BY published DESC;")
|
ORDER BY published DESC;")
|
||||||
|
|
||||||
if Kemal.config.ssl || CONFIG.https_only
|
if Kemal.config.ssl || config.https_only
|
||||||
secure = true
|
secure = true
|
||||||
else
|
else
|
||||||
secure = false
|
secure = false
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.domain
|
if config.domain
|
||||||
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: ".#{CONFIG.domain}", value: sid, expires: Time.now + 2.years,
|
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", domain: "#{config.domain}", value: sid, expires: Time.now + 2.years,
|
||||||
secure: secure, http_only: true)
|
secure: secure, http_only: true)
|
||||||
else
|
else
|
||||||
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years,
|
env.response.cookies["SID"] = HTTP::Cookie.new(name: "SID", value: sid, expires: Time.now + 2.years,
|
||||||
@@ -1153,14 +1220,15 @@ post "/preferences" do |env|
|
|||||||
volume = env.params.body["volume"]?.try &.as(String).to_i?
|
volume = env.params.body["volume"]?.try &.as(String).to_i?
|
||||||
volume ||= DEFAULT_USER_PREFERENCES.volume
|
volume ||= DEFAULT_USER_PREFERENCES.volume
|
||||||
|
|
||||||
comments_0 = env.params.body["comments_0"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[0]
|
comments = [] of String
|
||||||
comments_1 = env.params.body["comments_1"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[1]
|
2.times do |i|
|
||||||
comments = [comments_0, comments_1]
|
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[i])
|
||||||
|
end
|
||||||
|
|
||||||
captions_0 = env.params.body["captions_0"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[0]
|
captions = [] of String
|
||||||
captions_1 = env.params.body["captions_1"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[1]
|
3.times do |i|
|
||||||
captions_2 = env.params.body["captions_2"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[2]
|
captions << (env.params.body["captions[#{i}]"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[i])
|
||||||
captions = [captions_0, captions_1, captions_2]
|
end
|
||||||
|
|
||||||
related_videos = env.params.body["related_videos"]?.try &.as(String)
|
related_videos = env.params.body["related_videos"]?.try &.as(String)
|
||||||
related_videos ||= "off"
|
related_videos ||= "off"
|
||||||
@@ -1224,6 +1292,41 @@ post "/preferences" do |env|
|
|||||||
if user = env.get? "user"
|
if user = env.get? "user"
|
||||||
user = user.as(User)
|
user = user.as(User)
|
||||||
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email)
|
PG_DB.exec("UPDATE users SET preferences = $1 WHERE email = $2", preferences, user.email)
|
||||||
|
|
||||||
|
if config.admins.includes? user.email
|
||||||
|
config.default_home = env.params.body["default_home"]?.try &.as(String) || config.default_home
|
||||||
|
|
||||||
|
feed_menu = [] of String
|
||||||
|
4.times do |index|
|
||||||
|
option = env.params.body["feed_menu[#{index}]"]?.try &.as(String) || ""
|
||||||
|
if !option.empty?
|
||||||
|
feed_menu << option
|
||||||
|
end
|
||||||
|
end
|
||||||
|
config.feed_menu = feed_menu
|
||||||
|
|
||||||
|
top_enabled = env.params.body["top_enabled"]?.try &.as(String)
|
||||||
|
top_enabled ||= "off"
|
||||||
|
config.top_enabled = top_enabled == "on"
|
||||||
|
|
||||||
|
captcha_enabled = env.params.body["captcha_enabled"]?.try &.as(String)
|
||||||
|
captcha_enabled ||= "off"
|
||||||
|
config.captcha_enabled = captcha_enabled == "on"
|
||||||
|
|
||||||
|
login_enabled = env.params.body["login_enabled"]?.try &.as(String)
|
||||||
|
login_enabled ||= "off"
|
||||||
|
config.login_enabled = login_enabled == "on"
|
||||||
|
|
||||||
|
registration_enabled = env.params.body["registration_enabled"]?.try &.as(String)
|
||||||
|
registration_enabled ||= "off"
|
||||||
|
config.registration_enabled = registration_enabled == "on"
|
||||||
|
|
||||||
|
statistics_enabled = env.params.body["statistics_enabled"]?.try &.as(String)
|
||||||
|
statistics_enabled ||= "off"
|
||||||
|
config.statistics_enabled = statistics_enabled == "on"
|
||||||
|
|
||||||
|
File.write("config/config.yml", config.to_yaml)
|
||||||
|
end
|
||||||
else
|
else
|
||||||
env.response.cookies["PREFS"] = preferences
|
env.response.cookies["PREFS"] = preferences
|
||||||
end
|
end
|
||||||
@@ -1397,7 +1500,7 @@ get "/subscription_manager" do |env|
|
|||||||
subscriptions.sort_by! { |channel| channel.author.downcase }
|
subscriptions.sort_by! { |channel| channel.author.downcase }
|
||||||
|
|
||||||
if action_takeout
|
if action_takeout
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
|
|
||||||
if format == "json"
|
if format == "json"
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
@@ -1741,7 +1844,11 @@ end
|
|||||||
get "/feed/top" do |env|
|
get "/feed/top" do |env|
|
||||||
locale = LOCALES[env.get("locale").as(String)]?
|
locale = LOCALES[env.get("locale").as(String)]?
|
||||||
|
|
||||||
|
if config.top_enabled
|
||||||
templated "top"
|
templated "top"
|
||||||
|
else
|
||||||
|
env.redirect "/"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/feed/popular" do |env|
|
get "/feed/popular" do |env|
|
||||||
@@ -1845,14 +1952,19 @@ get "/feed/subscriptions" do |env|
|
|||||||
# Show latest video from a channel that a user hasn't watched
|
# Show latest video from a channel that a user hasn't watched
|
||||||
# "unseen_only" isn't really correct here, more accurate would be "unwatched_only"
|
# "unseen_only" isn't really correct here, more accurate would be "unwatched_only"
|
||||||
|
|
||||||
|
if user.watched.empty?
|
||||||
|
values = "'{}'"
|
||||||
|
else
|
||||||
|
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
|
||||||
|
end
|
||||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE \
|
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE \
|
||||||
NOT id = ANY (VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}) \
|
NOT id = ANY (#{values}) \
|
||||||
ORDER BY ucid, published #{sort}", as: ChannelVideo)
|
ORDER BY ucid, published #{sort}", as: ChannelVideo)
|
||||||
else
|
else
|
||||||
# Show latest video from each channel
|
# Show latest video from each channel
|
||||||
|
|
||||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} \
|
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} \
|
||||||
ORDER BY ucid, published", as: ChannelVideo)
|
ORDER BY ucid, published DESC", as: ChannelVideo)
|
||||||
end
|
end
|
||||||
|
|
||||||
videos.sort_by! { |video| video.published }.reverse!
|
videos.sort_by! { |video| video.published }.reverse!
|
||||||
@@ -1860,8 +1972,13 @@ get "/feed/subscriptions" do |env|
|
|||||||
if preferences.unseen_only
|
if preferences.unseen_only
|
||||||
# Only show unwatched
|
# Only show unwatched
|
||||||
|
|
||||||
|
if user.watched.empty?
|
||||||
|
values = "'{}'"
|
||||||
|
else
|
||||||
|
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
|
||||||
|
end
|
||||||
videos = PG_DB.query_all("SELECT * FROM #{view_name} WHERE \
|
videos = PG_DB.query_all("SELECT * FROM #{view_name} WHERE \
|
||||||
NOT id = ANY (VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}) \
|
NOT id = ANY (#{values}) \
|
||||||
ORDER BY published #{sort} LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
|
ORDER BY published #{sort} LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
|
||||||
else
|
else
|
||||||
# Sort subscriptions as normal
|
# Sort subscriptions as normal
|
||||||
@@ -1940,7 +2057,8 @@ end
|
|||||||
get "/feed/channel/:ucid" do |env|
|
get "/feed/channel/:ucid" do |env|
|
||||||
locale = LOCALES[env.get("locale").as(String)]?
|
locale = LOCALES[env.get("locale").as(String)]?
|
||||||
|
|
||||||
env.response.content_type = "text/xml"
|
env.response.content_type = "application/atom+xml"
|
||||||
|
|
||||||
ucid = env.params.url["ucid"]
|
ucid = env.params.url["ucid"]
|
||||||
|
|
||||||
begin
|
begin
|
||||||
@@ -1984,7 +2102,7 @@ get "/feed/channel/:ucid" do |env|
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
path = env.request.path
|
path = env.request.path
|
||||||
|
|
||||||
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||||
@@ -2020,11 +2138,19 @@ get "/feed/channel/:ucid" do |env|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
xml.element("content", type: "xhtml") do
|
||||||
|
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
||||||
|
xml.element("a", href: "#{host_url}/watch?v=#{video.id}") do
|
||||||
|
xml.element("img", src: "#{host_url}/vi/#{video.id}/mqdefault.jpg")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
xml.element("published") { xml.text video.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
xml.element("published") { xml.text video.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
||||||
|
|
||||||
xml.element("media:group") do
|
xml.element("media:group") do
|
||||||
xml.element("media:title") { xml.text video.title }
|
xml.element("media:title") { xml.text video.title }
|
||||||
xml.element("media:thumbnail", url: "/vi/#{video.id}/mqdefault.jpg",
|
xml.element("media:thumbnail", url: "#{host_url}/vi/#{video.id}/mqdefault.jpg",
|
||||||
width: "320", height: "180")
|
width: "320", height: "180")
|
||||||
xml.element("media:description") { xml.text video.description }
|
xml.element("media:description") { xml.text video.description }
|
||||||
end
|
end
|
||||||
@@ -2043,6 +2169,8 @@ end
|
|||||||
get "/feed/private" do |env|
|
get "/feed/private" do |env|
|
||||||
locale = LOCALES[env.get("locale").as(String)]?
|
locale = LOCALES[env.get("locale").as(String)]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/atom+xml"
|
||||||
|
|
||||||
token = env.params.query["token"]?
|
token = env.params.query["token"]?
|
||||||
|
|
||||||
if !token
|
if !token
|
||||||
@@ -2085,7 +2213,7 @@ get "/feed/private" do |env|
|
|||||||
|
|
||||||
if latest_only
|
if latest_only
|
||||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} \
|
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} \
|
||||||
ORDER BY ucid, published", as: ChannelVideo)
|
ORDER BY ucid, published DESC", as: ChannelVideo)
|
||||||
|
|
||||||
videos.sort_by! { |video| video.published }.reverse!
|
videos.sort_by! { |video| video.published }.reverse!
|
||||||
else
|
else
|
||||||
@@ -2110,7 +2238,7 @@ get "/feed/private" do |env|
|
|||||||
videos = videos[0..max_results]
|
videos = videos[0..max_results]
|
||||||
end
|
end
|
||||||
|
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
path = env.request.path
|
path = env.request.path
|
||||||
query = env.request.query.not_nil!
|
query = env.request.query.not_nil!
|
||||||
|
|
||||||
@@ -2135,12 +2263,20 @@ get "/feed/private" do |env|
|
|||||||
xml.element("uri") { xml.text "#{host_url}/channel/#{video.ucid}" }
|
xml.element("uri") { xml.text "#{host_url}/channel/#{video.ucid}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
xml.element("content", type: "xhtml") do
|
||||||
|
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
||||||
|
xml.element("a", href: "#{host_url}/watch?v=#{video.id}") do
|
||||||
|
xml.element("img", src: "#{host_url}/vi/#{video.id}/mqdefault.jpg")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
xml.element("published") { xml.text video.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
xml.element("published") { xml.text video.published.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
||||||
xml.element("updated") { xml.text video.updated.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
xml.element("updated") { xml.text video.updated.to_s("%Y-%m-%dT%H:%M:%S%:z") }
|
||||||
|
|
||||||
xml.element("media:group") do
|
xml.element("media:group") do
|
||||||
xml.element("media:title") { xml.text video.title }
|
xml.element("media:title") { xml.text video.title }
|
||||||
xml.element("media:thumbnail", url: "/vi/#{video.id}/mqdefault.jpg",
|
xml.element("media:thumbnail", url: "#{host_url}/vi/#{video.id}/mqdefault.jpg",
|
||||||
width: "320", height: "180")
|
width: "320", height: "180")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -2148,16 +2284,17 @@ get "/feed/private" do |env|
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
env.response.content_type = "application/atom+xml"
|
|
||||||
feed
|
feed
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/feed/playlist/:plid" do |env|
|
get "/feed/playlist/:plid" do |env|
|
||||||
locale = LOCALES[env.get("locale").as(String)]?
|
locale = LOCALES[env.get("locale").as(String)]?
|
||||||
|
|
||||||
|
env.response.content_type = "application/atom+xml"
|
||||||
|
|
||||||
plid = env.params.url["plid"]
|
plid = env.params.url["plid"]
|
||||||
|
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
path = env.request.path
|
path = env.request.path
|
||||||
|
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
@@ -2182,10 +2319,75 @@ get "/feed/playlist/:plid" do |env|
|
|||||||
document = document.gsub(match[0], "<uri>#{content}</uri>")
|
document = document.gsub(match[0], "<uri>#{content}</uri>")
|
||||||
end
|
end
|
||||||
|
|
||||||
env.response.content_type = "text/xml"
|
|
||||||
document
|
document
|
||||||
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 verify_token.starts_with? "v1"
|
||||||
|
_, time, nonce, signature = verify_token.split(":")
|
||||||
|
data = "#{time}:#{nonce}"
|
||||||
|
else
|
||||||
|
time, signature = verify_token.split(":")
|
||||||
|
data = "#{time}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if Time.now.to_unix - time.to_i > 600
|
||||||
|
halt env, status_code: 400
|
||||||
|
end
|
||||||
|
|
||||||
|
if OpenSSL::HMAC.hexdigest(:sha1, HMAC_KEY, data) != signature
|
||||||
|
halt env, status_code: 400
|
||||||
|
end
|
||||||
|
|
||||||
|
ucid = HTTP::Params.parse(URI.parse(topic).query.not_nil!)["channel_id"]
|
||||||
|
PG_DB.exec("UPDATE channels SET subscribed = $1 WHERE id = $2", Time.now, ucid)
|
||||||
|
|
||||||
|
halt env, status_code: 200, response: challenge
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/feed/webhook/:token" do |env|
|
||||||
|
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.write("#{token} : Invalid signature")
|
||||||
|
halt 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
|
||||||
|
published = Time.parse(entry.xpath_node("published").not_nil!.content, "%FT%X%z", Time::Location.local)
|
||||||
|
updated = Time.parse(entry.xpath_node("updated").not_nil!.content, "%FT%X%z", Time::Location.local)
|
||||||
|
|
||||||
|
video = get_video(id, PG_DB, proxies, region: nil)
|
||||||
|
video = ChannelVideo.new(id, video.title, published, updated, video.ucid, video.author, video.length_seconds)
|
||||||
|
|
||||||
|
PG_DB.exec("UPDATE users SET notifications = notifications || $1 \
|
||||||
|
WHERE updated < $2 AND $3 = ANY(subscriptions) AND $1 <> ALL(notifications)", video.id, video.published, video.ucid)
|
||||||
|
|
||||||
|
video_array = video.to_a
|
||||||
|
args = arg_array(video_array)
|
||||||
|
|
||||||
|
PG_DB.exec("INSERT INTO channel_videos VALUES (#{args}) \
|
||||||
|
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
|
||||||
|
updated = $4, ucid = $5, author = $6, length_seconds = $7", video_array)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
halt env, status_code: 200
|
||||||
|
end
|
||||||
|
|
||||||
# Channels
|
# Channels
|
||||||
|
|
||||||
# YouTube appears to let users set a "brand" URL that
|
# YouTube appears to let users set a "brand" URL that
|
||||||
@@ -2245,7 +2447,6 @@ get "/channel/:ucid" do |env|
|
|||||||
continuation = env.params.query["continuation"]?
|
continuation = env.params.query["continuation"]?
|
||||||
|
|
||||||
sort_by = env.params.query["sort_by"]?.try &.downcase
|
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||||
sort_by ||= "newest"
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
author, ucid, auto_generated, sub_count = get_about_info(ucid, locale)
|
author, ucid, auto_generated, sub_count = get_about_info(ucid, locale)
|
||||||
@@ -2263,10 +2464,17 @@ get "/channel/:ucid" do |env|
|
|||||||
end
|
end
|
||||||
|
|
||||||
if auto_generated
|
if auto_generated
|
||||||
|
sort_options = {"last", "oldest", "newest"}
|
||||||
|
sort_by ||= "last"
|
||||||
|
|
||||||
items, continuation = fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
|
items, continuation = fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
|
||||||
items.select! { |item| item.is_a?(SearchPlaylist) && !item.videos.empty? }
|
items.select! { |item| item.is_a?(SearchPlaylist) && !item.videos.empty? }
|
||||||
items = items.map { |item| item.as(SearchPlaylist) }
|
items = items.map { |item| item.as(SearchPlaylist) }
|
||||||
|
items.each { |item| item.author = "" }
|
||||||
else
|
else
|
||||||
|
sort_options = {"newest", "oldest", "popular"}
|
||||||
|
sort_by ||= "newest"
|
||||||
|
|
||||||
items, count = get_60_videos(ucid, page, auto_generated, sort_by)
|
items, count = get_60_videos(ucid, page, auto_generated, sort_by)
|
||||||
items.select! { |item| !item.paid }
|
items.select! { |item| !item.paid }
|
||||||
end
|
end
|
||||||
@@ -2289,8 +2497,63 @@ get "/channel/:ucid/videos" do |env|
|
|||||||
env.redirect "/channel/#{ucid}#{params}"
|
env.redirect "/channel/#{ucid}#{params}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/channel/:ucid/playlists" do |env|
|
||||||
|
locale = LOCALES[env.get("locale").as(String)]?
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
if user
|
||||||
|
user = user.as(User)
|
||||||
|
subscriptions = user.subscriptions
|
||||||
|
end
|
||||||
|
subscriptions ||= [] of String
|
||||||
|
|
||||||
|
ucid = env.params.url["ucid"]
|
||||||
|
|
||||||
|
continuation = env.params.query["continuation"]?
|
||||||
|
|
||||||
|
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||||
|
sort_by ||= "last"
|
||||||
|
|
||||||
|
begin
|
||||||
|
author, ucid, auto_generated, sub_count = get_about_info(ucid, locale)
|
||||||
|
rescue ex
|
||||||
|
error_message = ex.message
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
|
||||||
|
if auto_generated
|
||||||
|
next env.redirect "/channel/#{ucid}"
|
||||||
|
end
|
||||||
|
|
||||||
|
items, continuation = fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
|
||||||
|
items.select! { |item| item.is_a?(SearchPlaylist) && !item.videos.empty? }
|
||||||
|
items = items.map { |item| item.as(SearchPlaylist) }
|
||||||
|
items.each { |item| item.author = "" }
|
||||||
|
|
||||||
|
templated "playlists"
|
||||||
|
end
|
||||||
|
|
||||||
# API Endpoints
|
# API Endpoints
|
||||||
|
|
||||||
|
get "/api/v1/stats" do |env|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
if !config.statistics_enabled
|
||||||
|
error_message = {"error" => "Statistics are not enabled."}.to_json
|
||||||
|
halt env, status_code: 400, response: error_message
|
||||||
|
end
|
||||||
|
|
||||||
|
if statistics["error"]?
|
||||||
|
halt env, status_code: 500, response: statistics.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||||
|
statistics.to_pretty_json
|
||||||
|
else
|
||||||
|
statistics.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
get "/api/v1/captions/:id" do |env|
|
get "/api/v1/captions/:id" do |env|
|
||||||
locale = LOCALES[env.get("locale").as(String)]?
|
locale = LOCALES[env.get("locale").as(String)]?
|
||||||
|
|
||||||
@@ -2470,7 +2733,7 @@ get "/api/v1/insights/:id" do |env|
|
|||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
error_message = {"error" => "YouTube has removed publicly-available analytics."}.to_json
|
error_message = {"error" => "YouTube has removed publicly-available analytics."}.to_json
|
||||||
halt env, status_code: 503, response: error_message
|
halt env, status_code: 410, response: error_message
|
||||||
|
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
headers = HTTP::Headers.new
|
headers = HTTP::Headers.new
|
||||||
@@ -2636,7 +2899,7 @@ get "/api/v1/videos/:id" do |env|
|
|||||||
end
|
end
|
||||||
|
|
||||||
if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
|
if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
|
|
||||||
host_params = env.request.query_params
|
host_params = env.request.query_params
|
||||||
host_params.delete_all("v")
|
host_params.delete_all("v")
|
||||||
@@ -2854,6 +3117,11 @@ get "/api/v1/top" do |env|
|
|||||||
|
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
if !config.top_enabled
|
||||||
|
error_message = {"error" => "Administrator has disabled this endpoint."}.to_json
|
||||||
|
halt env, status_code: 400, response: error_message
|
||||||
|
end
|
||||||
|
|
||||||
videos = JSON.build do |json|
|
videos = JSON.build do |json|
|
||||||
json.array do
|
json.array do
|
||||||
top_videos.each do |video|
|
top_videos.each do |video|
|
||||||
@@ -3411,7 +3679,7 @@ get "/api/v1/search" do |env|
|
|||||||
date = env.params.query["date"]?.try &.downcase
|
date = env.params.query["date"]?.try &.downcase
|
||||||
date ||= ""
|
date ||= ""
|
||||||
|
|
||||||
duration = env.params.query["date"]?.try &.downcase
|
duration = env.params.query["duration"]?.try &.downcase
|
||||||
duration ||= ""
|
duration ||= ""
|
||||||
|
|
||||||
features = env.params.query["features"]?.try &.split(",").map { |feature| feature.downcase }
|
features = env.params.query["features"]?.try &.split(",").map { |feature| feature.downcase }
|
||||||
@@ -3825,7 +4093,7 @@ get "/api/manifest/hls_variant/*" do |env|
|
|||||||
env.response.content_type = "application/x-mpegURL"
|
env.response.content_type = "application/x-mpegURL"
|
||||||
env.response.headers.add("Access-Control-Allow-Origin", "*")
|
env.response.headers.add("Access-Control-Allow-Origin", "*")
|
||||||
|
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
|
|
||||||
manifest = manifest.body
|
manifest = manifest.body
|
||||||
manifest.gsub("https://www.youtube.com", host_url)
|
manifest.gsub("https://www.youtube.com", host_url)
|
||||||
@@ -3839,7 +4107,7 @@ get "/api/manifest/hls_playlist/*" do |env|
|
|||||||
halt env, status_code: manifest.status_code
|
halt env, status_code: manifest.status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
host_url = make_host_url(config, Kemal.config)
|
||||||
|
|
||||||
manifest = manifest.body.gsub("https://www.youtube.com", host_url)
|
manifest = manifest.body.gsub("https://www.youtube.com", host_url)
|
||||||
manifest = manifest.gsub(/https:\/\/r\d---.{11}\.c\.youtube\.com/, host_url)
|
manifest = manifest.gsub(/https:\/\/r\d---.{11}\.c\.youtube\.com/, host_url)
|
||||||
@@ -3957,7 +4225,7 @@ end
|
|||||||
get "/videoplayback" do |env|
|
get "/videoplayback" do |env|
|
||||||
query_params = env.params.query
|
query_params = env.params.query
|
||||||
|
|
||||||
fvip = query_params["fvip"]
|
fvip = query_params["fvip"]? || "3"
|
||||||
mn = query_params["mn"].split(",")[-1]
|
mn = query_params["mn"].split(",")[-1]
|
||||||
host = "https://r#{fvip}---#{mn}.googlevideo.com"
|
host = "https://r#{fvip}---#{mn}.googlevideo.com"
|
||||||
url = "/videoplayback?#{query_params.to_s}"
|
url = "/videoplayback?#{query_params.to_s}"
|
||||||
@@ -4001,7 +4269,8 @@ get "/videoplayback" do |env|
|
|||||||
env.response.status_code = response.status_code
|
env.response.status_code = response.status_code
|
||||||
|
|
||||||
if title = env.params.query["title"]?
|
if title = env.params.query["title"]?
|
||||||
env.response.headers["Content-Disposition"] = "attachment; filename=\"#{title}\""
|
# https://blog.fastmail.com/2011/06/24/download-non-english-filenames/
|
||||||
|
env.response.headers["Content-Disposition"] = "attachment; filename=\"#{URI.escape(title)}\"; filename*=UTF-8''#{URI.escape(title)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
response.headers.each do |key, value|
|
response.headers.each do |key, value|
|
||||||
|
@@ -4,6 +4,7 @@ class InvidiousChannel
|
|||||||
author: String,
|
author: String,
|
||||||
updated: Time,
|
updated: Time,
|
||||||
deleted: Bool,
|
deleted: Bool,
|
||||||
|
subscribed: Time?,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -15,10 +16,7 @@ class ChannelVideo
|
|||||||
updated: Time,
|
updated: Time,
|
||||||
ucid: String,
|
ucid: String,
|
||||||
author: String,
|
author: String,
|
||||||
length_seconds: {
|
length_seconds: {type: Int32, default: 0},
|
||||||
type: Int32,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -188,11 +186,31 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
|
|||||||
db.exec("DELETE FROM channel_videos * WHERE NOT id = ANY ('{#{ids.map { |id| %("#{id}") }.join(",")}}') AND ucid = $1", ucid)
|
db.exec("DELETE FROM channel_videos * WHERE NOT id = ANY ('{#{ids.map { |id| %("#{id}") }.join(",")}}') AND ucid = $1", ucid)
|
||||||
end
|
end
|
||||||
|
|
||||||
channel = InvidiousChannel.new(ucid, author, Time.now, false)
|
channel = InvidiousChannel.new(ucid, author, Time.now, false, nil)
|
||||||
|
|
||||||
return channel
|
return channel
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def subscribe_pubsub(ucid, key, config)
|
||||||
|
client = make_client(PUBSUB_URL)
|
||||||
|
time = Time.now.to_unix.to_s
|
||||||
|
nonce = Random::Secure.hex(4)
|
||||||
|
signature = "#{time}:#{nonce}"
|
||||||
|
|
||||||
|
host_url = make_host_url(config, Kemal.config)
|
||||||
|
|
||||||
|
body = {
|
||||||
|
"hub.callback" => "#{host_url}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}",
|
||||||
|
"hub.topic" => "https://www.youtube.com/xml/feeds/videos.xml?channel_id=#{ucid}",
|
||||||
|
"hub.verify" => "async",
|
||||||
|
"hub.mode" => "subscribe",
|
||||||
|
"hub.lease_seconds" => "432000",
|
||||||
|
"hub.secret" => key.to_s,
|
||||||
|
}
|
||||||
|
|
||||||
|
return client.post("/subscribe", form: body)
|
||||||
|
end
|
||||||
|
|
||||||
def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
|
def fetch_channel_playlists(ucid, author, auto_generated, continuation, sort_by)
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
|
|
||||||
|
@@ -252,7 +252,7 @@ end
|
|||||||
|
|
||||||
def fetch_reddit_comments(id)
|
def fetch_reddit_comments(id)
|
||||||
client = make_client(REDDIT_URL)
|
client = make_client(REDDIT_URL)
|
||||||
headers = HTTP::Headers{"User-Agent" => "web:invidio.us:v0.14.0 (by /u/omarroth)"}
|
headers = HTTP::Headers{"User-Agent" => "web:invidious:v#{CURRENT_VERSION} (by /u/omarroth)"}
|
||||||
|
|
||||||
query = "(url:3D#{id}%20OR%20url:#{id})%20(site:youtube.com%20OR%20site:youtu.be)"
|
query = "(url:3D#{id}%20OR%20url:#{id})%20(site:youtube.com%20OR%20site:youtu.be)"
|
||||||
search_results = client.get("/search.json?q=#{query}", headers)
|
search_results = client.get("/search.json?q=#{query}", headers)
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
class Config
|
class Config
|
||||||
YAML.mapping({
|
YAML.mapping({
|
||||||
|
video_threads: Int32, # Number of threads to use for updating videos in cache (mostly non-functional)
|
||||||
crawl_threads: Int32, # Number of threads to use for finding new videos from YouTube (used to populate "top" page)
|
crawl_threads: Int32, # Number of threads to use for finding new videos from YouTube (used to populate "top" page)
|
||||||
channel_threads: Int32, # Number of threads to use for crawling videos from channels (for updating subscriptions)
|
channel_threads: Int32, # Number of threads to use for crawling videos from channels (for updating subscriptions)
|
||||||
feed_threads: Int32, # Number of threads to use for updating feeds
|
feed_threads: Int32, # Number of threads to use for updating feeds
|
||||||
video_threads: Int32, # Number of threads to use for updating videos in cache (mostly non-functional)
|
|
||||||
db: NamedTuple( # Database configuration
|
db: NamedTuple( # Database configuration
|
||||||
user: String,
|
user: String,
|
||||||
password: String,
|
password: String,
|
||||||
@@ -11,11 +11,19 @@ user: String,
|
|||||||
port: Int32,
|
port: Int32,
|
||||||
dbname: String,
|
dbname: String,
|
||||||
),
|
),
|
||||||
dl_api_key: String?, # DetectLanguage API Key (used to filter non-English results from "top" page), mostly non-functional
|
|
||||||
https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
|
||||||
hmac_key: String?, # HMAC signing key for CSRF tokens
|
|
||||||
full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
|
full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
|
||||||
domain: String, # Domain to be used for links to resources on the site where an absolute URL is required
|
https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
||||||
|
hmac_key: String?, # HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
||||||
|
domain: String?, # Domain to be used for links to resources on the site where an absolute URL is required
|
||||||
|
use_pubsub_feeds: {type: Bool, default: false}, # Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
|
||||||
|
default_home: {type: String, default: "Top"},
|
||||||
|
feed_menu: {type: Array(String), default: ["Popular", "Top", "Trending", "Subscriptions"]},
|
||||||
|
top_enabled: {type: Bool, default: true},
|
||||||
|
captcha_enabled: {type: Bool, default: true},
|
||||||
|
login_enabled: {type: Bool, default: true},
|
||||||
|
registration_enabled: {type: Bool, default: true},
|
||||||
|
statistics_enabled: {type: Bool, default: false},
|
||||||
|
admins: {type: Array(String), default: [] of String},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -66,7 +74,7 @@ class DenyFrame < Kemal::Handler
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def rank_videos(db, n, filter, url)
|
def rank_videos(db, n)
|
||||||
top = [] of {Float64, String}
|
top = [] of {Float64, String}
|
||||||
|
|
||||||
db.query("SELECT id, wilson_score, published FROM videos WHERE views > 5000 ORDER BY published DESC LIMIT 1000") do |rs|
|
db.query("SELECT id, wilson_score, published FROM videos WHERE views > 5000 ORDER BY published DESC LIMIT 1000") do |rs|
|
||||||
@@ -87,41 +95,7 @@ def rank_videos(db, n, filter, url)
|
|||||||
top.reverse!
|
top.reverse!
|
||||||
top = top.map { |a, b| b }
|
top = top.map { |a, b| b }
|
||||||
|
|
||||||
if filter
|
|
||||||
language_list = [] of String
|
|
||||||
top.each do |id|
|
|
||||||
if language_list.size == n
|
|
||||||
break
|
|
||||||
else
|
|
||||||
client = make_client(url)
|
|
||||||
begin
|
|
||||||
video = get_video(id, db)
|
|
||||||
rescue ex
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
if video.language
|
|
||||||
language = video.language
|
|
||||||
else
|
|
||||||
description = XML.parse(video.description)
|
|
||||||
content = [video.title, description.content].join(" ")
|
|
||||||
content = content[0, 10000]
|
|
||||||
|
|
||||||
results = DetectLanguage.detect(content)
|
|
||||||
language = results[0].language
|
|
||||||
|
|
||||||
db.exec("UPDATE videos SET language = $1 WHERE id = $2", language, id)
|
|
||||||
end
|
|
||||||
|
|
||||||
if language == "en"
|
|
||||||
language_list << id
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return language_list
|
|
||||||
else
|
|
||||||
return top[0..n - 1]
|
return top[0..n - 1]
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def login_req(login_form, f_req)
|
def login_req(login_form, f_req)
|
||||||
|
@@ -193,14 +193,28 @@ def arg_array(array, start = 1)
|
|||||||
return args
|
return args
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_host_url(ssl, host)
|
def make_host_url(config, kemal_config)
|
||||||
|
ssl = config.https_only || kemal_config.ssl
|
||||||
|
|
||||||
if ssl
|
if ssl
|
||||||
scheme = "https://"
|
scheme = "https://"
|
||||||
else
|
else
|
||||||
scheme = "http://"
|
scheme = "http://"
|
||||||
end
|
end
|
||||||
|
|
||||||
return "#{scheme}#{host}"
|
if kemal_config.port != 80 && kemal_config.port != 443
|
||||||
|
port = ":#{kemal_config.port}"
|
||||||
|
else
|
||||||
|
port = ""
|
||||||
|
end
|
||||||
|
|
||||||
|
if !config.domain
|
||||||
|
return ""
|
||||||
|
end
|
||||||
|
|
||||||
|
host = config.domain.not_nil!.lchop(".")
|
||||||
|
|
||||||
|
return "#{scheme}#{host}#{port}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_referer(env, fallback = "/")
|
def get_referer(env, fallback = "/")
|
||||||
|
@@ -73,7 +73,7 @@ def refresh_channels(db, logger, max_threads = 1, full_refresh = false)
|
|||||||
db.exec("UPDATE channels SET updated = $1, author = $2, deleted = false WHERE id = $3", Time.now, channel.author, id)
|
db.exec("UPDATE channels SET updated = $1, author = $2, deleted = false WHERE id = $3", Time.now, channel.author, id)
|
||||||
rescue ex
|
rescue ex
|
||||||
if ex.message == "Deleted or invalid channel"
|
if ex.message == "Deleted or invalid channel"
|
||||||
db.exec("UPDATE channels SET deleted = true WHERE id = $1", id)
|
db.exec("UPDATE channels SET updated = $1, deleted = true WHERE id = $2", Time.now, id)
|
||||||
end
|
end
|
||||||
logger.write("#{id} : #{ex.message}\n")
|
logger.write("#{id} : #{ex.message}\n")
|
||||||
end
|
end
|
||||||
@@ -131,8 +131,17 @@ def refresh_feeds(db, logger, max_threads = 1)
|
|||||||
begin
|
begin
|
||||||
db.exec("REFRESH MATERIALIZED VIEW #{view_name}")
|
db.exec("REFRESH MATERIALIZED VIEW #{view_name}")
|
||||||
rescue ex
|
rescue ex
|
||||||
|
# Create view if it doesn't exist
|
||||||
|
if ex.message.try &.ends_with? "does not exist"
|
||||||
|
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
|
||||||
|
SELECT * FROM channel_videos WHERE \
|
||||||
|
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \
|
||||||
|
ORDER BY published DESC;")
|
||||||
|
logger.write("CREATE #{view_name}")
|
||||||
|
else
|
||||||
logger.write("REFRESH #{email} : #{ex.message}\n")
|
logger.write("REFRESH #{email} : #{ex.message}\n")
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
active_channel.send(true)
|
active_channel.send(true)
|
||||||
end
|
end
|
||||||
@@ -144,19 +153,32 @@ def refresh_feeds(db, logger, max_threads = 1)
|
|||||||
max_channel.send(max_threads)
|
max_channel.send(max_threads)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def subscribe_to_feeds(db, logger, key, config)
|
||||||
|
if config.use_pubsub_feeds
|
||||||
|
spawn do
|
||||||
|
loop do
|
||||||
|
db.query_all("SELECT id FROM channels WHERE CURRENT_TIMESTAMP - subscribed > '4 days'") do |rs|
|
||||||
|
rs.each do
|
||||||
|
ucid = rs.read(String)
|
||||||
|
response = subscribe_pubsub(ucid, key, config)
|
||||||
|
|
||||||
|
if response.status_code >= 400
|
||||||
|
logger.write("#{ucid} : #{response.body}\n")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
sleep 1.minute
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def pull_top_videos(config, db)
|
def pull_top_videos(config, db)
|
||||||
if config.dl_api_key
|
|
||||||
DetectLanguage.configure do |dl_config|
|
|
||||||
dl_config.api_key = config.dl_api_key.not_nil!
|
|
||||||
end
|
|
||||||
filter = true
|
|
||||||
end
|
|
||||||
|
|
||||||
filter ||= false
|
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
begin
|
begin
|
||||||
top = rank_videos(db, 40, filter, YT_URL)
|
top = rank_videos(db, 40)
|
||||||
rescue ex
|
rescue ex
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
@@ -184,11 +206,11 @@ end
|
|||||||
|
|
||||||
def pull_popular_videos(db)
|
def pull_popular_videos(db)
|
||||||
loop do
|
loop do
|
||||||
subscriptions = PG_DB.query_all("SELECT channel FROM \
|
subscriptions = db.query_all("SELECT channel FROM \
|
||||||
(SELECT UNNEST(subscriptions) AS channel FROM users) AS d \
|
(SELECT UNNEST(subscriptions) AS channel FROM users) AS d \
|
||||||
GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40", as: String)
|
GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40", as: String)
|
||||||
|
|
||||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM \
|
videos = db.query_all("SELECT DISTINCT ON (ucid) * FROM \
|
||||||
channel_videos WHERE ucid IN (#{arg_array(subscriptions)}) \
|
channel_videos WHERE ucid IN (#{arg_array(subscriptions)}) \
|
||||||
ORDER BY ucid, published DESC", subscriptions, as: ChannelVideo).sort_by { |video| video.published }.reverse
|
ORDER BY ucid, published DESC", subscriptions, as: ChannelVideo).sort_by { |video| video.published }.reverse
|
||||||
|
|
||||||
|
@@ -99,7 +99,10 @@ def template_mix(mix)
|
|||||||
html += <<-END_HTML
|
html += <<-END_HTML
|
||||||
<li class="pure-menu-item">
|
<li class="pure-menu-item">
|
||||||
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
|
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
|
||||||
<img style="width:100%;" src="/vi/#{video["videoId"]}/mqdefault.jpg">
|
<div class="thumbnail">
|
||||||
|
<img class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
|
||||||
|
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||||
|
</div>
|
||||||
<p style="width:100%">#{video["title"]}</p>
|
<p style="width:100%">#{video["title"]}</p>
|
||||||
<p>
|
<p>
|
||||||
<b style="width: 100%">#{video["author"]}</b>
|
<b style="width: 100%">#{video["author"]}</b>
|
||||||
|
@@ -234,7 +234,10 @@ def template_playlist(playlist)
|
|||||||
html += <<-END_HTML
|
html += <<-END_HTML
|
||||||
<li class="pure-menu-item">
|
<li class="pure-menu-item">
|
||||||
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}">
|
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}">
|
||||||
<img style="width:100%;" src="/vi/#{video["videoId"]}/mqdefault.jpg">
|
<div class="thumbnail">
|
||||||
|
<img class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
|
||||||
|
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||||
|
</div>
|
||||||
<p style="width:100%">#{video["title"]}</p>
|
<p style="width:100%">#{video["title"]}</p>
|
||||||
<p>
|
<p>
|
||||||
<b style="width: 100%">#{video["author"]}</b>
|
<b style="width: 100%">#{video["author"]}</b>
|
||||||
|
@@ -188,7 +188,7 @@ def produce_search_params(sort : String = "relevance", date : String = "", conte
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if body.size > 0
|
if !body.empty?
|
||||||
token = head + "\x12" + body.size.unsafe_chr + body
|
token = head + "\x12" + body.size.unsafe_chr + body
|
||||||
else
|
else
|
||||||
token = head
|
token = head
|
||||||
|
@@ -143,7 +143,7 @@ def get_user(sid, headers, db, refresh = true)
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
|
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
|
||||||
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
|
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
|
||||||
SELECT * FROM channel_videos WHERE \
|
SELECT * FROM channel_videos WHERE \
|
||||||
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
|
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
|
||||||
ORDER BY published DESC;")
|
ORDER BY published DESC;")
|
||||||
@@ -165,7 +165,7 @@ def get_user(sid, headers, db, refresh = true)
|
|||||||
|
|
||||||
begin
|
begin
|
||||||
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
|
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
|
||||||
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
|
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
|
||||||
SELECT * FROM channel_videos WHERE \
|
SELECT * FROM channel_videos WHERE \
|
||||||
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
|
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
|
||||||
ORDER BY published DESC;")
|
ORDER BY published DESC;")
|
||||||
@@ -247,7 +247,7 @@ def validate_response(challenge, token, user_id, operation, key, db, locale)
|
|||||||
raise translate(locale, "Invalid challenge")
|
raise translate(locale, "Invalid challenge")
|
||||||
end
|
end
|
||||||
|
|
||||||
challenge = OpenSSL::HMAC.digest(:sha256, HMAC_KEY, challenge)
|
challenge = OpenSSL::HMAC.digest(:sha256, key, challenge)
|
||||||
challenge = Base64.urlsafe_encode(challenge)
|
challenge = Base64.urlsafe_encode(challenge)
|
||||||
|
|
||||||
if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool)
|
if db.query_one?("SELECT EXISTS (SELECT true FROM nonces WHERE nonce = $1)", nonce, as: Bool)
|
||||||
|
@@ -271,11 +271,53 @@ class Video
|
|||||||
|
|
||||||
def fmt_stream(decrypt_function)
|
def fmt_stream(decrypt_function)
|
||||||
streams = [] of HTTP::Params
|
streams = [] of HTTP::Params
|
||||||
self.info["url_encoded_fmt_stream_map"].split(",") do |string|
|
|
||||||
|
if fmt_streams = self.player_response["streamingData"]?.try &.["formats"]?
|
||||||
|
fmt_streams.as_a.each do |fmt_stream|
|
||||||
|
if !fmt_stream.as_h?
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
fmt = {} of String => String
|
||||||
|
|
||||||
|
fmt["lmt"] = fmt_stream["lastModified"]?.try &.as_s || "0"
|
||||||
|
fmt["projection_type"] = "1"
|
||||||
|
fmt["type"] = fmt_stream["mimeType"].as_s
|
||||||
|
fmt["clen"] = fmt_stream["contentLength"]?.try &.as_s || "0"
|
||||||
|
fmt["bitrate"] = fmt_stream["bitrate"]?.try &.as_i.to_s || "0"
|
||||||
|
fmt["itag"] = fmt_stream["itag"].as_i.to_s
|
||||||
|
fmt["url"] = fmt_stream["url"].as_s
|
||||||
|
fmt["quality"] = fmt_stream["quality"].as_s
|
||||||
|
|
||||||
|
if fmt_stream["width"]?
|
||||||
|
fmt["size"] = "#{fmt_stream["width"]}x#{fmt_stream["height"]}"
|
||||||
|
fmt["height"] = fmt_stream["height"].as_i.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
if fmt_stream["fps"]?
|
||||||
|
fmt["fps"] = fmt_stream["fps"].as_i.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
if fmt_stream["qualityLabel"]?
|
||||||
|
fmt["quality_label"] = fmt_stream["qualityLabel"].as_s
|
||||||
|
end
|
||||||
|
|
||||||
|
params = HTTP::Params.new
|
||||||
|
fmt.each do |key, value|
|
||||||
|
params[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
streams << params
|
||||||
|
end
|
||||||
|
|
||||||
|
streams.sort_by! { |stream| stream["height"].to_i }.reverse!
|
||||||
|
elsif fmt_stream = self.info["url_encoded_fmt_stream_map"]?
|
||||||
|
fmt_stream.split(",").each do |string|
|
||||||
if !string.empty?
|
if !string.empty?
|
||||||
streams << HTTP::Params.parse(string)
|
streams << HTTP::Params.parse(string)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
streams.each { |s| s.add("label", "#{s["quality"]} - #{s["type"].split(";")[0].split("/")[1]}") }
|
streams.each { |s| s.add("label", "#{s["quality"]} - #{s["type"].split(";")[0].split("/")[1]}") }
|
||||||
streams = streams.uniq { |s| s["label"] }
|
streams = streams.uniq { |s| s["label"] }
|
||||||
@@ -296,81 +338,55 @@ class Video
|
|||||||
def adaptive_fmts(decrypt_function)
|
def adaptive_fmts(decrypt_function)
|
||||||
adaptive_fmts = [] of HTTP::Params
|
adaptive_fmts = [] of HTTP::Params
|
||||||
|
|
||||||
if self.info.has_key?("adaptive_fmts")
|
if fmts = self.player_response["streamingData"]?.try &.["adaptiveFormats"]?
|
||||||
self.info["adaptive_fmts"].split(",") do |string|
|
fmts.as_a.each do |adaptive_fmt|
|
||||||
|
if !adaptive_fmt.as_h?
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
fmt = {} of String => String
|
||||||
|
|
||||||
|
if init = adaptive_fmt["initRange"]?
|
||||||
|
fmt["init"] = "#{init["start"]}-#{init["end"]}"
|
||||||
|
end
|
||||||
|
fmt["init"] ||= "0-0"
|
||||||
|
|
||||||
|
fmt["lmt"] = adaptive_fmt["lastModified"]?.try &.as_s || "0"
|
||||||
|
fmt["projection_type"] = "1"
|
||||||
|
fmt["type"] = adaptive_fmt["mimeType"].as_s
|
||||||
|
fmt["clen"] = adaptive_fmt["contentLength"]?.try &.as_s || "0"
|
||||||
|
fmt["bitrate"] = adaptive_fmt["bitrate"]?.try &.as_i.to_s || "0"
|
||||||
|
fmt["itag"] = adaptive_fmt["itag"].as_i.to_s
|
||||||
|
fmt["url"] = adaptive_fmt["url"].as_s
|
||||||
|
|
||||||
|
if index = adaptive_fmt["indexRange"]?
|
||||||
|
fmt["index"] = "#{index["start"]}-#{index["end"]}"
|
||||||
|
end
|
||||||
|
fmt["index"] ||= "0-0"
|
||||||
|
|
||||||
|
if adaptive_fmt["width"]?
|
||||||
|
fmt["size"] = "#{adaptive_fmt["width"]}x#{adaptive_fmt["height"]}"
|
||||||
|
end
|
||||||
|
|
||||||
|
if adaptive_fmt["fps"]?
|
||||||
|
fmt["fps"] = adaptive_fmt["fps"].as_i.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
if adaptive_fmt["qualityLabel"]?
|
||||||
|
fmt["quality_label"] = adaptive_fmt["qualityLabel"].as_s
|
||||||
|
end
|
||||||
|
|
||||||
|
params = HTTP::Params.new
|
||||||
|
fmt.each do |key, value|
|
||||||
|
params[key] = value
|
||||||
|
end
|
||||||
|
|
||||||
|
adaptive_fmts << params
|
||||||
|
end
|
||||||
|
elsif fmts = self.info["adaptive_fmts"]?
|
||||||
|
fmts.split(",") do |string|
|
||||||
adaptive_fmts << HTTP::Params.parse(string)
|
adaptive_fmts << HTTP::Params.parse(string)
|
||||||
end
|
end
|
||||||
elsif dashmpd = self.player_response["streamingData"]["dashManifestUrl"]?.try &.as_s
|
|
||||||
client = make_client(YT_URL)
|
|
||||||
response = client.get(dashmpd)
|
|
||||||
document = XML.parse_html(response.body)
|
|
||||||
|
|
||||||
document.xpath_nodes(%q(//adaptationset)).each do |adaptation_set|
|
|
||||||
mime_type = adaptation_set["mimetype"]
|
|
||||||
|
|
||||||
document.xpath_nodes(%q(.//representation)).each do |representation|
|
|
||||||
codecs = representation["codecs"]
|
|
||||||
itag = representation["id"]
|
|
||||||
bandwidth = representation["bandwidth"]
|
|
||||||
url = representation.xpath_node(%q(.//baseurl)).not_nil!.content
|
|
||||||
|
|
||||||
clen = url.match(/clen\/(?<clen>\d+)/).try &.["clen"]
|
|
||||||
clen ||= "0"
|
|
||||||
lmt = url.match(/lmt\/(?<lmt>\d+)/).try &.["lmt"]
|
|
||||||
lmt ||= "#{((Time.now + 1.hour).to_unix_f.to_f64 * 1000000).to_i64}"
|
|
||||||
|
|
||||||
segment_list = representation.xpath_node(%q(.//segmentlist)).not_nil!
|
|
||||||
init = segment_list.xpath_node(%q(.//initialization))
|
|
||||||
|
|
||||||
# TODO: Replace with sane defaults when byteranges are absent
|
|
||||||
if init && !init["sourceurl"].starts_with? "sq"
|
|
||||||
init = init["sourceurl"].lchop("range/")
|
|
||||||
|
|
||||||
index = segment_list.xpath_node(%q(.//segmenturl)).not_nil!["media"]
|
|
||||||
index = index.lchop("range/")
|
|
||||||
index = "#{init.split("-")[1].to_i + 1}-#{index.split("-")[0].to_i}"
|
|
||||||
else
|
|
||||||
init = "0-0"
|
|
||||||
index = "1-1"
|
|
||||||
end
|
|
||||||
|
|
||||||
params = {
|
|
||||||
"type" => ["#{mime_type}; codecs=\"#{codecs}\""],
|
|
||||||
"url" => [url],
|
|
||||||
"projection_type" => ["1"],
|
|
||||||
"index" => [index],
|
|
||||||
"init" => [init],
|
|
||||||
"xtags" => [] of String,
|
|
||||||
"lmt" => [lmt],
|
|
||||||
"clen" => [clen],
|
|
||||||
"bitrate" => [bandwidth],
|
|
||||||
"itag" => [itag],
|
|
||||||
}
|
|
||||||
|
|
||||||
if mime_type == "video/mp4"
|
|
||||||
width = representation["width"]?
|
|
||||||
height = representation["height"]?
|
|
||||||
fps = representation["framerate"]?
|
|
||||||
|
|
||||||
metadata = itag_to_metadata?(itag)
|
|
||||||
if metadata
|
|
||||||
width ||= metadata["width"]?
|
|
||||||
height ||= metadata["height"]?
|
|
||||||
fps ||= metadata["fps"]?
|
|
||||||
end
|
|
||||||
|
|
||||||
if width && height
|
|
||||||
params["size"] = ["#{width}x#{height}"]
|
|
||||||
end
|
|
||||||
|
|
||||||
if width
|
|
||||||
params["quality_label"] = ["#{height}p"]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
adaptive_fmts << HTTP::Params.new(params)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if self.info["region"]?
|
if self.info["region"]?
|
||||||
@@ -387,13 +403,13 @@ class Video
|
|||||||
end
|
end
|
||||||
|
|
||||||
def video_streams(adaptive_fmts)
|
def video_streams(adaptive_fmts)
|
||||||
video_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("video") ? s : nil }
|
video_streams = adaptive_fmts.select { |s| s["type"].starts_with? "video" }
|
||||||
|
|
||||||
return video_streams
|
return video_streams
|
||||||
end
|
end
|
||||||
|
|
||||||
def audio_streams(adaptive_fmts)
|
def audio_streams(adaptive_fmts)
|
||||||
audio_streams = adaptive_fmts.compact_map { |s| s["type"].starts_with?("audio") ? s : nil }
|
audio_streams = adaptive_fmts.select { |s| s["type"].starts_with? "audio" }
|
||||||
audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse!
|
audio_streams.sort_by! { |s| s["bitrate"].to_i }.reverse!
|
||||||
audio_streams.each do |stream|
|
audio_streams.each do |stream|
|
||||||
stream["bitrate"] = (stream["bitrate"].to_f64/1000).to_i.to_s
|
stream["bitrate"] = (stream["bitrate"].to_f64/1000).to_i.to_s
|
||||||
@@ -620,7 +636,10 @@ def fetch_video(id, proxies, region)
|
|||||||
|
|
||||||
# Try to pull streams from embed URL
|
# Try to pull streams from embed URL
|
||||||
if info["reason"]?
|
if info["reason"]?
|
||||||
embed_info = HTTP::Params.parse(client.get("/get_video_info?video_id=#{id}&ps=default&eurl=&gl=US&hl=en&disable_polymer=1").body)
|
embed_page = client.get("/embed/#{id}").body
|
||||||
|
sts = embed_page.match(/"sts"\s*:\s*(?<sts>\d+)/).try &.["sts"]?
|
||||||
|
sts ||= ""
|
||||||
|
embed_info = HTTP::Params.parse(client.get("/get_video_info?video_id=#{id}&eurl=https://youtube.googleapis.com/v/#{id}&gl=US&hl=en&disable_polymer=1&sts=#{sts}").body)
|
||||||
|
|
||||||
if !embed_info["reason"]?
|
if !embed_info["reason"]?
|
||||||
embed_info.each do |key, value|
|
embed_info.each do |key, value|
|
||||||
|
@@ -21,12 +21,20 @@
|
|||||||
<div class="pure-g h-box">
|
<div class="pure-g h-box">
|
||||||
<div class="pure-u-1-3">
|
<div class="pure-u-1-3">
|
||||||
<a href="https://www.youtube.com/channel/<%= ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
|
<a href="https://www.youtube.com/channel/<%= ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
|
||||||
|
<div class="pure-u-1 pure-md-1-3">
|
||||||
|
<b><%= translate(locale, "Videos") %></b>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-md-1-3">
|
||||||
|
<% if !auto_generated %>
|
||||||
|
<a href="/channel/<%= ucid %>/playlists"><%= translate(locale, "Playlists") %></a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-3">
|
<div class="pure-u-1-3">
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-3">
|
<div class="pure-u-1-3">
|
||||||
<div class="pure-g" style="text-align:right;">
|
<div class="pure-g" style="text-align:right;">
|
||||||
<% {"newest", "oldest", "popular"}.each do |sort| %>
|
<% sort_options.each do |sort| %>
|
||||||
<div class="pure-u-1 pure-md-1-3">
|
<div class="pure-u-1 pure-md-1-3">
|
||||||
<% if sort_by == sort %>
|
<% if sort_by == sort %>
|
||||||
<b><%= translate(locale, sort) %></b>
|
<b><%= translate(locale, sort) %></b>
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<div class="h-box pure-g">
|
<div class="h-box pure-g">
|
||||||
<div class="pure-u-1-4"></div>
|
<div class="pure-u-1 pure-u-md-1-4"></div>
|
||||||
<div class="pure-u-1 pure-u-md-1-2">
|
<div class="pure-u-1 pure-u-md-1-2">
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<% feeds = ["Popular", "Top", "Trending"] %>
|
<% feed_menu = config.feed_menu.dup %>
|
||||||
<% if env.get? "user" %>
|
<% if !env.get?("user") %>
|
||||||
<% feeds << "Subscriptions" %>
|
<% feed_menu.reject! {|feed| feed == "Subscriptions"} %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% feeds.each do |feed| %>
|
<% feed_menu.each do |feed| %>
|
||||||
<div class="pure-u-1-<%= feeds.size %>">
|
<div class="pure-u-1-2 pure-u-md-1-<%= feed_menu.size %>">
|
||||||
<a href="/feed/<%= feed.downcase %>" style="text-align:center;" class="pure-menu-heading">
|
<a href="/feed/<%= feed.downcase %>" style="text-align:center;" class="pure-menu-heading">
|
||||||
<%= translate(locale, feed) %>
|
<%= translate(locale, feed) %>
|
||||||
</a>
|
</a>
|
||||||
@@ -15,5 +15,5 @@
|
|||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1-4"></div>
|
<div class="pure-u-1 pure-u-md-1-4"></div>
|
||||||
</div>
|
</div>
|
@@ -137,8 +137,6 @@ player.on('error', function(event) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
player.share(shareOptions);
|
|
||||||
|
|
||||||
<% if params[:video_start] > 0 || params[:video_end] > 0 %>
|
<% if params[:video_start] > 0 || params[:video_end] > 0 %>
|
||||||
player.markers({
|
player.markers({
|
||||||
onMarkerReached: function(marker) {
|
onMarkerReached: function(marker) {
|
||||||
@@ -188,4 +186,7 @@ if (bpb) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
// Since videojs-share can sometimes be blocked, we try to load it last
|
||||||
|
player.share(shareOptions);
|
||||||
</script>
|
</script>
|
||||||
|
@@ -1,14 +0,0 @@
|
|||||||
<% content_for "header" do %>
|
|
||||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
|
||||||
<title>Invidious</title>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<%= rendered "components/feed_menu" %>
|
|
||||||
|
|
||||||
<div class="pure-g">
|
|
||||||
<% top_videos.each_slice(4) do |slice| %>
|
|
||||||
<% slice.each do |item| %>
|
|
||||||
<%= rendered "components/item" %>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
@@ -28,6 +28,7 @@
|
|||||||
<label for="password"><%= translate(locale, "Password:") %></label>
|
<label for="password"><%= translate(locale, "Password:") %></label>
|
||||||
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
|
<input required class="pure-input-1" name="password" type="password" placeholder="Password">
|
||||||
|
|
||||||
|
<% if config.captcha_enabled %>
|
||||||
<% if captcha_type == "image" %>
|
<% if captcha_type == "image" %>
|
||||||
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
|
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
|
||||||
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
|
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
|
||||||
@@ -54,9 +55,12 @@
|
|||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button>
|
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary"><%= translate(locale, "Sign In") %></button>
|
||||||
|
<% if config.registration_enabled %>
|
||||||
<button type="submit" name="action" value="register" class="pure-button pure-button-primary"><%= translate(locale, "Register") %></button>
|
<button type="submit" name="action" value="register" class="pure-button pure-button-primary"><%= translate(locale, "Register") %></button>
|
||||||
|
<% end %>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
<% elsif account_type == "google" %>
|
<% elsif account_type == "google" %>
|
||||||
|
80
src/invidious/views/playlists.ecr
Normal file
80
src/invidious/views/playlists.ecr
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= author %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-2-3">
|
||||||
|
<h3><%= author %></h3>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3" style="text-align:right;">
|
||||||
|
<h3>
|
||||||
|
<a href="/feed/channel/<%= ucid %>"><i class="icon ion-logo-rss"></i></a>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<% sub_count_text = number_to_short_text(sub_count) %>
|
||||||
|
<%= rendered "components/subscribe_widget" %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-g pure-u-1-3">
|
||||||
|
<div class="pure-u-1 pure-md-1-3">
|
||||||
|
<a href="https://www.youtube.com/channel/<%= ucid %>"><%= translate(locale, "View channel on YouTube") %></a>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-md-1-3">
|
||||||
|
<a href="/channel/<%= ucid %>"><%= translate(locale, "Videos") %></a>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-md-1-3">
|
||||||
|
<% if !auto_generated %>
|
||||||
|
<b><%= translate(locale, "Playlists") %></b>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3">
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1-3">
|
||||||
|
<div class="pure-g" style="text-align:right;">
|
||||||
|
<% {"last", "oldest", "newest"}.each do |sort| %>
|
||||||
|
<div class="pure-u-1 pure-md-1-3">
|
||||||
|
<% if sort_by == sort %>
|
||||||
|
<b><%= translate(locale, sort) %></b>
|
||||||
|
<% else %>
|
||||||
|
<a href="/channel/<%= ucid %>/playlists?sort_by=<%= sort %>">
|
||||||
|
<%= translate(locale, sort) %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="h-box">
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<% items.each_slice(4) do |slice| %>
|
||||||
|
<% slice.each do |item| %>
|
||||||
|
<%= rendered "components/item" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-g h-box">
|
||||||
|
<div class="pure-u-1 pure-u-md-4-5"></div>
|
||||||
|
<div style="text-align:right;" class="pure-u-1 pure-u-md-1-5">
|
||||||
|
<% if items.size >= 28 %>
|
||||||
|
<a href="/channel/<%= ucid %>/playlists?continuation=<%= continuation %><% if sort_by != "last" %>&sort_by=<%= sort_by %><% end %>">
|
||||||
|
<%= translate(locale, "Next page") %>
|
||||||
|
</a>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
<% sub_count_text = number_to_short_text(sub_count) %>
|
||||||
|
<%= rendered "components/subscribe_widget_script" %>
|
||||||
|
</script>
|
@@ -1,6 +1,6 @@
|
|||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||||
<title><%= translate(locale, "Popular") %> - Invidious</title>
|
<title><% if config.default_home != "Popular" %><%= translate(locale, "Popular") %> - <% end %>Invidious</title>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= rendered "components/feed_menu" %>
|
<%= rendered "components/feed_menu" %>
|
||||||
|
@@ -58,45 +58,25 @@ function update_value(element) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="comments_0"><%= translate(locale, "Default comments: ") %></label>
|
<label for="comments[0]"><%= translate(locale, "Default comments: ") %></label>
|
||||||
<select name="comments_0" id="comments_0">
|
<% preferences.comments.each_with_index do |comments, index| %>
|
||||||
|
<select name="comments[<%= index %>]" id="comments[<%= index %>]">
|
||||||
<% {"", "youtube", "reddit"}.each do |option| %>
|
<% {"", "youtube", "reddit"}.each do |option| %>
|
||||||
<option value="<%= option %>" <% if preferences.comments[0] == option %> selected <% end %>><%= translate(locale, option) %></option>
|
<option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="comments_1"><%= translate(locale, "Fallback comments: ") %></label>
|
<label for="captions[0]"><%= translate(locale, "Default captions: ") %></label>
|
||||||
<select name="comments_1" id="comments_1">
|
<% preferences.captions.each_with_index do |caption, index| %>
|
||||||
<% {"", "youtube", "reddit"}.each do |option| %>
|
<select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]">
|
||||||
<option value="<%= option %>" <% if preferences.comments[1] == option %> selected <% end %>><%= translate(locale, option) %></option>
|
|
||||||
<% end %>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="captions_0"><%= translate(locale, "Default captions: ") %></label>
|
|
||||||
<select class="pure-u-1-5" name="captions_0" id="captions_0">
|
|
||||||
<% CAPTION_LANGUAGES.each do |option| %>
|
<% CAPTION_LANGUAGES.each do |option| %>
|
||||||
<option value="<%= option %>" <% if preferences.captions[0] == option %> selected <% end %>><%= translate(locale, option) %></option>
|
<option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="captions_fallback"><%= translate(locale, "Fallback captions: ") %></label>
|
|
||||||
<select class="pure-u-1-5" name="captions_1" id="captions_1">
|
|
||||||
<% CAPTION_LANGUAGES.each do |option| %>
|
|
||||||
<option value="<%= option %>" <% if preferences.captions[1] == option %> selected <% end %>><%= translate(locale, option) %></option>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</select>
|
|
||||||
|
|
||||||
<select class="pure-u-1-5" name="captions_2" id="captions_2">
|
|
||||||
<% CAPTION_LANGUAGES.each do |option| %>
|
|
||||||
<option value="<%= option %>" <% if preferences.captions[2] == option %> selected <% end %>><%= translate(locale, option) %></option>
|
|
||||||
<% end %>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
@@ -167,6 +147,55 @@ function update_value(element) {
|
|||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<% if env.get?("user") && config.admins.includes? env.get?("user").as(User).email %>
|
||||||
|
<legend><%= translate(locale, "Administrator preferences") %></legend>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="default_home"><%= translate(locale, "Default homepage: ") %></label>
|
||||||
|
<select name="default_home" id="default_home">
|
||||||
|
<% {"Popular", "Top", "Trending", "Subscriptions"}.each do |option| %>
|
||||||
|
<option value="<%= option %>" <% if config.default_home == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="feed_menu"><%= translate(locale, "Feed menu: ") %></label>
|
||||||
|
<% 4.times do |index| %>
|
||||||
|
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
|
||||||
|
<% {"", "Popular", "Top", "Trending", "Subscriptions"}.each do |option| %>
|
||||||
|
<option value="<%= option %>" <% if config.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="top_enabled"><%= translate(locale, "Top enabled? ") %></label>
|
||||||
|
<input name="top_enabled" id="top_enabled" type="checkbox" <% if config.top_enabled %>checked<% end %>>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled? ") %></label>
|
||||||
|
<input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if config.captcha_enabled %>checked<% end %>>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="login_enabled"><%= translate(locale, "Login enabled? ") %></label>
|
||||||
|
<input name="login_enabled" id="login_enabled" type="checkbox" <% if config.login_enabled %>checked<% end %>>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="registration_enabled"><%= translate(locale, "Registration enabled? ") %></label>
|
||||||
|
<input name="registration_enabled" id="registration_enabled" type="checkbox" <% if config.registration_enabled %>checked<% end %>>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="statistics_enabled"><%= translate(locale, "Report statistics? ") %></label>
|
||||||
|
<input name="statistics_enabled" id="statistics_enabled" type="checkbox" <% if config.statistics_enabled %>checked<% end %>>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% if env.get? "user" %>
|
<% if env.get? "user" %>
|
||||||
<legend><%= translate(locale, "Data preferences") %></legend>
|
<legend><%= translate(locale, "Data preferences") %></legend>
|
||||||
|
|
||||||
|
@@ -72,7 +72,7 @@ function mark_watched(target) {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="pure-g">
|
<div class="pure-g h-box">
|
||||||
<div class="pure-u-1 pure-u-md-1-5">
|
<div class="pure-u-1 pure-u-md-1-5">
|
||||||
<% if page >= 2 %>
|
<% if page >= 2 %>
|
||||||
<a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page - 1 %>">
|
<a href="/feed/subscriptions?max_results=<%= max_results %>&page=<%= page - 1 %>">
|
||||||
|
@@ -89,43 +89,48 @@
|
|||||||
<i class="icon ion-ios-cog"></i>
|
<i class="icon ion-ios-cog"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<% if config.login_enabled %>
|
||||||
<div class="pure-u-1-3">
|
<div class="pure-u-1-3">
|
||||||
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||||
<%= translate(locale, "Login") %>
|
<%= translate(locale, "Login") %>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<%= content %>
|
<%= content %>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>
|
<div class="pure-g">
|
||||||
<a href="https://github.com/omarroth">
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
|
<a href="https://github.com/omarroth/invidious">
|
||||||
<%= translate(locale, "Released under the AGPLv3 by Omar Roth.") %>
|
<%= translate(locale, "Released under the AGPLv3 by Omar Roth.") %>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</div>
|
||||||
<p>
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
<a href="https://github.com/omarroth/invidious">
|
<i class="icon ion-logo-bitcoin"></i>
|
||||||
<%= translate(locale, "Source available here.") %>
|
<%= translate(locale, "BTC: ") %>356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</div>
|
||||||
</a>
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
</p>
|
<i class="icon ion-logo-bitcoin"></i>
|
||||||
<p><%= translate(locale, "Liberapay: ") %>
|
<%= translate(locale, "BCH: ") %>qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</div>
|
||||||
<a href="https://liberapay.com/omarroth">
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
https://liberapay.com/omarroth
|
<i class="icon ion-logo-usd"></i>
|
||||||
</a>
|
<a href="https://liberapay.com/omarroth"><%= translate(locale, "Liberapay") %></a>
|
||||||
</p>
|
/
|
||||||
<p><%= translate(locale, "Patreon: ") %>
|
<a href="https://patreon.com/omarroth"><%= translate(locale, "Patreon") %></a>
|
||||||
<a href="https://patreon.com/omarroth">
|
</div>
|
||||||
https://patreon.com/omarroth
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
</a>
|
<i class="icon ion-logo-javascript"></i>
|
||||||
</p>
|
|
||||||
<p><%= translate(locale, "BTC: ") %>356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY</p>
|
|
||||||
<p><%= translate(locale, "BCH: ") %>qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk</p>
|
|
||||||
<p>
|
|
||||||
<a rel="jslicense" href="/licenses">
|
<a rel="jslicense" href="/licenses">
|
||||||
<%= translate(locale, "View JavaScript license information.") %>
|
<%= translate(locale, "View JavaScript license information.") %>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-md-1-3">
|
||||||
|
<i class="icon ion-logo-github"></i>
|
||||||
|
<%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %>
|
||||||
|
<i class="icon ion-logo-github"></i>
|
||||||
|
<%= CURRENT_BRANCH %></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||||
<title><%= translate(locale, "Top") %> - Invidious</title>
|
<title><% if config.default_home != "Top" %><%= translate(locale, "Top") %> - <% end %>Invidious</title>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= rendered "components/feed_menu" %>
|
<%= rendered "components/feed_menu" %>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||||
<title><%= translate(locale, "Trending") %> - Invidious</title>
|
<title><% if config.default_home != "Trending" %><%= translate(locale, "Trending") %> - <% end %>Invidious</title>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= rendered "components/feed_menu" %>
|
<%= rendered "components/feed_menu" %>
|
||||||
|
@@ -59,17 +59,17 @@
|
|||||||
<label for="download_widget"><%= translate(locale, "Download as: ") %></label>
|
<label for="download_widget"><%= translate(locale, "Download as: ") %></label>
|
||||||
<select style="width:100%" name="download_widget" id="download_widget">
|
<select style="width:100%" name="download_widget" id="download_widget">
|
||||||
<% video_streams.each do |option| %>
|
<% video_streams.each do |option| %>
|
||||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= video.title.dump_unquoted %>-<%= video.id %>.mp4"}'>
|
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.mp4"}'>
|
||||||
<%= option["quality_label"] %> - <%= option["type"].split(";")[0] %> @ <%= option["fps"] %>fps - video only
|
<%= option["quality_label"] %> - <%= option["type"].split(";")[0] %> @ <%= option["fps"] %>fps - video only
|
||||||
</option>
|
</option>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% audio_streams.each do |option| %>
|
<% audio_streams.each do |option| %>
|
||||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= video.title.dump_unquoted %>-<%= video.id %>.mp4"}'>
|
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.mp4"}'>
|
||||||
<%= option["type"].split(";")[0] %> @ <%= option["bitrate"] %>k - audio only
|
<%= option["type"].split(";")[0] %> @ <%= option["bitrate"] %>k - audio only
|
||||||
</option>
|
</option>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% fmt_stream.each do |option| %>
|
<% fmt_stream.each do |option| %>
|
||||||
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= video.title.dump_unquoted %>-<%= video.id %>.mp4"}'>
|
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.mp4"}'>
|
||||||
<%= itag_to_metadata?(option["itag"]).try &.["height"]? || "~240" %>p - <%= option["type"].split(";")[0] %>
|
<%= itag_to_metadata?(option["itag"]).try &.["height"]? || "~240" %>p - <%= option["type"].split(";")[0] %>
|
||||||
</option>
|
</option>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
Reference in New Issue
Block a user