Merge branch 'master' into 347-screenshots

pull/353/head
Omar Roth 6 years ago committed by GitHub
commit 6a8a49d8ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -43,6 +43,8 @@ Onion links:
## Installation ## Installation
See [Invidious-Updater](https://github.com/tmiland/Invidious-Updater) for a self-contained script that can automatically install and update Invidious.
### Docker: ### Docker:
#### Build and start cluster: #### Build and start cluster:
@ -105,6 +107,7 @@ $ psql invidious < /home/invidious/invidious/config/sql/channels.sql
$ psql invidious < /home/invidious/invidious/config/sql/videos.sql $ psql invidious < /home/invidious/invidious/config/sql/videos.sql
$ psql invidious < /home/invidious/invidious/config/sql/channel_videos.sql $ psql invidious < /home/invidious/invidious/config/sql/channel_videos.sql
$ psql invidious < /home/invidious/invidious/config/sql/users.sql $ psql invidious < /home/invidious/invidious/config/sql/users.sql
$ psql invidious < /home/invidious/invidious/config/sql/session_ids.sql
$ psql invidious < /home/invidious/invidious/config/sql/nonces.sql $ psql invidious < /home/invidious/invidious/config/sql/nonces.sql
$ exit $ exit
``` ```
@ -146,6 +149,7 @@ $ psql invidious < config/sql/channels.sql
$ psql invidious < config/sql/videos.sql $ psql invidious < config/sql/videos.sql
$ psql invidious < config/sql/channel_videos.sql $ psql invidious < config/sql/channel_videos.sql
$ psql invidious < config/sql/users.sql $ psql invidious < config/sql/users.sql
$ psql invidious < config/sql/session_ids.sql
$ psql invidious < config/sql/nonces.sql $ psql invidious < config/sql/nonces.sql
# Setup Invidious # Setup Invidious
@ -155,7 +159,7 @@ $ crystal build src/invidious.cr --release
## Update Invidious ## Update Invidious
You can find information about how to update in the wiki: [Updating](https://github.com/omarroth/invidious/wiki/Updating). You can see how to update Invidious [here](https://github.com/omarroth/invidious/wiki/Updating).
## Usage: ## Usage:
@ -192,13 +196,14 @@ $ ./sentry
## Extensions ## Extensions
Extensions for Invidious and for integrating Invidious into other projects [are in the wiki](https://github.com/omarroth/invidious/wiki/Extensions) [Extensions](https://github.com/omarroth/invidious/wiki/Extensions) can be found in the wiki, as well as documentation for integrating it into other projects.
## Made with Invidious ## Made with Invidious
- [FreeTube](https://github.com/FreeTubeApp/FreeTube): An Open Source YouTube app for privacy. - [FreeTube](https://github.com/FreeTubeApp/FreeTube): An Open Source YouTube app for privacy.
- [CloudTube](https://github.com/cloudrac3r/cadencegq): Website featuring pastebin, image host, and YouTube player - [CloudTube](https://github.com/cloudrac3r/cadencegq): Website featuring pastebin, image host, and YouTube player
- [PeerTubeify](https://gitlab.com/Ealhad/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists. - [PeerTubeify](https://gitlab.com/Ealhad/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists.
- [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A materialistic music player that streams music from YouTube.
## Contributing ## Contributing

@ -0,0 +1,5 @@
#!/bin/sh
psql invidious < config/sql/session_ids.sql
psql invidious -c "INSERT INTO session_ids (SELECT unnest(id), email, CURRENT_TIMESTAMP FROM users) ON CONFLICT (id) DO NOTHING"
psql invidious -c "ALTER TABLE users DROP COLUMN id"

@ -31,6 +31,6 @@ CREATE INDEX channel_videos_published_idx
CREATE INDEX channel_videos_ucid_idx CREATE INDEX channel_videos_ucid_idx
ON public.channel_videos ON public.channel_videos
USING hash USING btree
(ucid COLLATE pg_catalog."default"); (ucid COLLATE pg_catalog."default");

@ -5,10 +5,18 @@
CREATE TABLE public.nonces CREATE TABLE public.nonces
( (
nonce text, nonce text,
expire timestamp with time zone expire timestamp with time zone,
) CONSTRAINT nonces_id_key UNIQUE (nonce)
WITH (
OIDS=FALSE
); );
GRANT ALL ON TABLE public.nonces TO kemal; GRANT ALL ON TABLE public.nonces TO kemal;
-- Index: public.nonces_nonce_idx
-- DROP INDEX public.nonces_nonce_idx;
CREATE INDEX nonces_nonce_idx
ON public.nonces
USING btree
(nonce COLLATE pg_catalog."default");

@ -0,0 +1,23 @@
-- Table: public.session_ids
-- DROP TABLE public.session_ids;
CREATE TABLE public.session_ids
(
id text NOT NULL,
email text,
issued timestamp with time zone,
CONSTRAINT session_ids_pkey PRIMARY KEY (id)
);
GRANT ALL ON TABLE public.session_ids TO kemal;
-- Index: public.session_ids_id_idx
-- DROP INDEX public.session_ids_id_idx;
CREATE INDEX session_ids_id_idx
ON public.session_ids
USING btree
(id COLLATE pg_catalog."default");

@ -4,7 +4,6 @@
CREATE TABLE public.users CREATE TABLE public.users
( (
id text[] NOT NULL,
updated timestamp with time zone, updated timestamp with time zone,
notifications text[], notifications text[],
subscriptions text[], subscriptions text[],

@ -16,6 +16,7 @@ if [ ! -f /var/lib/postgresql/data/setupFinished ]; then
su postgres -c 'psql invidious < config/sql/videos.sql' su postgres -c 'psql invidious < config/sql/videos.sql'
su postgres -c 'psql invidious < config/sql/channel_videos.sql' su postgres -c 'psql invidious < config/sql/channel_videos.sql'
su postgres -c 'psql invidious < config/sql/users.sql' su postgres -c 'psql invidious < config/sql/users.sql'
su postgres -c 'psql invidious < config/sql/session_ids.sql'
su postgres -c 'psql invidious < config/sql/nonces.sql' su postgres -c 'psql invidious < config/sql/nonces.sql'
touch /var/lib/postgresql/data/setupFinished touch /var/lib/postgresql/data/setupFinished
echo "### invidious database setup finished" echo "### invidious database setup finished"

@ -1,152 +1,151 @@
{ {
"`x` subscribers": "`x` souscripteurs", "`x` subscribers": "`x` abonnés",
"`x` videos": "`x` vidéos", "`x` videos": "`x` vidéos",
"LIVE": "LIVE", "LIVE": "EN DIRECT",
"Shared `x` ago": "Partagé il y a `x`", "Shared `x` ago": "Partagé, il y a `x`",
"Unsubscribe": "Se désabonner", "Unsubscribe": "Se désabonner",
"Subscribe": "S'abonner", "Subscribe": "S'abonner",
"Login to subscribe to `x`": "Se connecter pour s'abonner à `x`", "Login to subscribe to `x`": "Vous devez vous connecter pour s'abonner à `x`",
"View channel on YouTube": "Voir la chaîne sur YouTube", "View channel on YouTube": "Voir la chaîne sur YouTube",
"newest": "récent", "newest": "Date d'ajout (la plus récente)",
"oldest": "aînée", "oldest": "Date d'ajout (la plus ancienne)",
"popular": "appréciés", "popular": "Les plus populaires",
"Preview page": "Page de prévisualisation",
"Next page": "Page suivante", "Next page": "Page suivante",
"Clear watch history?": "L'histoire de la montre est claire?", "Clear watch history?": "Êtes vous sûr de vouloir supprimer l'historique des vidéos regardées",
"Yes": "Oui", "Yes": "Oui",
"No": "Aucun", "No": "Non",
"Import and Export Data": "Importation et exportation de données", "Import and Export Data": "Importation et Exportation de Données",
"Import": "Importation", "Import": "Importer",
"Import Invidious data": "Importation de données invalides", "Import Invidious data": "Importer des données Invidious",
"Import YouTube subscriptions": "Importer des abonnements YouTube", "Import YouTube subscriptions": "Importer des abonnements YouTube",
"Import FreeTube subscriptions (.db)": "Importer des abonnements FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Importer des abonnements FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)",
"Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)", "Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)",
"Export": "Exporter", "Export": "Exporter",
"Export subscriptions as OPML": "Exporter les abonnements comme OPML", "Export subscriptions as OPML": "Exporter les abonnements en OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporter les abonnements comme OPML (pour NewPipe & FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporter les abonnements en OPML (pour NewPipe & FreeTube)",
"Export data as JSON": "Exporter les données au format JSON", "Export data as JSON": "Exporter les données au format JSON",
"Delete account?": "Supprimer un compte ?", "Delete account?": "Êtes-vous sûr de vouloir supprimer votre compte ?",
"History": "Histoire", "History": "Historique",
"Previous page": "Page précédente", "Previous page": "Page précédente",
"An alternative front-end to YouTube": "Un frontal alternatif à YouTube", "An alternative front-end to YouTube": "Un front-end alternatif à YouTube",
"JavaScript license information": "Informations sur la licence JavaScript", "JavaScript license information": "Informations sur les licences JavaScript",
"source": "origine", "source": "source",
"Login": "Connexion", "Login": "Connexion",
"Login/Register": "Connexion/S'inscrire", "Login/Register": "Connexion/S'inscrire",
"Login to Google": "Se connecter à Google", "Login to Google": "Se connecter à Google",
"User ID:": "ID utilisateur:", "User ID:": "Identifiant utilisateur :",
"Password:": "Mot de passe:", "Password:": "Mot de passe :",
"Time (h:mm:ss):": "Temps (h:mm:ss):", "Time (h:mm:ss):": "Heure (h:mm:ss):",
"Text CAPTCHA": "Texte CAPTCHA", "Text CAPTCHA": "CAPTCHA Texte",
"Image CAPTCHA": "Image CAPTCHA", "Image CAPTCHA": "CAPTCHA Image",
"Sign In": "S'identifier", "Sign In": "S'identifier",
"Register": "S'inscrire", "Register": "S'inscrire",
"Email:": "Courriel:", "Email:": "Email:",
"Google verification code:": "Code de vérification Google:", "Google verification code:": "Code de vérification Google :",
"Preferences": "Préférences", "Preferences": "Préférences",
"Player preferences": "Joueur préférences", "Player preferences": "Préférences du Lecteur",
"Always loop: ": "Toujours en boucle: ", "Always loop: ": "Lire en boucle: ",
"Autoplay: ": "Autoplay: ", "Autoplay: ": "Lire Automatiquement: ",
"Autoplay next video: ": "Lecture automatique de la vidéo suivante: ", "Autoplay next video: ": "Lire automatiquement la vidéo suivante : ",
"Listen by default: ": "Écouter par défaut: ", "Listen by default: ": "Audio Uniquement par défaut : ",
"Default speed: ": "Vitesse par défaut: ", "Default speed: ": "Vitesse par défaut: ",
"Preferred video quality: ": "Qualité vidéo préférée: ", "Preferred video quality: ": "Qualité vidéo souhaitée : ",
"Player volume: ": "Volume de lecteur: ", "Player volume: ": "Volume du lecteur: ",
"Default comments: ": "Commentaires par défaut: ", "Default comments: ": "Source des Commentaires : ",
"Default captions: ": "Légendes par défaut: ", "Default captions: ": "Sous-titres principal : ",
"Fallback captions: ": "Légendes de repli: ", "Fallback captions: ": "Sous-titre secondaire : ",
"Show related videos? ": "Voir les vidéos liées à ce sujet? ", "Show related videos? ": "Voir les vidéos liées à ce sujet? ",
"Visual preferences": "Préférences visuelles", "Visual preferences": "Préférences du site",
"Dark mode: ": "Mode sombre: ", "Dark mode: ": "Mode Sombre: ",
"Thin mode: ": "Mode Thin: ", "Thin mode: ": "Mode Simplifié: ",
"Subscription preferences": "Préférences d'abonnement", "Subscription preferences": "Préférences de la page d'abonnements",
"Redirect homepage to feed: ": "Rediriger la page d'accueil vers le flux: ", "Redirect homepage to feed: ": "Rediriger la page d'accueil vers la page d'abonnements : ",
"Number of videos shown in feed: ": "Nombre de vidéos montrées dans le flux: ", "Number of videos shown in feed: ": "Nombre de vidéos montrées dans la page d'abonnements : ",
"Sort videos by: ": "Trier les vidéos par: ", "Sort videos by: ": "Trier les vidéos par : ",
"published": "publié", "published": "publié",
"published - reverse": "publié - reverse", "published - reverse": "publié - inversé",
"alphabetically": "alphabétiquement", "alphabetically": "alphabétiquement",
"alphabetically - reverse": "alphabétiquement - contraire", "alphabetically - reverse": "alphabétiquement - inversé",
"channel name": "nom du canal", "channel name": "nom de la chaîne",
"channel name - reverse": "nom du canal - contraire", "channel name - reverse": "nom de la chaîne - inversé",
"Only show latest video from channel: ": "Afficher uniquement les dernières vidéos de la chaîne: ", "Only show latest video from channel: ": "Afficher uniquement la dernière vidéo de la chaîne : ",
"Only show latest unwatched video from channel: ": "Afficher uniquement les dernières vidéos non regardées de la chaîne: ", "Only show latest unwatched video from channel: ": "Afficher uniquement la dernière vidéo de la chaîne si elle n'a pas était regardée: ",
"Only show unwatched: ": "Afficher uniquement les images non surveillées: ", "Only show unwatched: ": "Afficher uniquement les vidéos regardées: ",
"Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a): ", "Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a) : ",
"Data preferences": "Préférences de données", "Data preferences": "Préférences liées aux données",
"Clear watch history": "Historique clair de la montre", "Clear watch history": "Supprimer l'historique des vidéos regardées",
"Import/Export data": "Données d'importation/exportation", "Import/Export data": "Importation/exportation de ",
"Manage subscriptions": "Gérer les abonnements", "Manage subscriptions": "Gérer les abonnements",
"Watch history": "Historique des montres", "Watch history": "Historique de visionnage",
"Delete account": "Supprimer un compte", "Delete account": "Supprimer votre compte",
"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",
"Import/Export": "Importer/Exporter", "Import/Export": "Importer/Exporter",
"unsubscribe": "se désabonner", "unsubscribe": "se désabonner",
"Subscriptions": "Abonnements", "Subscriptions": "Abonnements",
"`x` unseen notifications": "`x` notifications invisibles", "`x` unseen notifications": "`x` notifications non vues",
"search": "perquisition", "search": "Rechercher",
"Sign out": "Déconnexion", "Sign out": "Déconnexion",
"Released under the AGPLv3 by Omar Roth.": "Publié sous l'AGPLv3 par Omar Roth.", "Released under the AGPLv3 by Omar Roth.": "Publié sous licence AGPLv3 par Omar Roth.",
"Source available here.": "Source disponible ici.", "Source available here.": "Code Source",
"View JavaScript license information.": "Voir les informations de licence JavaScript.", "View JavaScript license information.": "Voir les informations des licences JavaScript.",
"Trending": "Tendances", "Trending": "Tendances",
"Watch video on Youtube": "Voir la vidéo sur Youtube", "Watch video on Youtube": "Voir la vidéo sur Youtube",
"Genre: ": "Genre: ", "Genre: ": "Genre: ",
"License: ": "Licence: ", "License: ": "Licence: ",
"Family friendly? ": "Convivialité familiale? ", "Family friendly? ": "Tout Public? ",
"Wilson score: ": "Wilson marque: ", "Wilson score: ": "Score de Wilson: ",
"Engagement: ": "Fiançailles: ", "Engagement: ": "Poucentage de spectateur aillant aimé Liker ou Disliker la vidéo : ",
"Whitelisted regions: ": "Régions en liste blanche: ", "Whitelisted regions: ": "Régions en liste blanche: ",
"Blacklisted regions: ": "Régions sur liste noire: ", "Blacklisted regions: ": "Régions sur liste noire: ",
"Shared `x`": "Partagée `x`", "Shared `x`": "Partagée `x`",
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Hi! On dirait que vous avez désactivé JavaScript. Cliquez ici pour voir les commentaires, gardez à l'esprit que le chargement peut prendre un peu plus de temps.", "Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Il semblerait que JavaScript sois désactivé. Cliquez ici pour voir les commentaires, gardez à l'esprit que le chargement peut prendre plus de temps.",
"View YouTube comments": "Voir les commentaires sur YouTube", "View YouTube comments": "Voir les commentaires YouTube",
"View more comments on Reddit": "Voir plus de commentaires sur Reddit", "View more comments on Reddit": "Voir plus de commentaires sur Reddit",
"View `x` comments": "Voir `x` commentaires", "View `x` comments": "Voir `x` commentaires",
"View Reddit comments": "Voir Reddit commentaires", "View Reddit comments": "Voir les commentaires Reddit",
"Hide replies": "Masquer les réponses", "Hide replies": "Masquer les réponses",
"Show replies": "Afficher les réponses", "Show replies": "Afficher les réponses",
"Incorrect password": "Mot de passe incorrect", "Incorrect password": "Mot de passe incorrect",
"Quota exceeded, try again in a few hours": "Quota dépassé, réessayez dans quelques heures", "Quota exceeded, try again in a few hours": "Nombre de tentative de connexion dépassé, réessayez dans quelques heures",
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Si vous ne parvenez pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.", "Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Si vous ne parvenez pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.",
"Invalid TFA code": "Code TFA invalide", "Invalid TFA code": "Code d'authentification à deux facteurs invalide",
"Login failed. This may be because two-factor authentication is not enabled on your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.", "Login failed. This may be because two-factor authentication is not enabled on your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.",
"Invalid answer": "Réponse non valide", "Invalid answer": "Réponse non valide",
"Invalid CAPTCHA": "CAPTCHA invalide", "Invalid CAPTCHA": "CAPTCHA invalide",
"CAPTCHA is a required field": "CAPTCHA est un champ obligatoire", "CAPTCHA is a required field": "Veuillez rentrez un CAPTCHA",
"User ID is a required field": "Utilisateur ID est un champ obligatoire", "User ID is a required field": "Veuillez rentrez un Identifiant Utilisateur",
"Password is a required field": "Mot de passe est un champ obligatoire", "Password is a required field": "Veuillez rentrez un Mot de passe",
"Invalid username or password": "Nom d'utilisateur ou mot de passe invalide", "Invalid username or password": "Nom d'utilisateur ou mot de passe invalide",
"Please sign in using 'Sign in with Google'": "Veuillez vous connecter en utilisant 'S'identifier avec Google'", "Please sign in using 'Sign in with Google'": "Veuillez vous connecter en utilisant \"S'identifier avec Google\"",
"Password cannot be empty": "Le mot de passe ne peut pas être vide", "Password cannot be empty": "Le mot de passe ne peut pas être vide",
"Password cannot be longer than 55 characters": "Le mot de passe ne doit pas comporter plus de 55 caractères.", "Password cannot be longer than 55 characters": "Le mot de passe ne doit pas comporter plus de 55 caractères.",
"Please sign in": "Veuillez ouvrir une session", "Please sign in": "Veuillez vous connecter",
"Invidious Private Feed for `x`": "Flux privé Invidious pour `x`", "Invidious Private Feed for `x`": "Flux RSS privé pour `x`",
"channel:`x`": "chenal:`x`", "channel:`x`": "chaîne:`x`",
"Deleted or invalid channel": "Canal supprimé ou non valide", "Deleted or invalid channel": "Chaîne supprimée ou invalide",
"This channel does not exist.": "Ce canal n'existe pas.", "This channel does not exist.": "Cette chaine n'existe pas.",
"Could not get channel info.": "Impossible d'obtenir des informations sur les chaînes.", "Could not get channel info.": "Impossible de charger les informations de cette chaîne.",
"Could not fetch comments": "Impossible d'aller chercher les commentaires", "Could not fetch comments": "Impossible de charger les commentaires",
"View `x` replies": "Voir `x` réponses", "View `x` replies": "Voir `x` réponses",
"`x` ago": "il y a `x`", "`x` ago": "il y a `x`",
"Load more": "Charger plus", "Load more": "Charger plus",
"`x` points": "`x` points", "`x` points": "`x` points",
"Could not create mix.": "Impossible de créer du mixage.", "Could not create mix.": "Impossible de charger cette liste de lecture.",
"Playlist is empty": "La liste de lecture est vide", "Playlist is empty": "La liste de lecture est vide",
"Invalid playlist.": "Liste de lecture invalide.", "Invalid playlist.": "Liste de lecture invalide.",
"Playlist does not exist.": "La liste de lecture n'existe pas.", "Playlist does not exist.": "La liste de lecture n'existe pas.",
"Could not pull trending pages.": "Impossible de tirer les pages de tendances.", "Could not pull trending pages.": "Impossible de charger les pages de tendances.",
"Hidden field \"challenge\" is a required field": "Champ caché \"contestation\" est un champ obligatoire", "Hidden field \"challenge\" is a required field": "Hidden field \"challenge\" is a required field",
"Hidden field \"token\" is a required field": "Champ caché \"jeton\" est un champ obligatoire", "Hidden field \"token\" is a required field": "Hidden field \"token\" is a required field",
"Invalid challenge": "Contestation non valide", "Invalid challenge": "Invalid challenge",
"Invalid token": "Jeton non valide", "Invalid token": "Invalid token",
"Invalid user": "Iutilisateur non valide", "Invalid user": "Invalid user",
"Token is expired, please try again": "Le jeton est expiré, veuillez réessayer", "Token is expired, please try again": "Token is expired, please try again",
"English": "Anglais", "English": "Anglais",
"English (auto-generated)": "Anglais (auto-généré)", "English (auto-generated)": "Anglais (générés automatiquement)",
"Afrikaans": "Afrikaans", "Afrikaans": "Afrikaans",
"Albanian": "Albanais", "Albanian": "Albanais",
"Amharic": "Amharique", "Amharic": "Amharique",
@ -258,21 +257,21 @@
"`x` hours": "`x` heures", "`x` hours": "`x` heures",
"`x` minutes": "`x` minutes", "`x` minutes": "`x` minutes",
"`x` seconds": "`x` secondes", "`x` seconds": "`x` secondes",
"Fallback comments: ": "Commentaires de repli: ", "Fallback comments: ": "Commentaires secondaires : ",
"Popular": "Populaire", "Popular": "Populaire",
"Top": "Haut", "Top": "Top",
"About": "Sur", "About": "A Propos",
"Rating: ": "Évaluation: ", "Rating: ": "Évaluation: ",
"Language: ": "Langue: ", "Language: ": "Langue: ",
"Default": "", "Default": "Défaut",
"Music": "", "Music": "Musique",
"Gaming": "", "Gaming": "Jeux Vidéo",
"News": "", "News": "Actualités",
"Movies": "", "Movies": "Films",
"Download": "", "Download": "Télécharger",
"Download as: ": "", "Download as: ": "Télécharger en :",
"%A %B %-d, %Y": "", "%A %B %-d, %Y": "%A %-d %B %Y",
"(edited)": "", "(edited)": "(modifié)",
"Youtube permalink of the comment": "", "Youtube permalink of the comment": "Lien YouTube permanent vers le commentaire",
"`x` marked it with a ❤": "" "`x` marked it with a ❤": "`x` l'a marqué d'un ❤"
} }

@ -1,284 +1,284 @@
{ {
"`x` subscribers": "`x` подписчиков", "`x` subscribers": "`x` подписчиков",
"`x` videos": "`x` видео", "`x` videos": "`x` видео",
"LIVE": "ПРЯМОЙ ЭФИР", "LIVE": "ПРЯМОЙ ЭФИР",
"Shared `x` ago": "Опубликовано `x` назад", "Shared `x` ago": "Опубликовано `x` назад",
"Unsubscribe": "Отписаться", "Unsubscribe": "Отписаться",
"Subscribe": "Подписаться", "Subscribe": "Подписаться",
"Login to subscribe to `x`": "Войти, чтобы подписаться на `x`", "Login to subscribe to `x`": "Войти, чтобы подписаться на `x`",
"View channel on YouTube": "Канал на YouTube", "View channel on YouTube": "Канал на YouTube",
"newest": "новые", "newest": "новые",
"oldest": "старые", "oldest": "старые",
"popular": "популярные", "popular": "популярные",
"Preview page": "Предварительный просмотр", "Preview page": "Предварительный просмотр",
"Next page": "Следующая страница", "Next page": "Следующая страница",
"Clear watch history?": "Очистить историю просмотров?", "Clear watch history?": "Очистить историю просмотров?",
"Yes": "Да", "Yes": "Да",
"No": "Нет", "No": "Нет",
"Import and Export Data": "Импорт и экспорт данных", "Import and Export Data": "Импорт и экспорт данных",
"Import": "Импорт", "Import": "Импорт",
"Import Invidious data": "Импортировать данные Invidious", "Import Invidious data": "Импортировать данные Invidious",
"Import YouTube subscriptions": "Импортировать YouTube подписки", "Import YouTube subscriptions": "Импортировать YouTube подписки",
"Import FreeTube subscriptions (.db)": "Импортировать FreeTube подписки (.db)", "Import FreeTube subscriptions (.db)": "Импортировать FreeTube подписки (.db)",
"Import NewPipe subscriptions (.json)": "Импортировать NewPipe подписки (.json)", "Import NewPipe subscriptions (.json)": "Импортировать NewPipe подписки (.json)",
"Import NewPipe data (.zip)": "Импортировать данные NewPipe (.zip)", "Import NewPipe data (.zip)": "Импортировать данные NewPipe (.zip)",
"Export": "Экспорт", "Export": "Экспорт",
"Export subscriptions as OPML": "Экспортировать подписки в OPML", "Export subscriptions as OPML": "Экспортировать подписки в OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в OPML (для NewPipe и FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в OPML (для NewPipe и FreeTube)",
"Export data as JSON": "Экспортировать данные в JSON", "Export data as JSON": "Экспортировать данные в JSON",
"Delete account?": "Удалить аккаунт?", "Delete account?": "Удалить аккаунт?",
"History": "История", "History": "История",
"Previous page": "Предыдущая страница", "Previous page": "Предыдущая страница",
"An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube", "An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube",
"JavaScript license information": "Лицензии JavaScript", "JavaScript license information": "Лицензии JavaScript",
"source": "источник", "source": "источник",
"Login": "Войти", "Login": "Войти",
"Login/Register": "Войти/Регистрация", "Login/Register": "Войти/Регистрация",
"Login to Google": "Войти через Google", "Login to Google": "Войти через Google",
"User ID:": "ID пользователя:", "User ID:": "ID пользователя:",
"Password:": "Пароль:", "Password:": "Пароль:",
"Time (h:mm:ss):": "Время (ч:мм:сс):", "Time (h:mm:ss):": "Время (ч:мм:сс):",
"Text CAPTCHA": "Текст капчи", "Text CAPTCHA": "Текст капчи",
"Image CAPTCHA": "Изображение капчи", "Image CAPTCHA": "Изображение капчи",
"Sign In": "Войти", "Sign In": "Войти",
"Register": "Регистрация", "Register": "Регистрация",
"Email:": "Эл. почта:", "Email:": "Эл. почта:",
"Google verification code:": "Код подтверждения Google:", "Google verification code:": "Код подтверждения Google:",
"Preferences": "Настройки", "Preferences": "Настройки",
"Player preferences": "Настройки проигрывателя", "Player preferences": "Настройки проигрывателя",
"Always loop: ": "Всегда повторять: ", "Always loop: ": "Всегда повторять: ",
"Autoplay: ": "Автовоспроизведение: ", "Autoplay: ": "Автовоспроизведение: ",
"Autoplay next video: ": "Автовоспроизведение следующего видео: ", "Autoplay next video: ": "Автовоспроизведение следующего видео: ",
"Listen by default: ": "Режим \"только аудио\" по-умолчанию: ", "Listen by default: ": "Режим \"только аудио\" по-умолчанию: ",
"Default speed: ": "Скорость по-умолчанию: ", "Default speed: ": "Скорость по-умолчанию: ",
"Preferred video quality: ": "Предпочтительное качество видео: ", "Preferred video quality: ": "Предпочтительное качество видео: ",
"Player volume: ": "Громкость воспроизведения: ", "Player volume: ": "Громкость воспроизведения: ",
"Default comments: ": "Источник комментариев: ", "Default comments: ": "Источник комментариев: ",
"youtube": "YouTube", "youtube": "YouTube",
"reddit": "Reddit", "reddit": "Reddit",
"Default captions: ": "Субтитры по-умолчанию: ", "Default captions: ": "Субтитры по-умолчанию: ",
"Fallback captions: ": "Резервные субтитры: ", "Fallback captions: ": "Резервные субтитры: ",
"Show related videos? ": "Показывать похожие видео? ", "Show related videos? ": "Показывать похожие видео? ",
"Visual preferences": "Визуальные настройки", "Visual preferences": "Визуальные настройки",
"Dark mode: ": "Темная тема: ", "Dark mode: ": "Темная тема: ",
"Thin mode: ": "Облегченный режим: ", "Thin mode: ": "Облегченный режим: ",
"Subscription preferences": "Настройки подписок", "Subscription preferences": "Настройки подписок",
"Redirect homepage to feed: ": "Отображать ленту вместо главной страницы: ", "Redirect homepage to feed: ": "Отображать ленту вместо главной страницы: ",
"Number of videos shown in feed: ": "Число видео в ленте: ", "Number of videos shown in feed: ": "Число видео в ленте: ",
"Sort videos by: ": "Сортировать видео по: ", "Sort videos by: ": "Сортировать видео по: ",
"published": "дате публикации", "published": "дате публикации",
"published - reverse": "дате - обратный порядок", "published - reverse": "дате - обратный порядок",
"alphabetically": "алфавиту", "alphabetically": "алфавиту",
"alphabetically - reverse": "алфавиту - обратный порядок", "alphabetically - reverse": "алфавиту - обратный порядок",
"channel name": "имени канала", "channel name": "имени канала",
"channel name - reverse": "имени канала - обратный порядок", "channel name - reverse": "имени канала - обратный порядок",
"Only show latest video from channel: ": "Отображать только последние видео с каждого канала: ", "Only show latest video from channel: ": "Отображать только последние видео с каждого канала: ",
"Only show latest unwatched video from channel: ": "Отображать только непросмотренные видео с каждого канала: ", "Only show latest unwatched video from channel: ": "Отображать только непросмотренные видео с каждого канала: ",
"Only show unwatched: ": "Отображать только непросмотренные видео: ", "Only show unwatched: ": "Отображать только непросмотренные видео: ",
"Only show notifications (if there are any): ": "Отображать только оповещения (если есть): ", "Only show notifications (if there are any): ": "Отображать только оповещения (если есть): ",
"Data preferences": "Настройки данных", "Data preferences": "Настройки данных",
"Clear watch history": "Очистить историю просмотра", "Clear watch history": "Очистить историю просмотра",
"Import/Export data": "Импорт/Экспорт данных", "Import/Export data": "Импорт/Экспорт данных",
"Manage subscriptions": "Управление подписками", "Manage subscriptions": "Управление подписками",
"Watch history": "История просмотров", "Watch history": "История просмотров",
"Delete account": "Удалить аккаунт", "Delete account": "Удалить аккаунт",
"Save preferences": "Сохранить настройки", "Save preferences": "Сохранить настройки",
"Subscription manager": "Менеджер подписок", "Subscription manager": "Менеджер подписок",
"`x` subscriptions": "`x` подписок", "`x` subscriptions": "`x` подписок",
"Import/Export": "Импорт/Экспорт", "Import/Export": "Импорт/Экспорт",
"unsubscribe": "отписаться", "unsubscribe": "отписаться",
"Subscriptions": "Подписки", "Subscriptions": "Подписки",
"`x` unseen notifications": "`x` новых оповещений", "`x` unseen notifications": "`x` новых оповещений",
"search": "поиск", "search": "поиск",
"Sign out": "Выйти", "Sign out": "Выйти",
"Released under the AGPLv3 by Omar Roth.": "Распространяется Omar Roth по AGPLv3.", "Released under the AGPLv3 by Omar Roth.": "Распространяется Omar Roth по AGPLv3.",
"Source available here.": "Исходный код доступен здесь.", "Source available here.": "Исходный код доступен здесь.",
"Liberapay: ": "Liberapay: ", "Liberapay: ": "Liberapay: ",
"Patreon: ": "Patreon: ", "Patreon: ": "Patreon: ",
"BTC: ": "BTC: ", "BTC: ": "BTC: ",
"BCH: ": "BCH: ", "BCH: ": "BCH: ",
"View JavaScript license information.": "Посмотреть лицензии JavaScript кода.", "View JavaScript license information.": "Посмотреть лицензии JavaScript кода.",
"Trending": "В тренде", "Trending": "В тренде",
"Watch video on Youtube": "Смотреть на YouTube", "Watch video on Youtube": "Смотреть на YouTube",
"Genre: ": "Жанр: ", "Genre: ": "Жанр: ",
"License: ": "Лицензия: ", "License: ": "Лицензия: ",
"Family friendly? ": "Семейный просмотр: ", "Family friendly? ": "Семейный просмотр: ",
"Wilson score: ": "Рейтинг Вильсона: ", "Wilson score: ": "Рейтинг Вильсона: ",
"Engagement: ": "Вовлеченность: ", "Engagement: ": "Вовлеченность: ",
"Whitelisted regions: ": "Доступно для: ", "Whitelisted regions: ": "Доступно для: ",
"Blacklisted regions: ": "Недоступно для: ", "Blacklisted regions: ": "Недоступно для: ",
"Shared `x`": "Опубликовано `x`", "Shared `x`": "Опубликовано `x`",
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Похоже, что у Вас отключен JavaScript. Нажмите сюда, чтобы увидеть комментарии (учтите, что они могут загружаться дольше).", "Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Похоже, что у Вас отключен JavaScript. Нажмите сюда, чтобы увидеть комментарии (учтите, что они могут загружаться дольше).",
"View YouTube comments": "Смотреть комментарии с YouTube", "View YouTube comments": "Смотреть комментарии с YouTube",
"View more comments on Reddit": "Больше комментариев на Reddit", "View more comments on Reddit": "Больше комментариев на Reddit",
"View `x` comments": "Показать `x` комментариев", "View `x` comments": "Показать `x` комментариев",
"View Reddit comments": "Смотреть комментарии с Reddit", "View Reddit comments": "Смотреть комментарии с Reddit",
"Hide replies": "Скрыть ответы", "Hide replies": "Скрыть ответы",
"Show replies": "Показать ответы", "Show replies": "Показать ответы",
"Incorrect password": "Неправильный пароль", "Incorrect password": "Неправильный пароль",
"Quota exceeded, try again in a few hours": "Превышена квота, попробуйте снова через несколько часов", "Quota exceeded, try again in a few hours": "Превышена квота, попробуйте снова через несколько часов",
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Вход не выполнен, проверьте, не включена ли двухфакторная аутентификация.", "Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Вход не выполнен, проверьте, не включена ли двухфакторная аутентификация.",
"Invalid TFA code": "Неправильный TFA код", "Invalid TFA code": "Неправильный TFA код",
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Не удалось войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.", "Login failed. This may be because two-factor authentication is not enabled on your account.": "Не удалось войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.",
"Invalid answer": "Неверный ответ", "Invalid answer": "Неверный ответ",
"Invalid CAPTCHA": "Неверная капча", "Invalid CAPTCHA": "Неверная капча",
"CAPTCHA is a required field": "Необходимо ввести капчу", "CAPTCHA is a required field": "Необходимо ввести капчу",
"User ID is a required field": "Необходимо ввести идентификатор пользователя", "User ID is a required field": "Необходимо ввести идентификатор пользователя",
"Password is a required field": "Необходимо ввести пароль", "Password is a required field": "Необходимо ввести пароль",
"Invalid username or password": "Недопустимый пароль или имя пользователя", "Invalid username or password": "Недопустимый пароль или имя пользователя",
"Please sign in using 'Sign in with Google'": "Пожалуйста войдите через Google", "Please sign in using 'Sign in with Google'": "Пожалуйста войдите через Google",
"Password cannot be empty": "Пароль не может быть пустым", "Password cannot be empty": "Пароль не может быть пустым",
"Password cannot be longer than 55 characters": "Пароль не может быть длиннее 55 символов", "Password cannot be longer than 55 characters": "Пароль не может быть длиннее 55 символов",
"Please sign in": "Пожалуйста, войдите", "Please sign in": "Пожалуйста, войдите",
"Invidious Private Feed for `x`": "Приватная лента Invidious для `x`", "Invidious Private Feed for `x`": "Приватная лента Invidious для `x`",
"channel:`x`": "канал: `x`", "channel:`x`": "канал: `x`",
"Deleted or invalid channel": "Канал удален или не найден", "Deleted or invalid channel": "Канал удален или не найден",
"This channel does not exist.": "Такой канал не существует.", "This channel does not exist.": "Такой канал не существует.",
"Could not get channel info.": "Невозможно получить информацию о канале.", "Could not get channel info.": "Невозможно получить информацию о канале.",
"Could not fetch comments": "Невозможно получить комментарии", "Could not fetch comments": "Невозможно получить комментарии",
"View `x` replies": "Показать `x` ответов", "View `x` replies": "Показать `x` ответов",
"`x` ago": "`x` назад", "`x` ago": "`x` назад",
"Load more": "Загрузить больше", "Load more": "Загрузить больше",
"`x` points": "`x` очков", "`x` points": "`x` очков",
"Could not create mix.": "Невозможно создать \"микс\".", "Could not create mix.": "Невозможно создать \"микс\".",
"Playlist is empty": "Плейлист пуст", "Playlist is empty": "Плейлист пуст",
"Invalid playlist.": "Некорректный плейлист.", "Invalid playlist.": "Некорректный плейлист.",
"Playlist does not exist.": "Плейлист не существует.", "Playlist does not exist.": "Плейлист не существует.",
"Could not pull trending pages.": "Невозможно получить страницы \"в тренде\".", "Could not pull trending pages.": "Невозможно получить страницы \"в тренде\".",
"Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле \"challenge\"", "Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле \"challenge\"",
"Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле \"токен\"", "Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле \"токен\"",
"Invalid challenge": "Неправильный ответ в \"challenge\"", "Invalid challenge": "Неправильный ответ в \"challenge\"",
"Invalid token": "Неправильный токен", "Invalid token": "Неправильный токен",
"Invalid user": "Недопустимое имя пользователя", "Invalid user": "Недопустимое имя пользователя",
"Token is expired, please try again": "Срок действия токена истек, попробуйте позже", "Token is expired, please try again": "Срок действия токена истек, попробуйте позже",
"English": "Английский", "English": "Английский",
"English (auto-generated)": "Английский (созданы автоматически)", "English (auto-generated)": "Английский (созданы автоматически)",
"Afrikaans": "Африкаанс", "Afrikaans": "Африкаанс",
"Albanian": "Албанский", "Albanian": "Албанский",
"Amharic": "Амхарский", "Amharic": "Амхарский",
"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` месяцев",
"`x` weeks": "`x` недель", "`x` weeks": "`x` недель",
"`x` days": "`x` дней", "`x` days": "`x` дней",
"`x` hours": "`x` часов", "`x` hours": "`x` часов",
"`x` minutes": "`x` минут", "`x` minutes": "`x` минут",
"`x` seconds": "`x` секунд", "`x` seconds": "`x` секунд",
"Fallback comments: ": "Резервные комментарии: ", "Fallback comments: ": "Резервные комментарии: ",
"Popular": "Популярное", "Popular": "Популярное",
"Top": "Топ", "Top": "Топ",
"About": "О сайте", "About": "О сайте",
"Rating: ": "Рейтинг: ", "Rating: ": "Рейтинг: ",
"Language: ": "Язык: ", "Language: ": "Язык: ",
"Default": "По-умолчанию", "Default": "По-умолчанию",
"Music": "Музыка", "Music": "Музыка",
"Gaming": "Игры", "Gaming": "Игры",
"News": "Новости", "News": "Новости",
"Movies": "Фильмы", "Movies": "Фильмы",
"Download": "Скачать", "Download": "Скачать",
"Download as: ": "Скачать как: ", "Download as: ": "Скачать как: ",
"%A %B %-d, %Y": "", "%A %B %-d, %Y": "%-d %B %Y, %A",
"(edited)": "", "(edited)": "(изменено)",
"Youtube permalink of the comment": "", "Youtube permalink of the comment": "Прямая ссылка на YouTube",
"`x` marked it with a ❤": "" "`x` marked it with a ❤": "❤ от автора канала \"`x`\""
} }

@ -163,9 +163,10 @@ before_all do |env|
# Invidious users only have SID # Invidious users only have SID
if !env.request.cookies.has_key? "SSID" if !env.request.cookies.has_key? "SSID"
user = PG_DB.query_one?("SELECT * FROM users WHERE $1 = ANY(id)", sid, as: User) email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
if user if email
user = PG_DB.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week) challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week)
env.set "challenge", challenge env.set "challenge", challenge
@ -177,7 +178,7 @@ before_all do |env|
end end
else else
begin begin
user = get_user(sid, headers, PG_DB, false) user, sid = get_user(sid, headers, PG_DB, false)
challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week) challenge, token = create_response(user.email, "sign_out", HMAC_KEY, PG_DB, 1.week)
env.set "challenge", challenge env.set "challenge", challenge
@ -312,7 +313,7 @@ get "/watch" do |env|
end end
if watched && !watched.includes? id if watched && !watched.includes? id
PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE $2 = id", [id], user.as(User).id) PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.as(User).email)
end end
if nojs if nojs
@ -818,7 +819,7 @@ post "/login" do |env|
# Prefer Authenticator app and SMS over unsupported protocols # Prefer Authenticator app and SMS over unsupported protocols
if challenge_results[0][-1][0][0][8] != 6 && challenge_results[0][-1][0][0][8] != 9 if challenge_results[0][-1][0][0][8] != 6 && challenge_results[0][-1][0][0][8] != 9
tfa = challenge_results[0][-1][0].as_a.select { |auth_type| auth_type[8] == 6 || auth_type[8] == 9 }[0] tfa = challenge_results[0][-1][0].as_a.select { |auth_type| auth_type[8] == 6 || auth_type[8] == 9 }[0]
select_challenge = "[#{challenge_results[0][-1][0].as_a.index(tfa).not_nil!}]" select_challenge = "[2,null,null,null,[#{tfa[8]}]]"
tl = challenge_results[1][2] tl = challenge_results[1][2]
@ -880,7 +881,7 @@ post "/login" do |env|
sid = login.cookies["SID"].value sid = login.cookies["SID"].value
user = get_user(sid, headers, PG_DB) user, sid = get_user(sid, headers, PG_DB)
# We are now logged in # We are now logged in
@ -986,7 +987,7 @@ post "/login" do |env|
if Crypto::Bcrypt::Password.new(user.password.not_nil!) == password if Crypto::Bcrypt::Password.new(user.password.not_nil!) == password
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
PG_DB.exec("UPDATE users SET id = id || $1 WHERE LOWER(email) = LOWER($2)", [sid], email) 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
@ -1024,13 +1025,14 @@ post "/login" do |env|
end end
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
user = create_user(sid, email, password) user, sid = create_user(sid, email, password)
user_array = user.to_a user_array = user.to_a
user_array[5] = user_array[5].to_json user_array[4] = user_array[4].to_json
args = arg_array(user_array) args = arg_array(user_array)
PG_DB.exec("INSERT INTO users VALUES (#{args})", user_array) PG_DB.exec("INSERT INTO users VALUES (#{args})", user_array)
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.now)
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 \ PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
@ -1078,7 +1080,7 @@ get "/signout" do |env|
user = env.get("user").as(User) user = env.get("user").as(User)
sid = env.get("sid").as(String) sid = env.get("sid").as(String)
PG_DB.exec("UPDATE users SET id = array_remove(id, $1) WHERE email = $2", sid, user.email) PG_DB.exec("DELETE FROM session_ids * WHERE id = $1", sid)
env.request.cookies.each do |cookie| env.request.cookies.each do |cookie|
cookie.expires = Time.new(1990, 1, 1) cookie.expires = Time.new(1990, 1, 1)
@ -1252,7 +1254,7 @@ get "/mark_watched" do |env|
if user if user
user = user.as(User) user = user.as(User)
if !user.watched.includes? id if !user.watched.includes? id
PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE $2 = id", [id], user.id) PG_DB.exec("UPDATE users SET watched = watched || $1 WHERE email = $2", [id], user.email)
end end
end end
@ -1347,9 +1349,10 @@ get "/subscription_manager" do |env|
locale = LOCALES[env.get("locale").as(String)]? locale = LOCALES[env.get("locale").as(String)]?
user = env.get? "user" user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env, "/") referer = get_referer(env, "/")
if !user if !user && !sid
next env.redirect referer next env.redirect referer
end end
@ -1360,7 +1363,7 @@ get "/subscription_manager" do |env|
headers = HTTP::Headers.new headers = HTTP::Headers.new
headers["Cookie"] = env.request.headers["Cookie"] headers["Cookie"] = env.request.headers["Cookie"]
user = get_user(user.id[0], headers, PG_DB) user, sid = get_user(sid, headers, PG_DB)
end end
action_takeout = env.params.query["action_takeout"]?.try &.to_i? action_takeout = env.params.query["action_takeout"]?.try &.to_i?
@ -1370,14 +1373,7 @@ get "/subscription_manager" do |env|
format = env.params.query["format"]? format = env.params.query["format"]?
format ||= "rss" format ||= "rss"
subscriptions = [] of InvidiousChannel subscriptions = PG_DB.query_all("SELECT * FROM channels WHERE id = ANY('{#{user.subscriptions.join(",")}}')", as: InvidiousChannel)
user.subscriptions.each do |ucid|
begin
subscriptions << get_channel(ucid, PG_DB, false, false)
rescue ex
next
end
end
subscriptions.sort_by! { |channel| channel.author.downcase } subscriptions.sort_by! { |channel| channel.author.downcase }
if action_takeout if action_takeout
@ -1756,10 +1752,12 @@ get "/feed/subscriptions" do |env|
locale = LOCALES[env.get("locale").as(String)]? locale = LOCALES[env.get("locale").as(String)]?
user = env.get? "user" user = env.get? "user"
sid = env.get? "sid"
referer = get_referer(env) referer = get_referer(env)
if user if user
user = user.as(User) user = user.as(User)
sid = sid.as(String)
preferences = user.preferences preferences = user.preferences
if preferences.unseen_only if preferences.unseen_only
@ -1771,7 +1769,7 @@ get "/feed/subscriptions" do |env|
headers["Cookie"] = env.request.headers["Cookie"] headers["Cookie"] = env.request.headers["Cookie"]
if !user.password if !user.password
user = get_user(user.id[0], headers, PG_DB) user, sid = get_user(sid, headers, PG_DB)
end end
max_results = preferences.max_results max_results = preferences.max_results
@ -3033,7 +3031,8 @@ end
ucid = env.params.url["ucid"] ucid = env.params.url["ucid"]
page = env.params.query["page"]?.try &.to_i? page = env.params.query["page"]?.try &.to_i?
page ||= 1 page ||= 1
sort_by = env.params.query["sort_by"]?.try &.downcase sort_by = env.params.query["sort"]?.try &.downcase
sort_by ||= env.params.query["sort_by"]?.try &.downcase
sort_by ||= "newest" sort_by ||= "newest"
begin begin
@ -3438,7 +3437,7 @@ get "/api/v1/mixes/:rdid" do |env|
rdid = env.params.url["rdid"] rdid = env.params.url["rdid"]
continuation = env.params.query["continuation"]? continuation = env.params.query["continuation"]?
continuation ||= rdid.lchop("RD") continuation ||= rdid.lchop("RD")[0, 11]
format = env.params.query["format"]? format = env.params.query["format"]?
format ||= "json" format ||= "json"
@ -3662,6 +3661,8 @@ get "/latest_version" do |env|
id = env.params.query["id"]? id = env.params.query["id"]?
itag = env.params.query["itag"]? itag = env.params.query["itag"]?
region = env.params.query["region"]?
local = env.params.query["local"]? local = env.params.query["local"]?
local ||= "false" local ||= "false"
local = local == "true" local = local == "true"
@ -3670,7 +3671,7 @@ get "/latest_version" do |env|
halt env, status_code: 400 halt env, status_code: 400
end end
video = get_video(id, PG_DB, proxies) video = get_video(id, PG_DB, proxies, region: region)
fmt_stream = video.fmt_stream(decrypt_function) fmt_stream = video.fmt_stream(decrypt_function)
adaptive_fmts = video.adaptive_fmts(decrypt_function) adaptive_fmts = video.adaptive_fmts(decrypt_function)
@ -3943,14 +3944,13 @@ end
error 500 do |env| error 500 do |env|
error_message = <<-END_HTML error_message = <<-END_HTML
Looks like you've found a bug in Invidious. Feel free to open a new issue Looks like you've found a bug in Invidious. Feel free to open a new issue
<a href="https://github.com/omarroth/invidious/issues/github.com/omarroth/invidious"> <a href="https://github.com/omarroth/invidious/issues">
here here
</a> </a>
or send an email to or send an email to
<a href="mailto:omarroth@protonmail.com"> <a href="mailto:omarroth@protonmail.com">
omarroth@protonmail.com omarroth@protonmail.com</a>.
</a>.
END_HTML END_HTML
templated "error" templated "error"
end end

@ -260,6 +260,132 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
return url return url
end end
def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated = false)
if !auto_generated
cursor = Base64.urlsafe_encode(cursor, false)
end
meta = IO::Memory.new
if auto_generated
meta.write(Bytes[0x08, 0x0a])
end
meta.write(Bytes[0x12, 0x09])
meta.print("playlists")
if auto_generated
meta.write(Bytes[0x20, 0x32])
else
# TODO: Look at 0x01, 0x00
case sort
when "oldest", "oldest_created"
meta.write(Bytes[0x18, 0x02])
when "newest", "newest_created"
meta.write(Bytes[0x18, 0x03])
when "last", "last_added"
meta.write(Bytes[0x18, 0x04])
end
meta.write(Bytes[0x20, 0x01])
end
meta.write(Bytes[0x30, 0x02])
meta.write(Bytes[0x38, 0x01])
meta.write(Bytes[0x60, 0x01])
meta.write(Bytes[0x6a, 0x00])
meta.write(Bytes[0x7a, cursor.size])
meta.print(cursor)
meta.write(Bytes[0xb8, 0x01, 0x00])
meta.rewind
meta = Base64.urlsafe_encode(meta.to_slice)
meta = URI.escape(meta)
continuation = IO::Memory.new
continuation.write(Bytes[0x12, ucid.size])
continuation.print(ucid)
continuation.write(Bytes[0x1a])
continuation.write(write_var_int(meta.size))
continuation.print(meta)
continuation.rewind
continuation = continuation.gets_to_end
wrapper = IO::Memory.new
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02])
wrapper.write(write_var_int(continuation.size))
wrapper.print(continuation)
wrapper.rewind
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
wrapper = URI.escape(wrapper)
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
return url
end
def extract_channel_playlists_cursor(url, auto_generated)
wrapper = HTTP::Params.parse(URI.parse(url).query.not_nil!)["continuation"]
wrapper = URI.unescape(wrapper)
wrapper = Base64.decode(wrapper)
# 0xe2 0xa9 0x85 0xb2 0x02
wrapper += 5
continuation_size = read_var_int(wrapper[0, 4])
wrapper += write_var_int(continuation_size).size
continuation = wrapper[0, continuation_size]
# 0x12
continuation += 1
ucid_size = continuation[0]
continuation += 1
ucid = continuation[0, ucid_size]
continuation += ucid_size
# 0x1a
continuation += 1
meta_size = read_var_int(continuation[0, 4])
continuation += write_var_int(meta_size).size
meta = continuation[0, meta_size]
continuation += meta_size
meta = String.new(meta)
meta = URI.unescape(meta)
meta = Base64.decode(meta)
# 0x12 0x09 playlists
meta += 11
until meta[0] == 0x7a
tag = read_var_int(meta[0, 4])
meta += write_var_int(tag).size
value = meta[0]
meta += 1
end
# 0x7a
meta += 1
cursor_size = meta[0]
meta += 1
cursor = meta[0, cursor_size]
cursor = String.new(cursor)
if !auto_generated
cursor = URI.unescape(cursor)
cursor = Base64.decode_string(cursor)
end
return cursor
end
def get_about_info(ucid, locale) def get_about_info(ucid, locale)
client = make_client(YT_URL) client = make_client(YT_URL)
@ -290,7 +416,7 @@ def get_about_info(ucid, locale)
sub_count ||= 0 sub_count ||= 0
author = about.xpath_node(%q(//span[contains(@class,"qualified-channel-title-text")]/a)).not_nil!.content author = about.xpath_node(%q(//span[contains(@class,"qualified-channel-title-text")]/a)).not_nil!.content
ucid = about.xpath_node(%q(//link[@rel="canonical"])).not_nil!["href"].split("/")[-1] ucid = about.xpath_node(%q(//meta[@itemprop="channelId"])).not_nil!["content"]
# Auto-generated channels # Auto-generated channels
# https://support.google.com/youtube/answer/2579942 # https://support.google.com/youtube/answer/2579942

@ -166,35 +166,33 @@ def extract_videos(nodeset, ucid = nil)
videos.map { |video| video.as(SearchVideo) } videos.map { |video| video.as(SearchVideo) }
end end
def extract_items(nodeset, ucid = nil) def extract_items(nodeset, ucid = nil, author_name = nil)
# TODO: Make this a 'common', so it makes more sense to be used here # TODO: Make this a 'common', so it makes more sense to be used here
items = [] of SearchItem items = [] of SearchItem
nodeset.each do |node| nodeset.each do |node|
anchor = node.xpath_node(%q(.//h3[contains(@class,"yt-lockup-title")]/a)) anchor = node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a))
if !anchor if !anchor
next next
end end
title = anchor.content.strip
id = anchor["href"]
if anchor["href"].starts_with? "https://www.googleadservices.com" if anchor["href"].starts_with? "https://www.googleadservices.com"
next next
end end
anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a)) anchor = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-byline")]/a))
if !anchor if anchor
author = ""
author_id = ""
else
author = anchor.content.strip author = anchor.content.strip
author_id = anchor["href"].split("/")[-1] author_id = anchor["href"].split("/")[-1]
end end
anchor = node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a)) author ||= author_name
if !anchor author_id ||= ucid
next
end author ||= ""
title = anchor.content.strip author_id ||= ""
id = anchor["href"]
description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")])) description_html = node.xpath_node(%q(.//div[contains(@class, "yt-lockup-description")]))
description_html, description = html_to_content(description_html) description_html, description = html_to_content(description_html)
@ -354,3 +352,94 @@ def extract_items(nodeset, ucid = nil)
return items return items
end end
def extract_shelf_items(nodeset, ucid = nil, author_name = nil)
items = [] of SearchPlaylist
nodeset.each do |shelf|
shelf_anchor = shelf.xpath_node(%q(.//h2[contains(@class, "branded-page-module-title")]))
if !shelf_anchor
next
end
title = shelf_anchor.xpath_node(%q(.//span[contains(@class, "branded-page-module-title-text")]))
if title
title = title.content.strip
end
title ||= ""
id = shelf_anchor.xpath_node(%q(.//a)).try &.["href"]
if !id
next
end
is_playlist = false
videos = [] of SearchPlaylistVideo
shelf.xpath_nodes(%q(.//ul[contains(@class, "yt-uix-shelfslider-list")]/li)).each do |child_node|
type = child_node.xpath_node(%q(./div))
if !type
next
end
case type["class"]
when .includes? "yt-lockup-video"
is_playlist = true
anchor = child_node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a))
if anchor
video_title = anchor.content.strip
video_id = HTTP::Params.parse(URI.parse(anchor["href"]).query.not_nil!)["v"]
end
video_title ||= ""
video_id ||= ""
anchor = child_node.xpath_node(%q(.//span[@class="video-time"]))
if anchor
length_seconds = decode_length_seconds(anchor.content)
end
length_seconds ||= 0
videos << SearchPlaylistVideo.new(
video_title,
video_id,
length_seconds
)
when .includes? "yt-lockup-playlist"
anchor = child_node.xpath_node(%q(.//h3[contains(@class, "yt-lockup-title")]/a))
if anchor
playlist_title = anchor.content.strip
params = HTTP::Params.parse(URI.parse(anchor["href"]).query.not_nil!)
plid = params["list"]
end
playlist_title ||= ""
plid ||= ""
items << SearchPlaylist.new(
playlist_title,
plid,
author_name,
ucid,
50,
Array(SearchPlaylistVideo).new
)
end
end
if is_playlist
plid = HTTP::Params.parse(URI.parse(id).query.not_nil!)["list"]
items << SearchPlaylist.new(
title,
plid,
author_name,
ucid,
videos.size,
videos
)
end
end
return items
end

@ -52,7 +52,10 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
item = item["playlistPanelVideoRenderer"] item = item["playlistPanelVideoRenderer"]
id = item["videoId"].as_s id = item["videoId"].as_s
title = item["title"]["simpleText"].as_s title = item["title"]?.try &.["simpleText"].as_s
if !title
next
end
author = item["longBylineText"]["runs"][0]["text"].as_s author = item["longBylineText"]["runs"][0]["text"].as_s
ucid = item["longBylineText"]["runs"][0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s ucid = item["longBylineText"]["runs"][0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s
length_seconds = decode_length_seconds(item["lengthText"]["simpleText"].as_s) length_seconds = decode_length_seconds(item["lengthText"]["simpleText"].as_s)

@ -161,117 +161,6 @@ def produce_playlist_url(id, index)
return url return url
end end
def produce_channel_playlists_url(ucid, cursor, sort = "newest")
cursor = Base64.urlsafe_encode(cursor, false)
meta = IO::Memory.new
meta.write(Bytes[0x12, 0x09])
meta.print("playlists")
# TODO: Look at 0x01, 0x00
case sort
when "oldest", "oldest_created"
meta.write(Bytes[0x18, 0x02])
when "newest", "newest_created"
meta.write(Bytes[0x18, 0x03])
when "last", "last_added"
meta.write(Bytes[0x18, 0x04])
end
meta.write(Bytes[0x20, 0x01])
meta.write(Bytes[0x30, 0x02])
meta.write(Bytes[0x38, 0x01])
meta.write(Bytes[0x60, 0x01])
meta.write(Bytes[0x6a, 0x00])
meta.write(Bytes[0x7a, cursor.size])
meta.print(cursor)
meta.write(Bytes[0xb8, 0x01, 0x00])
meta.rewind
meta = Base64.urlsafe_encode(meta.to_slice)
meta = URI.escape(meta)
continuation = IO::Memory.new
continuation.write(Bytes[0x12, ucid.size])
continuation.print(ucid)
continuation.write(Bytes[0x1a])
continuation.write(write_var_int(meta.size))
continuation.print(meta)
continuation.rewind
continuation = continuation.gets_to_end
wrapper = IO::Memory.new
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02])
wrapper.write(write_var_int(continuation.size))
wrapper.print(continuation)
wrapper.rewind
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
wrapper = URI.escape(wrapper)
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
return url
end
def extract_channel_playlists_cursor(url)
wrapper = HTTP::Params.parse(URI.parse(url).query.not_nil!)["continuation"]
wrapper = URI.unescape(wrapper)
wrapper = Base64.decode(wrapper)
# 0xe2 0xa9 0x85 0xb2 0x02
wrapper += 5
continuation_size = read_var_int(wrapper[0, 4])
wrapper += write_var_int(continuation_size).size
continuation = wrapper[0, continuation_size]
# 0x12
continuation += 1
ucid_size = continuation[0]
continuation += 1
ucid = continuation[0, ucid_size]
continuation += ucid_size
# 0x1a
continuation += 1
meta_size = read_var_int(continuation[0, 4])
continuation += write_var_int(meta_size).size
meta = continuation[0, meta_size]
continuation += meta_size
meta = String.new(meta)
meta = URI.unescape(meta)
meta = Base64.decode(meta)
# 0x12 0x09 playlists
meta += 11
until meta[0] == 0x7a
tag = read_var_int(meta[0, 4])
meta += write_var_int(tag).size
value = meta[0]
meta += 1
end
# 0x7a
meta += 1
cursor_size = meta[0]
meta += 1
cursor = meta[0, cursor_size]
cursor = String.new(cursor)
cursor = URI.unescape(cursor)
cursor = Base64.decode_string(cursor)
return cursor
end
def fetch_playlist(plid, locale) def fetch_playlist(plid, locale)
client = make_client(YT_URL) client = make_client(YT_URL)

@ -12,7 +12,6 @@ class User
end end
add_mapping({ add_mapping({
id: Array(String),
updated: Time, updated: Time,
notifications: Array(String), notifications: Array(String),
subscriptions: Array(String), subscriptions: Array(String),
@ -126,18 +125,21 @@ class Preferences
end end
def get_user(sid, headers, db, refresh = true) def get_user(sid, headers, db, refresh = true)
if db.query_one?("SELECT EXISTS (SELECT true FROM users WHERE $1 = ANY(id))", sid, as: Bool) if email = db.query_one?("SELECT email FROM session_ids WHERE id = $1", sid, as: String)
user = db.query_one("SELECT * FROM users WHERE $1 = ANY(id)", sid, as: User) user = db.query_one("SELECT * FROM users WHERE email = $1", email, as: User)
if refresh && Time.now - user.updated > 1.minute if refresh && Time.now - user.updated > 1.minute
user = fetch_user(sid, headers, db) user, sid = fetch_user(sid, headers, db)
user_array = user.to_a user_array = user.to_a
user_array[5] = user_array[5].to_json user_array[4] = user_array[4].to_json
args = arg_array(user_array) args = arg_array(user_array)
db.exec("INSERT INTO users VALUES (#{args}) \ db.exec("INSERT INTO users VALUES (#{args}) \
ON CONFLICT (email) DO UPDATE SET id = users.id || $1, updated = $2, subscriptions = $4", user_array) ON CONFLICT (email) DO UPDATE SET updated = $1, subscriptions = $3", user_array)
db.exec("INSERT INTO session_ids VALUES ($1,$2,$3) \
ON CONFLICT (id) DO NOTHING", sid, user.email, Time.now)
begin begin
view_name = "subscriptions_#{sha256(user.email)[0..7]}" view_name = "subscriptions_#{sha256(user.email)[0..7]}"
@ -149,14 +151,17 @@ def get_user(sid, headers, db, refresh = true)
end end
end end
else else
user = fetch_user(sid, headers, db) user, sid = fetch_user(sid, headers, db)
user_array = user.to_a user_array = user.to_a
user_array[5] = user_array[5].to_json user_array[4] = user_array[4].to_json
args = arg_array(user.to_a) args = arg_array(user.to_a)
db.exec("INSERT INTO users VALUES (#{args}) \ db.exec("INSERT INTO users VALUES (#{args}) \
ON CONFLICT (email) DO UPDATE SET id = users.id || $1, updated = $2, subscriptions = $4", user_array) ON CONFLICT (email) DO UPDATE SET updated = $1, subscriptions = $3", user_array)
db.exec("INSERT INTO session_ids VALUES ($1,$2,$3) \
ON CONFLICT (id) DO NOTHING", sid, user.email, Time.now)
begin begin
view_name = "subscriptions_#{sha256(user.email)[0..7]}" view_name = "subscriptions_#{sha256(user.email)[0..7]}"
@ -168,7 +173,7 @@ def get_user(sid, headers, db, refresh = true)
end end
end end
return user return user, sid
end end
def fetch_user(sid, headers, db) def fetch_user(sid, headers, db)
@ -196,17 +201,17 @@ def fetch_user(sid, headers, db)
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
user = User.new([sid], Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES, nil, token, [] of String) user = User.new(Time.now, [] of String, channels, email, DEFAULT_USER_PREFERENCES, nil, token, [] of String)
return user return user, sid
end end
def create_user(sid, email, password) def create_user(sid, email, password)
password = Crypto::Bcrypt::Password.create(password, cost: 10) password = Crypto::Bcrypt::Password.create(password, cost: 10)
token = Base64.urlsafe_encode(Random::Secure.random_bytes(32)) token = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
user = User.new([sid], Time.now, [] of String, [] of String, email, DEFAULT_USER_PREFERENCES, password.to_s, token, [] of String) user = User.new(Time.now, [] of String, [] of String, email, DEFAULT_USER_PREFERENCES, password.to_s, token, [] of String)
return user return user, sid
end end
def create_response(user_id, operation, key, db, expire = 6.hours) def create_response(user_id, operation, key, db, expire = 6.hours)

@ -20,7 +20,7 @@ function subscribe(timeouts = 0) {
var fallback = subscribe_button.innerHTML; var fallback = subscribe_button.innerHTML;
subscribe_button.onclick = unsubscribe; subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %></b>' subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe").gsub("'", "\\'") %> | <%= sub_count_text %></b>'
xhr.onreadystatechange = function() { xhr.onreadystatechange = function() {
if (xhr.readyState == 4) { if (xhr.readyState == 4) {
@ -55,7 +55,7 @@ function unsubscribe(timeouts = 0) {
var fallback = subscribe_button.innerHTML; var fallback = subscribe_button.innerHTML;
subscribe_button.onclick = subscribe; subscribe_button.onclick = subscribe;
subscribe_button.innerHTML = '<b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b>' subscribe_button.innerHTML = '<b><%= translate(locale, "Subscribe").gsub("'", "\\'") %> | <%= sub_count_text %></b>'
xhr.onreadystatechange = function() { xhr.onreadystatechange = function() {
if (xhr.readyState == 4) { if (xhr.readyState == 4) {

Loading…
Cancel
Save