diff options
| -rw-r--r-- | locales/ro.json | 336 | ||||
| -rw-r--r-- | shard.yml | 2 | ||||
| -rw-r--r-- | src/invidious.cr | 10 | ||||
| -rw-r--r-- | src/invidious/helpers/jobs.cr | 54 | ||||
| -rw-r--r-- | src/invidious/helpers/utils.cr | 12 |
5 files changed, 405 insertions, 9 deletions
diff --git a/locales/ro.json b/locales/ro.json new file mode 100644 index 00000000..75496a01 --- /dev/null +++ b/locales/ro.json @@ -0,0 +1,336 @@ +{ + "`x` subscribers": "`x` abonați", + "`x` videos": "`x` videoclipuri", + "`x` playlists": "`x` liste de redare", + "LIVE": "ÎN DIRECT", + "Shared `x` ago": "Adăugat acum `x`", + "Unsubscribe": "Dezabonați-vă", + "Subscribe": "Abonați-vă", + "View channel on YouTube": "Vedeți canalul pe YouTube", + "View playlist on YouTube": "Vedeți lista de redare pe YouTube", + "newest": "Data adăugării (cea mai recentă)", + "oldest": "Data adăugării (cea mai veche)", + "popular": "Cele mai populare", + "last": "Ultimele", + "Next page": "Pagina următoare", + "Previous page": "Pagina precedentă", + "Clear watch history?": "Doriți să ștergeți istoricul?", + "New password": "Parola nouă", + "New passwords must match": "Câmpurile \"Parolă nouă\" trebuie să fie identice", + "Cannot change password for Google accounts": "Parola pentru un cont Google nu poate fi schimbată de pe Invidious", + "Authorize token?": "Autorizați token-ul?", + "Authorize token for `x`?": "Autorizați token-ul pentru `x` ?", + "Yes": "Da", + "No": "Nu", + "Import and Export Data": "Importați și Exportați Datele", + "Import": "Importați", + "Import Invidious data": "Importați Datele de pe Invidious", + "Import YouTube subscriptions": "Importați abonamentele de pe YouTube", + "Import FreeTube subscriptions (.db)": "Importați abonamentele de pe FreeTube (.db)", + "Import NewPipe subscriptions (.json)": "Importați abonamentele de pe NewPipe (.json)", + "Import NewPipe data (.zip)": "Importați datele de pe NewPipe (.zip)", + "Export": "Exportați", + "Export subscriptions as OPML": "Exportați abonamentele în format OPML", + "Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportați abonamentele în format OPML (pentru NewPipe și FreeTube)", + "Export data as JSON": "Exportați datele în format JSON", + "Delete account?": "Sunteți siguri că doriți să vă ștergeți contul?", + "History": "Istoric", + "An alternative front-end to YouTube": "O alternativă front-end pentru YouTube", + "JavaScript license information": "Informații despre licențele JavaScript", + "source": "sursă", + "Log in": "Conectați-vă", + "Log in/register": "Conectați-vă/Creați-vă un cont", + "Log in with Google": "Conectați-vă cu Google", + "User ID": "ID Utilizator", + "Password": "Parolă", + "Time (h:mm:ss):": "Ora (h:mm:ss) :", + "Text CAPTCHA": "Text CAPTCHA", + "Image CAPTCHA": "Imagine CAPTCHA", + "Sign In": "Conectați-vă", + "Register": "Înregistrați-vă", + "E-mail": "E-mail", + "Google verification code": "Cod de verificare Google", + "Preferences": "Preferințe", + "Player preferences": "Setări de redare", + "Always loop: ": "Reluați videoclipul la nesfârșit: ", + "Autoplay: ": "Porniți videoclipurile automat: ", + "Play next by default: ": "Vizionați următoarele videoclipuri în mod implicit: ", + "Autoplay next video: ": "Porniți următorul videoclip automat: ", + "Listen by default: ": "Numai audio: ", + "Proxy videos: ": "Redați videoclipurile printr-un proxy: ", + "Default speed: ": "Viteza de redare implicită: ", + "Preferred video quality: ": "Calitatea videoclipurilor: ", + "Player volume: ": "Volumul videoclipurilor: ", + "Default comments: ": "Sursa comentariilor: ", + "youtube": "YouTube", + "reddit": "Reddit", + "Default captions: ": "Subtitrări implicite: ", + "Fallback captions: ": "Subtitrări alternative: ", + "Show related videos: ": "Afișați videoclipurile asemănătoare: ", + "Show annotations by default: ": "Afișați adnotările în mod implicit: ", + "Visual preferences": "Preferințele site-ului", + "Player style: ": "Stilul player-ului : ", + "Dark mode: ": "Modul întunecat : ", + "Theme: ": "Tema : ", + "dark": "întunecat", + "light": "luminos", + "Thin mode: ": "Mod lejer: ", + "Subscription preferences": "Preferințele paginii de abonamente", + "Show annotations by default for subscribed channels: ": "Afișați adnotările în mod implicit pentru canalele la care v-ați abonat: ", + "Redirect homepage to feed: ": "Redirecționați pagina principală la pagina de abonamente: ", + "Number of videos shown in feed: ": "Numărul de videoclipuri afișate pe pagina de abonamente: ", + "Sort videos by: ": "Sortați videoclipurile în funcție de: ", + "published": "data publicării", + "published - reverse": "data publicării - inversată", + "alphabetically": "în ordine alfabetică", + "alphabetically - reverse": "în ordine alfabetică - inversată", + "channel name": "numele canalului", + "channel name - reverse": "numele canalului - inversat", + "Only show latest video from channel: ": "Afișați numai cel mai recent videoclip publicat de canalele la care v-ați abonat: ", + "Only show latest unwatched video from channel: ": "Afișați numai cel mai recent videoclip nevizionat publicat de canalele la care v-ați abonat: ", + "Only show unwatched: ": "Afișați numai videoclipurile nevizionate: ", + "Only show notifications (if there are any): ": "Afișați numai notificările (dacă există): ", + "Enable web notifications": "Activați notificările web", + "`x` uploaded a video": "`x` a publicat un videoclip", + "`x` is live": "`x` este în direct", + "Data preferences": "Preferințe legate de date", + "Clear watch history": "Ștergeți istoricul videoclipurilor vizionate", + "Import/export data": "Importați/exportați datele", + "Change password": "Schimbați parola", + "Manage subscriptions": "Gestionați abonamentele", + "Manage tokens": "Gestionați tokenele", + "Watch history": "Istoricul videoclipurilor vizionate", + "Delete account": "Ștergeți contul", + "Administrator preferences": "Preferințele Administratorului", + "Default homepage: ": "Pagina principală implicită: ", + "Feed menu: ": "Preferințe legate de pagina de abonamente: ", + "Top enabled: ": "Top activat: ", + "CAPTCHA enabled: ": "CAPTCHA activat : ", + "Login enabled: ": "Autentificare activată : ", + "Registration enabled: ": "Înregistrate activată: ", + "Report statistics: ": "Raportarea statisticilor: ", + "Save preferences": "Salvați preferințele", + "Subscription manager": "Gestionați abonamentele", + "Token manager": "Manager de Tokene", + "Token": "Token", + "`x` subscriptions": "`x` abonamente", + "`x` tokens": "`x` tokens", + "Import/export": "Importați/Exportați", + "unsubscribe": "dezabonați-vă", + "revoke": "revocați", + "Subscriptions": "Abonamente", + "`x` unseen notifications": "`x` notificări nevăzute", + "search": "căutați", + "Log out": "Deconectați-vă", + "Released under the AGPLv3 by Omar Roth.": "Publicat sub licența AGPLv3 de Omar Roth.", + "Source available here.": "Codul sursă este disponibil aici.", + "View JavaScript license information.": "Informații legate de licența JavaScript.", + "View privacy policy.": "Politica de confidențialitate.", + "Trending": "Tendințe", + "Public": "Public", + "Unlisted": "Necatalogat", + "Private": "Privat", + "View all playlists": "Afișați toate listele de redare", + "Updated `x` ago": "Actualizat acum `x`", + "Delete playlist `x`?": "Sigur doriți să ștergeți lista de redare?", + "Delete playlist": "Ștergeți lista de redare", + "Create playlist": "Creați o listă de redare", + "Title": "Titlu", + "Playlist privacy": "Parametrii de confidențialitate ai listei de redare", + "Editing playlist `x`": "Modificați lista de redare `x`", + "Watch on YouTube": "Urmăriți videoclipul pe YouTube", + "Hide annotations": "Ascundeți adnotările", + "Show annotations": "Afișați adnotările", + "Genre: ": "Categorie: ", + "License: ": "Licență: ", + "Family friendly? ": "Adecvat pentru întreaga familie? ", + "Wilson score: ": "Scor Wilson: ", + "Engagement: ": "Procentul celor care au apăsat pe \"Îmi place\" sau \"Nu îmi place\" : ", + "Whitelisted regions: ": "Regiunile de pe lista albă: ", + "Blacklisted regions: ": "Regiunile de pe lista neagră: ", + "Shared `x`": "Publicat pe `x`", + "`x` views": "`x` vizionări", + "Premieres in `x`": "Premiera în `x`", + "Premieres `x`": "Premiera pe `x`", + "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Se pare că ați dezactivat JavaScript. Apăsați aici pentru a vizualiza comentariile. Țineți minte faptul că încărcarea lor ar putea să dureze puțin mai mult.", + "View YouTube comments": "Vedeți comentariile de pe YouTube", + "View more comments on Reddit": "Vedeți mai multe comentarii pe Reddit", + "View `x` comments": "Afișați `x` comentarii", + "View Reddit comments": "Afișați comentariile de pe Reddit", + "Hide replies": "Ascundeți replicile", + "Show replies": "Afișați replicile", + "Incorrect password": "Parolă incorectă", + "Quota exceeded, try again in a few hours": "Numărul de tentative de conectare a fost depășit. Va rugăm să încercați din nou în câteva ore.", + "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Conectare eșuată. Dacă nu reușiți să vă conectați, verificați dacă ați activat autentificarea cu doi factori (Autentificator sau SMS).", + "Invalid TFA code": "Codul de autentificare cu doi factori este invalid", + "Login failed. This may be because two-factor authentication is not turned on for your account.": "Conectare eșuată. Acest lucru ar putea fi cauzat de faptul că nu ați activat autentificarea cu doi factori.", + "Wrong answer": "Răspuns invalid", + "Erroneous CAPTCHA": "CAPTCHA invalid", + "CAPTCHA is a required field": "Câmpul CAPTCHA este obligatoriu", + "User ID is a required field": "Câmpul ID Utilizator este obligatoriu", + "Password is a required field": "Câmpul Parolă este obligatoriu", + "Wrong username or password": "Nume de utilizator sau parolă invalidă", + "Please sign in using 'Log in with Google'": "Vă rog conectați-vă folosind \"Conectați-vă cu Google\"", + "Password cannot be empty": "Parola nu poate fi goală", + "Password cannot be longer than 55 characters": "Parola nu poate să conțină mai mult de 55 de caractere", + "Please log in": "Vă rog conectați-vă", + "Invidious Private Feed for `x`": "Feed RSS privat pentru `x`", + "channel:`x`": "canal:`x`", + "Deleted or invalid channel": "Canal șters sau invalid", + "This channel does not exist.": "Acest canal nu există.", + "Could not get channel info.": "Nu am putut primi informații despre acest canal.", + "Could not fetch comments": "Încărcarea comentariilor a eșuat.", + "View `x` replies": "Afișați `x` replici", + "`x` ago": "acum `x`", + "Load more": "Vedeți mai mult", + "`x` points": "`x` puncte", + "Could not create mix.": "Nu am putut crea această listă de redare.", + "Empty playlist": "Lista de redare este goală", + "Not a playlist.": "Lista de redare este invalidă.", + "Playlist does not exist.": "Această listă de redare nu există.", + "Could not pull trending pages.": "Încărcarea paginilor de tendințe a eșuat.", + "Hidden field \"challenge\" is a required field": "Câmpul ascuns \"challenge\" este un câmp obligatoriu", + "Hidden field \"token\" is a required field": "Câmpul ascuns \"token\" este un câmp obligatoriu", + "Erroneous challenge": "Challenge invalid", + "Erroneous token": "Token invalid", + "No such user": "Acest utilizator nu există", + "Token is expired, please try again": "Token-ul este expirat, vă rugăm să reîncercați.", + "English": "Engleză", + "English (auto-generated)": "Engleză (generată automat)", + "Afrikaans": "Afrikaans", + "Albanian": "Albaneză", + "Amharic": "Amharică", + "Arabic": "Arabă", + "Armenian": "Arméniană", + "Azerbaijani": "Azeră", + "Bangla": "Bangla", + "Basque": "Basque", + "Belarusian": "Belarusă", + "Bosnian": "Bosniacă", + "Bulgarian": "Bulgară", + "Burmese": "Birmană", + "Catalan": "Catalană", + "Cebuano": "Cebuano", + "Chinese (Simplified)": "Chineză (Simplificată)", + "Chinese (Traditional)": "Chinois (Tradițională)", + "Corsican": "Corsicană", + "Croatian": "Croată", + "Czech": "Cehă", + "Danish": "Daneză", + "Dutch": "Olandeză", + "Esperanto": "Esperanto", + "Estonian": "Estoniană", + "Filipino": "Filipineză", + "Finnish": "Finlandeză", + "French": "Franceză", + "Galician": "Galiciană", + "Georgian": "Georgiană", + "German": "Germană", + "Greek": "Greacă", + "Gujarati": "Gujarati", + "Haitian Creole": "Creola Haitiană", + "Hausa": "Haousa", + "Hawaiian": "Hawaiană", + "Hebrew": "Ebraică", + "Hindi": "Hindi", + "Hmong": "Hmong", + "Hungarian": "Ungară", + "Icelandic": "Islandeză", + "Igbo": "Igbo", + "Indonesian": "Indoneziană", + "Irish": "Irlandeză", + "Italian": "Italiană", + "Japanese": "Japoneză", + "Javanese": "Javaneză", + "Kannada": "Kannada", + "Kazakh": "Kazakh", + "Khmer": "Khmer", + "Korean": "Coreană", + "Kurdish": "Kurdă", + "Kyrgyz": "Kirghize", + "Lao": "Lao", + "Latin": "Latină", + "Latvian": "Letonă", + "Lithuanian": "Lituaniană", + "Luxembourgish": "Luxemburgheză", + "Macedonian": "Macedoniană", + "Malagasy": "Malgașă", + "Malay": "Malaieză", + "Malayalam": "Malayalam", + "Maltese": "Malteză", + "Maori": "Maori", + "Marathi": "Marathi", + "Mongolian": "Mongoliană", + "Nepali": "Nepaleză", + "Norwegian Bokmål": "Norvegiană", + "Nyanja": "Nyanja", + "Pashto": "Pachtou", + "Persian": "Persană", + "Polish": "Poloneză", + "Portuguese": "Portugheză", + "Punjabi": "Punjabi", + "Romanian": "Română", + "Russian": "Rusă", + "Samoan": "Samoan", + "Scottish Gaelic": "Galic Scoțian", + "Serbian": "Sârbă", + "Shona": "Shona", + "Sindhi": "Sindhi", + "Sinhala": "Sinhala", + "Slovak": "Slovacă", + "Slovenian": "Slovenă", + "Somali": "Somaleză", + "Southern Sotho": "Sotho de Sud", + "Spanish": "Spaniolă", + "Spanish (Latin America)": "Spaniolă (America Latină)", + "Sundanese": "Sundaneză", + "Swahili": "Swahili", + "Swedish": "Suedeză", + "Tajik": "Tajik", + "Tamil": "Tamil", + "Telugu": "Telugu", + "Thai": "Tailandeză", + "Turkish": "Turcă", + "Ukrainian": "Ucrainiană", + "Urdu": "Urdu", + "Uzbek": "Uzbek", + "Vietnamese": "Vietnameză", + "Welsh": "Galeză", + "Western Frisian": "Frisiană de Vest", + "Xhosa": "Xhosa", + "Yiddish": "Yiddish", + "Yoruba": "Yoruba", + "Zulu": "Zoulou", + "`x` years": "`x` ani", + "`x` months": "`x` luni", + "`x` weeks": "`x` săptămâni", + "`x` days": "`x` zile", + "`x` hours": "`x` ore", + "`x` minutes": "`x` minute", + "`x` seconds": "`x` secunde", + "Fallback comments: ": "Comentarii alternative: ", + "Popular": "Popular", + "Top": "Top", + "About": "Despre", + "Rating: ": "Evaluare: ", + "Language: ": "Limbă: ", + "View as playlist": "Vizualizați ca listă de redare", + "Default": "Implicit", + "Music": "Muzică", + "Gaming": "Jocuri Video", + "News": "Noutăți", + "Movies": "Filme", + "Download": "Descărcați", + "Download as: ": "Descărcați ca: ", + "%A %B %-d, %Y": "%A %-d %B %Y", + "(edited)": "(editat)", + "YouTube comment permalink": "Permalink pentru comentariul de pe YouTube", + "permalink": "permalink", + "`x` marked it with a ❤": "`x` l-a marcat cu o ❤", + "Audio mode": "Mod audio", + "Video mode": "Mod video", + "Videos": "Videoclipuri", + "Playlists": "Liste de redare", + "Community": "Comunitate", + "Current version: ": "Versiunea actuală: " +}
\ No newline at end of file @@ -26,7 +26,7 @@ dependencies: version: ~> 0.1.2 lsquic: github: omarroth/lsquic.cr - version: ~> 0.1.5 + version: ~> 0.1.7 crystal: 0.31.1 diff --git a/src/invidious.cr b/src/invidious.cr index 147fe935..a4f20bc4 100644 --- a/src/invidious.cr +++ b/src/invidious.cr @@ -87,6 +87,7 @@ LOCALES = { "nb_NO" => load_locale("nb_NO"), "nl" => load_locale("nl"), "pl" => load_locale("pl"), + "ro" => load_locale("ro"), "ru" => load_locale("ru"), "tr" => load_locale("tr"), "uk" => load_locale("uk"), @@ -3852,7 +3853,7 @@ get "/api/v1/captions/:id" do |env| caption = caption[0] end - url = "#{caption.baseUrl}&tlang=#{tlang}" + url = URI.parse("#{caption.baseUrl}&tlang=#{tlang}").full_path # Auto-generated captions often have cues that aren't aligned properly with the video, # as well as some other markup that makes it cumbersome, so we try to fix that here @@ -4523,9 +4524,10 @@ get "/api/v1/search/suggestions" do |env| query ||= "" begin - response = QUIC::Client.get( - "https://suggestqueries.google.com/complete/search?hl=en&gl=#{region}&client=youtube&ds=yt&q=#{URI.encode_www_form(query)}&callback=suggestCallback" - ).body + client = QUIC::Client.new("suggestqueries.google.com") + client.family = CONFIG.force_resolve || Socket::Family::INET + client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC + 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 diff --git a/src/invidious/helpers/jobs.cr b/src/invidious/helpers/jobs.cr index 5838b5b3..f368d6df 100644 --- a/src/invidious/helpers/jobs.cr +++ b/src/invidious/helpers/jobs.cr @@ -290,6 +290,60 @@ def bypass_captcha(captcha_key, logger) response = YT_POOL.client &.post("/das_captcha", headers, form: inputs) yield response.cookies.select { |cookie| cookie.name != "PREF" } + elsif response.headers["Location"]?.try &.includes?("/sorry/index") + location = response.headers["Location"].try { |u| URI.parse(u) } + client = QUIC::Client.new(location.host.not_nil!) + response = client.get(location.full_path) + + html = XML.parse_html(response.body) + form = html.xpath_node(%(//form[@action="index"])).not_nil! + site_key = form.xpath_node(%(.//div[@class="g-recaptcha"])).try &.["data-sitekey"] + + inputs = {} of String => String + form.xpath_nodes(%(.//input[@name])).map do |node| + inputs[node["name"]] = node["value"] + end + + response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/createTask", body: { + "clientKey" => CONFIG.captcha_key, + "task" => { + "type" => "NoCaptchaTaskProxyless", + "websiteURL" => location.to_s, + "websiteKey" => site_key, + }, + }.to_json).body) + + if response["error"]? + raise response["error"].as_s + end + + task_id = response["taskId"].as_i + + loop do + sleep 10.seconds + + response = JSON.parse(HTTP::Client.post("https://api.anti-captcha.com/getTaskResult", body: { + "clientKey" => CONFIG.captcha_key, + "taskId" => task_id, + }.to_json).body) + + if response["status"]?.try &.== "ready" + break + elsif response["errorId"]?.try &.as_i != 0 + raise response["errorDescription"].as_s + end + end + + inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s + client.close + client = QUIC::Client.new("www.google.com") + response = client.post(location.full_path, form: inputs) + headers = HTTP::Headers{ + "Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0], + } + cookies = HTTP::Cookies.from_headers(headers) + + yield cookies end rescue ex logger.puts("Exception: #{ex.message}") diff --git a/src/invidious/helpers/utils.cr b/src/invidious/helpers/utils.cr index 53c18dd5..6fcfa8d2 100644 --- a/src/invidious/helpers/utils.cr +++ b/src/invidious/helpers/utils.cr @@ -31,8 +31,10 @@ struct QUICPool begin response = yield conn rescue ex - conn.destroy_engine + conn.close conn = QUIC::Client.new(url) + conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET + conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" response = yield conn ensure @@ -45,9 +47,11 @@ struct QUICPool private def build_pool ConnectionPool(QUIC::Client).new(capacity: capacity, timeout: timeout) do - client = QUIC::Client.new(url) - client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" - client + conn = QUIC::Client.new(url) + conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET + conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC + conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com" + conn end end end |
