mirror of
https://github.com/iv-org/invidious.git
synced 2025-08-03 19:28:29 +00:00
Merge branch 'master' into homepage
This commit is contained in:
commit
75793bc524
@ -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
@ -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
|
||||
|
@ -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: ": "Εναλλακτικά σχόλια: ",
|
||||
|
@ -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: ",
|
||||
|
@ -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
325
locales/tr.json
Normal 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: "
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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=")
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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
|
||||
|
@ -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!
|
||||
|
@ -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,
|
||||
|
@ -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 %>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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")) %>'
|
||||
}
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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 %>">
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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 %>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 %>
|
||||
|
Loading…
Reference in New Issue
Block a user