Merge branch 'master' into homepage

This commit is contained in:
Omar Roth 2019-09-24 19:57:20 -04:00 committed by GitHub
commit 75793bc524
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 638 additions and 307 deletions

View File

@ -312,10 +312,10 @@ input[type="search"]::-webkit-search-cancel-button {
}
/* Control Bar */
@media screen and (max-width: 480px) {
@media screen and (max-width: 640px) {
.video-js .vjs-control-bar,
.vjs-menu-button-popup .vjs-menu .vjs-menu-content {
overflow: -webkit-paged-x;
overflow-x: scroll;
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
FROM alpine:edge AS builder
RUN apk add -u crystal shards libc-dev \
RUN apk add --no-cache crystal shards libc-dev \
yaml-dev libxml2-dev sqlite-dev zlib-dev openssl-dev \
sqlite-static zlib-static openssl-libs-static
WORKDIR /invidious
@ -15,7 +15,7 @@ RUN crystal build --static --release \
./src/invidious.cr
FROM alpine:latest
RUN apk add -u imagemagick ttf-opensans
RUN apk add --no-cache imagemagick ttf-opensans
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious

View File

@ -1,10 +1,10 @@
{
"`x` subscribers": {
"(\\D|^)1(\\D|$)": "`x` συνδρομητής",
"([^0-9]|^)1([^,0-9]|$)": "`x` συνδρομητής",
"": "`x` συνδρομητές"
},
"`x` videos": {
"(\\D|^)1(\\D|$)": "`x` βίντεο",
"([^0-9]|^)1([^,0-9]|$)": "`x` βίντεο",
"": "`x` βίντεο"
},
"LIVE": "ΖΩΝΤΑΝΑ",
@ -119,11 +119,11 @@
"Token manager": "Διαχειριστής διασυνδέσεων",
"Token": "Διασύνδεση",
"`x` subscriptions": {
"(\\D|^)1(\\D|$)": "`x` συνδρομή",
"([^0-9]|^)1([^,0-9]|$)": "`x` συνδρομή",
"": "`x` συνδρομές"
},
"`x` tokens": {
"(\\D|^)1(\\D|$)": "`x` διασύνδεση",
"([^0-9]|^)1([^,0-9]|$)": "`x` διασύνδεση",
"": "`x` διασυνδέσεις"
},
"Import/export": "Εισαγωγή/εξαγωγή",
@ -131,7 +131,7 @@
"revoke": "ανάκληση",
"Subscriptions": "Συνδρομές",
"`x` unseen notifications": {
"(\\D|^)1(\\D|$)": "`x` καινούρια ειδοποίηση",
"([^0-9]|^)1([^,0-9]|$)": "`x` καινούρια ειδοποίηση",
"": "`x` καινούριες ειδοποιήσεις"
},
"search": "αναζήτηση",
@ -154,7 +154,7 @@
"Blacklisted regions: ": "Μη-επιτρεπτές περιοχές: ",
"Shared `x`": "Μοιράστηκε το `x`",
"`x` views": {
"(\\D|^)1(\\D|$)": "`x` προβολή",
"([^0-9]|^)1([^,0-9]|$)": "`x` προβολή",
"": "`x` προβολές"
},
"Premieres in `x`": "Πρώτη προβολή σε `x`",
@ -188,13 +188,13 @@
"Could not get channel info.": "Αδύναμια εύρεσης πληροφοριών καναλιού.",
"Could not fetch comments": "Αδυναμία λήψης σχολίων",
"View `x` replies": {
"(\\D|^)1(\\D|$)": "Προβολή `x` απάντησης",
"([^0-9]|^)1([^,0-9]|$)": "Προβολή `x` απάντησης",
"": "Προβολή `x` απαντήσεων"
},
"`x` ago": "Πριν `x`",
"Load more": "Φόρτωση περισσότερων",
"`x` points": {
"(\\D|^)1(\\D|$)": "`x` βαθμός",
"([^0-9]|^)1([^,0-9]|$)": "`x` βαθμός",
"": "`x` βαθμοί"
},
"Could not create mix.": "Αδυναμία δημιουργίας μίξης.",
@ -315,31 +315,31 @@
"Yoruba": "Γιορούμπα",
"Zulu": "Ζουλού",
"`x` years": {
"(\\D|^)1(\\D|$)": "`x` χρόνο",
"([^0-9]|^)1([^,0-9]|$)": "`x` χρόνο",
"": "`x` χρόνια"
},
"`x` months": {
"(\\D|^)1(\\D|$)": "`x` μήνα",
"([^0-9]|^)1([^,0-9]|$)": "`x` μήνα",
"": "`x` μήνες"
},
"`x` weeks": {
"(\\D|^)1(\\D|$)": "`x` εβδομάδα",
"([^0-9]|^)1([^,0-9]|$)": "`x` εβδομάδα",
"": "`x` εβδομάδες"
},
"`x` days": {
"(\\D|^)1(\\D|$)": "`x` ημέρα",
"([^0-9]|^)1([^,0-9]|$)": "`x` ημέρα",
"": "`x` ημέρες"
},
"`x` hours": {
"(\\D|^)1(\\D|$)": "`x` ώρα",
"([^0-9]|^)1([^,0-9]|$)": "`x` ώρα",
"": "`x` ώρες"
},
"`x` minutes": {
"(\\D|^)1(\\D|$)": "`x` λεπτό",
"([^0-9]|^)1([^,0-9]|$)": "`x` λεπτό",
"": "`x` λεπτά"
},
"`x` seconds": {
"(\\D|^)1(\\D|$)": "`x` δευτερόλεπτο",
"([^0-9]|^)1([^,0-9]|$)": "`x` δευτερόλεπτο",
"": "`x` δευτερόλεπτα"
},
"Fallback comments: ": "Εναλλακτικά σχόλια: ",

View File

@ -1,10 +1,10 @@
{
"`x` subscribers": {
"(\\D|^)1(\\D|$)": "`x` subscriber",
"([^0-9]|^)1([^,0-9]|$)": "`x` subscriber",
"": "`x` subscribers"
},
"`x` videos": {
"(\\D|^)1(\\D|$)": "`x` video",
"([^0-9]|^)1([^,0-9]|$)": "`x` video",
"": "`x` videos"
},
"LIVE": "LIVE",
@ -122,11 +122,11 @@
"Token manager": "Token manager",
"Token": "Token",
"`x` subscriptions": {
"(\\D|^)1(\\D|$)": "`x` subscription",
"([^0-9]|^)1([^,0-9]|$)": "`x` subscription",
"": "`x` subscriptions"
},
"`x` tokens": {
"(\\D|^)1(\\D|$)": "`x` token",
"([^0-9]|^)1([^,0-9]|$)": "`x` token",
"": "`x` tokens"
},
"Import/export": "Import/export",
@ -134,7 +134,7 @@
"revoke": "revoke",
"Subscriptions": "Subscriptions",
"`x` unseen notifications": {
"(\\D|^)1(\\D|$)": "`x` unseen notification",
"([^0-9]|^)1([^,0-9]|$)": "`x` unseen notification",
"": "`x` unseen notifications"
},
"search": "search",
@ -157,7 +157,7 @@
"Blacklisted regions: ": "Blacklisted regions: ",
"Shared `x`": "Shared `x`",
"`x` views": {
"(\\D|^)1(\\D|$)": "`x` views",
"([^0-9]|^)1([^,0-9]|$)": "`x` views",
"": "`x` views"
},
"Premieres in `x`": "Premieres in `x`",
@ -191,13 +191,13 @@
"Could not get channel info.": "Could not get channel info.",
"Could not fetch comments": "Could not fetch comments",
"View `x` replies": {
"(\\D|^)1(\\D|$)": "View `x` reply",
"([^0-9]|^)1([^,0-9]|$)": "View `x` reply",
"": "View `x` replies"
},
"`x` ago": "`x` ago",
"Load more": "Load more",
"`x` points": {
"(\\D|^)1(\\D|$)": "`x` point",
"([^0-9]|^)1([^,0-9]|$)": "`x` point",
"": "`x` points"
},
"Could not create mix.": "Could not create mix.",
@ -318,31 +318,31 @@
"Yoruba": "Yoruba",
"Zulu": "Zulu",
"`x` years": {
"(\\D|^)1(\\D|$)": "`x` year",
"([^0-9]|^)1([^,0-9]|$)": "`x` year",
"": "`x` years"
},
"`x` months": {
"(\\D|^)1(\\D|$)": "`x` month",
"([^0-9]|^)1([^,0-9]|$)": "`x` month",
"": "`x` months"
},
"`x` weeks": {
"(\\D|^)1(\\D|$)": "`x` week",
"([^0-9]|^)1([^,0-9]|$)": "`x` week",
"": "`x` weeks"
},
"`x` days": {
"(\\D|^)1(\\D|$)": "`x` day",
"([^0-9]|^)1([^,0-9]|$)": "`x` day",
"": "`x` days"
},
"`x` hours": {
"(\\D|^)1(\\D|$)": "`x` hour",
"([^0-9]|^)1([^,0-9]|$)": "`x` hour",
"": "`x` hours"
},
"`x` minutes": {
"(\\D|^)1(\\D|$)": "`x` minute",
"([^0-9]|^)1([^,0-9]|$)": "`x` minute",
"": "`x` minutes"
},
"`x` seconds": {
"(\\D|^)1(\\D|$)": "`x` second",
"([^0-9]|^)1([^,0-9]|$)": "`x` second",
"": "`x` seconds"
},
"Fallback comments: ": "Fallback comments: ",

View File

@ -1,10 +1,10 @@
{
"`x` subscribers": {
"(\\D|^)1(\\D|$)": "`x` iscritto",
"([^0-9]|^)1([^,0-9]|$)": "`x` iscritto",
"": "`x` iscritti"
},
"`x` videos": {
"(\\D|^)1(\\D|$)": "`x` video",
"([^0-9]|^)1([^,0-9]|$)": "`x` video",
"": "`x` video"
},
"LIVE": "IN DIRETTA",
@ -119,11 +119,11 @@
"Token manager": "Gestione dei gettoni",
"Token": "Gettone",
"`x` subscriptions": {
"(\\D|^)1(\\D|$)": "`x` iscrizione",
"([^0-9]|^)1([^,0-9]|$)": "`x` iscrizione",
"": "`x` iscrizioni"
},
"`x` tokens": {
"(\\D|^)1(\\D|$)": "`x` gettone",
"([^0-9]|^)1([^,0-9]|$)": "`x` gettone",
"": "`x` gettoni"
},
"Import/export": "Importa/esporta",
@ -131,7 +131,7 @@
"revoke": "revoca",
"Subscriptions": "Iscrizioni",
"`x` unseen notifications": {
"(\\D|^)1(\\D|$)": "`x` notifica non visualizzata",
"([^0-9]|^)1([^,0-9]|$)": "`x` notifica non visualizzata",
"": "`x` notifiche non visualizzate"
},
"search": "Cerca",
@ -154,7 +154,7 @@
"Blacklisted regions: ": "Regioni in lista nera: ",
"Shared `x`": "Condiviso `x`",
"`x` views": {
"(\\D|^)1(\\D|$)": "`x` visualizzazione",
"([^0-9]|^)1([^,0-9]|$)": "`x` visualizzazione",
"": "`x` visualizzazioni"
},
"Premieres in `x`": "",
@ -188,13 +188,13 @@
"Could not get channel info.": "Impossibile ottenere le informazioni del canale.",
"Could not fetch comments": "Impossibile recuperare i commenti",
"View `x` replies": {
"(\\D|^)1(\\D|$)": "Visualizza `x` risposta",
"([^0-9]|^)1([^,0-9]|$)": "Visualizza `x` risposta",
"": "Visualizza `x` risposte"
},
"`x` ago": "`x` fa",
"Load more": "Carica altro",
"`x` points": {
"(\\D|^)1(\\D|$)": "`x` punto",
"([^0-9]|^)1([^,0-9]|$)": "`x` punto",
"": "`x` punti"
},
"Could not create mix.": "Impossibile creare il mix.",
@ -315,31 +315,31 @@
"Yoruba": "Yoruba",
"Zulu": "Zulu",
"`x` years": {
"(\\D|^)1(\\D|$)": "`x` anno",
"([^0-9]|^)1([^,0-9]|$)": "`x` anno",
"": "`x` anni"
},
"`x` months": {
"(\\D|^)1(\\D|$)": "`x` mese",
"([^0-9]|^)1([^,0-9]|$)": "`x` mese",
"": "`x` mesi"
},
"`x` weeks": {
"(\\D|^)1(\\D|$)": "`x` settimana",
"([^0-9]|^)1([^,0-9]|$)": "`x` settimana",
"": "`x` settimane"
},
"`x` days": {
"(\\D|^)1(\\D|$)": "`x` giorno",
"([^0-9]|^)1([^,0-9]|$)": "`x` giorno",
"": "`x` giorni"
},
"`x` hours": {
"(\\D|^)1(\\D|$)": "`x` ora",
"([^0-9]|^)1([^,0-9]|$)": "`x` ora",
"": "`x` ore"
},
"`x` minutes": {
"(\\D|^)1(\\D|$)": "`x` minuto",
"([^0-9]|^)1([^,0-9]|$)": "`x` minuto",
"": "`x` minuti"
},
"`x` seconds": {
"(\\D|^)1(\\D|$)": "`x` secondo",
"([^0-9]|^)1([^,0-9]|$)": "`x` secondo",
"": "`x` secondi"
},
"Fallback comments: ": "Commenti alternativi: ",
@ -367,4 +367,4 @@
"Playlists": "Playlist",
"Community": "Comunità",
"Current version: ": "Versione attuale: "
}
}

325
locales/tr.json Normal file
View File

@ -0,0 +1,325 @@
{
"`x` subscribers.": "`x` abone.",
"`x` videos.": "`x` video.",
"LIVE": "CANLI",
"Shared `x` ago": "`x` önce paylaşıldı",
"Unsubscribe": "Abonelikten çık",
"Subscribe": "Abone ol",
"View channel on YouTube": "Kanalı YouTube'da görüntüle",
"View playlist on YouTube": "Çalma listesini YouTube'da görüntüle",
"newest": "en yeni",
"oldest": "en eski",
"popular": "popüler",
"last": "son",
"Next page": "Sonraki sayfa",
"Previous page": "Önceki sayfa",
"Clear watch history?": "İzleme geçmisini temizle?",
"New password": "Yeni parola",
"New passwords must match": "Yeni parolalar eşleşmek zorunda",
"Cannot change password for Google accounts": "Google hesapları için parola değiştirilemez",
"Authorize token?": "Jetonu yetkilendir?",
"Authorize token for `x`?": "`x` için jetonu yetkilendir?",
"Yes": "Evet",
"No": "Hayır",
"Import and Export Data": "Verileri İçe ve Dışa Aktar",
"Import": "İçe aktar",
"Import Invidious data": "İnvidious verilerini içe aktar",
"Import YouTube subscriptions": "YouTube aboneliklerini içe aktar",
"Import FreeTube subscriptions (.db)": "FreeTube aboneliklerini içe aktar (.db)",
"Import NewPipe subscriptions (.json)": "NewPipe aboneliklerini içe aktar (.json)",
"Import NewPipe data (.zip)": "NewPipe verilerini içe aktar (.zip)",
"Export": "Dışa aktar",
"Export subscriptions as OPML": "Abonelikleri OPML olarak dışa aktar",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Abonelikleri OPML olarak dışa aktar (NewPipe ve FreeTube için)",
"Export data as JSON": "Verileri JSON olarak dışa aktar",
"Delete account?": "Hesabı sil?",
"History": "Geçmiş",
"An alternative front-end to YouTube": "YouTube için alternatif bir ön-yüz",
"JavaScript license information": "JavaScript lisans bilgileri",
"source": "kaynak",
"Log in": "Oturum aç",
"Log in/register": "Oturum aç/kayıt ol",
"Log in with Google": "Google ile oturum aç",
"User ID": "Kullanıcı kimliği",
"Password": "Parola",
"Time (h:mm:ss):": "Zaman (h:mm:ss):",
"Text CAPTCHA": "Metin CAPTCHA",
"Image CAPTCHA": "Resim CAPTCHA",
"Sign In": "Oturum Aç",
"Register": "Kayıt Ol",
"E-mail": "E-posta",
"Google verification code": "Google doğrulama kodu",
"Preferences": "Tercihler",
"Player preferences": "Oynatıcı tercihleri",
"Always loop: ": "Sürekli döngü: ",
"Autoplay: ": "Otomatik oynat: ",
"Play next by default: ": "Varsayılan olarak sonrakini oynat: ",
"Autoplay next video: ": "Sonraki videoyu otomatik oynat: ",
"Listen by default: ": "Varsayılan olarak dinle: ",
"Proxy videos: ": "Videoları proxy'le: ",
"Default speed: ": "Varsayılan hız: ",
"Preferred video quality: ": "Tercih edilen video kalitesi: ",
"Player volume: ": "Oynatıcı ses seviyesi: ",
"Default comments: ": "Varsayılan yorumlar: ",
"youtube": "youtube",
"reddit": "reddit",
"Default captions: ": "Varsayılan altyazılar: ",
"Fallback captions: ": "Yedek altyazılar: ",
"Show related videos: ": "İlgili videoları göster: ",
"Show annotations by default: ": "Varsayılan olarak ek açıklamaları göster: ",
"Visual preferences": "Görsel tercihler",
"Player style: ": "Oynatıcı biçimi: ",
"Dark mode: ": "Karanlık mod: ",
"Theme: ": "Tema: ",
"dark": "karanlık",
"light": "aydınlık",
"Thin mode: ": "İnce mod: ",
"Subscription preferences": "Abonelik tercihleri",
"Show annotations by default for subscribed channels: ": "Abone olunan kanallar için ek açıklamaları varsayılan olarak göster: ",
"Redirect homepage to feed: ": "Ana sayfayı akışa yönlendir: ",
"Number of videos shown in feed: ": "Akışta gösterilen video sayısı: ",
"Sort videos by: ": "Videoları sıralama kriteri: ",
"published": "yayınlandı",
"published - reverse": "yayınlandı - ters",
"alphabetically": "alfabetik olarak",
"alphabetically - reverse": "alfabetik olarak - ters",
"channel name": "kanal adı",
"channel name - reverse": "kanal adı - ters",
"Only show latest video from channel: ": "Sadece kanaldaki en son videoyu göster: ",
"Only show latest unwatched video from channel: ": "Sadece kanaldaki en son izlenmemiş videoyu göster: ",
"Only show unwatched: ": "Sadece izlenmemişleri göster: ",
"Only show notifications (if there are any): ": "Sadece bildirimleri göster (eğer varsa): ",
"Enable web notifications": "Ağ bildirimlerini etkinleştir",
"`x` uploaded a video": "`x` bir video yükledi",
"`x` is live": "`x` canlı yayında",
"Data preferences": "Veri tercihleri",
"Clear watch history": "İzleme geçmişini temizle",
"Import/export data": "Verileri içe/dışa aktar",
"Change password": "Parolayı değiştir",
"Manage subscriptions": "Abonelikleri yönet",
"Manage tokens": "Jetonları yönet",
"Watch history": "İzleme geçmişi",
"Delete account": "Hesap silme",
"Administrator preferences": "Yönetici tercihleri",
"Default homepage: ": "Varsayılan ana sayfa: ",
"Feed menu: ": "Akış menüsü: ",
"Top enabled: ": "Top etkin: ",
"CAPTCHA enabled: ": "CAPTCHA etkin: ",
"Login enabled: ": "Oturum açma etkin: ",
"Registration enabled: ": "Kayıt olma etkin: ",
"Report statistics: ": "Rapor istatistikleri: ",
"Save preferences": "Tercihleri kaydet",
"Subscription manager": "Abonelik yöneticisi",
"Token manager": "Jeton yöneticisi",
"Token": "Jeton",
"`x` subscriptions.": "`x` abonelik.",
"`x` tokens.": "`x` jeton.",
"Import/export": "İçe/dışa aktar",
"unsubscribe": "abonelikten çık",
"revoke": "geri al",
"Subscriptions": "Abonelikler",
"`x` unseen notifications.": "`x` okunmamış bildirim.",
"search": "ara",
"Log out": ıkış yap",
"Released under the AGPLv3 by Omar Roth.": "Omar Roth tarafından AGPLv3 altında yayımlandı.",
"Source available here.": "Kaynak kodu burada mevcut.",
"View JavaScript license information.": "JavaScript lisans bilgilerini görüntüle.",
"View privacy policy.": "Gizlilik politikasını görüntüle.",
"Trending": "Trendler",
"Unlisted": "Listelenmemiş",
"Watch on YouTube": "YouTube'da izle",
"Hide annotations": "Ek açıklamaları gizle",
"Show annotations": "Ek açıklamaları göster",
"Genre: ": "Tür: ",
"License: ": "Lisans: ",
"Family friendly? ": "Aile için uygun? ",
"Wilson score: ": "Wilson puanı: ",
"Engagement: ": "İzleyenlerin oy verme oranı: ",
"Whitelisted regions: ": "Beyaz listeye alınan bölgeler: ",
"Blacklisted regions: ": "Kara listeye alınan bölgeler: ",
"Shared `x`": "`x` paylaşıldı",
"`x` views.": "`x` izlenme.",
"Premieres in `x`": "`x`içinde ilk gösterim",
"Premieres `x`": "`x` ilk gösterim",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Merhaba! JavaScript'i kapatmış gibi görünüyorsun. Yorumları görüntülemek için buraya tıkla, yüklenmelerinin biraz uzun sürebileceğini unutma.",
"View YouTube comments": "YouTube yorumlarını görüntüle",
"View more comments on Reddit": "Reddit'te daha fazla yorum görüntüle",
"View `x` comments": "`x` yorum görüntüle",
"View Reddit comments": "Reddit yorumlarını görüntüle",
"Hide replies": "Cevapları gizle",
"Show replies": "Cevapları göster",
"Incorrect password": "Yanlış parola",
"Quota exceeded, try again in a few hours": "Kota aşıldı, birkaç saat içinde tekrar deneyin",
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Oturum açılamadı, iki faktörlü kimlik doğrulamanın (Authenticator ya da SMS) açık olduğundan emin olun.",
"Invalid TFA code": "Geçersiz TFA kodu",
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Giriş başarısız. Bunun nedeni, hesabınız için iki faktörlü kimlik doğrulamanın açık olmaması olabilir.",
"Wrong answer": "Yanlış cevap",
"Erroneous CAPTCHA": "Hatalı CAPTCHA",
"CAPTCHA is a required field": "CAPTCHA zorunlu bir alandır",
"User ID is a required field": "Kullanıcı kimliği zorunlu bir alandır",
"Password is a required field": "Parola zorunlu bir alandır",
"Wrong username or password": "Yanlış kullanıcı adı ya da parola",
"Please sign in using 'Log in with Google'": "Lütfen 'Google ile giriş yap' seçeneğini kullanarak oturum açın",
"Password cannot be empty": "Parola boş olamaz",
"Password cannot be longer than 55 characters": "Parola 55 karakterden uzun olamaz",
"Please log in": "Lütfen oturum açın",
"Invidious Private Feed for `x`": "`x` için İnvidious Özel Akışı",
"channel:`x`": "kanal:`x`",
"Deleted or invalid channel": "Silinmiş ya da geçersiz kanal",
"This channel does not exist.": "Bu kanal mevcut değil.",
"Could not get channel info.": "Kanal bilgisi alınamadı.",
"Could not fetch comments": "Yorumlar alınamadı",
"View `x` replies.": "`x` yanıtı görüntüle.",
"`x` ago": "`x` önce",
"Load more": "Daha fazla yükle",
"`x` points.": "`x` puan.",
"Could not create mix.": "Mix oluşturulamadı.",
"Empty playlist": "Boş oynatma listesi",
"Not a playlist.": "Oynatma listesi değil.",
"Playlist does not exist.": "Oynatma listesi mevcut değil.",
"Could not pull trending pages.": "Trend sayfaları alınamıyor.",
"Hidden field \"challenge\" is a required field": "Gizli alan \"challenge\" zorunlu bir alandır",
"Hidden field \"token\" is a required field": "Gizli alan \"jeton\" zorunlu bir alandır",
"Erroneous challenge": "Hatalı challenge",
"Erroneous token": "Hatalı jeton",
"No such user": "Böyle bir kullanıcı yok",
"Token is expired, please try again": "Jetonun süresi doldu, lütfen tekrar deneyin",
"English": "İngilizce",
"English (auto-generated)": "İngilizce (otomatik oluşturuldu)",
"Afrikaans": "Afrikanca",
"Albanian": "Arnavutça",
"Amharic": "Amharca",
"Arabic": "Arapça",
"Armenian": "Ermenice",
"Azerbaijani": "Azerice",
"Bangla": "Bengalce",
"Basque": "Baskça",
"Belarusian": "Belarusça",
"Bosnian": "Boşnakça",
"Bulgarian": "Bulgarca",
"Burmese": "Birmanca",
"Catalan": "Katalanca",
"Cebuano": "Sebuanca",
"Chinese (Simplified)": "Çince (Basitleştirilmiş)",
"Chinese (Traditional)": "Çince (Geleneksel)",
"Corsican": "Korsikaca",
"Croatian": "Hırvatça",
"Czech": "Çekçe",
"Danish": "Danca",
"Dutch": "Flemenkçe",
"Esperanto": "Esperanto",
"Estonian": "Estonca",
"Filipino": "Filipince",
"Finnish": "Fince",
"French": "Fransızca",
"Galician": "Galiçyaca",
"Georgian": "Gürcüce",
"German": "Almanca",
"Greek": "Yunanca",
"Gujarati": "Guceratça",
"Haitian Creole": "Haiti Creole dili",
"Hausa": "Hausaca",
"Hawaiian": "Hawaii dili",
"Hebrew": "İbranice",
"Hindi": "Hintçe",
"Hmong": "Hmong",
"Hungarian": "Macarca",
"Icelandic": "İzlandaca",
"Igbo": "İgbo",
"Indonesian": "Endonezce",
"Irish": "İrlandaca",
"Italian": "İtalyanca",
"Japanese": "Japonca",
"Javanese": "Cava dili",
"Kannada": "Kannada dili",
"Kazakh": "Kazakça",
"Khmer": "Kmerce",
"Korean": "Korece",
"Kurdish": "Kürtçe",
"Kyrgyz": "Kırgızca",
"Lao": "Laoca",
"Latin": "Latince",
"Latvian": "Letonca",
"Lithuanian": "Litvanyaca",
"Luxembourgish": "Lüksemburgca",
"Macedonian": "Makedonca",
"Malagasy": "Malgaşça",
"Malay": "Malayca",
"Malayalam": "Malayalam dili",
"Maltese": "Maltaca",
"Maori": "Maori dili",
"Marathi": "Marati dili",
"Mongolian": "Moğolca",
"Nepali": "Nepalce",
"Norwegian Bokmål": "Norveççe Bokmål",
"Nyanja": "Çevaca",
"Pashto": "Peştuca",
"Persian": "Farsça",
"Polish": "Lehçe",
"Portuguese": "Portekizce",
"Punjabi": "Pencap dili",
"Romanian": "Rumence",
"Russian": "Rusça",
"Samoan": "Samoa dili",
"Scottish Gaelic": "İskoç Galcesi",
"Serbian": "Sırpça",
"Shona": "Şona dili",
"Sindhi": "Sintçe",
"Sinhala": "Seylanca",
"Slovak": "Slovakça",
"Slovenian": "Slovence",
"Somali": "Somalice",
"Southern Sotho": "Güney Sotho dili",
"Spanish": "İspanyolca",
"Spanish (Latin America)": "İspanyolca (Latin Amerika)",
"Sundanese": "Sundaca",
"Swahili": "Svahili dili",
"Swedish": "İsveççe",
"Tajik": "Tacikçe",
"Tamil": "Tamilce",
"Telugu": "Telugu dili",
"Thai": "Tayca",
"Turkish": "Türkçe",
"Ukrainian": "Ukraynaca",
"Urdu": "Urduca",
"Uzbek": "Özbekçe",
"Vietnamese": "Vietnamca",
"Welsh": "Galce",
"Western Frisian": "Batı Frizcesi",
"Xhosa": "Xhosa dili",
"Yiddish": "Yiddiş",
"Yoruba": "Yoruba dili",
"Zulu": "Zuluca",
"`x` years": "`x` yıl",
"`x` months": "`x` ay",
"`x` weeks": "`x` hafta",
"`x` days": "`x` gün",
"`x` hours": "`x` saat",
"`x` minutes": "`x` dakika",
"`x` seconds": "`x` saniye",
"Fallback comments: ": "Yedek yorumlar: ",
"Popular": "Popüler",
"Top": "Enler",
"About": "Hakkında",
"Rating: ": "Değerlendirme: ",
"Language: ": "Dil: ",
"View as playlist": "Oynatma listesi olarak görüntüle",
"Default": "Varsayılan",
"Music": "Müzik",
"Gaming": "Oyun",
"News": "Haberler",
"Movies": "Filmler",
"Download": "İndir",
"Download as: ": "Şu şekilde indir: ",
"%A %B %-d, %Y": "%A %B %-d, %Y",
"(edited)": "(düzenlendi)",
"YouTube comment permalink": "YouTube yorumu kalıcı linki",
"permalink": "kalıcı link",
"`x` marked it with a ❤": "`x` ❤ ile işaretlendi",
"Audio mode": "Ses modu",
"Video mode": "Video modu",
"Videos": "Videolar",
"Playlists": "Oynatma listeleri",
"Community": "Topluluk",
"Current version: ": "Şu anki versiyon: "
}

View File

@ -11,14 +11,14 @@ targets:
dependencies:
pg:
github: will/crystal-pg
version: ~> 0.18.1
version: ~> 0.19.0
sqlite3:
github: crystal-lang/crystal-sqlite3
version: ~> 0.13.0
version: ~> 0.14.0
kemal:
github: kemalcr/kemal
version: ~> 0.26.0
crystal: 0.30.1
crystal: 0.31.0
license: AGPLv3

View File

@ -17,7 +17,6 @@
require "digest/md5"
require "file_utils"
require "kemal"
require "markdown"
require "openssl/hmac"
require "option_parser"
require "pg"
@ -86,6 +85,7 @@ LOCALES = {
"nl" => load_locale("nl"),
"pl" => load_locale("pl"),
"ru" => load_locale("ru"),
"tr" => load_locale("tr"),
"uk" => load_locale("uk"),
"zh-CN" => load_locale("zh-CN"),
}
@ -300,7 +300,7 @@ before_all do |env|
current_page += "?#{query}"
end
env.set "current_page", URI.escape(current_page)
env.set "current_page", URI.encode_www_form(current_page)
end
get "/" do |env|
@ -395,7 +395,7 @@ get "/watch" do |env|
begin
video = get_video(id, PG_DB, region: params.region)
rescue ex : VideoRedirect
next env.redirect "/watch?v=#{ex.message}"
next env.redirect env.request.resource.gsub(id, ex.video_id)
rescue ex
error_message = ex.message
env.response.status_code = 500
@ -672,7 +672,7 @@ get "/embed/:id" do |env|
begin
video = get_video(id, PG_DB, region: params.region)
rescue ex : VideoRedirect
next env.redirect "/embed/#{ex.message}"
next env.redirect env.request.resource.gsub(id, ex.video_id)
rescue ex
error_message = ex.message
env.response.status_code = 500
@ -845,7 +845,7 @@ get "/results" do |env|
page ||= 1
if query
env.redirect "/search?q=#{URI.escape(query)}&page=#{page}"
env.redirect "/search?q=#{URI.encode_www_form(query)}&page=#{page}"
else
env.redirect "/"
end
@ -1022,6 +1022,7 @@ post "/login" do |env|
headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8"
headers["Google-Accounts-XSRF"] = "1"
headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.75 Safari/537.36"
response = client.post("/_/signin/sl/lookup", headers, login_req(lookup_req))
lookup_results = JSON.parse(response.body[5..-1])
@ -1053,7 +1054,7 @@ post "/login" do |env|
traceback << "done, returned #{response.status_code}.<br/>"
headers["Cookie"] = URI.unescape(headers["Cookie"])
headers["Cookie"] = URI.decode_www_form(headers["Cookie"])
if challenge_results[0][3]?.try &.== 7
error_message = translate(locale, "Account has temporarily been disabled")
@ -1061,6 +1062,13 @@ post "/login" do |env|
next templated "error"
end
# TODO: Handle Google's CAPTCHA
if captcha = challenge_results[0][-1]?.try &.[-1]?.try &.as_h?.try &.["5001"]?.try &.[-1].as_a?
error_message = "Unhandled CAPTCHA. Please try again later."
env.response.status_code = 401
next templated "error"
end
if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED"
error_message = translate(locale, "Incorrect password")
env.response.status_code = 401
@ -1078,7 +1086,7 @@ post "/login" do |env|
end
# Prefer Authenticator app and SMS over unsupported protocols
if !{6, 9, 12, 15}.includes?(challenge_results[0][-1][0][0][8]) && prompt_type == 4
if !{6, 9, 12, 15}.includes?(challenge_results[0][-1][0][0][8].as_i) && prompt_type == 2
tfa = challenge_results[0][-1][0].as_a.select { |auth_type| {6, 9, 12, 15}.includes? auth_type[8] }[0]
traceback << "Selecting challenge #{tfa[8]}..."
@ -1186,8 +1194,12 @@ post "/login" do |env|
break
end
# TODO: Occasionally there will be a second page after login confirming
# the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently choke on.
# Occasionally there will be a second page after login confirming
# the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently don't handle.
if location.includes? "/b/0/SmsAuthInterstitial"
traceback << "Unhandled dialog /b/0/SmsAuthInterstitial."
end
login = client.get(location, headers)
headers = login.cookies.add_request_headers(headers)
@ -1389,7 +1401,7 @@ post "/login" do |env|
user_array[4] = user_array[4].to_json
args = arg_array(user_array)
PG_DB.exec("INSERT INTO users VALUES (#{args})", user_array)
PG_DB.exec("INSERT INTO users VALUES (#{args})", args: user_array)
PG_DB.exec("INSERT INTO session_ids VALUES ($1, $2, $3)", sid, email, Time.utc)
view_name = "subscriptions_#{sha256(user.email)}"
@ -2475,7 +2487,7 @@ post "/authorize_token" do |env|
access_token = generate_token(user.email, scopes, expire, HMAC_KEY, PG_DB)
if callback_url
access_token = URI.escape(access_token)
access_token = URI.encode_www_form(access_token)
url = URI.parse(callback_url)
if url.query
@ -2696,6 +2708,8 @@ get "/feed/channel/:ucid" do |env|
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
rescue ex
error_message = ex.message
env.response.status_code = 500
@ -2958,7 +2972,7 @@ post "/feed/webhook/:token" do |env|
PG_DB.exec("INSERT INTO channel_videos VALUES (#{args}) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, premiere_timestamp = $9, views = $10", video_array)
live_now = $8, premiere_timestamp = $9, views = $10", args: video_array)
# Update all users affected by insert
if emails.empty?
@ -3099,6 +3113,8 @@ get "/channel/:ucid" do |env|
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
rescue ex
error_message = ex.message
env.response.status_code = 500
@ -3166,6 +3182,8 @@ get "/channel/:ucid/playlists" do |env|
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
rescue ex
error_message = ex.message
env.response.status_code = 500
@ -3204,6 +3222,8 @@ get "/channel/:ucid/community" do |env|
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
next env.redirect env.request.resource.gsub(ucid, ex.channel_id)
rescue ex
error_message = ex.message
env.response.status_code = 500
@ -3259,7 +3279,10 @@ get "/api/v1/storyboards/:id" do |env|
begin
video = get_video(id, PG_DB, region: region)
rescue ex : VideoRedirect
next env.redirect "/api/v1/storyboards/#{ex.message}"
error_message = {"error" => "Video is unavailable", "videoId" => ex.video_id}.to_json
env.response.status_code = 302
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
next error_message
rescue ex
env.response.status_code = 500
next
@ -3344,7 +3367,10 @@ get "/api/v1/captions/:id" do |env|
begin
video = get_video(id, PG_DB, region: region)
rescue ex : VideoRedirect
next env.redirect "/api/v1/captions/#{ex.message}"
error_message = {"error" => "Video is unavailable", "videoId" => ex.video_id}.to_json
env.response.status_code = 302
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
next error_message
rescue ex
env.response.status_code = 500
next
@ -3365,7 +3391,7 @@ get "/api/v1/captions/:id" do |env|
json.object do
json.field "label", caption.name.simpleText
json.field "languageCode", caption.languageCode
json.field "url", "/api/v1/captions/#{id}?label=#{URI.escape(caption.name.simpleText)}"
json.field "url", "/api/v1/captions/#{id}?label=#{URI.encode_www_form(caption.name.simpleText)}"
end
end
end
@ -3444,7 +3470,7 @@ get "/api/v1/captions/:id" do |env|
if title = env.params.query["title"]?
# https://blog.fastmail.com/2011/06/24/download-non-english-filenames/
env.response.headers["Content-Disposition"] = "attachment; filename=\"#{URI.escape(title)}\"; filename*=UTF-8''#{URI.escape(title)}"
env.response.headers["Content-Disposition"] = "attachment; filename=\"#{URI.encode_www_form(title)}\"; filename*=UTF-8''#{URI.encode_www_form(title)}"
end
webvtt
@ -3632,7 +3658,7 @@ get "/api/v1/annotations/:id" do |env|
id = id.sub(/^-/, 'A')
end
file = URI.escape("#{id[0, 3]}/#{id}.xml")
file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
client = make_client(ARCHIVE_URL)
location = client.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}")
@ -3684,7 +3710,10 @@ get "/api/v1/videos/:id" do |env|
begin
video = get_video(id, PG_DB, region: region)
rescue ex : VideoRedirect
next env.redirect "/api/v1/videos/#{ex.message}"
error_message = {"error" => "Video is unavailable", "videoId" => ex.video_id}.to_json
env.response.status_code = 302
env.response.headers["Location"] = env.request.resource.gsub(id, ex.video_id)
next error_message
rescue ex
error_message = {"error" => ex.message}.to_json
env.response.status_code = 500
@ -3787,6 +3816,11 @@ get "/api/v1/channels/:ucid" do |env|
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
error_message = {"error" => "Channel is unavailable", "authorId" => ex.channel_id}.to_json
env.response.status_code = 302
env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id)
next error_message
rescue ex
error_message = {"error" => ex.message}.to_json
env.response.status_code = 500
@ -3917,6 +3951,11 @@ end
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
error_message = {"error" => "Channel is unavailable", "authorId" => ex.channel_id}.to_json
env.response.status_code = 302
env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id)
next error_message
rescue ex
error_message = {"error" => ex.message}.to_json
env.response.status_code = 500
@ -3981,6 +4020,11 @@ end
begin
channel = get_about_info(ucid, locale)
rescue ex : ChannelRedirect
error_message = {"error" => "Channel is unavailable", "authorId" => ex.channel_id}.to_json
env.response.status_code = 302
env.response.headers["Location"] = env.request.resource.gsub(ucid, ex.channel_id)
next error_message
rescue ex
error_message = {"error" => ex.message}.to_json
env.response.status_code = 500
@ -4113,7 +4157,7 @@ get "/api/v1/search/suggestions" do |env|
begin
client = make_client(URI.parse("https://suggestqueries.google.com"))
response = client.get("/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.escape(query)}&callback=suggestCallback").body
response = client.get("/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback").body
body = response[35..-2]
body = JSON.parse(body).as_a
@ -4497,7 +4541,7 @@ post "/api/v1/auth/tokens/register" do |env|
access_token = generate_token(user.email, authorized_scopes, expire, HMAC_KEY, PG_DB)
if callback_url
access_token = URI.escape(access_token)
access_token = URI.encode_www_form(access_token)
if query = callback_url.query
query = HTTP::Params.parse(query.not_nil!)
@ -4565,11 +4609,7 @@ get "/api/manifest/dash/id/:id" do |env|
begin
video = get_video(id, PG_DB, region: region)
rescue ex : VideoRedirect
url = "/api/manifest/dash/id/#{ex.message}"
if env.params.query
url += "?#{env.params.query}"
end
next env.redirect url
next env.redirect env.request.resource.gsub(id, ex.video_id)
rescue ex
env.response.status_code = 403
next
@ -4736,7 +4776,7 @@ get "/api/manifest/hls_playlist/*" do |env|
raw_params = {} of String => Array(String)
path.each_slice(2) do |pair|
key, value = pair
value = URI.unescape(value)
value = URI.decode_www_form(value)
if raw_params[key]?
raw_params[key] << value
@ -4861,7 +4901,7 @@ get "/videoplayback/*" do |env|
raw_params = {} of String => Array(String)
path.each_slice(2) do |pair|
key, value = pair
value = URI.unescape(value)
value = URI.decode_www_form(value)
if raw_params[key]?
raw_params[key] << value
@ -5035,7 +5075,7 @@ get "/videoplayback" do |env|
if title = query_params["title"]?
# https://blog.fastmail.com/2011/06/24/download-non-english-filenames/
env.response.headers["Content-Disposition"] = "attachment; filename=\"#{URI.escape(title)}\"; filename*=UTF-8''#{URI.escape(title)}"
env.response.headers["Content-Disposition"] = "attachment; filename=\"#{URI.encode_www_form(title)}\"; filename*=UTF-8''#{URI.encode_www_form(title)}"
end
if !response.headers.includes_word?("Transfer-Encoding", "chunked")
@ -5348,4 +5388,6 @@ add_context_storage_type(Preferences)
add_context_storage_type(User)
Kemal.config.logger = logger
Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.config.host_binding : CONFIG.host_binding
Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port
Kemal.run

View File

@ -118,7 +118,7 @@ struct AboutChannel
description_html: String,
paid: Bool,
total_views: Int64,
sub_count: Int64,
sub_count: Int32,
joined: Time,
is_family_friendly: Bool,
allowed_regions: Array(String),
@ -127,6 +127,13 @@ struct AboutChannel
})
end
class ChannelRedirect < Exception
property channel_id : String
def initialize(@channel_id)
end
end
def get_batch_channels(channels, db, refresh = false, pull_all_videos = true, max_threads = 10)
finished_channel = Channel(String | Nil).new
@ -172,14 +179,14 @@ def get_channel(id, db, refresh = true, pull_all_videos = true)
args = arg_array(channel_array)
db.exec("INSERT INTO channels VALUES (#{args}) \
ON CONFLICT (id) DO UPDATE SET author = $2, updated = $3", channel_array)
ON CONFLICT (id) DO UPDATE SET author = $2, updated = $3", args: channel_array)
end
else
channel = fetch_channel(id, db, pull_all_videos: pull_all_videos)
channel_array = channel.to_a
args = arg_array(channel_array)
db.exec("INSERT INTO channels VALUES (#{args})", channel_array)
db.exec("INSERT INTO channels VALUES (#{args})", args: channel_array)
end
return channel
@ -268,7 +275,7 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
db.exec("INSERT INTO channel_videos VALUES (#{args}) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, views = $10", video_array)
live_now = $8, views = $10", args: video_array)
# Update all users affected by insert
if emails.empty?
@ -336,7 +343,7 @@ def fetch_channel(ucid, db, pull_all_videos = true, locale = nil)
db.exec("INSERT INTO channel_videos VALUES (#{args}) \
ON CONFLICT (id) DO UPDATE SET title = $2, published = $3, \
updated = $4, ucid = $5, author = $6, length_seconds = $7, \
live_now = $8, views = $10", video_array)
live_now = $8, views = $10", args: video_array)
# Update all users affected by insert
if emails.empty?
@ -469,7 +476,7 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
end
data = Base64.urlsafe_encode(data)
cursor = URI.escape(data)
cursor = URI.encode_www_form(data)
data = IO::Memory.new
@ -490,7 +497,7 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
IO.copy data, buffer
continuation = Base64.urlsafe_encode(buffer)
continuation = URI.escape(continuation)
continuation = URI.encode_www_form(continuation)
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
@ -540,7 +547,7 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated
data.rewind
data = Base64.urlsafe_encode(data)
continuation = URI.escape(data)
continuation = URI.encode_www_form(data)
data = IO::Memory.new
@ -561,7 +568,7 @@ def produce_channel_playlists_url(ucid, cursor, sort = "newest", auto_generated
IO.copy data, buffer
continuation = Base64.urlsafe_encode(buffer)
continuation = URI.escape(continuation)
continuation = URI.encode_www_form(continuation)
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
@ -571,7 +578,7 @@ end
def extract_channel_playlists_cursor(url, auto_generated)
continuation = HTTP::Params.parse(URI.parse(url).query.not_nil!)["continuation"]
continuation = URI.unescape(continuation)
continuation = URI.decode_www_form(continuation)
data = IO::Memory.new(Base64.decode(continuation))
# 0xe2 0xa9 0x85 0xb2 0x02
@ -590,7 +597,7 @@ def extract_channel_playlists_cursor(url, auto_generated)
data.read inner_continuation
continuation = String.new(inner_continuation)
continuation = URI.unescape(continuation)
continuation = URI.decode_www_form(continuation)
data = IO::Memory.new(Base64.decode(continuation))
# 0x12 0x09 playlists
@ -607,7 +614,7 @@ def extract_channel_playlists_cursor(url, auto_generated)
cursor = String.new(cursor)
if !auto_generated
cursor = URI.unescape(cursor)
cursor = URI.decode_www_form(cursor)
cursor = Base64.decode_string(cursor)
end
@ -870,7 +877,7 @@ def fetch_channel_community(ucid, continuation, locale, config, kemal_config, fo
end
def produce_channel_community_continuation(ucid, cursor)
cursor = URI.escape(cursor)
cursor = URI.encode_www_form(cursor)
data = IO::Memory.new
@ -891,13 +898,13 @@ def produce_channel_community_continuation(ucid, cursor)
IO.copy data, buffer
continuation = Base64.urlsafe_encode(buffer)
continuation = URI.escape(continuation)
continuation = URI.encode_www_form(continuation)
return continuation
end
def extract_channel_community_cursor(continuation)
continuation = URI.unescape(continuation)
continuation = URI.decode_www_form(continuation)
data = IO::Memory.new(Base64.decode(continuation))
# 0xe2 0xa9 0x85 0xb2 0x02
@ -916,7 +923,7 @@ def extract_channel_community_cursor(continuation)
data.read_byte
end
return URI.unescape(data.gets_to_end)
return URI.decode_www_form(data.gets_to_end)
end
def get_about_info(ucid, locale)
@ -927,6 +934,10 @@ def get_about_info(ucid, locale)
about = client.get("/user/#{ucid}/about?disable_polymer=1&gl=US&hl=en")
end
if md = about.headers["location"]?.try &.match(/\/channel\/(?<ucid>UC[a-zA-Z0-9_-]{22})/)
raise ChannelRedirect.new(channel_id: md["ucid"])
end
about = XML.parse_html(about.body)
if about.xpath_node(%q(//div[contains(@class, "channel-empty-message")]))
@ -940,12 +951,6 @@ def get_about_info(ucid, locale)
raise error_message
end
sub_count = about.xpath_node(%q(//span[contains(text(), "subscribers")]))
if sub_count
sub_count = sub_count.content.delete(", subscribers").to_i?
end
sub_count ||= 0
author = about.xpath_node(%q(//span[contains(@class,"qualified-channel-title-text")]/a)).not_nil!.content
author_url = about.xpath_node(%q(//span[contains(@class,"qualified-channel-title-text")]/a)).not_nil!["href"]
author_thumbnail = about.xpath_node(%q(//img[@class="channel-header-profile-image"])).not_nil!["src"]
@ -989,21 +994,14 @@ def get_about_info(ucid, locale)
)
end
total_views = 0_i64
sub_count = 0_i64
joined = about.xpath_node(%q(//span[contains(., "Joined")]))
.try &.content.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
joined = Time.unix(0)
metadata = about.xpath_nodes(%q(//span[@class="about-stat"]))
metadata.each do |item|
case item.content
when .includes? "views"
total_views = item.content.gsub(/\D/, "").to_i64
when .includes? "subscribers"
sub_count = item.content.delete("subscribers").gsub(/\D/, "").to_i64
when .includes? "Joined"
joined = Time.parse(item.content.lchop("Joined "), "%b %-d, %Y", Time::Location.local)
end
end
total_views = about.xpath_node(%q(//span[contains(., "views")]/b))
.try &.content.try &.gsub(/\D/, "").to_i64? || 0_i64
sub_count = about.xpath_node(%q(.//span[contains(@class, "subscriber-count")]))
.try &.["title"].try { |text| short_text_to_number(text) } || 0
# Auto-generated channels
# https://support.google.com/youtube/answer/2579942
@ -1015,7 +1013,7 @@ def get_about_info(ucid, locale)
tabs = about.xpath_nodes(%q(//ul[@id="channel-navigation-menu"]/li/a/span)).map { |node| node.content.downcase }
return AboutChannel.new(
AboutChannel.new(
ucid: ucid,
author: author,
auto_generated: auto_generated,

View File

@ -573,7 +573,7 @@ def content_to_comment_html(content)
end
def extract_comment_cursor(continuation)
continuation = URI.unescape(continuation)
continuation = URI.decode_www_form(continuation)
data = IO::Memory.new(Base64.decode(continuation))
# 0x12 0x26
@ -653,7 +653,7 @@ def produce_comment_continuation(video_id, cursor = "", sort_by = "top")
end
continuation = Base64.urlsafe_encode(data)
continuation = URI.escape(continuation)
continuation = URI.encode_www_form(continuation)
return continuation
end
@ -695,7 +695,7 @@ def produce_comment_reply_continuation(video_id, ucid, comment_id)
data.write(Bytes[0x48, 0x0a])
continuation = Base64.urlsafe_encode(data.to_slice)
continuation = URI.escape(continuation)
continuation = URI.encode_www_form(continuation)
return continuation
end

View File

@ -95,8 +95,8 @@ class AuthHandler < Kemal::Handler
begin
if token = env.request.headers["Authorization"]?
token = JSON.parse(URI.unescape(token.lchop("Bearer ")))
session = URI.unescape(token["session"].as_s)
token = JSON.parse(URI.decode_www_form(token.lchop("Bearer ")))
session = URI.decode_www_form(token["session"].as_s)
scopes, expire, signature = validate_request(token, session, env.request, HMAC_KEY, PG_DB, nil)
if email = PG_DB.query_one?("SELECT email FROM session_ids WHERE id = $1", session, as: String)
@ -238,40 +238,15 @@ class HTTP::Client
end
end
# https://github.com/will/crystal-pg/pull/171
class PG::Statement < ::DB::Statement
protected def perform_query(args : Enumerable) : ResultSet
params = args.map { |arg| PQ::Param.encode(arg) }
conn = self.conn
conn.send_parse_message(@sql)
conn.send_bind_message params
conn.send_describe_portal_message
conn.send_execute_message
conn.send_sync_message
conn.expect_frame PQ::Frame::ParseComplete
conn.expect_frame PQ::Frame::BindComplete
frame = conn.read
case frame
when PQ::Frame::RowDescription
fields = frame.fields
when PQ::Frame::NoData
fields = nil
else
raise "expected RowDescription or NoData, got #{frame}"
end
ResultSet.new(self, fields)
rescue IO::Error
raise DB::ConnectionLost.new(connection)
end
struct Crystal::ThreadLocalValue(T)
@values = Hash(Thread, T).new
protected def perform_exec(args : Enumerable) : ::DB::ExecResult
result = perform_query(args)
result.each { }
::DB::ExecResult.new(
rows_affected: result.rows_affected,
last_insert_id: 0_i64 # postgres doesn't support this
)
rescue IO::Error
raise DB::ConnectionLost.new(connection)
def get(&block : -> T)
th = Thread.current
if !@values[th]?
@values[th] = yield
else
@values[th]
end
end
end

View File

@ -236,6 +236,8 @@ struct Config
hsts: {type: Bool?, default: true}, # Enables 'Strict-Transport-Security'. Ensure that `domain` and all subdomains are served securely
disable_proxy: {type: Bool? | Array(String)?, default: false}, # Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
force_resolve: {type: Socket::Family, default: Socket::Family::UNSPEC, converter: FamilyConverter}, # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
port: {type: Int32, default: 3000}, # Port to listen for connections (overrided by command line argument)
host_binding: {type: String, default: "0.0.0.0"}, # Host to bind (overrided by command line argument)
})
end
@ -416,19 +418,19 @@ def extract_items(nodeset, ucid = nil, author_name = nil)
author_thumbnail ||= ""
subscriber_count = node.xpath_node(%q(.//span[contains(@class, "yt-subscriber-count")])).try &.["title"].gsub(/\D/, "").to_i?
subscriber_count ||= 0
subscriber_count = node.xpath_node(%q(.//span[contains(@class, "subscriber-count")]))
.try &.["title"].try { |text| short_text_to_number(text) } || 0
video_count = node.xpath_node(%q(.//ul[@class="yt-lockup-meta-info"]/li)).try &.content.split(" ")[0].gsub(/\D/, "").to_i?
video_count ||= 0
items << SearchChannel.new(
author: author,
ucid: ucid,
author_thumbnail: author_thumbnail,
subscriber_count: subscriber_count,
video_count: video_count,
description_html: description_html
video_count: video_count || 0,
description_html: description_html,
auto_generated: video_count ? false : true,
)
else
id = id.lchop("/watch?v=")

View File

@ -50,7 +50,7 @@ macro patched_json_mapping(_properties_, strict = false)
rescue exc : ::JSON::ParseException
raise ::JSON::MappingError.new(exc.message, self.class.to_s, nil, *%location, exc)
end
while %pull.kind != :end_object
until %pull.kind.end_object?
%key_location = %pull.location
key = %pull.read_object_key
case key

View File

@ -119,7 +119,7 @@ module Kemal
config = Kemal.config.serve_static
original_path = context.request.path.not_nil!
request_path = URI.unescape(original_path)
request_path = URI.decode_www_form(original_path)
# File path cannot contains '\0' (NUL) because all filesystem I know
# don't accept '\0' character as file name.

View File

@ -69,7 +69,7 @@ end
def validate_request(token, session, request, key, db, locale = nil)
case token
when String
token = JSON.parse(URI.unescape(token)).as_h
token = JSON.parse(URI.decode_www_form(token)).as_h
when JSON::Any
token = token.as_h
when Nil

View File

@ -157,7 +157,7 @@ def number_with_separator(number)
number.to_s.reverse.gsub(/(\d{3})(?=\d)/, "\\1,").reverse
end
def short_text_to_number(short_text)
def short_text_to_number(short_text : String) : Int32
case short_text
when .ends_with? "M"
number = short_text.rstrip(" mM").to_f
@ -246,7 +246,7 @@ def get_referer(env, fallback = "/", unroll = true)
if referer.query
params = HTTP::Params.parse(referer.query.not_nil!)
if params["referer"]?
referer = URI.parse(URI.unescape(params["referer"]))
referer = URI.parse(URI.decode_www_form(params["referer"]))
else
break
end

View File

@ -172,7 +172,7 @@ def produce_playlist_url(id, index)
continuation.print data
data = Base64.urlsafe_encode(continuation)
cursor = URI.escape(data)
cursor = URI.encode_www_form(data)
data = IO::Memory.new
@ -193,7 +193,7 @@ def produce_playlist_url(id, index)
IO.copy data, buffer
continuation = Base64.urlsafe_encode(buffer)
continuation = URI.escape(continuation)
continuation = URI.encode_www_form(continuation)
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"

View File

@ -185,8 +185,10 @@ struct SearchChannel
end
end
json.field "autoGenerated", self.auto_generated
json.field "subCount", self.subscriber_count
json.field "videoCount", self.video_count
json.field "description", html_to_content(self.description_html)
json.field "descriptionHtml", self.description_html
end
@ -209,6 +211,7 @@ struct SearchChannel
subscriber_count: Int32,
video_count: Int32,
description_html: String,
auto_generated: Bool,
})
end
@ -263,7 +266,7 @@ def search(query, page = 1, search_params = produce_search_params(content_type:
return {0, [] of SearchItem}
end
html = client.get("/results?q=#{URI.escape(query)}&page=#{page}&sp=#{search_params}&hl=en&disable_polymer=1").body
html = client.get("/results?q=#{URI.encode_www_form(query)}&page=#{page}&sp=#{search_params}&hl=en&disable_polymer=1").body
if html.empty?
return {0, [] of SearchItem}
end
@ -368,7 +371,7 @@ def produce_search_params(sort : String = "relevance", date : String = "", conte
end
token = Base64.urlsafe_encode(token.to_slice)
token = URI.escape(token)
token = URI.encode_www_form(token)
return token
end
@ -393,7 +396,7 @@ def produce_channel_search_url(ucid, query, page)
data.rewind
data = Base64.urlsafe_encode(data)
continuation = URI.escape(data)
continuation = URI.encode_www_form(data)
data = IO::Memory.new
@ -418,7 +421,7 @@ def produce_channel_search_url(ucid, query, page)
IO.copy data, buffer
continuation = Base64.urlsafe_encode(buffer)
continuation = URI.escape(continuation)
continuation = URI.encode_www_form(continuation)
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"

View File

@ -42,7 +42,7 @@ end
def extract_plid(url)
wrapper = HTTP::Params.parse(URI.parse(url).query.not_nil!)["bp"]
wrapper = URI.unescape(wrapper)
wrapper = URI.decode_www_form(wrapper)
wrapper = Base64.decode(wrapper)
# 0xe2 0x02 0x2e

View File

@ -108,7 +108,7 @@ def get_user(sid, headers, db, refresh = true)
args = arg_array(user_array)
db.exec("INSERT INTO users VALUES (#{args}) \
ON CONFLICT (email) DO UPDATE SET updated = $1, subscriptions = $3", user_array)
ON CONFLICT (email) DO UPDATE SET updated = $1, subscriptions = $3", args: user_array)
db.exec("INSERT INTO session_ids VALUES ($1,$2,$3) \
ON CONFLICT (id) DO NOTHING", sid, user.email, Time.utc)
@ -127,7 +127,7 @@ def get_user(sid, headers, db, refresh = true)
args = arg_array(user.to_a)
db.exec("INSERT INTO users VALUES (#{args}) \
ON CONFLICT (email) DO UPDATE SET updated = $1, subscriptions = $3", user_array)
ON CONFLICT (email) DO UPDATE SET updated = $1, subscriptions = $3", args: user_array)
db.exec("INSERT INTO session_ids VALUES ($1,$2,$3) \
ON CONFLICT (id) DO NOTHING", sid, user.email, Time.utc)
@ -296,7 +296,7 @@ def get_subscription_feed(db, user, max_results = 40, page = 1)
args = arg_array(notifications)
notifications = db.query_all("SELECT * FROM channel_videos WHERE id IN (#{args}) ORDER BY published DESC", notifications, as: ChannelVideo)
notifications = db.query_all("SELECT * FROM channel_videos WHERE id IN (#{args}) ORDER BY published DESC", args: notifications, as: ChannelVideo)
videos = [] of ChannelVideo
notifications.sort_by! { |video| video.published }.reverse!

View File

@ -316,10 +316,10 @@ struct Video
json.field "premiereTimestamp", self.premiere_timestamp.not_nil!.to_unix
end
if self.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
if player_response["streamingData"]?.try &.["hlsManifestUrl"]?
host_url = make_host_url(config, kemal_config)
hlsvp = self.player_response["streamingData"]["hlsManifestUrl"].as_s
hlsvp = player_response["streamingData"]["hlsManifestUrl"].as_s
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url)
json.field "hlsUrl", hlsvp
@ -408,7 +408,7 @@ struct Video
json.object do
json.field "label", caption.name.simpleText
json.field "languageCode", caption.languageCode
json.field "url", "/api/v1/captions/#{id}?label=#{URI.escape(caption.name.simpleText)}"
json.field "url", "/api/v1/captions/#{id}?label=#{URI.encode_www_form(caption.name.simpleText)}"
end
end
end
@ -489,7 +489,7 @@ struct Video
end
def live_now
live_now = self.player_response["videoDetails"]?.try &.["isLive"]?.try &.as_bool
live_now = player_response["videoDetails"]?.try &.["isLive"]?.try &.as_bool
if live_now.nil?
return false
@ -536,7 +536,7 @@ struct Video
end
def keywords
keywords = self.player_response["videoDetails"]?.try &.["keywords"]?.try &.as_a
keywords = player_response["videoDetails"]?.try &.["keywords"]?.try &.as_a
keywords ||= [] of String
return keywords
@ -545,7 +545,7 @@ struct Video
def fmt_stream(decrypt_function)
streams = [] of HTTP::Params
if fmt_streams = self.player_response["streamingData"]?.try &.["formats"]?
if fmt_streams = player_response["streamingData"]?.try &.["formats"]?
fmt_streams.as_a.each do |fmt_stream|
if !fmt_stream.as_h?
next
@ -619,7 +619,7 @@ struct Video
def adaptive_fmts(decrypt_function)
adaptive_fmts = [] of HTTP::Params
if fmts = self.player_response["streamingData"]?.try &.["adaptiveFormats"]?
if fmts = player_response["streamingData"]?.try &.["adaptiveFormats"]?
fmts.as_a.each do |adaptive_fmt|
if !adaptive_fmt.as_h?
next
@ -712,12 +712,12 @@ struct Video
end
def storyboards
storyboards = self.player_response["storyboards"]?
storyboards = player_response["storyboards"]?
.try &.as_h
.try &.["playerStoryboardSpecRenderer"]?
if !storyboards
storyboards = self.player_response["storyboards"]?
storyboards = player_response["storyboards"]?
.try &.as_h
.try &.["playerLiveStoryboardSpecRenderer"]?
@ -784,13 +784,8 @@ struct Video
end
def paid
reason = self.player_response["playabilityStatus"]?.try &.["reason"]?
if reason == "This video requires payment to watch."
paid = true
else
paid = false
end
reason = player_response["playabilityStatus"]?.try &.["reason"]?
paid = reason == "This video requires payment to watch." ? true : false
return paid
end
@ -836,7 +831,7 @@ struct Video
end
def length_seconds
self.player_response["videoDetails"]["lengthSeconds"].as_s.to_i
player_response["videoDetails"]["lengthSeconds"].as_s.to_i
end
db_mapping({
@ -882,6 +877,10 @@ struct CaptionName
end
class VideoRedirect < Exception
property video_id : String
def initialize(@video_id)
end
end
def get_video(id, db, refresh = true, region = nil, force_refresh = false)
@ -901,7 +900,7 @@ def get_video(id, db, refresh = true, region = nil, force_refresh = false)
db.exec("UPDATE videos SET (info,updated,title,views,likes,dislikes,wilson_score,\
published,description,language,author,ucid,allowed_regions,is_family_friendly,\
genre,genre_url,license,sub_count_text,author_thumbnail)\
= (#{args}) WHERE id = $1", video_array)
= (#{args}) WHERE id = $1", args: video_array)
rescue ex
db.exec("DELETE FROM videos * WHERE id = $1", id)
raise ex
@ -914,7 +913,7 @@ def get_video(id, db, refresh = true, region = nil, force_refresh = false)
args = arg_array(video_array)
if !region
db.exec("INSERT INTO videos VALUES (#{args}) ON CONFLICT (id) DO NOTHING", video_array)
db.exec("INSERT INTO videos VALUES (#{args}) ON CONFLICT (id) DO NOTHING", args: video_array)
end
end
@ -931,6 +930,9 @@ def extract_recommended(recommended_videos)
recommended_video = HTTP::Params.new
recommended_video["id"] = video_renderer["videoId"].as_s
recommended_video["title"] = video_renderer["title"]["simpleText"].as_s
next if !video_renderer["shortBylineText"]?
recommended_video["author"] = video_renderer["shortBylineText"]["runs"].as_a[0]["text"].as_s
recommended_video["ucid"] = video_renderer["shortBylineText"]["runs"].as_a[0]["navigationEndpoint"]["browseEndpoint"]["browseId"].as_s
recommended_video["author_thumbnail"] = video_renderer["channelThumbnail"]["thumbnails"][0]["url"].as_s
@ -1149,7 +1151,7 @@ def fetch_video(id, region)
response = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1&has_verified=1&bpctr=9999999999")
if md = response.headers["location"]?.try &.match(/v=(?<id>[a-zA-Z0-9_-]{11})/)
raise VideoRedirect.new(md["id"])
raise VideoRedirect.new(video_id: md["id"])
end
html = XML.parse_html(response.body)
@ -1203,6 +1205,11 @@ def fetch_video(id, region)
player_json = JSON.parse(info["player_response"])
reason = player_json["playabilityStatus"]?.try &.["reason"]?.try &.as_s
if reason == "This video is not available."
raise "This video is not available."
end
title = player_json["videoDetails"]["title"].as_s
author = player_json["videoDetails"]["author"]?.try &.as_s || ""
ucid = player_json["videoDetails"]["channelId"]?.try &.as_s || ""
@ -1255,7 +1262,7 @@ def fetch_video(id, region)
end
license = html.xpath_node(%q(//h4[contains(text(),"License")]/parent::*/ul/li)).try &.content || ""
sub_count_text = html.xpath_node(%q(//span[contains(@class, "yt-subscriber-count")])).try &.["title"]? || "0"
sub_count_text = html.xpath_node(%q(//span[contains(@class, "subscriber-count")])).try &.["title"]? || "0"
author_thumbnail = html.xpath_node(%(//span[@class="yt-thumb-clip"]/img)).try &.["data-thumb"]?.try &.gsub(/^\/\//, "https://") || ""
video = Video.new(id, info, Time.utc, title, views, likes, dislikes, wilson_score, published, description_html,

View File

@ -72,7 +72,7 @@
<input type="hidden" name="expire" value="<%= expire %>">
<% end %>
<input type="hidden" name="csrf_token" value="<%= URI.escape(csrf_token) %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
</form>
</div>
<% end %>

View File

@ -6,7 +6,7 @@
<div class="pure-u-1 pure-u-lg-1-5"></div>
<div class="pure-u-1 pure-u-lg-3-5">
<div class="h-box">
<form class="pure-form pure-form-aligned" action="/change_password?referer=<%= URI.escape(referer) %>" method="post">
<form class="pure-form pure-form-aligned" action="/change_password?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= translate(locale, "Change password") %></legend>
<fieldset>
@ -23,7 +23,7 @@
<%= translate(locale, "Change password") %>
</button>
<input type="hidden" name="csrf_token" value="<%= URI.escape(csrf_token) %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
</fieldset>
</form>
</div>

View File

@ -34,7 +34,7 @@
<div class="h-box">
<% ucid = channel.ucid %>
<% author = channel.author %>
<% sub_count_text = channel.sub_count.format %>
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>

View File

@ -3,7 +3,7 @@
<% end %>
<div class="h-box">
<form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.escape(referer) %>" method="post">
<form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= translate(locale, "Clear watch history?") %></legend>
<div class="pure-g">
@ -13,12 +13,12 @@
</button>
</div>
<div class="pure-u-1-2">
<a class="pure-button" href="<%= URI.escape(referer) %>">
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
<%= translate(locale, "No") %>
</a>
</div>
</div>
<input type="hidden" name="csrf_token" value="<%= URI.escape(csrf_token) %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
</form>
</div>

View File

@ -33,7 +33,7 @@
<div class="h-box">
<% ucid = channel.ucid %>
<% author = channel.author %>
<% sub_count_text = channel.sub_count.format %>
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>

View File

@ -11,7 +11,7 @@
<p><%= item.author %></p>
</a>
<p><%= translate(locale, "`x` subscribers", number_with_separator(item.subscriber_count)) %></p>
<p><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></p>
<% if !item.auto_generated %><p><%= translate(locale, "`x` videos", number_with_separator(item.video_count)) %></p><% end %>
<h5><%= item.description_html %></h5>
<% when SearchPlaylist %>
<% if item.id.starts_with? "RD" %>
@ -91,7 +91,7 @@
<img class="thumbnail" src="/vi/<%= item.id %>/mqdefault.jpg"/>
<% if env.get? "show_watched" %>
<form onsubmit="return false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched">
<a onclick="mark_watched(this)" data-id="<%= item.id %>" href="javascript:void(0)">
<button type="submit" style="all:unset">

View File

@ -2,7 +2,7 @@
<% if subscriptions.includes? ucid %>
<p>
<form action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
</button>
@ -11,7 +11,7 @@
<% else %>
<p>
<form action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>
</button>
@ -24,7 +24,7 @@
ucid: '<%= ucid %>',
author: '<%= HTML.escape(author) %>',
sub_count_text: '<%= HTML.escape(sub_count_text) %>',
csrf_token: '<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>',
csrf_token: '<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>',
subscribe_text: '<%= HTML.escape(translate(locale, "Subscribe")) %>',
unsubscribe_text: '<%= HTML.escape(translate(locale, "Unsubscribe")) %>'
}

View File

@ -11,7 +11,7 @@
<%= translate(locale, "and upload the file below.") %>
</div>
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.escape(referer) %>" method="post">
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.encode_www_form(referer) %>" method="post">
<fieldset>
<legend><%= translate(locale, "Import") %></legend>

View File

@ -3,7 +3,7 @@
<% end %>
<div class="h-box">
<form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.escape(referer) %>" method="post">
<form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.encode_www_form(referer) %>" method="post">
<legend><%= translate(locale, "Delete account?") %></legend>
<div class="pure-g">
@ -13,12 +13,12 @@
</button>
</div>
<div class="pure-u-1-2">
<a class="pure-button" href="<%= URI.escape(referer) %>">
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
<%= translate(locale, "No") %>
</a>
</div>
</div>
<input type="hidden" name="csrf_token" value="<%= URI.escape(csrf_token) %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(csrf_token) %>">
</form>
</div>

View File

@ -20,7 +20,7 @@
<script>
var watched_data = {
csrf_token: '<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>',
csrf_token: '<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>',
}
</script>
<script src="/js/watched_widget.js"></script>
@ -35,7 +35,7 @@ var watched_data = {
<div class="thumbnail">
<img class="thumbnail" src="/vi/<%= item %>/mqdefault.jpg"/>
<form onsubmit="return false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<p class="watched">
<a onclick="mark_unwatched(this)" data-id="<%= item %>" href="javascript:void(0)">
<button type="submit" style="all:unset">

View File

@ -22,7 +22,7 @@
<hr>
<% if account_type == "invidious" %>
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=invidious" method="post">
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.encode_www_form(referer) %>&type=invidious" method="post">
<fieldset>
<% if email %>
<input name="email" type="hidden" value="<%= email %>">
@ -44,7 +44,7 @@
<% captcha = captcha.not_nil! %>
<img style="width:100%" src='<%= captcha[:question] %>'/>
<% captcha[:tokens].each_with_index do |token, i| %>
<input type="hidden" name="token[<%= i %>]" value="<%= URI.escape(token) %>">
<input type="hidden" name="token[<%= i %>]" value="<%= URI.encode_www_form(token) %>">
<% end %>
<input type="hidden" name="captcha_type" value="image">
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
@ -52,7 +52,7 @@
<% when "text" %>
<% captcha = captcha.not_nil! %>
<% captcha[:tokens].each_with_index do |token, i| %>
<input type="hidden" name="token[<%= i %>]" value="<%= URI.escape(token) %>">
<input type="hidden" name="token[<%= i %>]" value="<%= URI.encode_www_form(token) %>">
<% end %>
<input type="hidden" name="captcha_type" value="text">
<label for="answer"><%= captcha[:question] %></label>
@ -85,7 +85,7 @@
</fieldset>
</form>
<% elsif account_type == "google" %>
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.escape(referer) %>&type=google" method="post">
<form class="pure-form pure-form-stacked" action="/login?referer=<%= URI.encode_www_form(referer) %>&type=google" method="post">
<fieldset>
<% if email %>
<input name="email" type="hidden" value="<%= email %>">

View File

@ -33,7 +33,7 @@
<div class="h-box">
<% ucid = channel.ucid %>
<% author = channel.author %>
<% sub_count_text = channel.sub_count.format %>
<% sub_count_text = number_to_short_text(channel.sub_count) %>
<%= rendered "components/subscribe_widget" %>
</div>

View File

@ -9,7 +9,7 @@ function update_value(element) {
</script>
<div class="h-box">
<form class="pure-form pure-form-aligned" action="/preferences?referer=<%= URI.escape(referer) %>" method="post">
<form class="pure-form pure-form-aligned" action="/preferences?referer=<%= URI.encode_www_form(referer) %>" method="post">
<fieldset>
<legend><%= translate(locale, "Player preferences") %></legend>
@ -242,15 +242,15 @@ function update_value(element) {
<legend><%= translate(locale, "Data preferences") %></legend>
<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.encode_www_form(referer) %>"><%= translate(locale, "Clear watch history") %></a>
</div>
<div class="pure-control-group">
<a href="/change_password?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Change password") %></a>
<a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Change password") %></a>
</div>
<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.encode_www_form(referer) %>"><%= translate(locale, "Import/export data") %></a>
</div>
<div class="pure-control-group">
@ -266,7 +266,7 @@ function update_value(element) {
</div>
<div class="pure-control-group">
<a href="/delete_account?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Delete account") %></a>
<a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Delete account") %></a>
</div>
<% end %>

View File

@ -3,73 +3,53 @@
<% end %>
<div class="h-box">
<%= Markdown.to_html(<<-END_PRIVACY_POLICY
## Privacy
<h2>Privacy</h2>
<p>This document concerns what data you provide to this website, the purpose of the data, how the data is stored, and how the data can be removed.</p>
This document concerns what data you provide to this website, the purpose of the data, how the data is stored, and how the data can be removed.
<h3>Data you directly provide</h3>
<p>Data that you provide to the website for the purpose of the site's operation (for example: an account name, account password, or channel subscription) will be stored in the website's database until the user decides to remove it. This data will not be intentionally shared with anyone or anything.</p>
<p>Information stored about a registered user is limited to:</p>
<ul>
<li>a list of session tokens for remaining logged in across devices</li>
<li>the last time an account was updated (to provide accurate notifications)</li>
<li>a list of video IDs identifying notifications from a user's subscriptions</li>
<li>a list of channel UCIDs the user is subscribed to</li>
<li>a user ID (for persistent storage of subscriptions and preferences)</li>
<li>a json object containing user preferences</li>
<li>a hashed password if applicable (not present on google accounts)</li>
<li>a randomly generated token for providing an RSS feed of a user's subscriptions</li>
<li>a list of video IDs identifying watched videos</li>
</ul>
<p>The above list reflects <a href="https://github.com/omarroth/invidious/blob/master/src/invidious/users.cr#L14-L51">this code</a>.</p>
<p>Users can clear their watch history using the <a href="/clear_watch_history">clear watch history</a> page.</p>
<p>If a user is logged in with a Google account, no password will ever be stored. This website uses the session token provided by Google to identify a user, but does not store the information required to make requests on a user's behalf without their knowledge or consent.</p>
### Data you directly provide
<h3>Data you passively provide</h3>
<p>When you request any resource from this website (for example: a page, a font, an image, or an API endpoint) information about the request may be logged.</p>
<p>Information about a request is limited to:</p>
<ul>
<li>the time the request was made</li>
<li>the status code of the response</li>
<li>the method of the request</li>
<li>the requested URL</li>
<li>how long it took to complete the request.</li>
</ul>
<p>No identifying information is logged, such as the visitor's cookie, user-agent, or IP address. Here are a couple lines to serve as an example:</p>
<pre><code>2019-01-19 16:37:47 +00:00 200 GET /api/v1/comments/xrlETJYzH-c?format=html&hl=en-US 1345.88ms
2019-01-19 16:37:53 +00:00 200 GET /vi/r5P-f5arPXE/maxres.jpg 1085.41ms
2019-01-19 16:37:54 +00:00 200 GET /watch 7.04ms</code></pre>
<p>This website does not store the visitor's user-agent or IP address and does not use fingerprinting, advertisements, or tracking of any form.</p>
<p>This website provides links to googlevideo.com to provide audio and video playback. googlevideo.com is owned by Google and is subject to their <a href="https://policies.google.com/privacy">privacy policy</a>.</p>
Data that you provide to the website for the purpose of the site's operation (for example: an account name, account password, or channel subscription) will be stored in the website's database until the user decides to remove it. This data will not be intentionally shared with anyone or anything.
<h3>Data stored in your browser</h3>
<p>This website uses browser cookies to authenticate registered users. This data consists of:</p>
<ul>
<li>An account token to keep you logged into the website between visits, which is sent when any page is loaded while you are logged in</li>
</ul>
<p>This website also provides an option to store site preferences, such as the theme or locale, without an account. Using this feature will store a cookie in the visitor's browser containing their preferences. This cookie is sent on every request and does not contain any identifying information.</p>
<p>You can remove this data from your browser by logging out of this website, or by using your browser's cookie-related controls to delete the data.</p>
Information stored about a registered user is limited to:
- a list of session tokens for remaining logged in across devices
- the last time an account was updated (to provide accurate notifications)
- a list of video IDs identifying notifications from a user's subscriptions
- a list of channel UCIDs the user is subscribed to
- a user ID (for persistent storage of subscriptions and preferences)
- a json object containing user preferences
- a hashed password if applicable (not present on google accounts)
- a randomly generated token for providing an RSS feed of a user's subscriptions
- a list of video IDs identifying watched videos
The above list reflects [this code](https://github.com/omarroth/invidious/blob/master/src/invidious/users.cr#L14-L51).
Users can clear their watch history using the [clear watch history](/clear_watch_history) page.
If a user is logged in with a Google account, no password will ever be stored. This website uses the session token provided by Google to identify a user, but does not store the information required to make requests on a user's behalf without their knowledge or consent.
### Data you passively provide
When you request any resource from this website (for example: a page, a font, an image, or an API endpoint) information about the request may be logged.
Information about a request is limited to:
- the time the request was made
- the status code of the response
- the method of the request
- the requested URL
- how long it took to complete the request.
No identifying information is logged, such as the visitor's cookie, user-agent, or IP address. Here are a couple lines to serve as an example:
```
2019-01-19 16:37:47 +00:00 200 GET /api/v1/comments/xrlETJYzH-c?format=html&hl=en-US 1345.88ms
2019-01-19 16:37:53 +00:00 200 GET /vi/r5P-f5arPXE/maxres.jpg 1085.41ms
2019-01-19 16:37:54 +00:00 200 GET /watch 7.04ms
```
This website does not store the visitor's user-agent or IP address and does not use fingerprinting, advertisements, or tracking of any form.
This website provides links to googlevideo.com to provide audio and video playback. googlevideo.com is owned by Google and is subject to their [privacy policy](https://policies.google.com/privacy).
### Data stored in your browser
This website uses browser cookies to authenticate registered users. This data consists of:
- An account token to keep you logged into the website between visits, which is sent when any page is loaded while you are logged in
This website also provides an option to store site preferences, such as the theme or locale, without an account. Using this feature will store a cookie in the visitor's browser containing their preferences. This cookie is sent on every request and does not contain any identifying information.
You can remove this data from your browser by logging out of this website, or by using your browser's cookie-related controls to delete the data.
### Removal of data
To remove data stored in your browser, you can log out of the website, or you can use your browser's cookie-related controls to delete the data.
To remove data that has been stored in the website's database, you can use the [delete my account](/delete_account) page.
END_PRIVACY_POLICY
)
%>
<h3>Removal of data</h3>
<p>To remove data stored in your browser, you can log out of the website, or you can use your browser's cookie-related controls to delete the data.</p>
<p>To remove data that has been stored in the website's database, you can use the <a href="/delete_account">delete my account</a> page.</p>
</div>

View File

@ -19,7 +19,7 @@
</div>
<div class="pure-u-1-3" style="text-align:right">
<h3>
<a href="/data_control?referer=<%= URI.escape(referer) %>">
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>">
<%= translate(locale, "Import/export") %>
</a>
</h3>
@ -38,7 +38,7 @@
<div class="pure-u-1-5" style="text-align:right">
<h3 style="padding-right:0.5em">
<form onsubmit="return false" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<a onclick="remove_subscription(this)" data-ucid="<%= channel.id %>" href="#">
<input style="all:unset" type="submit" value="<%= translate(locale, "unsubscribe") %>">
</a>
@ -78,6 +78,6 @@ function remove_subscription(target) {
}
}
xhr.send('csrf_token=<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>');
xhr.send('csrf_token=<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>');
}
</script>

View File

@ -47,7 +47,7 @@
<script>
var watched_data = {
csrf_token: '<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>',
csrf_token: '<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>',
}
</script>
<script src="/js/watched_widget.js"></script>

View File

@ -68,7 +68,7 @@
</div>
<div class="pure-u-1-4">
<form action="/signout?referer=<%= env.get?("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<a class="pure-menu-heading" href="#">
<input style="all:unset" type="submit" value="<%= translate(locale, "Log out") %>">
</a>

View File

@ -11,7 +11,7 @@
<div class="pure-u-1-3"></div>
<div class="pure-u-1-3" style="text-align:right">
<h3>
<a href="/preferences?referer=<%= URI.escape(referer) %>"><%= translate(locale, "Preferences") %></a>
<a href="/preferences?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Preferences") %></a>
</h3>
</div>
</div>
@ -30,7 +30,7 @@
<div class="pure-u-1-5" style="text-align:right">
<h3 style="padding-right:0.5em">
<form onsubmit="return false" action="/token_ajax?action_revoke_token=1&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<a onclick="revoke_token(this)" data-session="<%= token[:session] %>" href="#">
<input style="all:unset" type="submit" value="<%= translate(locale, "revoke") %>">
</a>
@ -70,6 +70,6 @@ function revoke_token(target) {
}
}
xhr.send('csrf_token=<%= URI.escape(env.get?("csrf_token").try &.as(String) || "") %>');
xhr.send('csrf_token=<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>');
}
</script>

View File

@ -106,22 +106,22 @@ var video_data = {
<label for="download_widget"><%= translate(locale, "Download as: ") %></label>
<select style="width:100%" name="download_widget" id="download_widget">
<% fmt_stream.each do |option| %>
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.encode_www_form(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
<%= itag_to_metadata?(option["itag"]).try &.["height"]? || "~240" %>p - <%= option["type"].split(";")[0] %>
</option>
<% end %>
<% video_streams.each do |option| %>
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.encode_www_form(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
<%= option["quality_label"] %> - <%= option["type"].split(";")[0] %> @ <%= option["fps"] %>fps - video only
</option>
<% end %>
<% audio_streams.each do |option| %>
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
<option value='{"id":"<%= video.id %>","itag":"<%= option["itag"] %>","title":"<%= URI.encode_www_form(video.title) %>-<%= video.id %>.<%= option["type"].split(";")[0].split("/")[1] %>"}'>
<%= option["type"].split(";")[0] %> @ <%= option["bitrate"] %>k - audio only
</option>
<% end %>
<% captions.each do |caption| %>
<option value='{"id":"<%= video.id %>","label":"<%= caption.name.simpleText %>","title":"<%= URI.escape(video.title) %>-<%= video.id %>.<%= caption.languageCode %>.vtt"}'>
<option value='{"id":"<%= video.id %>","label":"<%= caption.name.simpleText %>","title":"<%= URI.encode_www_form(video.title) %>-<%= video.id %>.<%= caption.languageCode %>.vtt"}'>
<%= translate(locale, "Subtitles - `x` (.vtt)", caption.name.simpleText) %>
</option>
<% end %>