mirror of
https://gitea.invidious.io/iv-org/invidious.git
synced 2024-08-15 00:53:41 +00:00
Add administrator preferences
This commit is contained in:
parent
2fe545e19a
commit
a39b1583da
22 changed files with 616 additions and 454 deletions
|
@ -82,6 +82,13 @@
|
||||||
"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? ": "",
|
||||||
"Save preferences": "حفظ التفضيلات",
|
"Save preferences": "حفظ التفضيلات",
|
||||||
"Subscription manager": "مدير الإشتراكات",
|
"Subscription manager": "مدير الإشتراكات",
|
||||||
"`x` subscriptions": "`x` مشتركين",
|
"`x` subscriptions": "`x` مشتركين",
|
||||||
|
|
|
@ -82,6 +82,13 @@
|
||||||
"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? ": "",
|
||||||
"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,13 @@
|
||||||
"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? ",
|
||||||
"Save preferences": "Save preferences",
|
"Save preferences": "Save preferences",
|
||||||
"Subscription manager": "Subscription manager",
|
"Subscription manager": "Subscription manager",
|
||||||
"`x` subscriptions": "`x` subscriptions",
|
"`x` subscriptions": "`x` subscriptions",
|
||||||
|
|
|
@ -80,6 +80,13 @@
|
||||||
"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? ": "",
|
||||||
"Save preferences": "",
|
"Save preferences": "",
|
||||||
"Subscription manager": "",
|
"Subscription manager": "",
|
||||||
"`x` subscriptions": "",
|
"`x` subscriptions": "",
|
||||||
|
|
|
@ -79,6 +79,13 @@
|
||||||
"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? ": "",
|
||||||
"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,13 @@
|
||||||
"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? ": "",
|
||||||
"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",
|
||||||
|
|
|
@ -1,280 +1,287 @@
|
||||||
{
|
{
|
||||||
"`x` subscribers": "`x` abonnenter",
|
"`x` subscribers": "`x` abonnenter",
|
||||||
"`x` videos": "`x` videoer",
|
"`x` videos": "`x` videoer",
|
||||||
"LIVE": "SANNTIDSVISNING",
|
"LIVE": "SANNTIDSVISNING",
|
||||||
"Shared `x` ago": "Delt for `x` siden",
|
"Shared `x` ago": "Delt for `x` siden",
|
||||||
"Unsubscribe": "Opphev abonnement",
|
"Unsubscribe": "Opphev abonnement",
|
||||||
"Subscribe": "Abonner",
|
"Subscribe": "Abonner",
|
||||||
"Login to subscribe to `x`": "Logg inn for å abonnere på `x`",
|
"Login to subscribe to `x`": "Logg inn for å abonnere på `x`",
|
||||||
"View channel on YouTube": "Vis kanal på YouTube",
|
"View channel on YouTube": "Vis kanal på YouTube",
|
||||||
"newest": "nyeste",
|
"newest": "nyeste",
|
||||||
"oldest": "eldste",
|
"oldest": "eldste",
|
||||||
"popular": "populært",
|
"popular": "populært",
|
||||||
"Preview page": "Forhåndsvis side",
|
"Preview page": "Forhåndsvis side",
|
||||||
"Next page": "Neste side",
|
"Next page": "Neste side",
|
||||||
"Clear watch history?": "Tøm visningshistorikk?",
|
"Clear watch history?": "Tøm visningshistorikk?",
|
||||||
"Yes": "Ja",
|
"Yes": "Ja",
|
||||||
"No": "Nei",
|
"No": "Nei",
|
||||||
"Import and Export Data": "Importer- og eksporter data",
|
"Import and Export Data": "Importer- og eksporter data",
|
||||||
"Import": "Importer",
|
"Import": "Importer",
|
||||||
"Import Invidious data": "Importer Invidious-data",
|
"Import Invidious data": "Importer Invidious-data",
|
||||||
"Import YouTube subscriptions": "Importer YouTube-abonnenter",
|
"Import YouTube subscriptions": "Importer YouTube-abonnenter",
|
||||||
"Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnenter (.db)",
|
"Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnenter (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnenter (.json)",
|
"Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnenter (.json)",
|
||||||
"Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)",
|
"Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)",
|
||||||
"Export": "Eksporter",
|
"Export": "Eksporter",
|
||||||
"Export subscriptions as OPML": "Eksporter abonnenter som OPML",
|
"Export subscriptions as OPML": "Eksporter abonnenter som OPML",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporter abonnenter som OPML (for NewPipe og FreeTube)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporter abonnenter som OPML (for NewPipe og FreeTube)",
|
||||||
"Export data as JSON": "Eksporter data som JSON",
|
"Export data as JSON": "Eksporter data som JSON",
|
||||||
"Delete account?": "Slett konto?",
|
"Delete account?": "Slett konto?",
|
||||||
"History": "Historikk",
|
"History": "Historikk",
|
||||||
"Previous page": "Forrige side",
|
"Previous page": "Forrige side",
|
||||||
"An alternative front-end to YouTube": "En alternativ grenseflate for YouTube",
|
"An alternative front-end to YouTube": "En alternativ grenseflate for YouTube",
|
||||||
"JavaScript license information": "JavaScript-lisensinformasjon",
|
"JavaScript license information": "JavaScript-lisensinformasjon",
|
||||||
"source": "kilde",
|
"source": "kilde",
|
||||||
"Login": "Logg inn",
|
"Login": "Logg inn",
|
||||||
"Login/Register": "Logg inn/registrer",
|
"Login/Register": "Logg inn/registrer",
|
||||||
"Login to Google": "Logg inn med Google",
|
"Login to Google": "Logg inn med Google",
|
||||||
"User ID:": "Bruker-ID:",
|
"User ID:": "Bruker-ID:",
|
||||||
"Password:": "Passord:",
|
"Password:": "Passord:",
|
||||||
"Time (h:mm:ss):": "Tid (h:mm:ss):",
|
"Time (h:mm:ss):": "Tid (h:mm:ss):",
|
||||||
"Text CAPTCHA": "Tekst-CAPTCHA",
|
"Text CAPTCHA": "Tekst-CAPTCHA",
|
||||||
"Image CAPTCHA": "Bilde-CAPTCHA",
|
"Image CAPTCHA": "Bilde-CAPTCHA",
|
||||||
"Sign In": "Innlogging",
|
"Sign In": "Innlogging",
|
||||||
"Register": "Registrer",
|
"Register": "Registrer",
|
||||||
"Email:": "E-post:",
|
"Email:": "E-post:",
|
||||||
"Google verification code:": "Google-bekreftelseskode:",
|
"Google verification code:": "Google-bekreftelseskode:",
|
||||||
"Preferences": "Innstillinger",
|
"Preferences": "Innstillinger",
|
||||||
"Player preferences": "Avspillerinnstillinger",
|
"Player preferences": "Avspillerinnstillinger",
|
||||||
"Always loop: ": "Alltid gjenta: ",
|
"Always loop: ": "Alltid gjenta: ",
|
||||||
"Autoplay: ": "Autoavspilling: ",
|
"Autoplay: ": "Autoavspilling: ",
|
||||||
"Autoplay next video: ": "Autospill neste video: ",
|
"Autoplay next video: ": "Autospill neste video: ",
|
||||||
"Listen by default: ": "Lytt som forvalg: ",
|
"Listen by default: ": "Lytt som forvalg: ",
|
||||||
"Default speed: ": "Forvalgt hastighet: ",
|
"Default speed: ": "Forvalgt hastighet: ",
|
||||||
"Preferred video quality: ": "Foretrukket videokvalitet: ",
|
"Preferred video quality: ": "Foretrukket videokvalitet: ",
|
||||||
"Player volume: ": "Avspillerlydstyrke: ",
|
"Player volume: ": "Avspillerlydstyrke: ",
|
||||||
"Default comments: ": "Forvalgte kommentarer: ",
|
"Default comments: ": "Forvalgte kommentarer: ",
|
||||||
"Default captions: ": "Forvalgte undertitler: ",
|
"Default captions: ": "Forvalgte undertitler: ",
|
||||||
"Fallback captions: ": "Tilbakefallsundertitler: ",
|
"Fallback captions: ": "Tilbakefallsundertitler: ",
|
||||||
"Show related videos? ": "Vis relaterte videoer? ",
|
"Show related videos? ": "Vis relaterte videoer? ",
|
||||||
"Visual preferences": "Visuelle innstillinger",
|
"Visual preferences": "Visuelle innstillinger",
|
||||||
"Dark mode: ": "Mørk drakt: ",
|
"Dark mode: ": "Mørk drakt: ",
|
||||||
"Thin mode: ": "Tynt modus: ",
|
"Thin mode: ": "Tynt modus: ",
|
||||||
"Subscription preferences": "Abonnementsinnstillinger",
|
"Subscription preferences": "Abonnementsinnstillinger",
|
||||||
"Redirect homepage to feed: ": "Videresend hjemmeside til flyt: ",
|
"Redirect homepage to feed: ": "Videresend hjemmeside til flyt: ",
|
||||||
"Number of videos shown in feed: ": "Antall videoer å vise i flyt: ",
|
"Number of videos shown in feed: ": "Antall videoer å vise i flyt: ",
|
||||||
"Sort videos by: ": "Sorter videoer etter: ",
|
"Sort videos by: ": "Sorter videoer etter: ",
|
||||||
"published": "publisert",
|
"published": "publisert",
|
||||||
"published - reverse": "publisert - motsatt",
|
"published - reverse": "publisert - motsatt",
|
||||||
"alphabetically": "alfabetisk",
|
"alphabetically": "alfabetisk",
|
||||||
"alphabetically - reverse": "alfabetisk - motsatt",
|
"alphabetically - reverse": "alfabetisk - motsatt",
|
||||||
"channel name": "kanalnavn",
|
"channel name": "kanalnavn",
|
||||||
"channel name - reverse": "kanalnavn - motsatt",
|
"channel name - reverse": "kanalnavn - motsatt",
|
||||||
"Only show latest video from channel: ": "Kun vis siste video fra kanal: ",
|
"Only show latest video from channel: ": "Kun vis siste video fra kanal: ",
|
||||||
"Only show latest unwatched video from channel: ": "Kun vis siste usette video fra kanal: ",
|
"Only show latest unwatched video from channel: ": "Kun vis siste usette video fra kanal: ",
|
||||||
"Only show unwatched: ": "Kun vis usette: ",
|
"Only show unwatched: ": "Kun vis usette: ",
|
||||||
"Only show notifications (if there are any): ": "Kun vis merknader (hvis det er noen): ",
|
"Only show notifications (if there are any): ": "Kun vis merknader (hvis det er noen): ",
|
||||||
"Data preferences": "Datainnstillinger",
|
"Data preferences": "Datainnstillinger",
|
||||||
"Clear watch history": "Tøm visningshistorikk",
|
"Clear watch history": "Tøm visningshistorikk",
|
||||||
"Import/Export data": "Importer/eksporter data",
|
"Import/Export data": "Importer/eksporter data",
|
||||||
"Manage subscriptions": "Behandle abonnementer",
|
"Manage subscriptions": "Behandle abonnementer",
|
||||||
"Watch history": "Visningshistorikk",
|
"Watch history": "Visningshistorikk",
|
||||||
"Delete account": "Slett konto",
|
"Delete account": "Slett konto",
|
||||||
"Save preferences": "Lagre innstillinger",
|
"Administrator preferences": "",
|
||||||
"Subscription manager": "Abonnementsbehandler",
|
"Default homepage: ": "",
|
||||||
"`x` subscriptions": "`x` abonnementer",
|
"Feed menu: ": "",
|
||||||
"Import/Export": "Importer/eksporter",
|
"Top enabled? ": "",
|
||||||
"unsubscribe": "opphev abonnement",
|
"CAPTCHA enabled? ": "",
|
||||||
"Subscriptions": "Abonnement",
|
"Login enabled? ": "",
|
||||||
"`x` unseen notifications": "`x` usette merknader",
|
"Registration enabled? ": "",
|
||||||
"search": "søk",
|
"Save preferences": "Lagre innstillinger",
|
||||||
"Sign out": "Logg ut",
|
"Subscription manager": "Abonnementsbehandler",
|
||||||
"Released under the AGPLv3 by Omar Roth.": "Utgitt med AGPLv3+lisens av Omar Roth.",
|
"`x` subscriptions": "`x` abonnementer",
|
||||||
"Source available here.": "Kildekode tilgjengelig her.",
|
"Import/Export": "Importer/eksporter",
|
||||||
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
|
"unsubscribe": "opphev abonnement",
|
||||||
"Trending": "Trendsettende",
|
"Subscriptions": "Abonnement",
|
||||||
"Watch video on Youtube": "Vis video på YouTube",
|
"`x` unseen notifications": "`x` usette merknader",
|
||||||
"Genre: ": "Sjanger: ",
|
"search": "søk",
|
||||||
"License: ": "Lisens: ",
|
"Sign out": "Logg ut",
|
||||||
"Family friendly? ": "Familievennlig? ",
|
"Released under the AGPLv3 by Omar Roth.": "Utgitt med AGPLv3+lisens av Omar Roth.",
|
||||||
"Wilson score: ": "Wilson-poengsum: ",
|
"Source available here.": "Kildekode tilgjengelig her.",
|
||||||
"Engagement: ": "Engasjement: ",
|
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
|
||||||
"Whitelisted regions: ": "Hvitlistede regioner: ",
|
"Trending": "Trendsettende",
|
||||||
"Blacklisted regions: ": "Svartelistede regioner: ",
|
"Watch video on Youtube": "Vis video på YouTube",
|
||||||
"Shared `x`": "Delt `x`",
|
"Genre: ": "Sjanger: ",
|
||||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Hei. Det ser ut til at du har JavaScript avslått. Klikk her for å vise kommentarer, ha i minnet at innlasting tar lengre tid.",
|
"License: ": "Lisens: ",
|
||||||
"View YouTube comments": "Vis YouTube-kommentarer",
|
"Family friendly? ": "Familievennlig? ",
|
||||||
"View more comments on Reddit": "Vis flere kommenterer på Reddit",
|
"Wilson score: ": "Wilson-poengsum: ",
|
||||||
"View `x` comments": "Vis `x` kommentarer",
|
"Engagement: ": "Engasjement: ",
|
||||||
"View Reddit comments": "Vis Reddit-kommentarer",
|
"Whitelisted regions: ": "Hvitlistede regioner: ",
|
||||||
"Hide replies": "Skjul svar",
|
"Blacklisted regions: ": "Svartelistede regioner: ",
|
||||||
"Show replies": "Vis svar",
|
"Shared `x`": "Delt `x`",
|
||||||
"Incorrect password": "Feil passord",
|
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Hei. Det ser ut til at du har JavaScript avslått. Klikk her for å vise kommentarer, ha i minnet at innlasting tar lengre tid.",
|
||||||
"Quota exceeded, try again in a few hours": "Kvote overskredet, prøv igjen om et par timer",
|
"View YouTube comments": "Vis YouTube-kommentarer",
|
||||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Kunne ikke logge inn, forsikre deg om at tofaktor-identitetsbekreftelse (Authenticator eller SMS) er skrudd på.",
|
"View more comments on Reddit": "Vis flere kommenterer på Reddit",
|
||||||
"Invalid TFA code": "Ugyldig tofaktorkode",
|
"View `x` comments": "Vis `x` kommentarer",
|
||||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Innlogging mislyktes. Dette kan være fordi tofaktor-identitetsbekreftelse er skrudd av på kontoen din.",
|
"View Reddit comments": "Vis Reddit-kommentarer",
|
||||||
"Invalid answer": "Ugyldig svar",
|
"Hide replies": "Skjul svar",
|
||||||
"Invalid CAPTCHA": "Ugyldig CAPTCHA",
|
"Show replies": "Vis svar",
|
||||||
"CAPTCHA is a required field": "CAPTCHA er et påkrevd felt",
|
"Incorrect password": "Feil passord",
|
||||||
"User ID is a required field": "Bruker-ID er et påkrevd felt",
|
"Quota exceeded, try again in a few hours": "Kvote overskredet, prøv igjen om et par timer",
|
||||||
"Password is a required field": "Passord er et påkrevd felt",
|
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Kunne ikke logge inn, forsikre deg om at tofaktor-identitetsbekreftelse (Authenticator eller SMS) er skrudd på.",
|
||||||
"Invalid username or password": "Ugyldig brukernavn eller passord",
|
"Invalid TFA code": "Ugyldig tofaktorkode",
|
||||||
"Please sign in using 'Sign in with Google'": "Logg inn ved bruk av \"Google-innlogging\"",
|
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Innlogging mislyktes. Dette kan være fordi tofaktor-identitetsbekreftelse er skrudd av på kontoen din.",
|
||||||
"Password cannot be empty": "Passordet kan ikke være tomt",
|
"Invalid answer": "Ugyldig svar",
|
||||||
"Password cannot be longer than 55 characters": "Passordet kan ikke være lengre enn 55 tegn",
|
"Invalid CAPTCHA": "Ugyldig CAPTCHA",
|
||||||
"Please sign in": "Logg inn",
|
"CAPTCHA is a required field": "CAPTCHA er et påkrevd felt",
|
||||||
"Invidious Private Feed for `x`": "Ugyldig privat flyt for `x`",
|
"User ID is a required field": "Bruker-ID er et påkrevd felt",
|
||||||
"channel:`x`": "kanal `x`",
|
"Password is a required field": "Passord er et påkrevd felt",
|
||||||
"Deleted or invalid channel": "Slettet eller ugyldig kanal",
|
"Invalid username or password": "Ugyldig brukernavn eller passord",
|
||||||
"This channel does not exist.": "Denne kanalen finnes ikke.",
|
"Please sign in using 'Sign in with Google'": "Logg inn ved bruk av \"Google-innlogging\"",
|
||||||
"Could not get channel info.": "Kunne ikke innhente kanalinfo.",
|
"Password cannot be empty": "Passordet kan ikke være tomt",
|
||||||
"Could not fetch comments": "Kunne ikke hente kommentarer",
|
"Password cannot be longer than 55 characters": "Passordet kan ikke være lengre enn 55 tegn",
|
||||||
"View `x` replies": "Vis `x` svar",
|
"Please sign in": "Logg inn",
|
||||||
"`x` ago": "`x` siden",
|
"Invidious Private Feed for `x`": "Ugyldig privat flyt for `x`",
|
||||||
"Load more": "Last inn flere",
|
"channel:`x`": "kanal `x`",
|
||||||
"`x` points": "`x` poeng",
|
"Deleted or invalid channel": "Slettet eller ugyldig kanal",
|
||||||
"Could not create mix.": "Kunne ikke opprette miks.",
|
"This channel does not exist.": "Denne kanalen finnes ikke.",
|
||||||
"Playlist is empty": "Spillelisten er tom",
|
"Could not get channel info.": "Kunne ikke innhente kanalinfo.",
|
||||||
"Invalid playlist.": "Ugyldig spilleliste.",
|
"Could not fetch comments": "Kunne ikke hente kommentarer",
|
||||||
"Playlist does not exist.": "Spillelisten finnes ikke.",
|
"View `x` replies": "Vis `x` svar",
|
||||||
"Could not pull trending pages.": "Kunne ikke hente trendsettende sider.",
|
"`x` ago": "`x` siden",
|
||||||
"Hidden field \"challenge\" is a required field": "Skjult felt \"utfordring\" er et påkrevd felt",
|
"Load more": "Last inn flere",
|
||||||
"Hidden field \"token\" is a required field": "Skjult felt \"symbol\" er et påkrevd felt",
|
"`x` points": "`x` poeng",
|
||||||
"Invalid challenge": "Ugyldig utfordring",
|
"Could not create mix.": "Kunne ikke opprette miks.",
|
||||||
"Invalid token": "Ugyldig symbol",
|
"Playlist is empty": "Spillelisten er tom",
|
||||||
"Invalid user": "Ugyldig bruker",
|
"Invalid playlist.": "Ugyldig spilleliste.",
|
||||||
"Token is expired, please try again": "Symbol utløpt, prøv igjen",
|
"Playlist does not exist.": "Spillelisten finnes ikke.",
|
||||||
"English": "Engelsk",
|
"Could not pull trending pages.": "Kunne ikke hente trendsettende sider.",
|
||||||
"English (auto-generated)": "Engelsk (auto-generert)",
|
"Hidden field \"challenge\" is a required field": "Skjult felt \"utfordring\" er et påkrevd felt",
|
||||||
"Afrikaans": "",
|
"Hidden field \"token\" is a required field": "Skjult felt \"symbol\" er et påkrevd felt",
|
||||||
"Albanian": "Albansk",
|
"Invalid challenge": "Ugyldig utfordring",
|
||||||
"Amharic": "",
|
"Invalid token": "Ugyldig symbol",
|
||||||
"Arabic": "Arabisk",
|
"Invalid user": "Ugyldig bruker",
|
||||||
"Armenian": "Armensk",
|
"Token is expired, please try again": "Symbol utløpt, prøv igjen",
|
||||||
"Azerbaijani": "",
|
"English": "Engelsk",
|
||||||
"Bangla": "",
|
"English (auto-generated)": "Engelsk (auto-generert)",
|
||||||
"Basque": "",
|
"Afrikaans": "",
|
||||||
"Belarusian": "Hviterussisk",
|
"Albanian": "Albansk",
|
||||||
"Bosnian": "Bosnisk",
|
"Amharic": "",
|
||||||
"Bulgarian": "Bulgarsk",
|
"Arabic": "Arabisk",
|
||||||
"Burmese": "Burmesisk",
|
"Armenian": "Armensk",
|
||||||
"Catalan": "Katalansk",
|
"Azerbaijani": "",
|
||||||
"Cebuano": "",
|
"Bangla": "",
|
||||||
"Chinese (Simplified)": "",
|
"Basque": "",
|
||||||
"Chinese (Traditional)": "",
|
"Belarusian": "Hviterussisk",
|
||||||
"Corsican": "",
|
"Bosnian": "Bosnisk",
|
||||||
"Croatian": "",
|
"Bulgarian": "Bulgarsk",
|
||||||
"Czech": "Tsjekkisk",
|
"Burmese": "Burmesisk",
|
||||||
"Danish": "Dansk",
|
"Catalan": "Katalansk",
|
||||||
"Dutch": "",
|
"Cebuano": "",
|
||||||
"Esperanto": "Esperanto",
|
"Chinese (Simplified)": "",
|
||||||
"Estonian": "",
|
"Chinese (Traditional)": "",
|
||||||
"Filipino": "",
|
"Corsican": "",
|
||||||
"Finnish": "Finsk",
|
"Croatian": "",
|
||||||
"French": "Fransk",
|
"Czech": "Tsjekkisk",
|
||||||
"Galician": "",
|
"Danish": "Dansk",
|
||||||
"Georgian": "",
|
"Dutch": "",
|
||||||
"German": "",
|
"Esperanto": "Esperanto",
|
||||||
"Greek": "",
|
"Estonian": "",
|
||||||
"Gujarati": "",
|
"Filipino": "",
|
||||||
"Haitian Creole": "",
|
"Finnish": "Finsk",
|
||||||
"Hausa": "",
|
"French": "Fransk",
|
||||||
"Hawaiian": "",
|
"Galician": "",
|
||||||
"Hebrew": "",
|
"Georgian": "",
|
||||||
"Hindi": "",
|
"German": "",
|
||||||
"Hmong": "",
|
"Greek": "",
|
||||||
"Hungarian": "Ungarsk",
|
"Gujarati": "",
|
||||||
"Icelandic": "Islandsk",
|
"Haitian Creole": "",
|
||||||
"Igbo": "",
|
"Hausa": "",
|
||||||
"Indonesian": "Indonesisk",
|
"Hawaiian": "",
|
||||||
"Irish": "Irsk",
|
"Hebrew": "",
|
||||||
"Italian": "Italiensk",
|
"Hindi": "",
|
||||||
"Japanese": "Japansk",
|
"Hmong": "",
|
||||||
"Javanese": "",
|
"Hungarian": "Ungarsk",
|
||||||
"Kannada": "",
|
"Icelandic": "Islandsk",
|
||||||
"Kazakh": "",
|
"Igbo": "",
|
||||||
"Khmer": "",
|
"Indonesian": "Indonesisk",
|
||||||
"Korean": "",
|
"Irish": "Irsk",
|
||||||
"Kurdish": "",
|
"Italian": "Italiensk",
|
||||||
"Kyrgyz": "",
|
"Japanese": "Japansk",
|
||||||
"Lao": "",
|
"Javanese": "",
|
||||||
"Latin": "",
|
"Kannada": "",
|
||||||
"Latvian": "",
|
"Kazakh": "",
|
||||||
"Lithuanian": "",
|
"Khmer": "",
|
||||||
"Luxembourgish": "",
|
"Korean": "",
|
||||||
"Macedonian": "",
|
"Kurdish": "",
|
||||||
"Malagasy": "",
|
"Kyrgyz": "",
|
||||||
"Malay": "",
|
"Lao": "",
|
||||||
"Malayalam": "",
|
"Latin": "",
|
||||||
"Maltese": "",
|
"Latvian": "",
|
||||||
"Maori": "",
|
"Lithuanian": "",
|
||||||
"Marathi": "",
|
"Luxembourgish": "",
|
||||||
"Mongolian": "",
|
"Macedonian": "",
|
||||||
"Nepali": "",
|
"Malagasy": "",
|
||||||
"Norwegian": "Norsk bokmål",
|
"Malay": "",
|
||||||
"Nyanja": "",
|
"Malayalam": "",
|
||||||
"Pashto": "",
|
"Maltese": "",
|
||||||
"Persian": "",
|
"Maori": "",
|
||||||
"Polish": "",
|
"Marathi": "",
|
||||||
"Portuguese": "",
|
"Mongolian": "",
|
||||||
"Punjabi": "",
|
"Nepali": "",
|
||||||
"Romanian": "",
|
"Norwegian": "Norsk bokmål",
|
||||||
"Russian": "Russisk",
|
"Nyanja": "",
|
||||||
"Samoan": "",
|
"Pashto": "",
|
||||||
"Scottish Gaelic": "",
|
"Persian": "",
|
||||||
"Serbian": "Serbisk",
|
"Polish": "",
|
||||||
"Shona": "",
|
"Portuguese": "",
|
||||||
"Sindhi": "",
|
"Punjabi": "",
|
||||||
"Sinhala": "",
|
"Romanian": "",
|
||||||
"Slovak": "Slovakisk",
|
"Russian": "Russisk",
|
||||||
"Slovenian": "Slovensk",
|
"Samoan": "",
|
||||||
"Somali": "Somali",
|
"Scottish Gaelic": "",
|
||||||
"Southern Sotho": "",
|
"Serbian": "Serbisk",
|
||||||
"Spanish": "Spansk",
|
"Shona": "",
|
||||||
"Spanish (Latin America)": "",
|
"Sindhi": "",
|
||||||
"Sundanese": "",
|
"Sinhala": "",
|
||||||
"Swahili": "",
|
"Slovak": "Slovakisk",
|
||||||
"Swedish": "Svensk",
|
"Slovenian": "Slovensk",
|
||||||
"Tajik": "",
|
"Somali": "Somali",
|
||||||
"Tamil": "",
|
"Southern Sotho": "",
|
||||||
"Telugu": "",
|
"Spanish": "Spansk",
|
||||||
"Thai": "",
|
"Spanish (Latin America)": "",
|
||||||
"Turkish": "Tyrkisk",
|
"Sundanese": "",
|
||||||
"Ukrainian": "Ukrainsk",
|
"Swahili": "",
|
||||||
"Urdu": "",
|
"Swedish": "Svensk",
|
||||||
"Uzbek": "",
|
"Tajik": "",
|
||||||
"Vietnamese": "Vietnamesisk",
|
"Tamil": "",
|
||||||
"Welsh": "",
|
"Telugu": "",
|
||||||
"Western Frisian": "",
|
"Thai": "",
|
||||||
"Xhosa": "",
|
"Turkish": "Tyrkisk",
|
||||||
"Yiddish": "",
|
"Ukrainian": "Ukrainsk",
|
||||||
"Yoruba": "",
|
"Urdu": "",
|
||||||
"Zulu": "",
|
"Uzbek": "",
|
||||||
"`x` years": "`x` år",
|
"Vietnamese": "Vietnamesisk",
|
||||||
"`x` months": "`x` måneder",
|
"Welsh": "",
|
||||||
"`x` weeks": "`x` uker",
|
"Western Frisian": "",
|
||||||
"`x` days": "`x` dager",
|
"Xhosa": "",
|
||||||
"`x` hours": "`x` timer",
|
"Yiddish": "",
|
||||||
"`x` minutes": "`x` minutter",
|
"Yoruba": "",
|
||||||
"`x` seconds": "`x` sekunder",
|
"Zulu": "",
|
||||||
"Fallback comments: ": "Tilbakefallskommentarer: ",
|
"`x` years": "`x` år",
|
||||||
"Popular": "Pupulært",
|
"`x` months": "`x` måneder",
|
||||||
"Top": "Topp",
|
"`x` weeks": "`x` uker",
|
||||||
"About": "Om",
|
"`x` days": "`x` dager",
|
||||||
"Rating: ": "Vurdering: ",
|
"`x` hours": "`x` timer",
|
||||||
"Language: ": "Språk: ",
|
"`x` minutes": "`x` minutter",
|
||||||
"Default": "Forvalg",
|
"`x` seconds": "`x` sekunder",
|
||||||
"Music": "Musikk",
|
"Fallback comments: ": "Tilbakefallskommentarer: ",
|
||||||
"Gaming": "Spill",
|
"Popular": "Pupulært",
|
||||||
"News": "Nyheter",
|
"Top": "Topp",
|
||||||
"Movies": "Filmer",
|
"About": "Om",
|
||||||
"Download": "Last ned",
|
"Rating: ": "Vurdering: ",
|
||||||
"Download as: ": "Last ned som: ",
|
"Language: ": "Språk: ",
|
||||||
"%A %B %-d, %Y": "",
|
"Default": "Forvalg",
|
||||||
"(edited)": "(redigert)",
|
"Music": "Musikk",
|
||||||
"Youtube permalink of the comment": "Permanent YouTube-lenke til innholdet",
|
"Gaming": "Spill",
|
||||||
"`x` marked it with a ❤": "`x` levnet et ❤",
|
"News": "Nyheter",
|
||||||
"Audio mode": "Lydmodus",
|
"Movies": "Filmer",
|
||||||
"Video mode": "Video-modus"
|
"Download": "Last ned",
|
||||||
|
"Download as: ": "Last ned som: ",
|
||||||
|
"%A %B %-d, %Y": "",
|
||||||
|
"(edited)": "(redigert)",
|
||||||
|
"Youtube permalink of the comment": "Permanent YouTube-lenke til innholdet",
|
||||||
|
"`x` marked it with a ❤": "`x` levnet et ❤",
|
||||||
|
"Audio mode": "Lydmodus",
|
||||||
|
"Video mode": "Video-modus"
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,13 @@
|
||||||
"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? ": "",
|
||||||
"Save preferences": "Opslaan voorkeuren",
|
"Save preferences": "Opslaan voorkeuren",
|
||||||
"Subscription manager": "Abonnees beheerder",
|
"Subscription manager": "Abonnees beheerder",
|
||||||
"`x` subscriptions": "`x` abonnees",
|
"`x` subscriptions": "`x` abonnees",
|
||||||
|
|
|
@ -80,6 +80,13 @@
|
||||||
"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? ": "",
|
||||||
"Save preferences": "Zapisz preferencje",
|
"Save preferences": "Zapisz preferencje",
|
||||||
"Subscription manager": "Manager subskrybcji",
|
"Subscription manager": "Manager subskrybcji",
|
||||||
"`x` subscriptions": "`x` subskrybcji",
|
"`x` subscriptions": "`x` subskrybcji",
|
||||||
|
|
|
@ -82,6 +82,13 @@
|
||||||
"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? ": "",
|
||||||
"Save preferences": "Сохранить настройки",
|
"Save preferences": "Сохранить настройки",
|
||||||
"Subscription manager": "Менеджер подписок",
|
"Subscription manager": "Менеджер подписок",
|
||||||
"`x` subscriptions": "`x` подписок",
|
"`x` subscriptions": "`x` подписок",
|
||||||
|
|
241
src/invidious.cr
241
src/invidious.cr
|
@ -31,42 +31,38 @@ 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.random_bytes(32)
|
||||||
|
|
||||||
crawl_threads = CONFIG.crawl_threads
|
config = CONFIG
|
||||||
channel_threads = CONFIG.channel_threads
|
|
||||||
feed_threads = CONFIG.feed_threads
|
|
||||||
video_threads = CONFIG.video_threads
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -107,28 +103,30 @@ 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|
|
config.video_threads.times do |i|
|
||||||
spawn do
|
spawn do
|
||||||
refresh_videos(PG_DB, logger)
|
refresh_videos(PG_DB, logger)
|
||||||
end
|
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
|
||||||
top_videos = videos
|
pull_top_videos(config, PG_DB) do |videos|
|
||||||
sleep 1.minutes
|
top_videos = videos
|
||||||
Fiber.yield
|
sleep 1.minutes
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -231,7 +229,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 +378,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
host_params = env.request.query_params
|
host_params = env.request.query_params
|
||||||
host_params.delete_all("v")
|
host_params.delete_all("v")
|
||||||
|
|
||||||
|
@ -467,7 +478,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
host_params = env.request.query_params
|
host_params = env.request.query_params
|
||||||
host_params.delete_all("v")
|
host_params.delete_all("v")
|
||||||
|
|
||||||
|
@ -553,7 +564,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
|
|
||||||
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 +689,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 +732,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 +897,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,54 +933,56 @@ 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 answer
|
if config.captcha_enabled
|
||||||
answer = answer.lstrip('0')
|
if answer
|
||||||
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
answer = answer.lstrip('0')
|
||||||
|
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
||||||
|
|
||||||
challenge = env.params.body["challenge"]?
|
challenge = env.params.body["challenge"]?
|
||||||
token = env.params.body["token"]?
|
token = env.params.body["token"]?
|
||||||
|
|
||||||
begin
|
|
||||||
validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB, locale)
|
|
||||||
rescue ex
|
|
||||||
if ex.message == translate(locale, "Invalid user")
|
|
||||||
error_message = translate(locale, "Invalid answer")
|
|
||||||
else
|
|
||||||
error_message = ex.message
|
|
||||||
end
|
|
||||||
|
|
||||||
next templated "error"
|
|
||||||
end
|
|
||||||
elsif text_answer
|
|
||||||
text_answer = Digest::MD5.hexdigest(text_answer.downcase.strip)
|
|
||||||
|
|
||||||
challenges = env.params.body.select { |k, v| k.match(/text_challenge\d+/) }
|
|
||||||
tokens = env.params.body.select { |k, v| k.match(/text_token\d+/) }
|
|
||||||
|
|
||||||
found_valid_captcha = false
|
|
||||||
|
|
||||||
error_message = translate(locale, "Invalid CAPTCHA")
|
|
||||||
challenges.each_with_index do |challenge, i|
|
|
||||||
begin
|
begin
|
||||||
challenge = challenge[1]
|
validate_response(challenge, token, answer, "sign_in", HMAC_KEY, PG_DB, locale)
|
||||||
token = tokens[i][1]
|
|
||||||
validate_response(challenge, token, text_answer, "sign_in", HMAC_KEY, PG_DB, locale)
|
|
||||||
found_valid_captcha = true
|
|
||||||
rescue ex
|
rescue ex
|
||||||
if ex.message == translate(locale, "Invalid user")
|
if ex.message == translate(locale, "Invalid user")
|
||||||
error_message = translate(locale, "Invalid answer")
|
error_message = translate(locale, "Invalid answer")
|
||||||
else
|
else
|
||||||
error_message = ex.message
|
error_message = ex.message
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if !found_valid_captcha
|
next templated "error"
|
||||||
|
end
|
||||||
|
elsif text_answer
|
||||||
|
text_answer = Digest::MD5.hexdigest(text_answer.downcase.strip)
|
||||||
|
|
||||||
|
challenges = env.params.body.select { |k, v| k.match(/text_challenge\d+/) }
|
||||||
|
tokens = env.params.body.select { |k, v| k.match(/text_token\d+/) }
|
||||||
|
|
||||||
|
found_valid_captcha = false
|
||||||
|
|
||||||
|
error_message = translate(locale, "Invalid CAPTCHA")
|
||||||
|
challenges.each_with_index do |challenge, i|
|
||||||
|
begin
|
||||||
|
challenge = challenge[1]
|
||||||
|
token = tokens[i][1]
|
||||||
|
validate_response(challenge, token, text_answer, "sign_in", HMAC_KEY, PG_DB, locale)
|
||||||
|
found_valid_captcha = true
|
||||||
|
rescue ex
|
||||||
|
if ex.message == translate(locale, "Invalid user")
|
||||||
|
error_message = translate(locale, "Invalid answer")
|
||||||
|
else
|
||||||
|
error_message = ex.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if !found_valid_captcha
|
||||||
|
next templated "error"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
error_message = translate(locale, "CAPTCHA is a required field")
|
||||||
next templated "error"
|
next templated "error"
|
||||||
end
|
end
|
||||||
else
|
|
||||||
error_message = translate(locale, "CAPTCHA is a required field")
|
|
||||||
next templated "error"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
action = env.params.body["action"]?
|
action = env.params.body["action"]?
|
||||||
|
@ -992,14 +1015,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 +1039,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 +1077,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 +1181,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 +1253,37 @@ 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"
|
||||||
|
|
||||||
|
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 +1457,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
|
|
||||||
if format == "json"
|
if format == "json"
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
|
@ -1741,7 +1801,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)]?
|
||||||
|
|
||||||
templated "top"
|
if config.top_enabled
|
||||||
|
templated "top"
|
||||||
|
else
|
||||||
|
env.redirect "/"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/feed/popular" do |env|
|
get "/feed/popular" do |env|
|
||||||
|
@ -1984,7 +2048,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
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|
|
||||||
|
@ -2118,7 +2182,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
path = env.request.path
|
path = env.request.path
|
||||||
query = env.request.query.not_nil!
|
query = env.request.query.not_nil!
|
||||||
|
|
||||||
|
@ -2173,7 +2237,7 @@ get "/feed/playlist/:plid" do |env|
|
||||||
|
|
||||||
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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
path = env.request.path
|
path = env.request.path
|
||||||
|
|
||||||
client = make_client(YT_URL)
|
client = make_client(YT_URL)
|
||||||
|
@ -2487,7 +2551,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
|
||||||
|
@ -2653,7 +2717,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
|
|
||||||
host_params = env.request.query_params
|
host_params = env.request.query_params
|
||||||
host_params.delete_all("v")
|
host_params.delete_all("v")
|
||||||
|
@ -2871,6 +2935,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|
|
||||||
|
@ -3842,7 +3911,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
|
|
||||||
manifest = manifest.body
|
manifest = manifest.body
|
||||||
manifest.gsub("https://www.youtube.com", host_url)
|
manifest.gsub("https://www.youtube.com", host_url)
|
||||||
|
@ -3856,7 +3925,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(Kemal.config.ssl || config.https_only, config.domain)
|
||||||
|
|
||||||
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)
|
||||||
|
|
|
@ -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,18 @@ 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
|
full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
|
||||||
https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
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
|
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
|
domain: String, # Domain to be used for links to resources on the site where an absolute URL is required
|
||||||
domain: String, # Domain to be used for links to resources on the site where an absolute URL is required
|
dl_api_key: String?, # DetectLanguage API Key (used to filter non-English results from "top" page), mostly non-functional
|
||||||
|
default_home: {type: String, default: "Top"},
|
||||||
|
feed_menu: {type: Array(String), default: ["Popular", "Top", "Trending"]},
|
||||||
|
top_enabled: {type: Bool, default: true},
|
||||||
|
captcha_enabled: {type: Bool, default: true},
|
||||||
|
login_enabled: {type: Bool, default: true},
|
||||||
|
registration_enabled: {type: Bool, default: true},
|
||||||
|
admins: {type: Array(String), default: [] of String},
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -133,7 +133,7 @@ def refresh_feeds(db, logger, max_threads = 1)
|
||||||
rescue ex
|
rescue ex
|
||||||
# Create view if it doesn't exist
|
# Create view if it doesn't exist
|
||||||
if ex.message.try &.ends_with? "does not exist"
|
if ex.message.try &.ends_with? "does not exist"
|
||||||
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'#{email.gsub("'", "\\'")}')::text[]) \
|
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{email.gsub("'", "\\'")}')::text[]) \
|
||||||
ORDER BY published DESC;")
|
ORDER BY published DESC;")
|
||||||
|
@ -193,11 +193,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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
<div class="pure-u-1 pure-u-md-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-2 pure-u-md-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>
|
||||||
|
|
|
@ -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>
|
|
|
@ -27,36 +27,40 @@
|
||||||
|
|
||||||
<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 captcha_type == "image" %>
|
|
||||||
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
|
|
||||||
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
|
|
||||||
<input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>">
|
|
||||||
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
|
|
||||||
<input required type="text" name="answer" type="text" placeholder="h:mm:ss">
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text">
|
|
||||||
<%= translate(locale, "Text CAPTCHA") %>
|
|
||||||
</a>
|
|
||||||
</label>
|
|
||||||
<% else %>
|
|
||||||
<% text_captcha.not_nil![:tokens].each_with_index do |token, i| %>
|
|
||||||
<input type="hidden" name="text_challenge<%= i %>" value="<%= token[0] %>">
|
|
||||||
<input type="hidden" name="text_token<%= i %>" value="<%= token[1] %>">
|
|
||||||
<% end %>
|
|
||||||
<label for="text_answer"><%= text_captcha.not_nil![:question] %></label>
|
|
||||||
<input required type="text" name="text_answer" type="text" placeholder="Answer">
|
|
||||||
|
|
||||||
<label>
|
<% if config.captcha_enabled %>
|
||||||
<a href="/login?referer=<%= URI.escape(referer) %>&type=invidious">
|
<% if captcha_type == "image" %>
|
||||||
<%= translate(locale, "Image CAPTCHA") %>
|
<img style="width:100%" src='<%= captcha.not_nil![:image] %>'/>
|
||||||
</a>
|
<input type="hidden" name="token" value="<%= captcha.not_nil![:token] %>">
|
||||||
</label>
|
<input type="hidden" name="challenge" value="<%= captcha.not_nil![:challenge] %>">
|
||||||
|
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
|
||||||
|
<input required type="text" name="answer" type="text" placeholder="h:mm:ss">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<a href="/login?referer=<%= URI.escape(referer) %>&type=invidious&captcha=text">
|
||||||
|
<%= translate(locale, "Text CAPTCHA") %>
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
<% else %>
|
||||||
|
<% text_captcha.not_nil![:tokens].each_with_index do |token, i| %>
|
||||||
|
<input type="hidden" name="text_challenge<%= i %>" value="<%= token[0] %>">
|
||||||
|
<input type="hidden" name="text_token<%= i %>" value="<%= token[1] %>">
|
||||||
|
<% end %>
|
||||||
|
<label for="text_answer"><%= text_captcha.not_nil![:question] %></label>
|
||||||
|
<input required type="text" name="text_answer" type="text" placeholder="Answer">
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<a href="/login?referer=<%= URI.escape(referer) %>&type=invidious">
|
||||||
|
<%= translate(locale, "Image CAPTCHA") %>
|
||||||
|
</a>
|
||||||
|
</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" %>
|
||||||
|
@ -67,7 +71,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 tfa %>
|
<% if tfa %>
|
||||||
<label for="tfa"><%= translate(locale, "Google verification code:") %></label>
|
<label for="tfa"><%= translate(locale, "Google verification code:") %></label>
|
||||||
<input required class="pure-input-1" name="tfa" type="text" placeholder="Google verification code">
|
<input required class="pure-input-1" name="tfa" type="text" placeholder="Google verification code">
|
||||||
|
|
|
@ -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,13 +147,57 @@ 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>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<% if env.get? "user" %>
|
<% if env.get? "user" %>
|
||||||
<legend><%= translate(locale, "Data preferences") %></legend>
|
<legend><%= translate(locale, "Data preferences") %></legend>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<a href="/clear_watch_history?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Clear watch history") %></a>
|
<a href="/clear_watch_history?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Clear watch history") %></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<a href="/data_control?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Import/Export data") %></a>
|
<a href="/data_control?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Import/Export data") %></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -89,12 +89,14 @@
|
||||||
<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 %>
|
||||||
|
|
|
@ -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" %>
|
||||||
|
|
Loading…
Reference in a new issue