Merge remote-tracking branch 'origin/master'

This commit is contained in:
Gender Shrapnel 2024-08-15 14:58:13 +02:00
commit 8670b7b97a
62 changed files with 1490 additions and 420 deletions

View File

@ -23,6 +23,10 @@ Lint/ShadowingOuterLocalVar:
Lint/NotNil: Lint/NotNil:
Enabled: false Enabled: false
Lint/SpecFilename:
Excluded:
- spec/parsers_helper.cr
# #
# Style # Style
@ -38,7 +42,20 @@ Style/ParenthesesAroundCondition:
Enabled: false Enabled: false
# This requires a rewrite of most data structs (and their usage) in Invidious. # This requires a rewrite of most data structs (and their usage) in Invidious.
Style/QueryBoolMethods: Naming/QueryBoolMethods:
Enabled: false
Naming/AccessorMethodName:
Enabled: false
Naming/BlockParameterName:
Enabled: false
# Hides TODO comment warnings.
#
# Call `bin/ameba --only Documentation/DocumentationAdmonition` to
# list them
Documentation/DocumentationAdmonition:
Enabled: false Enabled: false

View File

@ -90,10 +90,10 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Build Docker - name: Build Docker
run: docker-compose build --build-arg release=0 run: docker compose build --build-arg release=0
- name: Run Docker - name: Run Docker
run: docker-compose up -d run: docker compose up -d
- name: Test Docker - name: Test Docker
run: while curl -Isf http://localhost:3000; do sleep 1; done run: while curl -Isf http://localhost:3000; do sleep 1; done

View File

@ -351,7 +351,12 @@ if (video_data.params.save_player_pos) {
const rememberedTime = get_video_time(); const rememberedTime = get_video_time();
let lastUpdated = 0; let lastUpdated = 0;
if(!hasTimeParam) set_seconds_after_start(rememberedTime); if(!hasTimeParam) {
if (rememberedTime >= video_data.length_seconds - 20)
set_seconds_after_start(0);
else
set_seconds_after_start(rememberedTime);
}
player.on('timeupdate', function () { player.on('timeupdate', function () {
const raw = player.currentTime(); const raw = player.currentTime();

View File

@ -1,6 +1,6 @@
######################################### #########################################
# #
# Database configuration # Database and other external servers
# #
######################################### #########################################
@ -41,6 +41,19 @@ db:
#check_tables: false #check_tables: false
##
## Path to an external signature resolver, used to emulate
## the Youtube client's Javascript. If no such server is
## available, some videos will not be playable.
##
## When this setting is commented out, no external
## resolver will be used.
##
## Accepted values: a path to a UNIX socket or "<IP>:<Port>"
## Default: <none>
##
#signature_server:
######################################### #########################################
# #
@ -173,6 +186,18 @@ https_only: false
## ##
# use_innertube_for_captions: false # use_innertube_for_captions: false
##
## Send Google session informations. This is useful when Invidious is blocked
## by the message "This helps protect our community."
## See https://github.com/iv-org/invidious/issues/4734.
##
## Warning: These strings gives much more identifiable information to Google!
##
## Accepted values: String
## Default: <none>
##
# po_token: ""
# visitor_data: ""
# ----------------------------- # -----------------------------
# Logging # Logging
@ -343,21 +368,6 @@ full_refresh: false
## ##
feed_threads: 1 feed_threads: 1
##
## Enable/Disable the polling job that keeps the decryption
## function (for "secured" videos) up to date.
##
## Note: This part of the code generate a small amount of data every minute.
## This may not be desired if you have bandwidth limits set by your ISP.
##
## Note 2: This part of the code is currently broken, so changing
## this setting has no impact.
##
## Accepted values: true, false
## Default: false
##
#decrypt_polling: false
jobs: jobs:

View File

@ -487,5 +487,11 @@
"generic_views_count": "{{count}} гледане", "generic_views_count": "{{count}} гледане",
"generic_views_count_plural": "{{count}} гледания", "generic_views_count_plural": "{{count}} гледания",
"Next page": "Следваща страница", "Next page": "Следваща страница",
"Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)" "Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)",
"toggle_theme": "Смени темата",
"Add to playlist": "Добави към плейлист",
"Add to playlist: ": "Добави към плейлист: ",
"Answer": "Отговор",
"Search for videos": "Търсене на видеа",
"The Popular feed has been disabled by the administrator.": "Популярната страница е деактивирана от администратора."
} }

View File

@ -487,5 +487,7 @@
"generic_button_edit": "Edita", "generic_button_edit": "Edita",
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
"generic_button_delete": "Suprimeix", "generic_button_delete": "Suprimeix",
"Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)" "Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)",
"Answer": "Resposta",
"toggle_theme": "Commuta el tema"
} }

385
locales/cy.json Normal file
View File

@ -0,0 +1,385 @@
{
"Time (h:mm:ss):": "Amser (h:mm:ss):",
"Password": "Cyfrinair",
"preferences_quality_dash_option_auto": "Awtomatig",
"preferences_quality_dash_option_best": "Gorau",
"preferences_quality_dash_option_worst": "Gwaethaf",
"preferences_quality_dash_option_360p": "360p",
"published": "dyddiad cyhoeddi",
"preferences_quality_dash_option_4320p": "4320p",
"preferences_quality_dash_option_480p": "480p",
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"preferences_comments_label": "Ffynhonnell sylwadau: ",
"preferences_captions_label": "Isdeitlau rhagosodedig: ",
"youtube": "YouTube",
"reddit": "Reddit",
"Fallback captions: ": "Isdeitlau amgen: ",
"preferences_related_videos_label": "Dangos fideos perthnasol: ",
"dark": "tywyll",
"preferences_dark_mode_label": "Thema: ",
"light": "golau",
"preferences_sort_label": "Trefnu fideo yn ôl: ",
"Import/export data": "Mewnforio/allforio data",
"Delete account": "Dileu eich cyfrif",
"preferences_category_admin": "Hoffterau gweinyddu",
"playlist_button_add_items": "Ychwanegu fideos",
"Delete playlist": "Dileu'r rhestr chwarae",
"Create playlist": "Creu rhestr chwarae",
"Show less": "Dangos llai",
"Show more": "Dangos rhagor",
"Watch on YouTube": "Gwylio ar YouTube",
"search_message_no_results": "Dim canlyniadau.",
"search_message_change_filters_or_query": "Ceisiwch ehangu eich chwiliad ac/neu newid yr hidlyddion.",
"License: ": "Trwydded: ",
"Standard YouTube license": "Trwydded safonol YouTube",
"Family friendly? ": "Addas i bawb? ",
"Wilson score: ": "Sgôr Wilson: ",
"Show replies": "Dangos ymatebion",
"Music in this video": "Cerddoriaeth yn y fideo hwn",
"Artist: ": "Artist: ",
"Erroneous CAPTCHA": "CAPTCHA anghywir",
"This channel does not exist.": "Dyw'r sianel hon ddim yn bodoli.",
"Not a playlist.": "Ddim yn rhestr chwarae.",
"Could not fetch comments": "Wedi methu llwytho sylwadau",
"Playlist does not exist.": "Dyw'r rhestr chwarae ddim yn bodoli.",
"Erroneous challenge": "Her annilys",
"channel_tab_podcasts_label": "Podlediadau",
"channel_tab_playlists_label": "Rhestrau chwarae",
"channel_tab_streams_label": "Fideos byw",
"crash_page_read_the_faq": "darllen y <a href=\"`x`\">cwestiynau cyffredin</a>",
"crash_page_switch_instance": "ceisio <a href=\"`x`\">defnyddio gweinydd arall</a>",
"crash_page_refresh": "ceisio <a href=\"`x`\">ail-lwytho'r dudalen</a>",
"search_filters_features_option_four_k": "4K",
"search_filters_features_label": "Nodweddion",
"search_filters_duration_option_medium": "Canolig (4 - 20 munud)",
"search_filters_features_option_live": "Yn fyw",
"search_filters_duration_option_long": "Hir (> 20 munud)",
"search_filters_date_option_year": "Eleni",
"search_filters_type_label": "Math",
"search_filters_date_option_month": "Y mis hwn",
"generic_views_count_0": "{{count}} o wyliadau",
"generic_views_count_1": "{{count}} gwyliad",
"generic_views_count_2": "{{count}} wyliad",
"generic_views_count_3": "{{count}} o wyliadau",
"generic_views_count_4": "{{count}} o wyliadau",
"generic_views_count_5": "{{count}} o wyliadau",
"Answer": "Ateb",
"Add to playlist: ": "Ychwanegu at y rhestr chwarae: ",
"Add to playlist": "Ychwanegu at y rhestr chwarae",
"generic_button_cancel": "Diddymu",
"generic_button_rss": "RSS",
"LIVE": "YN FYW",
"Import YouTube watch history (.json)": "Mewnforio hanes gwylio YouTube (.json)",
"generic_videos_count_0": "{{count}} fideo",
"generic_videos_count_1": "{{count}} fideo",
"generic_videos_count_2": "{{count}} fideo",
"generic_videos_count_3": "{{count}} fideo",
"generic_videos_count_4": "{{count}} fideo",
"generic_videos_count_5": "{{count}} fideo",
"generic_subscribers_count_0": "{{count}} tanysgrifiwr",
"generic_subscribers_count_1": "{{count}} tanysgrifiwr",
"generic_subscribers_count_2": "{{count}} danysgrifiwr",
"generic_subscribers_count_3": "{{count}} thanysgrifiwr",
"generic_subscribers_count_4": "{{count}} o danysgrifwyr",
"generic_subscribers_count_5": "{{count}} o danysgrifwyr",
"Authorize token?": "Awdurdodi'r tocyn?",
"Authorize token for `x`?": "Awdurdodi'r tocyn ar gyfer `x`?",
"English": "Saesneg",
"English (United Kingdom)": "Saesneg (Y Deyrnas Unedig)",
"English (United States)": "Saesneg (Yr Unol Daleithiau)",
"Afrikaans": "Affricaneg",
"English (auto-generated)": "Saesneg (awtomatig)",
"Amharic": "Amhareg",
"Albanian": "Albaneg",
"Arabic": "Arabeg",
"crash_page_report_issue": "Os nad yw'r awgrymiadau uchod wedi helpu, <a href=\"`x`\">codwch 'issue' newydd ar Github </a> (yn Saesneg, gorau oll) a chynnwys y testun canlynol yn eich neges (peidiwch â chyfieithu'r testun hwn):",
"Search for videos": "Chwilio am fideos",
"The Popular feed has been disabled by the administrator.": "Mae'r ffrwd fideos poblogaidd wedi ei hanalluogi gan y gweinyddwr.",
"generic_channels_count_0": "{{count}} sianel",
"generic_channels_count_1": "{{count}} sianel",
"generic_channels_count_2": "{{count}} sianel",
"generic_channels_count_3": "{{count}} sianel",
"generic_channels_count_4": "{{count}} sianel",
"generic_channels_count_5": "{{count}} sianel",
"generic_button_delete": "Dileu",
"generic_button_edit": "Golygu",
"generic_button_save": "Cadw",
"Shared `x` ago": "Rhannwyd `x` yn ôl",
"Unsubscribe": "Dad-danysgrifio",
"Subscribe": "Tanysgrifio",
"View channel on YouTube": "Gweld y sianel ar YouTube",
"View playlist on YouTube": "Gweld y rhestr chwarae ar YouTube",
"newest": "diweddaraf",
"oldest": "hynaf",
"popular": "poblogaidd",
"Next page": "Tudalen nesaf",
"Previous page": "Tudalen flaenorol",
"Clear watch history?": "Clirio'ch hanes gwylio?",
"New password": "Cyfrinair newydd",
"Import and Export Data": "Mewnforio ac allforio data",
"Import": "Mewnforio",
"Import Invidious data": "Mewnforio data JSON Invidious",
"Import YouTube subscriptions": "Mewnforio tanysgrifiadau YouTube ar fformat CSV neu OPML",
"Import YouTube playlist (.csv)": "Mewnforio rhestr chwarae YouTube (.csv)",
"Export": "Allforio",
"Export data as JSON": "Allforio data Invidious ar fformat JSON",
"Delete account?": "Ydych chi'n siŵr yr hoffech chi ddileu eich cyfrif?",
"History": "Hanes",
"JavaScript license information": "Gwybodaeth am y drwydded JavaScript",
"generic_subscriptions_count_0": "{{count}} tanysgrifiad",
"generic_subscriptions_count_1": "{{count}} tanysgrifiad",
"generic_subscriptions_count_2": "{{count}} danysgrifiad",
"generic_subscriptions_count_3": "{{count}} thanysgrifiad",
"generic_subscriptions_count_4": "{{count}} o danysgrifiadau",
"generic_subscriptions_count_5": "{{count}} o danysgrifiadau",
"Yes": "Iawn",
"No": "Na",
"Import FreeTube subscriptions (.db)": "Mewnforio tanysgrifiadau FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Mewnforio tanysgrifiadau NewPipe (.json)",
"Import NewPipe data (.zip)": "Mewnforio data NewPipe (.zip)",
"An alternative front-end to YouTube": "Pen blaen amgen i YouTube",
"source": "ffynhonnell",
"Log in": "Mewngofnodi",
"Log in/register": "Mewngofnodi/Cofrestru",
"User ID": "Enw defnyddiwr",
"preferences_quality_option_dash": "DASH (ansawdd addasol)",
"Sign In": "Mewngofnodi",
"Register": "Cofrestru",
"E-mail": "Ebost",
"Preferences": "Hoffterau",
"preferences_category_player": "Hoffterau'r chwaraeydd",
"preferences_autoplay_label": "Chwarae'n awtomatig: ",
"preferences_local_label": "Llwytho fideos drwy ddirprwy weinydd: ",
"preferences_watch_history_label": "Galluogi hanes gwylio: ",
"preferences_speed_label": "Cyflymder rhagosodedig: ",
"preferences_quality_label": "Ansawdd fideos: ",
"preferences_quality_option_hd720": "HD720",
"preferences_quality_option_medium": "Canolig",
"preferences_quality_option_small": "Bach",
"preferences_quality_dash_option_2160p": "2160p",
"preferences_quality_dash_option_1440p": "1440p",
"preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_720p": "720p",
"invidious": "Invidious",
"Text CAPTCHA": "CAPTCHA testun",
"Image CAPTCHA": "CAPTCHA delwedd",
"preferences_continue_label": "Chwarae'r fideo nesaf fel rhagosodiad: ",
"preferences_continue_autoplay_label": "Chwarae'r fideo nesaf yn awtomatig: ",
"preferences_listen_label": "Sain yn unig: ",
"preferences_quality_dash_label": "Ansawdd fideos DASH a ffefrir: ",
"preferences_volume_label": "Uchder sain y chwaraeydd: ",
"preferences_category_visual": "Hoffterau'r wefan",
"preferences_region_label": "Gwlad y cynnwys: ",
"preferences_player_style_label": "Arddull y chwaraeydd: ",
"Dark mode: ": "Modd tywyll: ",
"preferences_thin_mode_label": "Modd tenau: ",
"preferences_category_misc": "Hoffterau amrywiol",
"preferences_category_subscription": "Hoffterau tanysgrifio",
"preferences_max_results_label": "Nifer o fideos a ddangosir yn eich ffrwd: ",
"alphabetically": "yr wyddor",
"alphabetically - reverse": "yr wyddor - am yn ôl",
"published - reverse": "dyddiad cyhoeddi - am yn ôl",
"channel name": "enw'r sianel",
"channel name - reverse": "enw'r sianel - am yn ôl",
"Only show latest video from channel: ": "Dangos fideo diweddaraf y sianeli rydych chi'n tanysgrifio iddynt: ",
"Only show latest unwatched video from channel: ": "Dangos fideo heb ei wylio diweddaraf y sianeli rydych chi'n tanysgrifio iddynt: ",
"Enable web notifications": "Galluogi hysbysiadau gwe",
"`x` uploaded a video": "uwchlwythodd `x` fideo",
"`x` is live": "mae `x` yn darlledu'n fyw",
"preferences_category_data": "Hoffterau data",
"Clear watch history": "Clirio'ch hanes gwylio",
"Change password": "Newid eich cyfrinair",
"Manage subscriptions": "Rheoli tanysgrifiadau",
"Manage tokens": "Rheoli tocynnau",
"Watch history": "Hanes gwylio",
"preferences_default_home_label": "Hafan ragosodedig: ",
"preferences_show_nick_label": "Dangos eich enw defnyddiwr ar frig y dudalen: ",
"preferences_annotations_label": "Dangos nodiadau fel rhagosodiad: ",
"preferences_unseen_only_label": "Dangos fideos heb eu gwylio yn unig: ",
"preferences_notifications_only_label": "Dangos hysbysiadau yn unig (os oes unrhyw rai): ",
"Token manager": "Rheolydd tocynnau",
"Token": "Tocyn",
"unsubscribe": "dad-danysgrifio",
"Subscriptions": "Tanysgrifiadau",
"Import/export": "Mewngofnodi/allgofnodi",
"search": "chwilio",
"Log out": "Allgofnodi",
"View privacy policy.": "Polisi preifatrwydd",
"Trending": "Pynciau llosg",
"Public": "Cyhoeddus",
"Private": "Preifat",
"Updated `x` ago": "Diweddarwyd `x` yn ôl",
"Delete playlist `x`?": "Ydych chi'n siŵr yr hoffech chi ddileu'r rhestr chwarae `x`?",
"Title": "Teitl",
"Playlist privacy": "Preifatrwydd y rhestr chwarae",
"search_message_use_another_instance": " Gallwch hefyd <a href=\"`x`\">chwilio ar weinydd arall</a>.",
"Popular enabled: ": "Tudalen fideos poblogaidd wedi'i galluogi: ",
"CAPTCHA enabled: ": "CAPTCHA wedi'i alluogi: ",
"Registration enabled: ": "Cofrestru wedi'i alluogi: ",
"Save preferences": "Cadw'r hoffterau",
"Subscription manager": "Rheolydd tanysgrifio",
"revoke": "tynnu",
"subscriptions_unseen_notifs_count_0": "{{count}} hysbysiad heb ei weld",
"subscriptions_unseen_notifs_count_1": "{{count}} hysbysiad heb ei weld",
"subscriptions_unseen_notifs_count_2": "{{count}} hysbysiad heb eu gweld",
"subscriptions_unseen_notifs_count_3": "{{count}} hysbysiad heb eu gweld",
"subscriptions_unseen_notifs_count_4": "{{count}} hysbysiad heb eu gweld",
"subscriptions_unseen_notifs_count_5": "{{count}} hysbysiad heb eu gweld",
"Released under the AGPLv3 on Github.": "Cyhoeddwyd dan drwydded AGPLv3 ar GitHub",
"Unlisted": "Heb ei restru",
"Switch Invidious Instance": "Newid gweinydd Invidious",
"Report statistics: ": "Galluogi ystadegau'r gweinydd: ",
"View all playlists": "Gweld pob rhestr chwarae",
"Editing playlist `x`": "Yn golygu'r rhestr chwarae `x`",
"Whitelisted regions: ": "Rhanbarthau a ganiateir: ",
"Blacklisted regions: ": "Rhanbarthau a rwystrir: ",
"Song: ": "Cân: ",
"Album: ": "Albwm: ",
"Shared `x`": "Rhannwyd `x`",
"View YouTube comments": "Dangos sylwadau YouTube",
"View more comments on Reddit": "Dangos rhagor o sylwadau ar Reddit",
"View Reddit comments": "Dangos sylwadau Reddit",
"Hide replies": "Cuddio ymatebion",
"Incorrect password": "Cyfrinair anghywir",
"Wrong answer": "Ateb anghywir",
"CAPTCHA is a required field": "Rhaid rhoi'r CAPTCHA",
"User ID is a required field": "Rhaid rhoi enw defnyddiwr",
"Password is a required field": "Rhaid rhoi cyfrinair",
"Wrong username or password": "Enw defnyddiwr neu gyfrinair anghywir",
"Password cannot be empty": "All y cyfrinair ddim bod yn wag",
"Password cannot be longer than 55 characters": "All y cyfrinair ddim bod yn hirach na 55 nod",
"Please log in": "Mewngofnodwch",
"channel:`x`": "sianel: `x`",
"Deleted or invalid channel": "Sianel wedi'i dileu neu'n annilys",
"Could not get channel info.": "Wedi methu llwytho gwybodaeth y sianel.",
"`x` ago": "`x` yn ôl",
"Load more": "Llwytho rhagor",
"Empty playlist": "Rhestr chwarae wag",
"Hide annotations": "Cuddio nodiadau",
"Show annotations": "Dangos nodiadau",
"Premieres in `x`": "Yn dechrau mewn `x`",
"Premieres `x`": "Yn dechrau `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.": "Helo! Mae'n ymddangos eich bod wedi diffodd JavaScript. Cliciwch yma i weld sylwadau, ond cofiwch y gall gymryd mwy o amser i'w llwytho.",
"View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Gweld `x` sylw",
"": "Gweld `x` sylw"
},
"Could not create mix.": "Wedi methu creu'r cymysgiad hwn.",
"Erroneous token": "Tocyn annilys",
"No such user": "Dyw'r defnyddiwr hwn ddim yn bodoli",
"Token is expired, please try again": "Mae'r tocyn hwn wedi dod i ben, ceisiwch eto",
"Bangla": "Bangleg",
"Basque": "Basgeg",
"Bulgarian": "Bwlgareg",
"Catalan": "Catalaneg",
"Chinese": "Tsieineeg",
"Chinese (China)": "Tsieineeg (Tsieina)",
"Chinese (Hong Kong)": "Tsieineeg (Hong Kong)",
"Chinese (Taiwan)": "Tsieineeg (Taiwan)",
"Danish": "Daneg",
"Dutch": "Iseldireg",
"Esperanto": "Esperanteg",
"Finnish": "Ffinneg",
"French": "Ffrangeg",
"German": "Almaeneg",
"Greek": "Groeg",
"Could not pull trending pages.": "Wedi methu llwytho tudalennau pynciau llosg.",
"Hidden field \"challenge\" is a required field": "Mae'r maes cudd \"her\" yn ofynnol",
"Hidden field \"token\" is a required field": "Mae'r maes cudd \"tocyn\" yn ofynnol",
"Hebrew": "Hebraeg",
"Hungarian": "Hwngareg",
"Irish": "Gwyddeleg",
"Italian": "Eidaleg",
"Welsh": "Cymraeg",
"generic_count_hours_0": "{{count}} awr",
"generic_count_hours_1": "{{count}} awr",
"generic_count_hours_2": "{{count}} awr",
"generic_count_hours_3": "{{count}} awr",
"generic_count_hours_4": "{{count}} awr",
"generic_count_hours_5": "{{count}} awr",
"generic_count_minutes_0": "{{count}} munud",
"generic_count_minutes_1": "{{count}} munud",
"generic_count_minutes_2": "{{count}} funud",
"generic_count_minutes_3": "{{count}} munud",
"generic_count_minutes_4": "{{count}} o funudau",
"generic_count_minutes_5": "{{count}} o funudau",
"generic_count_weeks_0": "{{count}} wythnos",
"generic_count_weeks_1": "{{count}} wythnos",
"generic_count_weeks_2": "{{count}} wythnos",
"generic_count_weeks_3": "{{count}} wythnos",
"generic_count_weeks_4": "{{count}} wythnos",
"generic_count_weeks_5": "{{count}} wythnos",
"generic_count_seconds_0": "{{count}} eiliad",
"generic_count_seconds_1": "{{count}} eiliad",
"generic_count_seconds_2": "{{count}} eiliad",
"generic_count_seconds_3": "{{count}} eiliad",
"generic_count_seconds_4": "{{count}} o eiliadau",
"generic_count_seconds_5": "{{count}} o eiliadau",
"Fallback comments: ": "Sylwadau amgen: ",
"Popular": "Poblogaidd",
"preferences_locale_label": "Iaith: ",
"About": "Ynghylch",
"Search": "Chwilio",
"search_filters_features_option_c_commons": "Comin Creu",
"search_filters_features_option_subtitles": "Isdeitlau (CC)",
"search_filters_features_option_hd": "HD",
"permalink": "dolen barhaol",
"search_filters_duration_option_short": "Byr (< 4 munud)",
"search_filters_duration_option_none": "Unrhyw hyd",
"search_filters_duration_label": "Hyd",
"search_filters_type_option_show": "Rhaglen",
"search_filters_type_option_movie": "Ffilm",
"search_filters_type_option_playlist": "Rhestr chwarae",
"search_filters_type_option_channel": "Sianel",
"search_filters_type_option_video": "Fideo",
"search_filters_type_option_all": "Unrhyw fath",
"search_filters_date_option_week": "Yr wythnos hon",
"search_filters_date_option_today": "Heddiw",
"search_filters_date_option_hour": "Yr awr ddiwethaf",
"search_filters_date_option_none": "Unrhyw ddyddiad",
"search_filters_date_label": "Dyddiad uwchlwytho",
"search_filters_title": "Hidlyddion",
"Playlists": "Rhestrau chwarae",
"Video mode": "Modd fideo",
"Audio mode": "Modd sain",
"Channel Sponsor": "Noddwr y sianel",
"(edited)": "(golygwyd)",
"Download": "Islwytho",
"Movies": "Ffilmiau",
"News": "Newyddion",
"Gaming": "Gemau",
"Music": "Cerddoriaeth",
"Download is disabled": "Mae islwytho wedi'i analluogi",
"Download as: ": "Islwytho fel: ",
"View as playlist": "Gweld fel rhestr chwarae",
"Default": "Rhagosodiad",
"YouTube comment permalink": "Dolen barhaol i'r sylw ar YouTube",
"crash_page_before_reporting": "Cyn adrodd nam, sicrhewch eich bod wedi:",
"crash_page_search_issue": "<a href=\"`x`\">chwilio am y nam ar GitHub</a>",
"videoinfo_watch_on_youTube": "Gwylio ar YouTube",
"videoinfo_started_streaming_x_ago": "Yn ffrydio'n fyw ers `x` o funudau",
"videoinfo_invidious_embed_link": "Dolen mewnblannu",
"footer_documentation": "Dogfennaeth",
"footer_donate_page": "Rhoddi",
"Current version: ": "Fersiwn gyfredol: ",
"search_filters_apply_button": "Rhoi'r hidlyddion ar waith",
"search_filters_sort_option_date": "Dyddiad uwchlwytho",
"search_filters_sort_option_relevance": "Perthnasedd",
"search_filters_sort_label": "Trefnu yn ôl",
"search_filters_features_option_location": "Lleoliad",
"search_filters_features_option_hdr": "HDR",
"search_filters_features_option_three_d": "3D",
"search_filters_features_option_vr180": "VR180",
"search_filters_features_option_three_sixty": "360°",
"videoinfo_youTube_embed_link": "Mewnblannu",
"download_subtitles": "Isdeitlau - `x` (.vtt)",
"user_created_playlists": "`x` rhestr chwarae wedi'u creu",
"user_saved_playlists": "`x` rhestr chwarae wedi'u cadw",
"Video unavailable": "Fideo ddim ar gael",
"crash_page_you_found_a_bug": "Mae'n debyg eich bod wedi dod o hyd i nam yn Invidious!",
"channel_tab_channels_label": "Sianeli",
"channel_tab_community_label": "Cymuned",
"channel_tab_shorts_label": "Fideos byrion",
"channel_tab_videos_label": "Fideos"
}

View File

@ -21,7 +21,7 @@
"Import and Export Data": "Daten importieren und exportieren", "Import and Export Data": "Daten importieren und exportieren",
"Import": "Importieren", "Import": "Importieren",
"Import Invidious data": "Invidious-JSON-Daten importieren", "Import Invidious data": "Invidious-JSON-Daten importieren",
"Import YouTube subscriptions": "YouTube-/OPML-Abonnements importieren", "Import YouTube subscriptions": "YouTube-CSV/OPML-Abonnements importieren",
"Import FreeTube subscriptions (.db)": "FreeTube Abonnements importieren (.db)", "Import FreeTube subscriptions (.db)": "FreeTube Abonnements importieren (.db)",
"Import NewPipe subscriptions (.json)": "NewPipe Abonnements importieren (.json)", "Import NewPipe subscriptions (.json)": "NewPipe Abonnements importieren (.json)",
"Import NewPipe data (.zip)": "NewPipe Daten importieren (.zip)", "Import NewPipe data (.zip)": "NewPipe Daten importieren (.zip)",

View File

@ -486,5 +486,8 @@
"Switch Invidious Instance": "Αλλαγή Instance Invidious", "Switch Invidious Instance": "Αλλαγή Instance Invidious",
"Standard YouTube license": "Τυπική άδεια YouTube", "Standard YouTube license": "Τυπική άδεια YouTube",
"search_filters_duration_option_medium": "Μεσαία (4 - 20 λεπτά)", "search_filters_duration_option_medium": "Μεσαία (4 - 20 λεπτά)",
"search_filters_date_label": "Ημερομηνία αναφόρτωσης" "search_filters_date_label": "Ημερομηνία αναφόρτωσης",
"Search for videos": "Αναζήτηση βίντεο",
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
"Answer": "Απάντηση"
} }

View File

@ -17,7 +17,7 @@
"View playlist on YouTube": "دیدن فهرست پخش در یوتیوب", "View playlist on YouTube": "دیدن فهرست پخش در یوتیوب",
"newest": "تازه‌ترین", "newest": "تازه‌ترین",
"oldest": "کهنه‌ترین", "oldest": "کهنه‌ترین",
"popular": "محبوب", "popular": "پرطرفدار",
"last": "آخرین", "last": "آخرین",
"Next page": "صفحه بعد", "Next page": "صفحه بعد",
"Previous page": "صفحه قبل", "Previous page": "صفحه قبل",
@ -31,7 +31,7 @@
"Import and Export Data": "درون‌برد و برون‌برد داده", "Import and Export Data": "درون‌برد و برون‌برد داده",
"Import": "درون‌برد", "Import": "درون‌برد",
"Import Invidious data": "وارد کردن داده JSON اینویدیوس", "Import Invidious data": "وارد کردن داده JSON اینویدیوس",
"Import YouTube subscriptions": "وارد کردن اشتراک OPML/ یوتیوب", "Import YouTube subscriptions": "وارد کردن فایل CSV یا OPML سابسکرایب های یوتیوب",
"Import FreeTube subscriptions (.db)": "درون‌برد اشتراک‌های فری‌تیوب (.db)", "Import FreeTube subscriptions (.db)": "درون‌برد اشتراک‌های فری‌تیوب (.db)",
"Import NewPipe subscriptions (.json)": "درون‌برد اشتراک‌های نیوپایپ (.json)", "Import NewPipe subscriptions (.json)": "درون‌برد اشتراک‌های نیوپایپ (.json)",
"Import NewPipe data (.zip)": "درون‌برد داده نیوپایپ (.zip)", "Import NewPipe data (.zip)": "درون‌برد داده نیوپایپ (.zip)",
@ -328,7 +328,7 @@
"generic_count_seconds": "{{count}} ثانیه", "generic_count_seconds": "{{count}} ثانیه",
"generic_count_seconds_plural": "{{count}} ثانیه", "generic_count_seconds_plural": "{{count}} ثانیه",
"Fallback comments: ": "نظرات عقب گرد: ", "Fallback comments: ": "نظرات عقب گرد: ",
"Popular": "محبوب", "Popular": "پربیننده",
"Search": "جست و جو", "Search": "جست و جو",
"Top": "بالا", "Top": "بالا",
"About": "درباره", "About": "درباره",
@ -484,5 +484,17 @@
"channel_tab_shorts_label": "Shortها", "channel_tab_shorts_label": "Shortها",
"channel_tab_playlists_label": "فهرست‌های پخش", "channel_tab_playlists_label": "فهرست‌های پخش",
"channel_tab_channels_label": "کانال‌ها", "channel_tab_channels_label": "کانال‌ها",
"error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. <a href=\"`x`\">کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید.</a>" "error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. <a href=\"`x`\">کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید.</a>",
"Add to playlist": "به لیست پخش افزوده شود",
"Answer": "پاسخ",
"Search for videos": "جست و جو برای ویدیوها",
"Add to playlist: ": "افزودن به لیست پخش ",
"The Popular feed has been disabled by the administrator.": "بخش ویدیوهای پرطرفدار توسط مدیر غیرفعال شده است.",
"carousel_slide": "اسلاید {{current}} از {{total}}",
"carousel_skip": "رد شدن از گرداننده",
"carousel_go_to": "به اسلاید `x` برو",
"crash_page_search_issue": "دنبال <a href=\"`x`\"> گشتیم بین مشکلات در گیت هاب </a>",
"crash_page_report_issue": "اگر هیچ یک از روش های بالا کمکی نکردند لطفا <a href=\"`x`\"> (ترجیحا به انگلیسی) یک سوال جدید در گیت هاب بپرسید و </a> طوری که سوالتون شامل متن زیر باشه:",
"channel_tab_releases_label": "آثار",
"toggle_theme": "تغییر وضعیت تم"
} }

View File

@ -28,7 +28,7 @@
"Export": "Vie", "Export": "Vie",
"Export subscriptions as OPML": "Vie tilaukset OPML-muodossa", "Export subscriptions as OPML": "Vie tilaukset OPML-muodossa",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Vie tilaukset OPML-muodossa (NewPipe & FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Vie tilaukset OPML-muodossa (NewPipe & FreeTube)",
"Export data as JSON": "Vie Invidious-data JSON-muodossa", "Export data as JSON": "Vie Invidiousin tiedot JSON-muodossa",
"Delete account?": "Poista tili?", "Delete account?": "Poista tili?",
"History": "Historia", "History": "Historia",
"An alternative front-end to YouTube": "Vaihtoehtoinen front-end YouTubelle", "An alternative front-end to YouTube": "Vaihtoehtoinen front-end YouTubelle",
@ -46,12 +46,12 @@
"E-mail": "Sähköposti", "E-mail": "Sähköposti",
"Preferences": "Asetukset", "Preferences": "Asetukset",
"preferences_category_player": "Soittimen asetukset", "preferences_category_player": "Soittimen asetukset",
"preferences_video_loop_label": "Toista jatkuvasti aina: ", "preferences_video_loop_label": "Toista aina uudelleen: ",
"preferences_autoplay_label": "Automaattinen toisto: ", "preferences_autoplay_label": "Automaattinen toiston aloitus: ",
"preferences_continue_label": "Toista seuraava oletuksena: ", "preferences_continue_label": "Toista seuraava oletuksena: ",
"preferences_continue_autoplay_label": "Toista seuraava video automaattisesti: ", "preferences_continue_autoplay_label": "Aloita seuraava video automaattisesti: ",
"preferences_listen_label": "Kuuntele oletuksena: ", "preferences_listen_label": "Kuuntele oletuksena: ",
"preferences_local_label": "Proxytä videot: ", "preferences_local_label": "Videot välityspalvelimen kautta: ",
"preferences_speed_label": "Oletusnopeus: ", "preferences_speed_label": "Oletusnopeus: ",
"preferences_quality_label": "Ensisijainen videon laatu: ", "preferences_quality_label": "Ensisijainen videon laatu: ",
"preferences_volume_label": "Soittimen äänenvoimakkuus: ", "preferences_volume_label": "Soittimen äänenvoimakkuus: ",
@ -63,7 +63,7 @@
"preferences_related_videos_label": "Näytä aiheeseen liittyviä videoita: ", "preferences_related_videos_label": "Näytä aiheeseen liittyviä videoita: ",
"preferences_annotations_label": "Näytä huomautukset oletuksena: ", "preferences_annotations_label": "Näytä huomautukset oletuksena: ",
"preferences_extend_desc_label": "Laajenna automaattisesti videon kuvausta: ", "preferences_extend_desc_label": "Laajenna automaattisesti videon kuvausta: ",
"preferences_vr_mode_label": "Interaktiiviset 360-asteiset videot (vaatii WebGL:n): ", "preferences_vr_mode_label": "Interaktiiviset 360-videot (vaatii WebGL:n): ",
"preferences_category_visual": "Visuaaliset asetukset", "preferences_category_visual": "Visuaaliset asetukset",
"preferences_player_style_label": "Soittimen tyyli: ", "preferences_player_style_label": "Soittimen tyyli: ",
"Dark mode: ": "Tumma tila: ", "Dark mode: ": "Tumma tila: ",
@ -137,9 +137,9 @@
"Show less": "Näytä vähemmän", "Show less": "Näytä vähemmän",
"Watch on YouTube": "Katso YouTubessa", "Watch on YouTube": "Katso YouTubessa",
"Switch Invidious Instance": "Vaihda Invidious-instanssia", "Switch Invidious Instance": "Vaihda Invidious-instanssia",
"Hide annotations": "Piilota merkkaukset", "Hide annotations": "Piilota huomautukset",
"Show annotations": "Näytä merkkaukset", "Show annotations": "Näytä huomautukset",
"Genre: ": "Genre: ", "Genre: ": "Tyylilaji: ",
"License: ": "Lisenssi: ", "License: ": "Lisenssi: ",
"Family friendly? ": "Kaiken ikäisille sopiva? ", "Family friendly? ": "Kaiken ikäisille sopiva? ",
"Wilson score: ": "Wilson-pistemäärä: ", "Wilson score: ": "Wilson-pistemäärä: ",
@ -168,7 +168,7 @@
"Wrong username or password": "Väärä käyttäjänimi tai salasana", "Wrong username or password": "Väärä käyttäjänimi tai salasana",
"Password cannot be empty": "Salasana ei voi olla tyhjä", "Password cannot be empty": "Salasana ei voi olla tyhjä",
"Password cannot be longer than 55 characters": "Salasana ei voi olla yli 55 merkkiä pitkä", "Password cannot be longer than 55 characters": "Salasana ei voi olla yli 55 merkkiä pitkä",
"Please log in": "Kirjaudu sisään, ole hyvä", "Please log in": "Kirjaudu sisään",
"Invidious Private Feed for `x`": "Invidiousin yksityinen syöte `x`:lle", "Invidious Private Feed for `x`": "Invidiousin yksityinen syöte `x`:lle",
"channel:`x`": "kanava:`x`", "channel:`x`": "kanava:`x`",
"Deleted or invalid channel": "Poistettu tai virheellinen kanava", "Deleted or invalid channel": "Poistettu tai virheellinen kanava",
@ -178,7 +178,7 @@
"`x` ago": "`x` sitten", "`x` ago": "`x` sitten",
"Load more": "Lataa lisää", "Load more": "Lataa lisää",
"Could not create mix.": "Sekoituksen luominen epäonnistui.", "Could not create mix.": "Sekoituksen luominen epäonnistui.",
"Empty playlist": "Tyhjennä soittolista", "Empty playlist": "Tyhjä soittolista",
"Not a playlist.": "Ei ole soittolista.", "Not a playlist.": "Ei ole soittolista.",
"Playlist does not exist.": "Soittolistaa ei ole olemassa.", "Playlist does not exist.": "Soittolistaa ei ole olemassa.",
"Could not pull trending pages.": "Nousussa olevien sivujen lataus epäonnistui.", "Could not pull trending pages.": "Nousussa olevien sivujen lataus epäonnistui.",
@ -216,11 +216,11 @@
"Filipino": "filipino", "Filipino": "filipino",
"Finnish": "suomi", "Finnish": "suomi",
"French": "ranska", "French": "ranska",
"Galician": "galego", "Galician": "galicia",
"Georgian": "georgia", "Georgian": "georgia",
"German": "saksa", "German": "saksa",
"Greek": "kreikka", "Greek": "kreikka",
"Gujarati": "gujarati", "Gujarati": "guarati",
"Haitian Creole": "haitinkreoli", "Haitian Creole": "haitinkreoli",
"Hausa": "hausa", "Hausa": "hausa",
"Hawaiian": "havaiji", "Hawaiian": "havaiji",
@ -327,11 +327,11 @@
"search_filters_duration_label": "Kesto", "search_filters_duration_label": "Kesto",
"search_filters_features_label": "Ominaisuudet", "search_filters_features_label": "Ominaisuudet",
"search_filters_sort_label": "Luokittele", "search_filters_sort_label": "Luokittele",
"search_filters_date_option_hour": "Viimeisin tunti", "search_filters_date_option_hour": "Tunnin sisään",
"search_filters_date_option_today": "Tänään", "search_filters_date_option_today": "Tänään",
"search_filters_date_option_week": "Tämä viikko", "search_filters_date_option_week": "Tällä viikolla",
"search_filters_date_option_month": "Tämä kuukausi", "search_filters_date_option_month": "Tässä kuussa",
"search_filters_date_option_year": "Tämä vuosi", "search_filters_date_option_year": "Tänä vuonna",
"search_filters_type_option_video": "Video", "search_filters_type_option_video": "Video",
"search_filters_type_option_channel": "Kanava", "search_filters_type_option_channel": "Kanava",
"search_filters_type_option_playlist": "Soittolista", "search_filters_type_option_playlist": "Soittolista",
@ -346,7 +346,7 @@
"search_filters_features_option_location": "Sijainti", "search_filters_features_option_location": "Sijainti",
"search_filters_features_option_hdr": "HDR", "search_filters_features_option_hdr": "HDR",
"Current version: ": "Tämänhetkinen versio: ", "Current version: ": "Tämänhetkinen versio: ",
"next_steps_error_message": "Sinun tulisi kokeilla seuraavia: ", "next_steps_error_message": "Kokeile seuraavia: ",
"next_steps_error_message_refresh": "Päivitä", "next_steps_error_message_refresh": "Päivitä",
"next_steps_error_message_go_to_youtube": "Siirry YouTubeen", "next_steps_error_message_go_to_youtube": "Siirry YouTubeen",
"generic_count_hours": "{{count}} tunti", "generic_count_hours": "{{count}} tunti",
@ -391,7 +391,7 @@
"subscriptions_unseen_notifs_count": "{{count}} näkemätön ilmoitus", "subscriptions_unseen_notifs_count": "{{count}} näkemätön ilmoitus",
"subscriptions_unseen_notifs_count_plural": "{{count}} näkemätöntä ilmoitusta", "subscriptions_unseen_notifs_count_plural": "{{count}} näkemätöntä ilmoitusta",
"crash_page_switch_instance": "yrittänyt <a href=\"`x`\">käyttää toista instassia</a>", "crash_page_switch_instance": "yrittänyt <a href=\"`x`\">käyttää toista instassia</a>",
"videoinfo_invidious_embed_link": "Upotuslinkki", "videoinfo_invidious_embed_link": "Upotettava linkki",
"user_saved_playlists": "`x` tallennetua soittolistaa", "user_saved_playlists": "`x` tallennetua soittolistaa",
"crash_page_report_issue": "Jos mikään näistä ei auttanut, <a href=\"`x`\">avaathan uuden issuen GitHubissa</a> (mieluiten englanniksi) ja sisällytät seuraavan tekstin viestissäsi (ÄLÄ käännä tätä tekstiä):", "crash_page_report_issue": "Jos mikään näistä ei auttanut, <a href=\"`x`\">avaathan uuden issuen GitHubissa</a> (mieluiten englanniksi) ja sisällytät seuraavan tekstin viestissäsi (ÄLÄ käännä tätä tekstiä):",
"preferences_quality_option_hd720": "HD720", "preferences_quality_option_hd720": "HD720",
@ -410,7 +410,7 @@
"preferences_quality_dash_option_auto": "Auto", "preferences_quality_dash_option_auto": "Auto",
"preferences_quality_dash_option_best": "Paras", "preferences_quality_dash_option_best": "Paras",
"preferences_quality_option_dash": "DASH (mukautuva laatu)", "preferences_quality_option_dash": "DASH (mukautuva laatu)",
"preferences_quality_dash_label": "Haluttava DASH-videolaatu: ", "preferences_quality_dash_label": "Ensisijainen DASH-videolaatu: ",
"generic_count_years": "{{count}} vuosi", "generic_count_years": "{{count}} vuosi",
"generic_count_years_plural": "{{count}} vuotta", "generic_count_years_plural": "{{count}} vuotta",
"search_filters_features_option_purchased": "Ostettu", "search_filters_features_option_purchased": "Ostettu",
@ -421,39 +421,39 @@
"preferences_save_player_pos_label": "Tallenna toistokohta: ", "preferences_save_player_pos_label": "Tallenna toistokohta: ",
"footer_donate_page": "Lahjoita", "footer_donate_page": "Lahjoita",
"footer_source_code": "Lähdekoodi", "footer_source_code": "Lähdekoodi",
"adminprefs_modified_source_code_url_label": "URL muokattuun lähdekoodirepositoryyn", "adminprefs_modified_source_code_url_label": "URL muokatun lähdekoodin repositorioon",
"Released under the AGPLv3 on Github.": "Julkaistu AGPLv3-lisenssin alla GitHubissa.", "Released under the AGPLv3 on Github.": "Julkaistu AGPLv3-lisenssillä GitHubissa.",
"search_filters_duration_option_short": "Lyhyt (< 4 minuuttia)", "search_filters_duration_option_short": "Lyhyt (< 4 minuuttia)",
"search_filters_duration_option_long": "Pitkä (> 20 minuuttia)", "search_filters_duration_option_long": "Pitkä (> 20 minuuttia)",
"footer_documentation": "Dokumentaatio", "footer_documentation": "Dokumentaatio",
"footer_original_source_code": "Alkuperäinen lähdekoodi", "footer_original_source_code": "Alkuperäinen lähdekoodi",
"footer_modfied_source_code": "Muokattu lähdekoodi", "footer_modfied_source_code": "Muokattu lähdekoodi",
"Japanese (auto-generated)": "Japani (automaattisesti luotu)", "Japanese (auto-generated)": "japani (automaattisesti luotu)",
"German (auto-generated)": "Saksa (automaattisesti luotu)", "German (auto-generated)": "saksa (automaattisesti luotu)",
"Portuguese (auto-generated)": "portugali (automaattisesti luotu)", "Portuguese (auto-generated)": "portugali (automaattisesti luotu)",
"Russian (auto-generated)": "Venäjä (automaattisesti luotu)", "Russian (auto-generated)": "Venäjä (automaattisesti luotu)",
"preferences_watch_history_label": "Ota katseluhistoria käyttöön: ", "preferences_watch_history_label": "Ota katseluhistoria käyttöön: ",
"English (United Kingdom)": "Englanti (Iso-Britannia)", "English (United Kingdom)": "englanti (Iso-Britannia)",
"English (United States)": "Englanti (Yhdysvallat)", "English (United States)": "englanti (Yhdysvallat)",
"Cantonese (Hong Kong)": "Kantoninkiina (Hong Kong)", "Cantonese (Hong Kong)": "kantoninkiina (Hongkong)",
"Chinese": "Kiina", "Chinese": "kiina",
"Chinese (China)": "Kiina (Kiina)", "Chinese (China)": "kiina (Kiina)",
"Chinese (Hong Kong)": "Kiina (Hong Kong)", "Chinese (Hong Kong)": "kiina (Hongkong)",
"Chinese (Taiwan)": "Kiina (Taiwan)", "Chinese (Taiwan)": "kiina (Taiwan)",
"Dutch (auto-generated)": "Hollanti (automaattisesti luotu)", "Dutch (auto-generated)": "hollanti (automaattisesti luotu)",
"French (auto-generated)": "Ranska (automaattisesti luotu)", "French (auto-generated)": "ranska (automaattisesti luotu)",
"Indonesian (auto-generated)": "Indonesia (automaattisesti luotu)", "Indonesian (auto-generated)": "indonesia (automaattisesti luotu)",
"Interlingue": "Interlingue", "Interlingue": "interlingue",
"Italian (auto-generated)": "Italia (automaattisesti luotu)", "Italian (auto-generated)": "Italia (automaattisesti luotu)",
"Korean (auto-generated)": "Korea (automaattisesti luotu)", "Korean (auto-generated)": "korea (automaattisesti luotu)",
"Portuguese (Brazil)": "portugali (Brasilia)", "Portuguese (Brazil)": "portugali (Brasilia)",
"Spanish (auto-generated)": "Espanja (automaattisesti luotu)", "Spanish (auto-generated)": "espanja (automaattisesti luotu)",
"Spanish (Mexico)": "Espanja (Meksiko)", "Spanish (Mexico)": "espanja (Meksiko)",
"Spanish (Spain)": "Espanja (Espanja)", "Spanish (Spain)": "espanja (Espanja)",
"Turkish (auto-generated)": "Turkki (automaattisesti luotu)", "Turkish (auto-generated)": "turkki (automaattisesti luotu)",
"Vietnamese (auto-generated)": "Vietnam (automaattisesti luotu)", "Vietnamese (auto-generated)": "vietnam (automaattisesti luotu)",
"search_filters_title": "Suodatin", "search_filters_title": "Suodattimet",
"search_message_no_results": "Ei tuloksia löydetty.", "search_message_no_results": "Tuloksia ei löytynyt.",
"search_message_change_filters_or_query": "Yritä hakukyselysi laajentamista ja/tai suodattimien muuttamista.", "search_message_change_filters_or_query": "Yritä hakukyselysi laajentamista ja/tai suodattimien muuttamista.",
"search_filters_duration_option_none": "Mikä tahansa kesto", "search_filters_duration_option_none": "Mikä tahansa kesto",
"search_filters_features_option_vr180": "VR180", "search_filters_features_option_vr180": "VR180",
@ -464,5 +464,37 @@
"search_filters_date_option_none": "Milloin tahansa", "search_filters_date_option_none": "Milloin tahansa",
"search_filters_type_option_all": "Mikä tahansa tyyppi", "search_filters_type_option_all": "Mikä tahansa tyyppi",
"Popular enabled: ": "Suosittu käytössä: ", "Popular enabled: ": "Suosittu käytössä: ",
"error_video_not_in_playlist": "Pyydettyä videota ei löydy tästä soittolistasta. <a href=\"`x`\">Klikkaa tähän päästäksesi soittolistan etusivulle.</a>" "error_video_not_in_playlist": "Pyydettyä videota ei ole tässä soittolistassa. <a href=\"`x`\">Klikkaa tästä päästäksesi soittolistan kotisivulle.</a>",
"Import YouTube playlist (.csv)": "Tuo YouTube-soittolista (.csv)",
"Music in this video": "Musiikki tässä videossa",
"Add to playlist": "Lisää soittolistaan",
"Add to playlist: ": "Lisää soittolistaan: ",
"Search for videos": "Etsi videoita",
"generic_button_rss": "RSS",
"Answer": "Vastaus",
"Standard YouTube license": "Vakio YouTube-lisenssi",
"Song: ": "Kappale: ",
"Album: ": "Albumi: ",
"Download is disabled": "Lataus on poistettu käytöstä",
"Channel Sponsor": "Kanavan sponsori",
"channel_tab_podcasts_label": "Podcastit",
"channel_tab_releases_label": "Julkaisut",
"channel_tab_shorts_label": "Shorts-videot",
"carousel_slide": "Dia {{current}}/{{total}}",
"carousel_skip": "Ohita karuselli",
"carousel_go_to": "Siirry diaan `x`",
"channel_tab_playlists_label": "Soittolistat",
"channel_tab_channels_label": "Kanavat",
"generic_button_delete": "Poista",
"generic_button_edit": "Muokkaa",
"generic_button_save": "Tallenna",
"generic_button_cancel": "Peru",
"playlist_button_add_items": "Lisää videoita",
"Artist: ": "Esittäjä: ",
"channel_tab_streams_label": "Suoratoistot",
"generic_channels_count": "{{count}} kanava",
"generic_channels_count_plural": "{{count}} kanavaa",
"The Popular feed has been disabled by the administrator.": "Järjestelmänvalvoja on poistanut Suositut-syötteen.",
"Import YouTube watch history (.json)": "Tuo Youtube-katseluhistoria (.json)",
"toggle_theme": "Vaihda teemaa"
} }

View File

@ -18,7 +18,7 @@
"generic_subscriptions_count_1": "{{count}} d'abonnements", "generic_subscriptions_count_1": "{{count}} d'abonnements",
"generic_subscriptions_count_2": "{{count}} abonnements", "generic_subscriptions_count_2": "{{count}} abonnements",
"generic_button_delete": "Supprimer", "generic_button_delete": "Supprimer",
"generic_button_edit": "Editer", "generic_button_edit": "Modifier",
"generic_button_save": "Enregistrer", "generic_button_save": "Enregistrer",
"generic_button_cancel": "Annuler", "generic_button_cancel": "Annuler",
"generic_button_rss": "RSS", "generic_button_rss": "RSS",
@ -44,7 +44,7 @@
"Import and Export Data": "Importer et exporter des données", "Import and Export Data": "Importer et exporter des données",
"Import": "Importer", "Import": "Importer",
"Import Invidious data": "Importer des données Invidious au format JSON", "Import Invidious data": "Importer des données Invidious au format JSON",
"Import YouTube subscriptions": "Importer des abonnements YouTube/OPML", "Import YouTube subscriptions": "Importer des abonnements YouTube aux formats OPML/CSV",
"Import FreeTube subscriptions (.db)": "Importer des abonnements FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Importer des abonnements FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)",
"Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)", "Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)",
@ -504,5 +504,14 @@
"Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)", "Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)",
"channel_tab_releases_label": "Parutions", "channel_tab_releases_label": "Parutions",
"channel_tab_podcasts_label": "Émissions audio", "channel_tab_podcasts_label": "Émissions audio",
"Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)" "Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)",
"Add to playlist: ": "Ajouter à la playlist: ",
"Add to playlist": "Ajouter à la playlist",
"Answer": "Répondre",
"Search for videos": "Rechercher des vidéos",
"The Popular feed has been disabled by the administrator.": "Le flux populaire a été désactivé par l'administrateur.",
"carousel_skip": "Passez le carrousel",
"carousel_slide": "Diapositive {{current}} sur {{total}}",
"carousel_go_to": "Aller à la diapositive `x`",
"toggle_theme": "Changer le Thème"
} }

View File

@ -464,5 +464,23 @@
"search_filters_features_option_vr180": "180°-os virtuális valóság", "search_filters_features_option_vr180": "180°-os virtuális valóság",
"search_filters_apply_button": "Keresés a megadott szűrőkkel", "search_filters_apply_button": "Keresés a megadott szűrőkkel",
"Popular enabled: ": "Népszerű engedélyezve ", "Popular enabled: ": "Népszerű engedélyezve ",
"error_video_not_in_playlist": "A lejátszási listában keresett videó nem létezik. <a href=\"`x`\">Kattintson ide a lejátszási listához jutáshoz.</a>" "error_video_not_in_playlist": "A lejátszási listában keresett videó nem létezik. <a href=\"`x`\">Kattintson ide a lejátszási listához jutáshoz.</a>",
"generic_button_delete": "Törlés",
"generic_button_rss": "RSS",
"Import YouTube playlist (.csv)": "Youtube lejátszási lista (.csv) importálása",
"Standard YouTube license": "Alap YouTube-licensz",
"Add to playlist": "Hozzáadás lejátszási listához",
"Add to playlist: ": "Hozzáadás a lejátszási listához: ",
"Answer": "Válasz",
"Search for videos": "Keresés videókhoz",
"generic_channels_count": "{{count}} csatorna",
"generic_channels_count_plural": "{{count}} csatornák",
"generic_button_edit": "Szerkesztés",
"generic_button_save": "Mentés",
"generic_button_cancel": "Mégsem",
"playlist_button_add_items": "Videók hozzáadása",
"Music in this video": "Zene ezen videóban",
"Song: ": "Dal: ",
"Album: ": "Album: ",
"Import YouTube watch history (.json)": "Youtube megtekintési előzmények (.json) importálása"
} }

View File

@ -1,39 +1,39 @@
{ {
"LIVE": "BEINT", "LIVE": "BEINT",
"Shared `x` ago": "Deilt `x` síðan", "Shared `x` ago": "Deilt fyrir `x` síðan",
"Unsubscribe": "Afskrá", "Unsubscribe": "Afskrá",
"Subscribe": "Áskrifa", "Subscribe": "Áskrifa",
"View channel on YouTube": "Skoða rás á YouTube", "View channel on YouTube": "Skoða rás á YouTube",
"View playlist on YouTube": "Skoða spilunarlisti á YouTube", "View playlist on YouTube": "Skoða spilunarlista á YouTube",
"newest": "nýjasta", "newest": "nýjasta",
"oldest": "elsta", "oldest": "elsta",
"popular": "vinsælt", "popular": "vinsælt",
"last": "síðast", "last": "síðast",
"Next page": "Næsta síða", "Next page": "Næsta síða",
"Previous page": "Fyrri síða", "Previous page": "Fyrri síða",
"Clear watch history?": "Hreinsa áhorfssögu?", "Clear watch history?": "Hreinsa áhorfsferil?",
"New password": "Nýtt lykilorð", "New password": "Nýtt lykilorð",
"New passwords must match": "Nýtt lykilorð verður að passa", "New passwords must match": "Nýtt lykilorð verður að passa",
"Authorize token?": "Leyfa tákn?", "Authorize token?": "Leyfa teikn?",
"Authorize token for `x`?": "Leyfa tákn fyrir `x`?", "Authorize token for `x`?": "Leyfa teikn fyrir `x`?",
"Yes": "Já", "Yes": "Já",
"No": "Nei", "No": "Nei",
"Import and Export Data": "Innflutningur og Útflutningur Gagna", "Import and Export Data": "Inn- og útflutningur gagna",
"Import": "Flytja inn", "Import": "Flytja inn",
"Import Invidious data": "Flytja inn Invidious gögn", "Import Invidious data": "Flytja inn Invidious JSON-gögn",
"Import YouTube subscriptions": "Flytja inn YouTube áskriftir", "Import YouTube subscriptions": "Flytja inn YouTube CSV eða OPML-áskriftir",
"Import FreeTube subscriptions (.db)": "Flytja inn FreeTube áskriftir (.db)", "Import FreeTube subscriptions (.db)": "Flytja inn FreeTube áskriftir (.db)",
"Import NewPipe subscriptions (.json)": "Flytja inn NewPipe áskriftir (.json)", "Import NewPipe subscriptions (.json)": "Flytja inn NewPipe áskriftir (.json)",
"Import NewPipe data (.zip)": "Flytja inn NewPipe gögn (.zip)", "Import NewPipe data (.zip)": "Flytja inn NewPipe gögn (.zip)",
"Export": "Flytja út", "Export": "Flytja út",
"Export subscriptions as OPML": "Flytja út áskriftir sem OPML", "Export subscriptions as OPML": "Flytja út áskriftir sem OPML",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Flytja út áskriftir sem OPML (fyrir NewPipe & FreeTube)", "Export subscriptions as OPML (for NewPipe & FreeTube)": "Flytja út áskriftir sem OPML (fyrir NewPipe & FreeTube)",
"Export data as JSON": "Flytja út gögn sem JSON", "Export data as JSON": "Flytja út Invidious-gögn sem JSON",
"Delete account?": "Eyða reikningi?", "Delete account?": "Eyða reikningi?",
"History": "Saga", "History": "Ferill",
"An alternative front-end to YouTube": "Önnur framhlið fyrir YouTube", "An alternative front-end to YouTube": "Annað viðmót fyrir YouTube",
"JavaScript license information": "JavaScript leyfi upplýsingar", "JavaScript license information": "Upplýsingar um notkunarleyfi JavaScript",
"source": "uppspretta", "source": "uppruni",
"Log in": "Skrá inn", "Log in": "Skrá inn",
"Log in/register": "Innskráning/nýskráning", "Log in/register": "Innskráning/nýskráning",
"User ID": "Notandakenni", "User ID": "Notandakenni",
@ -47,33 +47,33 @@
"Preferences": "Kjörstillingar", "Preferences": "Kjörstillingar",
"preferences_category_player": "Kjörstillingar spilara", "preferences_category_player": "Kjörstillingar spilara",
"preferences_video_loop_label": "Alltaf lykkja: ", "preferences_video_loop_label": "Alltaf lykkja: ",
"preferences_autoplay_label": "Spila sjálfkrafa: ", "preferences_autoplay_label": "Sjálfvirk spilun: ",
"preferences_continue_label": "Spila næst sjálfgefið: ", "preferences_continue_label": "Spila næst sjálfgefið: ",
"preferences_continue_autoplay_label": "Spila næst sjálfkrafa: ", "preferences_continue_autoplay_label": "Spila næsta myndskeið sjálfkrafa: ",
"preferences_listen_label": "Hlusta sjálfgefið: ", "preferences_listen_label": "Hlusta sjálfgefið: ",
"preferences_local_label": "Proxy myndbönd? ", "preferences_local_label": "Milliþjónn fyrir myndskeið: ",
"preferences_speed_label": "Sjálfgefinn hraði: ", "preferences_speed_label": "Sjálfgefinn hraði: ",
"preferences_quality_label": "Æskilegt myndbands gæði: ", "preferences_quality_label": "Æskileg gæði myndmerkis: ",
"preferences_volume_label": "Spilara hljóðstyrkur: ", "preferences_volume_label": "Spilara hljóðstyrkur: ",
"preferences_comments_label": "Sjálfgefin ummæli: ", "preferences_comments_label": "Sjálfgefin ummæli: ",
"youtube": "YouTube", "youtube": "YouTube",
"reddit": "reddit", "reddit": "Reddit",
"preferences_captions_label": "Sjálfgefin texti: ", "preferences_captions_label": "Sjálfgefin texti: ",
"Fallback captions: ": "Varatextar: ", "Fallback captions: ": "Varatextar: ",
"preferences_related_videos_label": "Sýna tengd myndbönd? ", "preferences_related_videos_label": "Sýna tengd myndskeið? ",
"preferences_annotations_label": "Á að sýna glósur sjálfgefið? ", "preferences_annotations_label": "Á að sýna glósur sjálfgefið? ",
"preferences_category_visual": "Sjónrænar stillingar", "preferences_category_visual": "Sjónrænar stillingar",
"preferences_player_style_label": "Spilara stíl: ", "preferences_player_style_label": "Stíll spilara: ",
"Dark mode: ": "Myrkur ham: ", "Dark mode: ": "Dökkur hamur: ",
"preferences_dark_mode_label": "Þema: ", "preferences_dark_mode_label": "Þema: ",
"dark": "dimmt", "dark": "dökkt",
"light": "ljóst", "light": "ljóst",
"preferences_thin_mode_label": "Þunnt ham: ", "preferences_thin_mode_label": "Grannur hamur: ",
"preferences_category_subscription": "Áskriftarstillingar", "preferences_category_subscription": "Áskriftarstillingar",
"preferences_annotations_subscribed_label": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ", "preferences_annotations_subscribed_label": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ",
"Redirect homepage to feed: ": "Endurbeina heimasíðu að straumi: ", "Redirect homepage to feed: ": "Endurbeina heimasíðu að streymi: ",
"preferences_max_results_label": "Fjöldi myndbanda sem sýndir eru í straumi: ", "preferences_max_results_label": "Fjöldi myndskeiða sem sýnd eru í streymi: ",
"preferences_sort_label": "Raða myndbönd eftir: ", "preferences_sort_label": "Raða myndskeiðum eftir: ",
"published": "birt", "published": "birt",
"published - reverse": "birt - afturábak", "published - reverse": "birt - afturábak",
"alphabetically": "í stafrófsröð", "alphabetically": "í stafrófsröð",
@ -88,31 +88,31 @@
"`x` uploaded a video": "`x` hlóð upp myndband", "`x` uploaded a video": "`x` hlóð upp myndband",
"`x` is live": "`x` er í beinni", "`x` is live": "`x` er í beinni",
"preferences_category_data": "Gagnastillingar", "preferences_category_data": "Gagnastillingar",
"Clear watch history": "Hreinsa áhorfssögu", "Clear watch history": "Hreinsa áhorfsferil",
"Import/export data": "Flytja inn/út gögn", "Import/export data": "Flytja inn/út gögn",
"Change password": "Breyta lykilorði", "Change password": "Breyta lykilorði",
"Manage subscriptions": "Stjórna áskriftum", "Manage subscriptions": "Sýsla með áskriftir",
"Manage tokens": "Stjórna tákn", "Manage tokens": "Sýsla með teikn",
"Watch history": "Áhorfssögu", "Watch history": "Áhorfsferill",
"Delete account": "Eyða reikningi", "Delete account": "Eyða reikningi",
"preferences_category_admin": "Kjörstillingar stjórnanda", "preferences_category_admin": "Kjörstillingar stjórnanda",
"preferences_default_home_label": "Sjálfgefin heimasíða: ", "preferences_default_home_label": "Sjálfgefin heimasíða: ",
"preferences_feed_menu_label": "Straum valmynd: ", "preferences_feed_menu_label": "Streymisvalmynd: ",
"Top enabled: ": "Toppur virkur? ", "Top enabled: ": "Vinsælast virkt? ",
"CAPTCHA enabled: ": "CAPTCHA virk? ", "CAPTCHA enabled: ": "CAPTCHA virk? ",
"Login enabled: ": "Innskráning virk? ", "Login enabled: ": "Innskráning virk? ",
"Registration enabled: ": "Nýskráning virkjuð? ", "Registration enabled: ": "Nýskráning virkjuð? ",
"Report statistics: ": "Skrá talnagögn? ", "Report statistics: ": "Skrá tölfræði? ",
"Save preferences": "Vista stillingar", "Save preferences": "Vista stillingar",
"Subscription manager": "Áskriftarstjóri", "Subscription manager": "Áskriftarstjóri",
"Token manager": "Táknstjóri", "Token manager": "Teiknastjórnun",
"Token": "Tákn", "Token": "Teikn",
"Import/export": "Flytja inn/út", "Import/export": "Flytja inn/út",
"unsubscribe": "afskrá", "unsubscribe": "afskrá",
"revoke": "afturkalla", "revoke": "afturkalla",
"Subscriptions": "Áskriftir", "Subscriptions": "Áskriftir",
"search": "leita", "search": "leita",
"Log out": "Útskrá", "Log out": "Skrá út",
"Source available here.": "Frumkóði aðgengilegur hér.", "Source available here.": "Frumkóði aðgengilegur hér.",
"View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.", "View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.",
"View privacy policy.": "Skoða meðferð persónuupplýsinga.", "View privacy policy.": "Skoða meðferð persónuupplýsinga.",
@ -122,13 +122,13 @@
"Private": "Einka", "Private": "Einka",
"View all playlists": "Skoða alla spilunarlista", "View all playlists": "Skoða alla spilunarlista",
"Updated `x` ago": "Uppfært `x` síðann", "Updated `x` ago": "Uppfært `x` síðann",
"Delete playlist `x`?": "Eiða spilunarlista `x`?", "Delete playlist `x`?": "Eyða spilunarlista `x`?",
"Delete playlist": "Eiða spilunarlista", "Delete playlist": "Eyða spilunarlista",
"Create playlist": "Búa til spilunarlista", "Create playlist": "Búa til spilunarlista",
"Title": "Titill", "Title": "Titill",
"Playlist privacy": "Spilunarlista opinberri", "Playlist privacy": "Friðhelgi spilunarlista",
"Editing playlist `x`": "Að breyta spilunarlista `x`", "Editing playlist `x`": "Breyti spilunarlista `x`",
"Watch on YouTube": "Horfa á YouTube", "Watch on YouTube": "Skoða á YouTube",
"Hide annotations": "Fela glósur", "Hide annotations": "Fela glósur",
"Show annotations": "Sýna glósur", "Show annotations": "Sýna glósur",
"Genre: ": "Tegund: ", "Genre: ": "Tegund: ",
@ -160,26 +160,26 @@
"Wrong username or password": "Rangt notandanafn eða lykilorð", "Wrong username or password": "Rangt notandanafn eða lykilorð",
"Password cannot be empty": "Lykilorð má ekki vera autt", "Password cannot be empty": "Lykilorð má ekki vera autt",
"Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir", "Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir",
"Please log in": "Vinsamlegast skráðu þig inn", "Please log in": "Skráðu þig inn",
"Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`", "Invidious Private Feed for `x`": "Persónulegt Invidious-streymi fyrir `x`",
"channel:`x`": "rás:`x`", "channel:`x`": "rás:`x`",
"Deleted or invalid channel": "Eytt eða ógild rás", "Deleted or invalid channel": "Eytt eða ógild rás",
"This channel does not exist.": "Þessi rás er ekki til.", "This channel does not exist.": "Þessi rás er ekki til.",
"Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.", "Could not get channel info.": "Ekki tókst að fá upplýsingar um rásina.",
"Could not fetch comments": "Ekki tókst að sækja ummæli", "Could not fetch comments": "Ekki tókst að sækja ummæli",
"`x` ago": "`x` síðan", "`x` ago": "`x` síðan",
"Load more": "Hlaða meira", "Load more": "Hlaða meira",
"Could not create mix.": "Ekki tókst að búa til blöndu.", "Could not create mix.": "Ekki tókst að búa til blöndu.",
"Empty playlist": "Tómur spilunarlisti", "Empty playlist": "Tómur spilunarlisti",
"Not a playlist.": "Ekki spilunarlisti.", "Not a playlist.": "Er ekki spilunarlisti.",
"Playlist does not exist.": "Spilunarlisti er ekki til.", "Playlist does not exist.": "Spilunarlisti er ekki til.",
"Could not pull trending pages.": "Ekki tókst að draga vinsælar síður.", "Could not pull trending pages.": "Ekki tókst að draga vinsælar síður.",
"Hidden field \"challenge\" is a required field": "Falinn reitur \"áskorun\" er nauðsynlegur reitur", "Hidden field \"challenge\" is a required field": "Falinn reitur \"áskorun\" er nauðsynlegur reitur",
"Hidden field \"token\" is a required field": "Falinn reitur \"tákn\" er nauðsynlegur reitur", "Hidden field \"token\" is a required field": "Falinn reitur \"teikn\" er nauðsynlegur reitur",
"Erroneous challenge": "Röng áskorun", "Erroneous challenge": "Röng áskorun",
"Erroneous token": "Rangt tákn", "Erroneous token": "Rangt teikn",
"No such user": "Enginn slíkur notandi", "No such user": "Enginn slíkur notandi",
"Token is expired, please try again": "Tákn er útrunnið, vinsamlegast reyndu aftur", "Token is expired, please try again": "Teiknið er útrunnið, reyndu aftur",
"English": "Enska", "English": "Enska",
"English (auto-generated)": "Enska (sjálfkrafa)", "English (auto-generated)": "Enska (sjálfkrafa)",
"Afrikaans": "Afríkanska", "Afrikaans": "Afríkanska",
@ -267,14 +267,14 @@
"Somali": "Sómalska", "Somali": "Sómalska",
"Southern Sotho": "Suður Sótó", "Southern Sotho": "Suður Sótó",
"Spanish": "Spænska", "Spanish": "Spænska",
"Spanish (Latin America)": "Spænska (Rómönsku Ameríka)", "Spanish (Latin America)": "Spænska (Rómanska Ameríka)",
"Sundanese": "Sundaneska", "Sundanese": "Sundaneska",
"Swahili": "Svahílí", "Swahili": "Svahílí",
"Swedish": "Sænska", "Swedish": "Sænska",
"Tajik": "Tadsikíska", "Tajik": "Tadsikíska",
"Tamil": "Tamílska", "Tamil": "Tamílska",
"Telugu": "Telúgú", "Telugu": "Telúgú",
"Thai": "Tlenska", "Thai": "Tælenska",
"Turkish": "Tyrkneska", "Turkish": "Tyrkneska",
"Ukrainian": "Úkraníska", "Ukrainian": "Úkraníska",
"Urdu": "Úrdú", "Urdu": "Úrdú",
@ -286,9 +286,9 @@
"Yiddish": "Jiddíska", "Yiddish": "Jiddíska",
"Yoruba": "Jórúba", "Yoruba": "Jórúba",
"Zulu": "Zúlú", "Zulu": "Zúlú",
"Fallback comments: ": "Vara ummæli: ", "Fallback comments: ": "Ummæli til vara: ",
"Popular": "Vinsælt", "Popular": "Vinsælt",
"Top": "Topp", "Top": "Vinsælast",
"About": "Um", "About": "Um",
"Rating: ": "Einkunn: ", "Rating: ": "Einkunn: ",
"preferences_locale_label": "Tungumál: ", "preferences_locale_label": "Tungumál: ",
@ -307,9 +307,194 @@
"`x` marked it with a ❤": "`x` merkti það með ❤", "`x` marked it with a ❤": "`x` merkti það með ❤",
"Audio mode": "Hljóð ham", "Audio mode": "Hljóð ham",
"Video mode": "Myndband ham", "Video mode": "Myndband ham",
"channel_tab_videos_label": "Myndbönd", "channel_tab_videos_label": "Myndskeið",
"Playlists": "Spilunarlistar", "Playlists": "Spilunarlistar",
"channel_tab_community_label": "Samfélag", "channel_tab_community_label": "Samfélag",
"Current version: ": "Núverandi útgáfa: ", "Current version: ": "Núverandi útgáfa: ",
"preferences_watch_history_label": "Virkja áhorfssögu: " "preferences_watch_history_label": "Virkja áhorfsferil: ",
"Chinese (China)": "Kínverska (Kína)",
"Turkish (auto-generated)": "Tyrkneska (sjálfvirkt útbúið)",
"Search": "Leita",
"preferences_save_player_pos_label": "Vista staðsetningu í afspilun: ",
"Popular enabled: ": "Vinsælt virkjað: ",
"search_filters_features_option_purchased": "Keypt",
"Standard YouTube license": "Staðlað YouTube-notkunarleyfi",
"French (auto-generated)": "Franska (sjálfvirkt útbúið)",
"Spanish (Spain)": "Spænska (Spánn)",
"search_filters_title": "Síur",
"search_filters_date_label": "Dags. innsendingar",
"search_filters_features_option_four_k": "4K",
"search_filters_features_option_hd": "HD",
"crash_page_read_the_faq": "lesið <a href=\"`x`\">Algengar spurningar (FAQ)</a>",
"Add to playlist": "Bæta á spilunarlista",
"Add to playlist: ": "Bæta á spilunarlista: ",
"Answer": "Svar",
"Search for videos": "Leita að myndskeiðum",
"generic_channels_count": "{{count}} rás",
"generic_channels_count_plural": "{{count}} rásir",
"generic_videos_count": "{{count}} myndskeið",
"generic_videos_count_plural": "{{count}} myndskeið",
"The Popular feed has been disabled by the administrator.": "Kerfisstjórinn hefur gert Vinsælt-streymið óvirkt.",
"generic_playlists_count": "{{count}} spilunarlisti",
"generic_playlists_count_plural": "{{count}} spilunarlistar",
"generic_subscribers_count": "{{count}} áskrifandi",
"generic_subscribers_count_plural": "{{count}} áskrifendur",
"generic_subscriptions_count": "{{count}} áskrift",
"generic_subscriptions_count_plural": "{{count}} áskriftir",
"generic_button_delete": "Eyða",
"Import YouTube watch history (.json)": "Flytja inn YouTube áhorfsferil (.json)",
"preferences_vr_mode_label": "Gagnvirk 360 gráðu myndskeið (krefst WebGL): ",
"preferences_quality_dash_option_auto": "Sjálfvirkt",
"preferences_quality_dash_option_best": "Best",
"preferences_quality_dash_option_worst": "Verst",
"preferences_quality_dash_label": "Æskileg DASH-gæði myndmerkis: ",
"preferences_extend_desc_label": "Sjálfvirkt útvíkka lýsingu á myndskeiði: ",
"preferences_region_label": "Land efnis: ",
"preferences_show_nick_label": "Birta gælunafn efst: ",
"tokens_count": "{{count}} teikn",
"tokens_count_plural": "{{count}} teikn",
"subscriptions_unseen_notifs_count": "{{count}} óskoðuð tilkynning",
"subscriptions_unseen_notifs_count_plural": "{{count}} óskoðaðar tilkynningar",
"Released under the AGPLv3 on Github.": "Gefið út með AGPLv3-notkunarleyfi á GitHub.",
"Music in this video": "Tónlist í þessu myndskeiði",
"Artist: ": "Flytjandi: ",
"Album: ": "Hljómplata: ",
"comments_view_x_replies": "Skoða {{count}} svar",
"comments_view_x_replies_plural": "Skoða {{count}} svör",
"comments_points_count": "{{count}} punktur",
"comments_points_count_plural": "{{count}} punktar",
"Cantonese (Hong Kong)": "Kantónska (Hong Kong)",
"Chinese": "Kínverska",
"Chinese (Hong Kong)": "Kínverska (Hong Kong)",
"Chinese (Taiwan)": "Kínverska (Taívan)",
"Japanese (auto-generated)": "Japanska (sjálfvirkt útbúið)",
"generic_count_minutes": "{{count}} mínúta",
"generic_count_minutes_plural": "{{count}} mínútur",
"generic_count_seconds": "{{count}} sekúnda",
"generic_count_seconds_plural": "{{count}} sekúndur",
"search_filters_date_option_hour": "Síðustu klukkustund",
"search_filters_apply_button": "Virkja valdar síur",
"next_steps_error_message_go_to_youtube": "Fara á YouTube",
"footer_original_source_code": "Upprunalegur grunnkóði",
"videoinfo_started_streaming_x_ago": "Byrjaði streymi fyrir `x` síðan",
"next_steps_error_message": "Á eftir þessu ættirðu að prófa: ",
"videoinfo_invidious_embed_link": "Ívefja tengil",
"download_subtitles": "Skjátextar - `x` (.vtt)",
"user_created_playlists": "`x` útbjó spilunarlista",
"user_saved_playlists": "`x` vistaði spilunarlista",
"Video unavailable": "Myndskeið ekki tiltækt",
"videoinfo_watch_on_youTube": "Skoða á YouTube",
"crash_page_you_found_a_bug": "Það lítur út eins og þú hafir fundið galla í Invidious!",
"crash_page_before_reporting": "Áður en þú tilkynnir villu, gakktu úr skugga um að þú hafir:",
"crash_page_switch_instance": "reynt að <a href=\"`x`\">nota annað tilvik</a>",
"crash_page_report_issue": "Ef ekkert af ofantöldu hjálpaði, ættirðu að <a href=\"`x`\">opna nýja verkbeiðni (issue) á GitHub</a> (helst á ensku) og láta fylgja eftirfarandi texta í skilaboðunum þínum (alls EKKI þýða þennan texta):",
"channel_tab_shorts_label": "Stuttmyndir",
"carousel_slide": "Skyggna {{current}} af {{total}}",
"carousel_go_to": "Fara á skyggnu `x`",
"channel_tab_streams_label": "Bein streymi",
"channel_tab_playlists_label": "Spilunarlistar",
"toggle_theme": "Víxla þema",
"carousel_skip": "Sleppa hringekjunni",
"preferences_quality_option_medium": "Miðlungs",
"search_message_use_another_instance": " Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.",
"footer_source_code": "Grunnkóði",
"English (United Kingdom)": "Enska (Bretland)",
"English (United States)": "Enska (Bandarísk)",
"Vietnamese (auto-generated)": "Víetnamska (sjálfvirkt útbúið)",
"generic_count_months": "{{count}} mánuður",
"generic_count_months_plural": "{{count}} mánuðir",
"search_filters_sort_option_rating": "Einkunn",
"videoinfo_youTube_embed_link": "Ívefja",
"error_video_not_in_playlist": "Umbeðið myndskeið fyrirfinnst ekki í þessum spilunarlista. <a href=\"`x`\">Smelltu hér til að fara á heimasíðu spilunarlistans.</a>",
"generic_views_count": "{{count}} áhorf",
"generic_views_count_plural": "{{count}} áhorf",
"playlist_button_add_items": "Bæta við myndskeiðum",
"Show more": "Sýna meira",
"Show less": "Sýna minna",
"Song: ": "Lag: ",
"channel_tab_podcasts_label": "Hlaðvörp (podcasts)",
"channel_tab_releases_label": "Útgáfur",
"Download is disabled": "Niðurhal er óvirkt",
"search_filters_features_option_location": "Staðsetning",
"preferences_quality_dash_option_720p": "720p",
"Switch Invidious Instance": "Skipta um Invidious-tilvik",
"search_message_no_results": "Engar niðurstöður fundust.",
"search_message_change_filters_or_query": "Reyndu að víkka leitarsviðið og/eða breyta síunum.",
"Dutch (auto-generated)": "Hollenska (sjálfvirkt útbúið)",
"German (auto-generated)": "Þýska (sjálfvirkt útbúið)",
"Indonesian (auto-generated)": "Indónesíska (sjálfvirkt útbúið)",
"Interlingue": "Interlingue",
"Italian (auto-generated)": "Ítalska (sjálfvirkt útbúið)",
"Russian (auto-generated)": "Rússneska (sjálfvirkt útbúið)",
"Spanish (auto-generated)": "Spænska (sjálfvirkt útbúið)",
"Spanish (Mexico)": "Spænska (Mexíkó)",
"generic_count_hours": "{{count}} klukkustund",
"generic_count_hours_plural": "{{count}} klukkustundir",
"generic_count_years": "{{count}} ár",
"generic_count_years_plural": "{{count}} ár",
"generic_count_weeks": "{{count}} vika",
"generic_count_weeks_plural": "{{count}} vikur",
"search_filters_date_option_none": "Hvaða dagsetning sem er",
"Channel Sponsor": "Styrktaraðili rásar",
"search_filters_date_option_week": "Í þessari viku",
"search_filters_date_option_month": "Í þessum mánuði",
"search_filters_date_option_year": "Á þessu ári",
"search_filters_type_option_playlist": "Spilunarlisti",
"search_filters_type_option_show": "Þáttur",
"search_filters_duration_label": "Tímalengd",
"search_filters_duration_option_long": "Langt (> 20 mínútur)",
"search_filters_features_option_live": "Beint",
"search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_vr180": "VR180",
"search_filters_features_option_three_d": "3D",
"search_filters_features_option_hdr": "HDR",
"search_filters_sort_label": "Raða eftir",
"search_filters_sort_option_relevance": "Samsvörun",
"footer_donate_page": "Styrkja",
"footer_modfied_source_code": "Breyttur grunnkóði",
"crash_page_refresh": "reynt að <a href=\"`x`\">endurlesa síðuna</a>",
"crash_page_search_issue": "leitað að <a href=\"`x`\">fyrirliggjandi villum á GitHub</a>",
"none": "ekkert",
"adminprefs_modified_source_code_url_label": "Slóð á gagnasafn með breyttum grunnkóða",
"preferences_quality_option_hd720": "HD720",
"preferences_quality_option_small": "Lítið",
"preferences_category_misc": "Ýmsar kjörstillingar",
"preferences_automatic_instance_redirect_label": "Sjálfvirk endurbeining tilvika (farið til vara á redirect.invidious.io): ",
"Portuguese (auto-generated)": "Portúgalska (sjálfvirkt útbúið)",
"Portuguese (Brazil)": "Portúgalska (Brasilía)",
"generic_button_edit": "Breyta",
"generic_button_save": "Vista",
"generic_button_cancel": "Hætta við",
"generic_button_rss": "RSS",
"preferences_quality_dash_option_4320p": "4320p",
"preferences_quality_dash_option_2160p": "2160p",
"preferences_quality_dash_option_1440p": "1440p",
"preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_480p": "480p",
"preferences_quality_dash_option_360p": "360p",
"preferences_quality_dash_option_240p": "240p",
"preferences_quality_dash_option_144p": "144p",
"invidious": "Invidious",
"Korean (auto-generated)": "Kóreska (sjálfvirkt útbúið)",
"generic_count_days": "{{count}} dagur",
"generic_count_days_plural": "{{count}} dagar",
"search_filters_date_option_today": "Í dag",
"search_filters_type_label": "Tegund",
"search_filters_type_option_all": "Hvaða tegund sem er",
"search_filters_type_option_video": "Myndskeið",
"search_filters_type_option_channel": "Rás",
"search_filters_type_option_movie": "Kvikmynd",
"search_filters_duration_option_none": "Hvaða lengd sem er",
"search_filters_duration_option_short": "Stutt (< 4 mínútur)",
"search_filters_duration_option_medium": "Miðlungs (4 - 20 mínútur)",
"search_filters_features_label": "Eiginleikar",
"search_filters_features_option_subtitles": "Skjátextar/CC",
"search_filters_features_option_c_commons": "Creative Commons",
"search_filters_sort_option_date": "Dags. innsendingar",
"search_filters_sort_option_views": "Fjöldi áhorfa",
"next_steps_error_message_refresh": "Endurlesa",
"footer_documentation": "Leiðbeiningar",
"channel_tab_channels_label": "Rásir",
"Import YouTube playlist (.csv)": "Flytja inn YouTube spilunarlista (.csv)",
"preferences_quality_option_dash": "DASH (aðlaganleg gæði)"
} }

View File

@ -30,7 +30,7 @@
"Import and Export Data": "Importazione ed esportazione dati", "Import and Export Data": "Importazione ed esportazione dati",
"Import": "Importa", "Import": "Importa",
"Import Invidious data": "Importa dati Invidious in formato JSON", "Import Invidious data": "Importa dati Invidious in formato JSON",
"Import YouTube subscriptions": "Importa le iscrizioni da YouTube/OPML", "Import YouTube subscriptions": "Importa iscrizioni in CSV o OPML di YouTube",
"Import FreeTube subscriptions (.db)": "Importa le iscrizioni da FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Importa le iscrizioni da FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Importa le iscrizioni da NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importa le iscrizioni da NewPipe (.json)",
"Import NewPipe data (.zip)": "Importa i dati di NewPipe (.zip)", "Import NewPipe data (.zip)": "Importa i dati di NewPipe (.zip)",

View File

@ -12,14 +12,14 @@
"Dark mode: ": "다크 모드: ", "Dark mode: ": "다크 모드: ",
"preferences_player_style_label": "플레이어 스타일: ", "preferences_player_style_label": "플레이어 스타일: ",
"preferences_category_visual": "환경 설정", "preferences_category_visual": "환경 설정",
"preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ", "preferences_vr_mode_label": "360도 영상 활성화 (WebGL 필요): ",
"preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ", "preferences_extend_desc_label": "자동으로 비디오 설명 펼치기: ",
"preferences_annotations_label": "기본으로 주석 표시: ", "preferences_annotations_label": "기본으로 주석 표시: ",
"preferences_related_videos_label": "관련 동영상 보기: ", "preferences_related_videos_label": "관련 동영상 보기: ",
"Fallback captions: ": "대체 자막: ", "Fallback captions: ": "대체 자막: ",
"preferences_captions_label": "기본 자막: ", "preferences_captions_label": "기본 자막: ",
"reddit": "레딧", "reddit": "Reddit",
"youtube": "유튜브", "youtube": "YouTube",
"preferences_comments_label": "기본 댓글: ", "preferences_comments_label": "기본 댓글: ",
"preferences_volume_label": "플레이어 볼륨: ", "preferences_volume_label": "플레이어 볼륨: ",
"preferences_quality_label": "선호하는 비디오 품질: ", "preferences_quality_label": "선호하는 비디오 품질: ",
@ -65,23 +65,23 @@
"Authorize token?": "토큰을 승인하시겠습니까?", "Authorize token?": "토큰을 승인하시겠습니까?",
"New passwords must match": "새 비밀번호는 일치해야 합니다", "New passwords must match": "새 비밀번호는 일치해야 합니다",
"New password": "새 비밀번호", "New password": "새 비밀번호",
"Clear watch history?": "재생 기록을 삭제 하시겠습니까?", "Clear watch history?": "시청 기록을 지우시겠습니까?",
"Previous page": "이전 페이지", "Previous page": "이전 페이지",
"Next page": "다음 페이지", "Next page": "다음 페이지",
"last": "마지막", "last": "마지막",
"Shared `x` ago": "`x` 전", "Shared `x` ago": "`x` 전",
"popular": "인기", "popular": "인기",
"oldest": "오래된순", "oldest": "과거순",
"newest": "최신순", "newest": "최신순",
"View playlist on YouTube": "유튜브에서 재생목록 보기", "View playlist on YouTube": "유튜브에서 재생목록 보기",
"View channel on YouTube": "유튜브에서 채널 보기", "View channel on YouTube": "유튜브에서 채널 보기",
"Subscribe": "구독", "Subscribe": "구독",
"Unsubscribe": "구독 취소", "Unsubscribe": "구독 취소",
"LIVE": "실시간", "LIVE": "실시간",
"generic_views_count_0": "{{count}}", "generic_views_count_0": "조회수 {{count}}회",
"generic_videos_count_0": "{{count}} 동영상", "generic_videos_count_0": "동영상 {{count}}개",
"generic_playlists_count_0": "{{count}} 재생목록", "generic_playlists_count_0": "재생목록 {{count}}개",
"generic_subscribers_count_0": "{{count}} 구독자", "generic_subscribers_count_0": "구독자 {{count}}명",
"generic_subscriptions_count_0": "{{count}} 구독", "generic_subscriptions_count_0": "{{count}} 구독",
"search_filters_type_option_playlist": "재생목록", "search_filters_type_option_playlist": "재생목록",
"Korean": "한국어", "Korean": "한국어",
@ -109,23 +109,23 @@
"This channel does not exist.": "이 채널은 존재하지 않습니다.", "This channel does not exist.": "이 채널은 존재하지 않습니다.",
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널", "Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
"channel:`x`": "채널:`x`", "channel:`x`": "채널:`x`",
"Show replies": "댓글 보기", "Show replies": "댓글 보기",
"Hide replies": "댓글 숨기기", "Hide replies": "댓글 숨기기",
"Incorrect password": "잘못된 비밀번호", "Incorrect password": "잘못된 비밀번호",
"License: ": "라이선스: ", "License: ": "라이선스: ",
"Genre: ": "장르: ", "Genre: ": "장르: ",
"Editing playlist `x`": "재생목록 `x` 수정하기", "Editing playlist `x`": "재생목록 `x` 수정하기",
"Playlist privacy": "재생목록 공개 범위", "Playlist privacy": "재생목록 공개 범위",
"Watch on YouTube": "유튜브에서 보기", "Watch on YouTube": "YouTube에서 보기",
"Show less": "간략히", "Show less": "간략히",
"Show more": "더보기", "Show more": "더보기",
"Title": "제목", "Title": "제목",
"Create playlist": "재생목록 생성", "Create playlist": "재생목록 생성",
"Trending": "급상승", "Trending": "급상승",
"Delete playlist": "재생목록 삭제", "Delete playlist": "재생목록 삭제",
"Delete playlist `x`?": "재생목록 `x` 를 삭제 하시겠습니까?", "Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?",
"Updated `x` ago": "`x` 전에 업데이트됨", "Updated `x` ago": "`x` 전에 업데이트됨",
"Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.", "Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.",
"View all playlists": "모든 재생목록 보기", "View all playlists": "모든 재생목록 보기",
"Private": "비공개", "Private": "비공개",
"Unlisted": "목록에 없음", "Unlisted": "목록에 없음",
@ -135,12 +135,12 @@
"Source available here.": "소스는 여기에서 사용할 수 있습니다.", "Source available here.": "소스는 여기에서 사용할 수 있습니다.",
"Log out": "로그아웃", "Log out": "로그아웃",
"search": "검색", "search": "검색",
"subscriptions_unseen_notifs_count_0": "{{count}} 읽지 않은 알림", "subscriptions_unseen_notifs_count_0": "읽지 않은 알림 {{count}}개",
"Subscriptions": "구독", "Subscriptions": "구독",
"revoke": "철회", "revoke": "철회",
"unsubscribe": "구독 취소", "unsubscribe": "구독 취소",
"Import/export": "가져오기/내보내기", "Import/export": "가져오기/내보내기",
"tokens_count_0": "{{count}} 토큰", "tokens_count_0": "토큰 {{count}}개",
"Token": "토큰", "Token": "토큰",
"Token manager": "토큰 관리자", "Token manager": "토큰 관리자",
"Subscription manager": "구독 관리자", "Subscription manager": "구독 관리자",
@ -163,7 +163,7 @@
"Clear watch history": "시청 기록 지우기", "Clear watch history": "시청 기록 지우기",
"preferences_category_data": "데이터 설정", "preferences_category_data": "데이터 설정",
"`x` is live": "`x` 이(가) 라이브 중입니다", "`x` is live": "`x` 이(가) 라이브 중입니다",
"`x` uploaded a video": "`x` 동영상 게시됨", "`x` uploaded a video": "`x` 이(가) 동영상을 게시했습니다",
"Enable web notifications": "웹 알림 활성화", "Enable web notifications": "웹 알림 활성화",
"preferences_notifications_only_label": "알림만 표시 (있는 경우): ", "preferences_notifications_only_label": "알림만 표시 (있는 경우): ",
"preferences_unseen_only_label": "시청하지 않은 것만 표시: ", "preferences_unseen_only_label": "시청하지 않은 것만 표시: ",
@ -241,7 +241,7 @@
"Could not create mix.": "믹스를 생성할 수 없습니다.", "Could not create mix.": "믹스를 생성할 수 없습니다.",
"`x` ago": "`x` 전", "`x` ago": "`x` 전",
"comments_view_x_replies_0": "답글 {{count}}개 보기", "comments_view_x_replies_0": "답글 {{count}}개 보기",
"View Reddit comments": "레딧 댓글 보기", "View Reddit comments": "Reddit 댓글 보기",
"Engagement: ": "약속: ", "Engagement: ": "약속: ",
"Wilson score: ": "Wilson Score: ", "Wilson score: ": "Wilson Score: ",
"Family friendly? ": "전연령 영상입니까? ", "Family friendly? ": "전연령 영상입니까? ",
@ -267,8 +267,8 @@
"Bulgarian": "불가리아어", "Bulgarian": "불가리아어",
"Bosnian": "보스니아어", "Bosnian": "보스니아어",
"Belarusian": "벨라루스어", "Belarusian": "벨라루스어",
"View more comments on Reddit": "레딧에서 더 많은 댓글 보기", "View more comments on Reddit": "Reddit에서 댓글 더 보기",
"View YouTube comments": "유튜브 댓글 보기", "View YouTube comments": "YouTube 댓글 보기",
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
"Shared `x`": "`x` 업로드", "Shared `x`": "`x` 업로드",
"Whitelisted regions: ": "차단되지 않은 지역: ", "Whitelisted regions: ": "차단되지 않은 지역: ",
@ -289,7 +289,7 @@
"Empty playlist": "재생목록 비어 있음", "Empty playlist": "재생목록 비어 있음",
"Show annotations": "주석 보이기", "Show annotations": "주석 보이기",
"Hide annotations": "주석 숨기기", "Hide annotations": "주석 숨기기",
"Switch Invidious Instance": "인비디어스 인스턴스 변경", "Switch Invidious Instance": "Invidious 인스턴스 변경",
"Spanish": "스페인어", "Spanish": "스페인어",
"Southern Sotho": "소토어", "Southern Sotho": "소토어",
"Somali": "소말리어", "Somali": "소말리어",
@ -329,7 +329,7 @@
"Swedish": "스웨덴어", "Swedish": "스웨덴어",
"Spanish (Latin America)": "스페인어 (라틴 아메리카)", "Spanish (Latin America)": "스페인어 (라틴 아메리카)",
"comments_points_count_0": "{{count}} 포인트", "comments_points_count_0": "{{count}} 포인트",
"Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드", "Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드",
"Premieres `x`": "최초 공개 `x`", "Premieres `x`": "최초 공개 `x`",
"Premieres in `x`": "`x` 후 최초 공개", "Premieres in `x`": "`x` 후 최초 공개",
"next_steps_error_message": "다음 방법을 시도해 보세요: ", "next_steps_error_message": "다음 방법을 시도해 보세요: ",
@ -408,7 +408,7 @@
"preferences_quality_dash_option_1080p": "1080p", "preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_worst": "최저", "preferences_quality_dash_option_worst": "최저",
"preferences_watch_history_label": "시청 기록 저장: ", "preferences_watch_history_label": "시청 기록 저장: ",
"invidious": "인비디어스", "invidious": "Invidious",
"preferences_quality_option_small": "낮음", "preferences_quality_option_small": "낮음",
"preferences_quality_dash_option_auto": "자동", "preferences_quality_dash_option_auto": "자동",
"preferences_quality_dash_option_480p": "480p", "preferences_quality_dash_option_480p": "480p",
@ -419,7 +419,7 @@
"Portuguese (Brazil)": "포르투갈어 (브라질)", "Portuguese (Brazil)": "포르투갈어 (브라질)",
"search_message_no_results": "결과가 없습니다.", "search_message_no_results": "결과가 없습니다.",
"search_message_change_filters_or_query": "필터를 변경하시거나 검색어를 넓게 시도해보세요.", "search_message_change_filters_or_query": "필터를 변경하시거나 검색어를 넓게 시도해보세요.",
"search_message_use_another_instance": " 당신은 <a href=\"`x`\">다른 인스턴스에서 검색</a>할 수도 있습니다.", "search_message_use_another_instance": " <a href=\"`x`\">다른 인스턴스에서 검색</a>할 수도 있습니다.",
"English (United States)": "영어 (미국)", "English (United States)": "영어 (미국)",
"Chinese": "중국어", "Chinese": "중국어",
"Chinese (China)": "중국어 (중국)", "Chinese (China)": "중국어 (중국)",
@ -453,7 +453,7 @@
"channel_tab_streams_label": "실시간 스트리밍", "channel_tab_streams_label": "실시간 스트리밍",
"channel_tab_channels_label": "채널", "channel_tab_channels_label": "채널",
"channel_tab_playlists_label": "재생목록", "channel_tab_playlists_label": "재생목록",
"Standard YouTube license": "표준 유튜브 라이선스", "Standard YouTube license": "표준 YouTube 라이선스",
"Song: ": "제목: ", "Song: ": "제목: ",
"Channel Sponsor": "채널 스폰서", "Channel Sponsor": "채널 스폰서",
"Album: ": "앨범: ", "Album: ": "앨범: ",

View File

@ -21,7 +21,7 @@
"Import and Export Data": "Importer- og eksporter data", "Import and Export Data": "Importer- og eksporter data",
"Import": "Importer", "Import": "Importer",
"Import Invidious data": "Importer Invidious-JSON-data", "Import Invidious data": "Importer Invidious-JSON-data",
"Import YouTube subscriptions": "Importer YouTube/OPML-abonnementer", "Import YouTube subscriptions": "Importer YouTube CSV eller OPML-abonnementer",
"Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnementer (.db)", "Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnementer (.db)",
"Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnementer (.json)", "Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnementer (.json)",
"Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)", "Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)",
@ -487,5 +487,12 @@
"playlist_button_add_items": "Legg til videoer", "playlist_button_add_items": "Legg til videoer",
"generic_channels_count": "{{count}} kanal", "generic_channels_count": "{{count}} kanal",
"generic_channels_count_plural": "{{count}} kanaler", "generic_channels_count_plural": "{{count}} kanaler",
"Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)" "Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)",
"carousel_go_to": "Gå til lysark `x`",
"Search for videos": "Søk i videoer",
"Answer": "Svar",
"carousel_slide": "Lysark {{current}} av {{total}}",
"carousel_skip": "Hopp over karusellen",
"Add to playlist": "Legg til i spilleliste",
"Add to playlist: ": "Legg til i spilleliste: "
} }

View File

@ -21,7 +21,7 @@
"Import and Export Data": "Gegevens im- en exporteren", "Import and Export Data": "Gegevens im- en exporteren",
"Import": "Importeren", "Import": "Importeren",
"Import Invidious data": "JSON-gegevens Invidious importeren", "Import Invidious data": "JSON-gegevens Invidious importeren",
"Import YouTube subscriptions": "YouTube-/OPML-abonnementen importeren", "Import YouTube subscriptions": "YouTube CVS of OPML-abonnementen importeren",
"Import FreeTube subscriptions (.db)": "FreeTube-abonnementen importeren (.db)", "Import FreeTube subscriptions (.db)": "FreeTube-abonnementen importeren (.db)",
"Import NewPipe subscriptions (.json)": "NewPipe-abonnementen importeren (.json)", "Import NewPipe subscriptions (.json)": "NewPipe-abonnementen importeren (.json)",
"Import NewPipe data (.zip)": "NewPipe-gegevens importeren (.zip)", "Import NewPipe data (.zip)": "NewPipe-gegevens importeren (.zip)",
@ -86,7 +86,7 @@
"Only show latest unwatched video from channel: ": "Alleen nieuwste niet-bekeken video van kanaal tonen: ", "Only show latest unwatched video from channel: ": "Alleen nieuwste niet-bekeken video van kanaal tonen: ",
"preferences_unseen_only_label": "Alleen niet-bekeken videos tonen: ", "preferences_unseen_only_label": "Alleen niet-bekeken videos tonen: ",
"preferences_notifications_only_label": "Alleen meldingen tonen (als die er zijn): ", "preferences_notifications_only_label": "Alleen meldingen tonen (als die er zijn): ",
"Enable web notifications": "Systemmeldingen inschakelen", "Enable web notifications": "Systeemmeldingen inschakelen",
"`x` uploaded a video": "`x` heeft een video geüpload", "`x` uploaded a video": "`x` heeft een video geüpload",
"`x` is live": "`x` zendt nu live uit", "`x` is live": "`x` zendt nu live uit",
"preferences_category_data": "Gegevensinstellingen", "preferences_category_data": "Gegevensinstellingen",
@ -192,15 +192,15 @@
"Arabic": "Arabisch", "Arabic": "Arabisch",
"Armenian": "Armeens", "Armenian": "Armeens",
"Azerbaijani": "Azerbeidzjaans", "Azerbaijani": "Azerbeidzjaans",
"Bangla": "Bangla", "Bangla": "Bengaals",
"Basque": "Baskisch", "Basque": "Baskisch",
"Belarusian": "Wit-Rrussisch", "Belarusian": "Wit-Russisch",
"Bosnian": "Bosnisch", "Bosnian": "Bosnisch",
"Bulgarian": "Bulgaars", "Bulgarian": "Bulgaars",
"Burmese": "Birmaans", "Burmese": "Birmaans",
"Catalan": "Catalaans", "Catalan": "Catalaans",
"Cebuano": "Cebuano", "Cebuano": "Cebuaans",
"Chinese (Simplified)": "Chinees (Veereenvoudigd)", "Chinese (Simplified)": "Chinees (Vereenvoudigd)",
"Chinese (Traditional)": "Chinees (Traditioneel)", "Chinese (Traditional)": "Chinees (Traditioneel)",
"Corsican": "Corsicaans", "Corsican": "Corsicaans",
"Croatian": "Kroatisch", "Croatian": "Kroatisch",
@ -217,23 +217,23 @@
"German": "Duits", "German": "Duits",
"Greek": "Grieks", "Greek": "Grieks",
"Gujarati": "Gujarati", "Gujarati": "Gujarati",
"Haitian Creole": "Creools", "Haitian Creole": "Haïtiaans Creools",
"Hausa": "Hausa", "Hausa": "Hausa",
"Hawaiian": "Hawaïaans", "Hawaiian": "Hawaïaans",
"Hebrew": "Heebreeuws", "Hebrew": "Hebreeuws",
"Hindi": "Hindi", "Hindi": "Hindi",
"Hmong": "Hmong", "Hmong": "Hmong",
"Hungarian": "Hongaars", "Hungarian": "Hongaars",
"Icelandic": "IJslands", "Icelandic": "IJslands",
"Igbo": "Igbo", "Igbo": "Ikbo",
"Indonesian": "Indonesisch", "Indonesian": "Indonesisch",
"Irish": "Iers", "Irish": "Iers",
"Italian": "Italiaans", "Italian": "Italiaans",
"Japanese": "Japans", "Japanese": "Japans",
"Javanese": "Javaans", "Javanese": "Javaans",
"Kannada": "Kannada", "Kannada": "Kannada-taal",
"Kazakh": "Kazachs", "Kazakh": "Kazachs",
"Khmer": "Khmer", "Khmer": "Khmer-taal",
"Korean": "Koreaans", "Korean": "Koreaans",
"Kurdish": "Koerdisch", "Kurdish": "Koerdisch",
"Kyrgyz": "Kirgizisch", "Kyrgyz": "Kirgizisch",
@ -245,10 +245,10 @@
"Macedonian": "Macedonisch", "Macedonian": "Macedonisch",
"Malagasy": "Malagassisch", "Malagasy": "Malagassisch",
"Malay": "Maleisisch", "Malay": "Maleisisch",
"Malayalam": "Malayalam", "Malayalam": "Malayalam-taal",
"Maltese": "Maltees", "Maltese": "Maltees",
"Maori": "Maorisch", "Maori": "Maorisch",
"Marathi": "Marathi", "Marathi": "Marathi-taal",
"Mongolian": "Mongools", "Mongolian": "Mongools",
"Nepali": "Nepalees", "Nepali": "Nepalees",
"Norwegian Bokmål": "Noors (Bokmål)", "Norwegian Bokmål": "Noors (Bokmål)",
@ -309,7 +309,7 @@
"(edited)": "(bewerkt)", "(edited)": "(bewerkt)",
"YouTube comment permalink": "Link naar YouTube-reactie", "YouTube comment permalink": "Link naar YouTube-reactie",
"permalink": "permalink", "permalink": "permalink",
"`x` marked it with a ❤": "`x` heeft dit gemarkeerd met ❤", "`x` marked it with a ❤": "`x` heeft dit gemarkeerd met een ❤",
"Audio mode": "Audiomodus", "Audio mode": "Audiomodus",
"Video mode": "Videomodus", "Video mode": "Videomodus",
"channel_tab_videos_label": "Video's", "channel_tab_videos_label": "Video's",
@ -396,7 +396,7 @@
"Dutch (auto-generated)": "Nederlands (automatisch gegenereerd)", "Dutch (auto-generated)": "Nederlands (automatisch gegenereerd)",
"tokens_count": "{{count}} token", "tokens_count": "{{count}} token",
"tokens_count_plural": "{{count}} tokens", "tokens_count_plural": "{{count}} tokens",
"generic_count_seconds": "{{count}} second", "generic_count_seconds": "{{count}} seconde",
"generic_count_seconds_plural": "{{count}} seconden", "generic_count_seconds_plural": "{{count}} seconden",
"generic_count_weeks": "{{count}} week", "generic_count_weeks": "{{count}} week",
"generic_count_weeks_plural": "{{count}} weken", "generic_count_weeks_plural": "{{count}} weken",
@ -449,7 +449,7 @@
"generic_playlists_count_plural": "{{count}} afspeellijsten", "generic_playlists_count_plural": "{{count}} afspeellijsten",
"Chinese (Hong Kong)": "Chinees (Hongkong)", "Chinese (Hong Kong)": "Chinees (Hongkong)",
"Korean (auto-generated)": "Koreaans (automatisch gegenereerd)", "Korean (auto-generated)": "Koreaans (automatisch gegenereerd)",
"search_filters_apply_button": "Geselecteerd filters toepassen", "search_filters_apply_button": "Geselecteerde filters toepassen",
"search_message_use_another_instance": " Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.", "search_message_use_another_instance": " Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.",
"Cantonese (Hong Kong)": "Kantonees (Hongkong)", "Cantonese (Hong Kong)": "Kantonees (Hongkong)",
"Chinese (China)": "Chinees (China)", "Chinese (China)": "Chinees (China)",

View File

@ -41,7 +41,7 @@
"Time (h:mm:ss):": "Hora (h:mm:ss):", "Time (h:mm:ss):": "Hora (h:mm:ss):",
"Text CAPTCHA": "Mudar para um desafio de texto", "Text CAPTCHA": "Mudar para um desafio de texto",
"Image CAPTCHA": "Mudar para um desafio visual", "Image CAPTCHA": "Mudar para um desafio visual",
"Sign In": "Entrar", "Sign In": "Fazer login",
"Register": "Criar conta", "Register": "Criar conta",
"E-mail": "E-mail", "E-mail": "E-mail",
"Preferences": "Preferências", "Preferences": "Preferências",

View File

@ -253,7 +253,7 @@
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)", "Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
"Import YouTube subscriptions": "Importar subscrições via YouTube/OPML", "Import YouTube subscriptions": "Importar via YouTube csv ou subscrição OPML",
"Import Invidious data": "Importar dados JSON do Invidious", "Import Invidious data": "Importar dados JSON do Invidious",
"Import": "Importar", "Import": "Importar",
"No": "Não", "No": "Não",

View File

@ -21,7 +21,7 @@
"Import and Export Data": "Импорт и экспорт данных", "Import and Export Data": "Импорт и экспорт данных",
"Import": "Импорт", "Import": "Импорт",
"Import Invidious data": "Импортировать JSON с данными Invidious", "Import Invidious data": "Импортировать JSON с данными Invidious",
"Import YouTube subscriptions": "Импортировать подписки из YouTube/OPML", "Import YouTube subscriptions": "Импортировать подписки из CSV или OPML",
"Import FreeTube subscriptions (.db)": "Импортировать подписки из FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Импортировать подписки из FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Импортировать подписки из NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Импортировать подписки из NewPipe (.json)",
"Import NewPipe data (.zip)": "Импортировать данные из NewPipe (.zip)", "Import NewPipe data (.zip)": "Импортировать данные из NewPipe (.zip)",
@ -504,5 +504,11 @@
"generic_channels_count_0": "{{count}} канал", "generic_channels_count_0": "{{count}} канал",
"generic_channels_count_1": "{{count}} канала", "generic_channels_count_1": "{{count}} канала",
"generic_channels_count_2": "{{count}} каналов", "generic_channels_count_2": "{{count}} каналов",
"Import YouTube watch history (.json)": "Импортировать историю просмотра из YouTube (.json)" "Import YouTube watch history (.json)": "Импортировать историю просмотра из YouTube (.json)",
"Add to playlist": "Добавить в плейлист",
"Add to playlist: ": "Добавить в плейлист: ",
"Answer": "Ответить",
"Search for videos": "Поиск видео",
"The Popular feed has been disabled by the administrator.": "Популярная лента была отключена администратором.",
"toggle_theme": "Переключатель тем"
} }

View File

@ -174,7 +174,7 @@
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hej! Izgleda da ste isključili JavaScript. Kliknite ovde da biste videli komentare, imajte na umu da će možda potrajati malo duže da se učitaju.", "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hej! Izgleda da ste isključili JavaScript. Kliknite ovde da biste videli komentare, imajte na umu da će možda potrajati malo duže da se učitaju.",
"View `x` comments": { "View `x` comments": {
"([^.,0-9]|^)1([^.,0-9]|$)": "Pogledaj `x` komentar", "([^.,0-9]|^)1([^.,0-9]|$)": "Pogledaj `x` komentar",
"": "Pogledaj`x` komentare" "": "Pogledaj`x` komentara"
}, },
"View Reddit comments": "Pogledaj Reddit komentare", "View Reddit comments": "Pogledaj Reddit komentare",
"CAPTCHA is a required field": "CAPTCHA je obavezno polje", "CAPTCHA is a required field": "CAPTCHA je obavezno polje",
@ -211,7 +211,7 @@
"About": "O sajtu", "About": "O sajtu",
"footer_source_code": "Izvorni kôd", "footer_source_code": "Izvorni kôd",
"footer_original_source_code": "Originalni izvorni kôd", "footer_original_source_code": "Originalni izvorni kôd",
"preferences_related_videos_label": "Prikaži povezane video snimke: ", "preferences_related_videos_label": "Prikaži srodne video snimke: ",
"preferences_annotations_label": "Podrazumevano prikaži napomene: ", "preferences_annotations_label": "Podrazumevano prikaži napomene: ",
"preferences_extend_desc_label": "Automatski proširi opis video snimka: ", "preferences_extend_desc_label": "Automatski proširi opis video snimka: ",
"preferences_vr_mode_label": "Interaktivni video snimci od 360 stepeni (zahteva WebGl): ", "preferences_vr_mode_label": "Interaktivni video snimci od 360 stepeni (zahteva WebGl): ",

View File

@ -60,7 +60,7 @@
"reddit": "Reddit", "reddit": "Reddit",
"preferences_captions_label": "Подразумевани титлови: ", "preferences_captions_label": "Подразумевани титлови: ",
"Fallback captions: ": "Резервни титлови: ", "Fallback captions: ": "Резервни титлови: ",
"preferences_related_videos_label": "Прикажи повезане видео снимке: ", "preferences_related_videos_label": "Прикажи сродне видео снимке: ",
"preferences_annotations_label": "Подразумевано прикажи напомене: ", "preferences_annotations_label": "Подразумевано прикажи напомене: ",
"preferences_category_visual": "Визуелна подешавања", "preferences_category_visual": "Визуелна подешавања",
"preferences_player_style_label": "Стил плејера: ", "preferences_player_style_label": "Стил плејера: ",
@ -246,7 +246,7 @@
"preferences_locale_label": "Језик: ", "preferences_locale_label": "Језик: ",
"Persian": "Персијски", "Persian": "Персијски",
"View `x` comments": { "View `x` comments": {
"": "Погледај `x` коментаре", "": "Погледај `x` коментара",
"([^.,0-9]|^)1([^.,0-9]|$)": "Погледај `x` коментар" "([^.,0-9]|^)1([^.,0-9]|$)": "Погледај `x` коментар"
}, },
"search_filters_type_option_channel": "Канал", "search_filters_type_option_channel": "Канал",

View File

@ -21,7 +21,7 @@
"Import and Export Data": "Importera och exportera data", "Import and Export Data": "Importera och exportera data",
"Import": "Importera", "Import": "Importera",
"Import Invidious data": "Importera Invidious JSON data", "Import Invidious data": "Importera Invidious JSON data",
"Import YouTube subscriptions": "Importera YouTube/OPML prenumerationer", "Import YouTube subscriptions": "Importera YouTube CSV eller OPML prenumerationer",
"Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)", "Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)",
"Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)", "Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)",
"Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)", "Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)",

View File

@ -21,7 +21,7 @@
"Import and Export Data": "Імпорт і експорт даних", "Import and Export Data": "Імпорт і експорт даних",
"Import": "Імпорт", "Import": "Імпорт",
"Import Invidious data": "Імпортувати JSON-дані Invidious", "Import Invidious data": "Імпортувати JSON-дані Invidious",
"Import YouTube subscriptions": "Імпортувати підписки з YouTube чи OPML", "Import YouTube subscriptions": "Імпортувати підписки YouTube з CSV чи OPML",
"Import FreeTube subscriptions (.db)": "Імпортувати підписки з FreeTube (.db)", "Import FreeTube subscriptions (.db)": "Імпортувати підписки з FreeTube (.db)",
"Import NewPipe subscriptions (.json)": "Імпортувати підписки з NewPipe (.json)", "Import NewPipe subscriptions (.json)": "Імпортувати підписки з NewPipe (.json)",
"Import NewPipe data (.zip)": "Імпортувати дані з NewPipe (.zip)", "Import NewPipe data (.zip)": "Імпортувати дані з NewPipe (.zip)",

View File

@ -301,7 +301,6 @@ Spectator.describe Invidious::Search::Filters do
it "Encodes features filter (single)" do it "Encodes features filter (single)" do
Invidious::Search::Filters::Features.each do |value| Invidious::Search::Filters::Features.each do |value|
string = described_class.format_features(value)
filters = described_class.new(features: value) filters = described_class.new(features: value)
expect("#{filters.to_iv_params}") expect("#{filters.to_iv_params}")

View File

@ -67,7 +67,7 @@ Spectator.describe "parse_video_info" do
# Video metadata # Video metadata
expect(info["genre"].as_s).to eq("Entertainment") expect(info["genre"].as_s).to eq("Entertainment")
expect(info["genreUcid"].as_s).to be_empty expect(info["genreUcid"].as_s?).to be_nil
expect(info["license"].as_s).to be_empty expect(info["license"].as_s).to be_empty
# Author infos # Author infos
@ -151,7 +151,7 @@ Spectator.describe "parse_video_info" do
# Video metadata # Video metadata
expect(info["genre"].as_s).to eq("Music") expect(info["genre"].as_s).to eq("Music")
expect(info["genreUcid"].as_s).to be_empty expect(info["genreUcid"].as_s?).to be_nil
expect(info["license"].as_s).to be_empty expect(info["license"].as_s).to be_empty
# Author infos # Author infos

View File

@ -94,7 +94,7 @@ Spectator.describe "parse_video_info" do
# Video metadata # Video metadata
expect(info["genre"].as_s).to eq("Entertainment") expect(info["genre"].as_s).to eq("Entertainment")
expect(info["genreUcid"].as_s).to be_empty expect(info["genreUcid"].as_s?).to be_nil
expect(info["license"].as_s).to be_empty expect(info["license"].as_s).to be_empty
# Author infos # Author infos

View File

@ -153,6 +153,15 @@ Invidious::Database.check_integrity(CONFIG)
{% puts "\nDone checking player dependencies, now compiling Invidious...\n" %} {% puts "\nDone checking player dependencies, now compiling Invidious...\n" %}
{% end %} {% end %}
# Misc
DECRYPT_FUNCTION =
if sig_helper_address = CONFIG.signature_server.presence
IV::DecryptFunction.new(sig_helper_address)
else
nil
end
# Start jobs # Start jobs
if CONFIG.channel_threads > 0 if CONFIG.channel_threads > 0
@ -163,11 +172,6 @@ if CONFIG.feed_threads > 0
Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB) Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB)
end end
DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling)
if CONFIG.decrypt_polling
Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new
end
if CONFIG.statistics_enabled if CONFIG.statistics_enabled
Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE) Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE)
end end

View File

@ -72,6 +72,7 @@ def get_about_info(ucid, locale) : AboutChannel
# Raises a KeyError on failure. # Raises a KeyError on failure.
banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]? banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
banners ||= initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "banner", "imageBannerViewModel", "image", "sources")
banner = banners.try &.[-1]?.try &.["url"].as_s? banner = banners.try &.[-1]?.try &.["url"].as_s?
# if banner.includes? "channels/c4/default_banner" # if banner.includes? "channels/c4/default_banner"
@ -147,9 +148,17 @@ def get_about_info(ucid, locale) : AboutChannel
end end
end end
sub_count = initdata sub_count = 0
.dig?("header", "c4TabbedHeaderRenderer", "subscriberCountText", "simpleText").try &.as_s?
.try { |text| short_text_to_number(text.split(" ")[0]).to_i32 } || 0 if (metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a)
metadata_rows.each do |row|
metadata_part = row.dig?("metadataParts").try &.as_a.find { |i| i.dig?("text", "content").try &.as_s.includes?("subscribers") }
if !metadata_part.nil?
sub_count = short_text_to_number(metadata_part.dig("text", "content").as_s.split(" ")[0]).to_i32
end
break if sub_count != 0
end
end
AboutChannel.new( AboutChannel.new(
ucid: ucid, ucid: ucid,

View File

@ -232,7 +232,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
id: video_id, id: video_id,
title: title, title: title,
published: published, published: published,
updated: Time.utc, updated: updated,
ucid: ucid, ucid: ucid,
author: author, author: author,
length_seconds: length_seconds, length_seconds: length_seconds,

View File

@ -5,35 +5,35 @@ def text_to_parsed_content(text : String) : JSON::Any
# In first case line is just a simple node before # In first case line is just a simple node before
# check patterns inside line # check patterns inside line
# { 'text': line } # { 'text': line }
currentNodes = [] of JSON::Any current_nodes = [] of JSON::Any
initialNode = {"text" => line} initial_node = {"text" => line}
currentNodes << (JSON.parse(initialNode.to_json)) current_nodes << (JSON.parse(initial_node.to_json))
# For each match with url pattern, get last node and preserve # For each match with url pattern, get last node and preserve
# last node before create new node with url information # last node before create new node with url information
# { 'text': match, 'navigationEndpoint': { 'urlEndpoint' : 'url': match } } # { 'text': match, 'navigationEndpoint': { 'urlEndpoint' : 'url': match } }
line.scan(/https?:\/\/[^ ]*/).each do |urlMatch| line.scan(/https?:\/\/[^ ]*/).each do |url_match|
# Retrieve last node and update node without match # Retrieve last node and update node without match
lastNode = currentNodes[currentNodes.size - 1].as_h last_node = current_nodes[-1].as_h
splittedLastNode = lastNode["text"].as_s.split(urlMatch[0]) splitted_last_node = last_node["text"].as_s.split(url_match[0])
lastNode["text"] = JSON.parse(splittedLastNode[0].to_json) last_node["text"] = JSON.parse(splitted_last_node[0].to_json)
currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json) current_nodes[-1] = JSON.parse(last_node.to_json)
# Create new node with match and navigation infos # Create new node with match and navigation infos
currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}} current_node = {"text" => url_match[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => url_match[0]}}}
currentNodes << (JSON.parse(currentNode.to_json)) current_nodes << (JSON.parse(current_node.to_json))
# If text remain after match create new simple node with text after match # If text remain after match create new simple node with text after match
afterNode = {"text" => splittedLastNode.size > 1 ? splittedLastNode[1] : ""} after_node = {"text" => splitted_last_node.size > 1 ? splitted_last_node[1] : ""}
currentNodes << (JSON.parse(afterNode.to_json)) current_nodes << (JSON.parse(after_node.to_json))
end end
# After processing of matches inside line # After processing of matches inside line
# Add \n at end of last node for preserve carriage return # Add \n at end of last node for preserve carriage return
lastNode = currentNodes[currentNodes.size - 1].as_h last_node = current_nodes[-1].as_h
lastNode["text"] = JSON.parse("#{currentNodes[currentNodes.size - 1]["text"]}\n".to_json) last_node["text"] = JSON.parse("#{last_node["text"]}\n".to_json)
currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json) current_nodes[-1] = JSON.parse(last_node.to_json)
# Finally add final nodes to nodes returned # Finally add final nodes to nodes returned
currentNodes.each do |node| current_nodes.each do |node|
nodes << (node) nodes << (node)
end end
end end
@ -53,8 +53,8 @@ def content_to_comment_html(content, video_id : String? = "")
text = HTML.escape(run["text"].as_s) text = HTML.escape(run["text"].as_s)
if navigationEndpoint = run.dig?("navigationEndpoint") if navigation_endpoint = run.dig?("navigationEndpoint")
text = parse_link_endpoint(navigationEndpoint, text, video_id) text = parse_link_endpoint(navigation_endpoint, text, video_id)
end end
text = "<b>#{text}</b>" if run["bold"]? text = "<b>#{text}</b>" if run["bold"]?

View File

@ -74,8 +74,6 @@ class Config
# Database configuration using 12-Factor "Database URL" syntax # Database configuration using 12-Factor "Database URL" syntax
@[YAML::Field(converter: Preferences::URIConverter)] @[YAML::Field(converter: Preferences::URIConverter)]
property database_url : URI = URI.parse("") property database_url : URI = URI.parse("")
# Use polling to keep decryption function up to date
property decrypt_polling : Bool = false
# Used for crawling channels: threads should check all videos uploaded by a channel # Used for crawling channels: threads should check all videos uploaded by a channel
property full_refresh : Bool = false property full_refresh : Bool = false
@ -121,6 +119,10 @@ class Config
# Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729) # Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
@[YAML::Field(converter: Preferences::FamilyConverter)] @[YAML::Field(converter: Preferences::FamilyConverter)]
property force_resolve : Socket::Family = Socket::Family::UNSPEC property force_resolve : Socket::Family = Socket::Family::UNSPEC
# External signature solver server socket (either a path to a UNIX domain socket or "<IP>:<Port>")
property signature_server : String? = nil
# Port to listen for connections (overridden by command line argument) # Port to listen for connections (overridden by command line argument)
property port : Int32 = 3000 property port : Int32 = 3000
# Host to bind (overridden by command line argument) # Host to bind (overridden by command line argument)
@ -131,6 +133,11 @@ class Config
# Use Innertube's transcripts API instead of timedtext for closed captions # Use Innertube's transcripts API instead of timedtext for closed captions
property use_innertube_for_captions : Bool = false property use_innertube_for_captions : Bool = false
# visitor data ID for Google session
property visitor_data : String? = nil
# poToken for passing bot attestation
property po_token : String? = nil
# Saved cookies in "name1=value1; name2=value2..." format # Saved cookies in "name1=value1; name2=value2..." format
@[YAML::Field(converter: Preferences::StringToCookies)] @[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new property cookies : HTTP::Cookies = HTTP::Cookies.new

View File

@ -149,12 +149,12 @@ module Invidious::Frontend::Comments
if comments["videoId"]? if comments["videoId"]?
html << <<-END_HTML html << <<-END_HTML
<a href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a> <a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
| |
END_HTML END_HTML
elsif comments["authorId"]? elsif comments["authorId"]?
html << <<-END_HTML html << <<-END_HTML
<a href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a> <a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
| |
END_HTML END_HTML
end end

View File

@ -6,9 +6,9 @@ module Invidious::Frontend::Misc
if prefs.automatic_instance_redirect if prefs.automatic_instance_redirect
current_page = env.get?("current_page").as(String) current_page = env.get?("current_page").as(String)
redirect_url = "/redirect?referer=#{current_page}" return "/redirect?referer=#{current_page}"
else else
redirect_url = "https://redirect.invidious.io#{env.request.resource}" return "https://redirect.invidious.io#{env.request.resource}"
end end
end end
end end

View File

@ -3,9 +3,9 @@
# IPv6 addresses. # IPv6 addresses.
# #
class TCPSocket class TCPSocket
def initialize(host : String, port, dns_timeout = nil, connect_timeout = nil, family = Socket::Family::UNSPEC) def initialize(host, port, dns_timeout = nil, connect_timeout = nil, blocking = false, family = Socket::Family::UNSPEC)
Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo| Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo|
super(addrinfo.family, addrinfo.type, addrinfo.protocol) super(addrinfo.family, addrinfo.type, addrinfo.protocol, blocking)
connect(addrinfo, timeout: connect_timeout) do |error| connect(addrinfo, timeout: connect_timeout) do |error|
close close
error error
@ -26,7 +26,7 @@ class HTTP::Client
end end
hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host
io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, @family io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, family: @family
io.read_timeout = @read_timeout if @read_timeout io.read_timeout = @read_timeout if @read_timeout
io.write_timeout = @write_timeout if @write_timeout io.write_timeout = @write_timeout if @write_timeout
io.sync = false io.sync = false
@ -35,7 +35,7 @@ class HTTP::Client
if tls = @tls if tls = @tls
tcp_socket = io tcp_socket = io
begin begin
io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host) io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host.rchop('.'))
rescue exc rescue exc
# don't leak the TCP socket when the SSL connection failed # don't leak the TCP socket when the SSL connection failed
tcp_socket.close tcp_socket.close

View File

@ -190,7 +190,7 @@ def error_redirect_helper(env : HTTP::Server::Context)
<a href="/redirect?referer=#{env.get("current_page")}">#{switch_instance}</a> <a href="/redirect?referer=#{env.get("current_page")}">#{switch_instance}</a>
</li> </li>
<li> <li>
<a href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a> <a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
</li> </li>
</ul> </ul>
END_HTML END_HTML

View File

@ -97,7 +97,7 @@ class AuthHandler < Kemal::Handler
if token = env.request.headers["Authorization"]? if token = env.request.headers["Authorization"]?
token = JSON.parse(URI.decode_www_form(token.lchop("Bearer "))) token = JSON.parse(URI.decode_www_form(token.lchop("Bearer ")))
session = URI.decode_www_form(token["session"].as_s) session = URI.decode_www_form(token["session"].as_s)
scopes, expire, signature = validate_request(token, session, env.request, HMAC_KEY, nil) scopes, _, _ = validate_request(token, session, env.request, HMAC_KEY, nil)
if email = Invidious::Database::SessionIDs.select_email(session) if email = Invidious::Database::SessionIDs.select_email(session)
user = Invidious::Database::Users.select!(email: email) user = Invidious::Database::Users.select!(email: email)

View File

@ -95,7 +95,6 @@ module I18next::Plurals
"hr" => PluralForms::Special_Hungarian_Serbian, "hr" => PluralForms::Special_Hungarian_Serbian,
"it" => PluralForms::Special_Spanish_Italian, "it" => PluralForms::Special_Spanish_Italian,
"pt" => PluralForms::Special_French_Portuguese, "pt" => PluralForms::Special_French_Portuguese,
"pt" => PluralForms::Special_French_Portuguese,
"sr" => PluralForms::Special_Hungarian_Serbian, "sr" => PluralForms::Special_Hungarian_Serbian,
} }
@ -189,7 +188,7 @@ module I18next::Plurals
# Emulate the `rule.numbers.size == 2 && rule.numbers[0] == 1` check # Emulate the `rule.numbers.size == 2 && rule.numbers[0] == 1` check
# from original i18next code # from original i18next code
private def is_simple_plural(form : PluralForms) : Bool private def simple_plural?(form : PluralForms) : Bool
case form case form
when .single_gt_one? then return true when .single_gt_one? then return true
when .single_not_one? then return true when .single_not_one? then return true
@ -211,7 +210,7 @@ module I18next::Plurals
idx = SuffixIndex.get_index(plural_form, count) idx = SuffixIndex.get_index(plural_form, count)
# Simple plurals are handled differently in all versions (but v4) # Simple plurals are handled differently in all versions (but v4)
if @simplify_plural_suffix && is_simple_plural(plural_form) if @simplify_plural_suffix && simple_plural?(plural_form)
return (idx == 1) ? "_plural" : "" return (idx == 1) ? "_plural" : ""
end end
@ -262,9 +261,9 @@ module I18next::Plurals
when .special_hebrew? then return special_hebrew(count) when .special_hebrew? then return special_hebrew(count)
when .special_odia? then return special_odia(count) when .special_odia? then return special_odia(count)
# Mixed v3/v4 forms # Mixed v3/v4 forms
when .special_spanish_italian? then return special_cldr_Spanish_Italian(count) when .special_spanish_italian? then return special_cldr_spanish_italian(count)
when .special_french_portuguese? then return special_cldr_French_Portuguese(count) when .special_french_portuguese? then return special_cldr_french_portuguese(count)
when .special_hungarian_serbian? then return special_cldr_Hungarian_Serbian(count) when .special_hungarian_serbian? then return special_cldr_hungarian_serbian(count)
else else
# default, if nothing matched above # default, if nothing matched above
return 0_u8 return 0_u8
@ -535,7 +534,7 @@ module I18next::Plurals
# #
# This rule is mostly compliant to CLDR v42 # This rule is mostly compliant to CLDR v42
# #
def self.special_cldr_Spanish_Italian(count : Int) : UInt8 def self.special_cldr_spanish_italian(count : Int) : UInt8
return 0_u8 if (count == 1) # one return 0_u8 if (count == 1) # one
return 1_u8 if (count != 0 && count % 1_000_000 == 0) # many return 1_u8 if (count != 0 && count % 1_000_000 == 0) # many
return 2_u8 # other return 2_u8 # other
@ -545,7 +544,7 @@ module I18next::Plurals
# #
# This rule is mostly compliant to CLDR v42 # This rule is mostly compliant to CLDR v42
# #
def self.special_cldr_French_Portuguese(count : Int) : UInt8 def self.special_cldr_french_portuguese(count : Int) : UInt8
return 0_u8 if (count == 0 || count == 1) # one return 0_u8 if (count == 0 || count == 1) # one
return 1_u8 if (count % 1_000_000 == 0) # many return 1_u8 if (count % 1_000_000 == 0) # many
return 2_u8 # other return 2_u8 # other
@ -555,7 +554,7 @@ module I18next::Plurals
# #
# This rule is mostly compliant to CLDR v42 # This rule is mostly compliant to CLDR v42
# #
def self.special_cldr_Hungarian_Serbian(count : Int) : UInt8 def self.special_cldr_hungarian_serbian(count : Int) : UInt8
n_mod_10 = count % 10 n_mod_10 = count % 10
n_mod_100 = count % 100 n_mod_100 = count % 100

View File

@ -34,24 +34,11 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
context context
end end
def puts(message : String)
@io << message << '\n'
@io.flush
end
def write(message : String) def write(message : String)
@io << message @io << message
@io.flush @io.flush
end end
def set_log_level(level : String)
@level = LogLevel.parse(level)
end
def set_log_level(level : LogLevel)
@level = level
end
{% for level in %w(trace debug info warn error fatal) %} {% for level in %w(trace debug info warn error fatal) %}
def {{level.id}}(message : String) def {{level.id}}(message : String)
if LogLevel::{{level.id.capitalize}} >= @level if LogLevel::{{level.id.capitalize}} >= @level

View File

@ -0,0 +1,332 @@
require "uri"
require "socket"
require "socket/tcp_socket"
require "socket/unix_socket"
{% if flag?(:advanced_debug) %}
require "io/hexdump"
{% end %}
private alias NetworkEndian = IO::ByteFormat::NetworkEndian
module Invidious::SigHelper
enum UpdateStatus
Updated
UpdateNotRequired
Error
end
# -------------------
# Payload types
# -------------------
abstract struct Payload
end
struct StringPayload < Payload
getter string : String
def initialize(str : String)
raise Exception.new("SigHelper: String can't be empty") if str.empty?
@string = str
end
def self.from_bytes(slice : Bytes)
size = IO::ByteFormat::NetworkEndian.decode(UInt16, slice)
if size == 0 # Error code
raise Exception.new("SigHelper: Server encountered an error")
end
if (slice.bytesize - 2) != size
raise Exception.new("SigHelper: String size mismatch")
end
if str = String.new(slice[2..])
return self.new(str)
else
raise Exception.new("SigHelper: Can't read string from socket")
end
end
def to_io(io)
# `.to_u16` raises if there is an overflow during the conversion
io.write_bytes(@string.bytesize.to_u16, NetworkEndian)
io.write(@string.to_slice)
end
end
private enum Opcode
FORCE_UPDATE = 0
DECRYPT_N_SIGNATURE = 1
DECRYPT_SIGNATURE = 2
GET_SIGNATURE_TIMESTAMP = 3
GET_PLAYER_STATUS = 4
PLAYER_UPDATE_TIMESTAMP = 5
end
private record Request,
opcode : Opcode,
payload : Payload?
# ----------------------
# High-level functions
# ----------------------
class Client
@mux : Multiplexor
def initialize(uri_or_path)
@mux = Multiplexor.new(uri_or_path)
end
# Forces the server to re-fetch the YouTube player, and extract the necessary
# components from it (nsig function code, sig function code, signature timestamp).
def force_update : UpdateStatus
request = Request.new(Opcode::FORCE_UPDATE, nil)
value = send_request(request) do |bytes|
IO::ByteFormat::NetworkEndian.decode(UInt16, bytes)
end
case value
when 0x0000 then return UpdateStatus::Error
when 0xFFFF then return UpdateStatus::UpdateNotRequired
when 0xF44F then return UpdateStatus::Updated
else
code = value.nil? ? "nil" : value.to_s(base: 16)
raise Exception.new("SigHelper: Invalid status code received #{code}")
end
end
# Decrypt a provided n signature using the server's current nsig function
# code, and return the result (or an error).
def decrypt_n_param(n : String) : String?
request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n))
n_dec = self.send_request(request) do |bytes|
StringPayload.from_bytes(bytes).string
end
return n_dec
end
# Decrypt a provided s signature using the server's current sig function
# code, and return the result (or an error).
def decrypt_sig(sig : String) : String?
request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig))
sig_dec = self.send_request(request) do |bytes|
StringPayload.from_bytes(bytes).string
end
return sig_dec
end
# Return the signature timestamp from the server's current player
def get_signature_timestamp : UInt64?
request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil)
return self.send_request(request) do |bytes|
IO::ByteFormat::NetworkEndian.decode(UInt64, bytes)
end
end
# Return the current player's version
def get_player : UInt32?
request = Request.new(Opcode::GET_PLAYER_STATUS, nil)
return self.send_request(request) do |bytes|
has_player = (bytes[0] == 0xFF)
player_version = IO::ByteFormat::NetworkEndian.decode(UInt32, bytes[1..4])
has_player ? player_version : nil
end
end
# Return when the player was last updated
def get_player_timestamp : UInt64?
request = Request.new(Opcode::PLAYER_UPDATE_TIMESTAMP, nil)
return self.send_request(request) do |bytes|
IO::ByteFormat::NetworkEndian.decode(UInt64, bytes)
end
end
private def send_request(request : Request, &)
channel = @mux.send(request)
slice = channel.receive
return yield slice
rescue ex
LOGGER.debug("SigHelper: Error when sending a request")
LOGGER.trace(ex.inspect_with_backtrace)
return nil
end
end
# ---------------------
# Low level functions
# ---------------------
class Multiplexor
alias TransactionID = UInt32
record Transaction, channel = ::Channel(Bytes).new
@prng = Random.new
@mutex = Mutex.new
@queue = {} of TransactionID => Transaction
@conn : Connection
def initialize(uri_or_path)
@conn = Connection.new(uri_or_path)
listen
end
def listen : Nil
raise "Socket is closed" if @conn.closed?
LOGGER.debug("SigHelper: Multiplexor listening")
# TODO: reopen socket if unexpectedly closed
spawn do
loop do
receive_data
Fiber.yield
end
end
end
def send(request : Request)
transaction = Transaction.new
transaction_id = @prng.rand(TransactionID)
# Add transaction to queue
@mutex.synchronize do
# On a 32-bits random integer, this should never happen. Though, just in case, ...
if @queue[transaction_id]?
raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!")
end
@queue[transaction_id] = transaction
end
write_packet(transaction_id, request)
return transaction.channel
end
def receive_data
transaction_id, slice = read_packet
@mutex.synchronize do
if transaction = @queue.delete(transaction_id)
# Remove transaction from queue and send data to the channel
transaction.channel.send(slice)
LOGGER.trace("SigHelper: Transaction unqueued and data sent to channel")
else
raise Exception.new("SigHelper: Received transaction was not in queue")
end
end
end
# Read a single packet from the socket
private def read_packet : {TransactionID, Bytes}
# Header
transaction_id = @conn.read_bytes(UInt32, NetworkEndian)
length = @conn.read_bytes(UInt32, NetworkEndian)
LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} / length #{length}")
if length > 67_000
raise Exception.new("SigHelper: Packet longer than expected (#{length})")
end
# Payload
slice = Bytes.new(length)
@conn.read(slice) if length > 0
LOGGER.trace("SigHelper: payload = #{slice}")
LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} - Done")
return transaction_id, slice
end
# Write a single packet to the socket
private def write_packet(transaction_id : TransactionID, request : Request)
LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} / opcode #{request.opcode}")
io = IO::Memory.new(1024)
io.write_bytes(request.opcode.to_u8, NetworkEndian)
io.write_bytes(transaction_id, NetworkEndian)
if payload = request.payload
payload.to_io(io)
end
@conn.send(io)
@conn.flush
LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} - Done")
end
end
class Connection
@socket : UNIXSocket | TCPSocket
{% if flag?(:advanced_debug) %}
@io : IO::Hexdump
{% end %}
def initialize(host_or_path : String)
case host_or_path
when .starts_with?('/')
# Make sure that the file exists
if File.exists?(host_or_path)
@socket = UNIXSocket.new(host_or_path)
else
raise Exception.new("SigHelper: '#{host_or_path}' no such file")
end
when .starts_with?("tcp://")
uri = URI.parse(host_or_path)
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
else
uri = URI.parse("tcp://#{host_or_path}")
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
end
LOGGER.info("SigHelper: Using helper at '#{host_or_path}'")
{% if flag?(:advanced_debug) %}
@io = IO::Hexdump.new(@socket, output: STDERR, read: true, write: true)
{% end %}
@socket.sync = false
@socket.blocking = false
end
def closed? : Bool
return @socket.closed?
end
def close : Nil
@socket.close if !@socket.closed?
end
def flush(*args, **options)
@socket.flush(*args, **options)
end
def send(*args, **options)
@socket.send(*args, **options)
end
# Wrap IO functions, with added debug tooling if needed
{% for function in %w(read read_bytes write write_bytes) %}
def {{function.id}}(*args, **options)
{% if flag?(:advanced_debug) %}
@io.{{function.id}}(*args, **options)
{% else %}
@socket.{{function.id}}(*args, **options)
{% end %}
end
{% end %}
end
end

View File

@ -1,73 +1,55 @@
alias SigProc = Proc(Array(String), Int32, Array(String)) require "http/params"
require "./sig_helper"
struct DecryptFunction class Invidious::DecryptFunction
@decrypt_function = [] of {SigProc, Int32} @last_update : Time = Time.utc - 42.days
@decrypt_time = Time.monotonic
def initialize(@use_polling = true) def initialize(uri_or_path)
@client = SigHelper::Client.new(uri_or_path)
self.check_update
end end
def update_decrypt_function def check_update
@decrypt_function = fetch_decrypt_function now = Time.utc
end
private def fetch_decrypt_function(id = "CvFH_6DNRCY") # If we have updated in the last 5 minutes, do nothing
document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en").body return if (now - @last_update) > 5.minutes
url = document.match(/src="(?<url>\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"]
player = YT_POOL.client &.get(url).body
function_name = player.match(/^(?<name>[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"] # Get the amount of time elapsed since when the player was updated, in the
function_body = player.match(/^#{Regex.escape(function_name)}=function\(\w\){(?<body>[^}]+)}/m).not_nil!["body"] # event where multiple invidious processes are run in parallel.
function_body = function_body.split(";")[1..-2] update_time_elapsed = (@client.get_player_timestamp || 301).seconds
var_name = function_body[0][0, 2] if update_time_elapsed > 5.minutes
var_body = player.delete("\n").match(/var #{Regex.escape(var_name)}={(?<body>(.*?))};/).not_nil!["body"] LOGGER.debug("Signature: Player might be outdated, updating")
@client.force_update
operations = {} of String => SigProc @last_update = Time.utc
var_body.split("},").each do |operation|
op_name = operation.match(/^[^:]+/).not_nil![0]
op_body = operation.match(/\{[^}]+/).not_nil![0]
case op_body
when "{a.reverse()"
operations[op_name] = ->(a : Array(String), _b : Int32) { a.reverse }
when "{a.splice(0,b)"
operations[op_name] = ->(a : Array(String), b : Int32) { a.delete_at(0..(b - 1)); a }
else
operations[op_name] = ->(a : Array(String), b : Int32) { c = a[0]; a[0] = a[b % a.size]; a[b % a.size] = c; a }
end end
end end
decrypt_function = [] of {SigProc, Int32} def decrypt_nsig(n : String) : String?
function_body.each do |function| self.check_update
function = function.lchop(var_name).delete("[].") return @client.decrypt_n_param(n)
rescue ex
op_name = function.match(/[^\(]+/).not_nil![0] LOGGER.debug(ex.message || "Signature: Unknown error")
value = function.match(/\(\w,(?<value>[\d]+)\)/).not_nil!["value"].to_i LOGGER.trace(ex.inspect_with_backtrace)
return nil
decrypt_function << {operations[op_name], value}
end end
return decrypt_function def decrypt_signature(str : String) : String?
self.check_update
return @client.decrypt_sig(str)
rescue ex
LOGGER.debug(ex.message || "Signature: Unknown error")
LOGGER.trace(ex.inspect_with_backtrace)
return nil
end end
def decrypt_signature(fmt : Hash(String, JSON::Any)) def get_sts : UInt64?
return "" if !fmt["s"]? || !fmt["sp"]? self.check_update
return @client.get_signature_timestamp
sp = fmt["sp"].as_s rescue ex
sig = fmt["s"].as_s.split("") LOGGER.debug(ex.message || "Signature: Unknown error")
if !@use_polling LOGGER.trace(ex.inspect_with_backtrace)
now = Time.monotonic return nil
if now - @decrypt_time > 60.seconds || @decrypt_function.size == 0
@decrypt_function = fetch_decrypt_function
@decrypt_time = Time.monotonic
end
end
@decrypt_function.each do |proc, value|
sig = proc.call(sig, value)
end
return "&#{sp}=#{sig.join("")}"
end end
end end

View File

@ -52,9 +52,9 @@ def recode_length_seconds(time)
end end
def decode_interval(string : String) : Time::Span def decode_interval(string : String) : Time::Span
rawMinutes = string.try &.to_i32? raw_minutes = string.try &.to_i32?
if !rawMinutes if !raw_minutes
hours = /(?<hours>\d+)h/.match(string).try &.["hours"].try &.to_i32 hours = /(?<hours>\d+)h/.match(string).try &.["hours"].try &.to_i32
hours ||= 0 hours ||= 0
@ -63,7 +63,7 @@ def decode_interval(string : String) : Time::Span
time = Time::Span.new(hours: hours, minutes: minutes) time = Time::Span.new(hours: hours, minutes: minutes)
else else
time = Time::Span.new(minutes: rawMinutes) time = Time::Span.new(minutes: raw_minutes)
end end
return time return time

View File

@ -11,11 +11,12 @@ module Invidious::HttpServer
params = url.query_params params = url.query_params
params["host"] = url.host.not_nil! # Should never be nil, in theory params["host"] = url.host.not_nil! # Should never be nil, in theory
params["region"] = region if !region.nil? params["region"] = region if !region.nil?
url.query_params = params
if absolute if absolute
return "#{HOST_URL}#{url.request_target}?#{params}" return "#{HOST_URL}#{url.request_target}"
else else
return "#{url.request_target}?#{params}" return url.request_target
end end
end end

View File

@ -1,14 +0,0 @@
class Invidious::Jobs::UpdateDecryptFunctionJob < Invidious::Jobs::BaseJob
def begin
loop do
begin
DECRYPT_FUNCTION.update_decrypt_function
rescue ex
LOGGER.error("UpdateDecryptFunctionJob : #{ex.message}")
ensure
sleep 1.minute
Fiber.yield
end
end
end
end

View File

@ -114,25 +114,31 @@ module Invidious::JSONify::APIv1
json.field "projectionType", fmt["projectionType"] json.field "projectionType", fmt["projectionType"]
if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"]) height = fmt["height"]?.try &.as_i
fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30 width = fmt["width"]?.try &.as_i
fps = fmt["fps"]?.try &.as_i
if fps
json.field "fps", fps json.field "fps", fps
end
if height && width
json.field "size", "#{width}x#{height}"
json.field "resolution", "#{height}p"
quality_label = "#{width > height ? height : width}p"
if fps && fps > 30
quality_label += fps.to_s
end
json.field "qualityLabel", quality_label
end
if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"])
json.field "container", fmt_info["ext"] json.field "container", fmt_info["ext"]
json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"] json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"]
if fmt_info["height"]?
json.field "resolution", "#{fmt_info["height"]}p"
quality_label = "#{fmt_info["height"]}p"
if fps > 30
quality_label += "60"
end
json.field "qualityLabel", quality_label
if fmt_info["width"]?
json.field "size", "#{fmt_info["width"]}x#{fmt_info["height"]}"
end
end
end end
# Livestream chunk infos # Livestream chunk infos
@ -163,26 +169,31 @@ module Invidious::JSONify::APIv1
json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]? json.field "bitrate", fmt["bitrate"].as_i.to_s if fmt["bitrate"]?
fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"]) height = fmt["height"]?.try &.as_i
if fmt_info width = fmt["width"]?.try &.as_i
fps = fmt_info["fps"]?.try &.to_i || fmt["fps"]?.try &.as_i || 30
fps = fmt["fps"]?.try &.as_i
if fps
json.field "fps", fps json.field "fps", fps
end
if height && width
json.field "size", "#{width}x#{height}"
json.field "resolution", "#{height}p"
quality_label = "#{width > height ? height : width}p"
if fps && fps > 30
quality_label += fps.to_s
end
json.field "qualityLabel", quality_label
end
if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"])
json.field "container", fmt_info["ext"] json.field "container", fmt_info["ext"]
json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"] json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"]
if fmt_info["height"]?
json.field "resolution", "#{fmt_info["height"]}p"
quality_label = "#{fmt_info["height"]}p"
if fps > 30
quality_label += "60"
end
json.field "qualityLabel", quality_label
if fmt_info["width"]?
json.field "size", "#{fmt_info["width"]}x#{fmt_info["height"]}"
end
end
end end
end end
end end

View File

@ -366,6 +366,8 @@ def fetch_playlist(plid : String)
if text.includes? "video" if text.includes? "video"
video_count = text.gsub(/\D/, "").to_i? || 0 video_count = text.gsub(/\D/, "").to_i? || 0
elsif text.includes? "episode"
video_count = text.gsub(/\D/, "").to_i? || 0
elsif text.includes? "view" elsif text.includes? "view"
views = text.gsub(/\D/, "").to_i64? || 0_i64 views = text.gsub(/\D/, "").to_i64? || 0_i64
else else

View File

@ -53,7 +53,7 @@ module Invidious::Routes::Account
return error_template(401, "Password is a required field") return error_template(401, "Password is a required field")
end end
new_passwords = env.params.body.select { |k, v| k.match(/^new_password\[\d+\]$/) }.map { |k, v| v } new_passwords = env.params.body.select { |k, _| k.match(/^new_password\[\d+\]$/) }.map { |_, v| v }
if new_passwords.size <= 1 || new_passwords.uniq.size != 1 if new_passwords.size <= 1 || new_passwords.uniq.size != 1
return error_template(400, "New passwords must match") return error_template(400, "New passwords must match")
@ -240,7 +240,7 @@ module Invidious::Routes::Account
return error_template(400, ex) return error_template(400, ex)
end end
scopes = env.params.body.select { |k, v| k.match(/^scopes\[\d+\]$/) }.map { |k, v| v } scopes = env.params.body.select { |k, _| k.match(/^scopes\[\d+\]$/) }.map { |_, v| v }
callback_url = env.params.body["callbackUrl"]? callback_url = env.params.body["callbackUrl"]?
expire = env.params.body["expire"]?.try &.to_i? expire = env.params.body["expire"]?.try &.to_i?

View File

@ -74,7 +74,9 @@ module Invidious::Routes::API::V1::Misc
response = playlist.to_json(offset, video_id: video_id) response = playlist.to_json(offset, video_id: video_id)
json_response = JSON.parse(response) json_response = JSON.parse(response)
if json_response["videos"].as_a[0]["index"] != offset if json_response["videos"].as_a.empty?
json_response = JSON.parse(response)
elsif json_response["videos"].as_a[0]["index"] != offset
offset = json_response["videos"].as_a[0]["index"].as_i offset = json_response["videos"].as_a[0]["index"].as_i
lookback = offset < 50 ? offset : 50 lookback = offset < 50 ? offset : 50
response = playlist.to_json(offset - lookback) response = playlist.to_json(offset - lookback)
@ -177,8 +179,8 @@ module Invidious::Routes::API::V1::Misc
begin begin
resolved_url = YoutubeAPI.resolve_url(url.as(String)) resolved_url = YoutubeAPI.resolve_url(url.as(String))
endpoint = resolved_url["endpoint"] endpoint = resolved_url["endpoint"]
pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || "" page_type = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || ""
if pageType == "WEB_PAGE_TYPE_UNKNOWN" if page_type == "WEB_PAGE_TYPE_UNKNOWN"
return error_json(400, "Unknown url") return error_json(400, "Unknown url")
end end
@ -194,7 +196,7 @@ module Invidious::Routes::API::V1::Misc
json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]? json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]?
json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]? json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]?
json.field "params", params.try &.as_s json.field "params", params.try &.as_s
json.field "pageType", pageType json.field "pageType", page_type
end end
end end
end end

View File

@ -141,7 +141,11 @@ module Invidious::Routes::API::V1::Videos
end end
end end
else else
webvtt = YT_POOL.client &.get("#{url}&fmt=vtt").body uri = URI.parse(url)
query_params = uri.query_params
query_params["fmt"] = "vtt"
uri.query_params = query_params
webvtt = YT_POOL.client &.get(uri.request_target).body
if webvtt.starts_with?("<?xml") if webvtt.starts_with?("<?xml")
webvtt = caption.timedtext_to_vtt(webvtt) webvtt = caption.timedtext_to_vtt(webvtt)
@ -215,7 +219,7 @@ module Invidious::Routes::API::V1::Videos
storyboard[:storyboard_count].times do |i| storyboard[:storyboard_count].times do |i|
url = storyboard[:url] url = storyboard[:url]
authority = /(i\d?).ytimg.com/.match(url).not_nil![1]? authority = /(i\d?).ytimg.com/.match!(url)[1]?
url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "") url = url.gsub("$M", i).gsub(%r(https://i\d?.ytimg.com/sb/), "")
url = "#{HOST_URL}/sb/#{authority}/#{url}" url = "#{HOST_URL}/sb/#{authority}/#{url}"
@ -250,7 +254,7 @@ module Invidious::Routes::API::V1::Videos
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id)) if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
annotations = cached_annotation.annotations annotations = cached_annotation.annotations
else else
index = CHARS_SAFE.index(id[0]).not_nil!.to_s.rjust(2, '0') index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
# IA doesn't handle leading hyphens, # IA doesn't handle leading hyphens,
# so we use https://archive.org/details/youtubeannotations_64 # so we use https://archive.org/details/youtubeannotations_64

View File

@ -214,7 +214,7 @@ module Invidious::Routes::PreferencesRoute
statistics_enabled ||= "off" statistics_enabled ||= "off"
CONFIG.statistics_enabled = statistics_enabled == "on" CONFIG.statistics_enabled = statistics_enabled == "on"
CONFIG.modified_source_code_url = env.params.body["modified_source_code_url"]?.try &.as(String) CONFIG.modified_source_code_url = env.params.body["modified_source_code_url"]?.presence
File.write("config/config.yml", CONFIG.to_yaml) File.write("config/config.yml", CONFIG.to_yaml)
end end

View File

@ -124,7 +124,7 @@ struct Invidious::User
playlist = create_playlist(title, privacy, user) playlist = create_playlist(title, privacy, user)
Invidious::Database::Playlists.update_description(playlist.id, description) Invidious::Database::Playlists.update_description(playlist.id, description)
videos = item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx| item["videos"]?.try &.as_a?.try &.each_with_index do |video_id, idx|
if idx > CONFIG.playlist_length_limit if idx > CONFIG.playlist_length_limit
raise InfoException.new("Playlist cannot have more than #{CONFIG.playlist_length_limit} videos") raise InfoException.new("Playlist cannot have more than #{CONFIG.playlist_length_limit} videos")
end end
@ -182,7 +182,7 @@ struct Invidious::User
if is_opml?(type, extension) if is_opml?(type, extension)
subscriptions = XML.parse(body) subscriptions = XML.parse(body)
user.subscriptions += subscriptions.xpath_nodes(%q(//outline[@type="rss"])).map do |channel| user.subscriptions += subscriptions.xpath_nodes(%q(//outline[@type="rss"])).map do |channel|
channel["xmlUrl"].match(/UC[a-zA-Z0-9_-]{22}/).not_nil![0] channel["xmlUrl"].match!(/UC[a-zA-Z0-9_-]{22}/)[0]
end end
elsif extension == "json" || type == "application/json" elsif extension == "json" || type == "application/json"
subscriptions = JSON.parse(body) subscriptions = JSON.parse(body)

View File

@ -98,20 +98,51 @@ struct Video
# Methods for parsing streaming data # Methods for parsing streaming data
def convert_url(fmt)
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
sp = cfr["sp"]
url = URI.parse(cfr["url"])
params = url.query_params
LOGGER.debug("Videos: Decoding '#{cfr}'")
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
params[sp] = unsig if unsig
else
url = URI.parse(fmt["url"].as_s)
params = url.query_params
end
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
params["n"] = n if n
if token = CONFIG.po_token
params["pot"] = token
end
params["host"] = url.host.not_nil!
if region = self.info["region"]?.try &.as_s
params["region"] = region
end
url.query_params = params
LOGGER.trace("Videos: new url is '#{url}'")
return url.to_s
rescue ex
LOGGER.debug("Videos: Error when parsing video URL")
LOGGER.trace(ex.inspect_with_backtrace)
return ""
end
def fmt_stream def fmt_stream
return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream
fmt_stream = info["streamingData"]?.try &.["formats"]?.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any) fmt_stream = info.dig?("streamingData", "formats")
fmt_stream.each do |fmt| .try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
if s = (fmt["cipher"]? || fmt["signatureCipher"]?).try { |h| HTTP::Params.parse(h.as_s) }
s.each do |k, v|
fmt[k] = JSON::Any.new(v)
end
fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}")
end
fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") fmt_stream.each do |fmt|
fmt["url"] = JSON::Any.new("#{fmt["url"]}&region=#{self.info["region"]}") if self.info["region"]? fmt["url"] = JSON::Any.new(self.convert_url(fmt))
end end
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 } fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
@ -121,21 +152,17 @@ struct Video
def adaptive_fmts def adaptive_fmts
return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) if @adaptive_fmts return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) if @adaptive_fmts
fmt_stream = info["streamingData"]?.try &.["adaptiveFormats"]?.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
fmt_stream.each do |fmt|
if s = (fmt["cipher"]? || fmt["signatureCipher"]?).try { |h| HTTP::Params.parse(h.as_s) }
s.each do |k, v|
fmt[k] = JSON::Any.new(v)
end
fmt["url"] = JSON::Any.new("#{fmt["url"]}#{DECRYPT_FUNCTION.decrypt_signature(fmt)}")
end
fmt["url"] = JSON::Any.new("#{fmt["url"]}&host=#{URI.parse(fmt["url"].as_s).host}") fmt_stream = info.dig("streamingData", "adaptiveFormats")
fmt["url"] = JSON::Any.new("#{fmt["url"]}&region=#{self.info["region"]}") if self.info["region"]? .try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
fmt_stream.each do |fmt|
fmt["url"] = JSON::Any.new(self.convert_url(fmt))
end end
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 } fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
@adaptive_fmts = fmt_stream @adaptive_fmts = fmt_stream
return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) return @adaptive_fmts.as(Array(Hash(String, JSON::Any)))
end end
@ -250,7 +277,7 @@ struct Video
end end
def genre_url : String? def genre_url : String?
info["genreUcid"]? ? "/channel/#{info["genreUcid"]}" : nil info["genreUcid"].try &.as_s? ? "/channel/#{info["genreUcid"]}" : nil
end end
def is_vr : Bool? def is_vr : Bool?
@ -394,10 +421,6 @@ end
def fetch_video(id, region) def fetch_video(id, region)
info = extract_video_info(video_id: id) info = extract_video_info(video_id: id)
allowed_regions = info
.dig?("microformat", "playerMicroformatRenderer", "availableCountries")
.try &.as_a.map &.as_s || [] of String
if reason = info["reason"]? if reason = info["reason"]?
if reason == "Video unavailable" if reason == "Video unavailable"
raise NotFoundException.new(reason.as_s || "") raise NotFoundException.new(reason.as_s || "")

View File

@ -55,7 +55,7 @@ def extract_video_info(video_id : String)
client_config = YoutubeAPI::ClientConfig.new client_config = YoutubeAPI::ClientConfig.new
# Fetch data from the player endpoint # Fetch data from the player endpoint
player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config) player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
@ -102,7 +102,9 @@ def extract_video_info(video_id : String)
new_player_response = nil new_player_response = nil
if reason.nil? # Don't use Android client if po_token is passed because po_token doesn't
# work for Android client.
if reason.nil? && CONFIG.po_token.nil?
# Fetch the video streams using an Android client in order to get the # Fetch the video streams using an Android client in order to get the
# decrypted URLs and maybe fix throttling issues (#2194). See the # decrypted URLs and maybe fix throttling issues (#2194). See the
# following issue for an explanation about decrypted URLs: # following issue for an explanation about decrypted URLs:
@ -112,7 +114,10 @@ def extract_video_info(video_id : String)
end end
# Last hope # Last hope
if new_player_response.nil? # Only trigger if reason found and po_token or didn't work wth Android client.
# TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required
# if the IP address is not blocked.
if CONFIG.po_token && reason || CONFIG.po_token.nil? && new_player_response.nil?
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
new_player_response = try_fetch_streaming_data(video_id, client_config) new_player_response = try_fetch_streaming_data(video_id, client_config)
end end
@ -424,7 +429,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
"shortDescription" => JSON::Any.new(short_description.try &.as_s || nil), "shortDescription" => JSON::Any.new(short_description.try &.as_s || nil),
# Video metadata # Video metadata
"genre" => JSON::Any.new(genre.try &.as_s || ""), "genre" => JSON::Any.new(genre.try &.as_s || ""),
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s || ""), "genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?),
"license" => JSON::Any.new(license.try &.as_s || ""), "license" => JSON::Any.new(license.try &.as_s || ""),
# Music section # Music section
"music" => JSON.parse(music_list.to_json), "music" => JSON.parse(music_list.to_json),

View File

@ -1,6 +1,6 @@
<div class="flex-right flexible"> <div class="flex-right flexible">
<div class="icon-buttons"> <div class="icon-buttons">
<a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" href="https://www.youtube.com/watch<%=endpoint_params%>"> <a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>">
<i class="icon ion-logo-youtube"></i> <i class="icon ion-logo-youtube"></i>
</a> </a>
<a title="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1"> <a title="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1">

View File

@ -83,7 +83,7 @@
<% if !playlist.is_a? InvidiousPlaylist %> <% if !playlist.is_a? InvidiousPlaylist %>
<div class="pure-u-2-3"> <div class="pure-u-2-3">
<a href="https://www.youtube.com/playlist?list=<%= playlist.id %>"> <a rel="noreferrer noopener" href="https://www.youtube.com/playlist?list=<%= playlist.id %>">
<%= translate(locale, "View playlist on YouTube") %> <%= translate(locale, "View playlist on YouTube") %>
</a> </a>
<span> | </span> <span> | </span>

View File

@ -310,7 +310,7 @@
<div class="pure-control-group"> <div class="pure-control-group">
<label for="modified_source_code_url"><%= translate(locale, "adminprefs_modified_source_code_url_label") %></label> <label for="modified_source_code_url"><%= translate(locale, "adminprefs_modified_source_code_url_label") %></label>
<input name="modified_source_code_url" id="modified_source_code_url" type="input" <% if CONFIG.modified_source_code_url %>checked<% end %>> <input name="modified_source_code_url" id="modified_source_code_url" type="url" value="<%= CONFIG.modified_source_code_url %>">
</div> </div>
<% end %> <% end %>

View File

@ -123,8 +123,8 @@ we're going to need to do it here in order to allow for translations.
link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param) link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param)
end end
-%> -%>
<a id="link-yt-watch" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= translate(locale, "videoinfo_watch_on_youTube") %></a> <a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= translate(locale, "videoinfo_watch_on_youTube") %></a>
(<a id="link-yt-embed" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>) (<a id="link-yt-embed" rel="noreferrer noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>)
</span> </span>
<p id="watch-on-another-invidious-instance"> <p id="watch-on-another-invidious-instance">

View File

@ -24,7 +24,7 @@ struct YoutubeConnectionPool
@pool = build_pool() @pool = build_pool()
end end
def client(&block) def client(&)
conn = pool.checkout conn = pool.checkout
begin begin
response = yield conn response = yield conn
@ -69,7 +69,7 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false)
return client return client
end end
def make_client(url : URI, region = nil, force_resolve : Bool = false, &block) def make_client(url : URI, region = nil, force_resolve : Bool = false, &)
client = make_client(url, region, force_resolve) client = make_client(url, region, force_resolve)
begin begin
yield client yield client

View File

@ -109,7 +109,6 @@ private module Parsers
end end
live_now = false live_now = false
paid = false
premium = false premium = false
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) } premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
@ -856,7 +855,7 @@ end
# #
# This function yields the container so that items can be parsed separately. # This function yields the container so that items can be parsed separately.
# #
def extract_items(initial_data : InitialData, &block) def extract_items(initial_data : InitialData, &)
if unpackaged_data = initial_data["contents"]?.try &.as_h if unpackaged_data = initial_data["contents"]?.try &.as_h
elsif unpackaged_data = initial_data["response"]?.try &.as_h elsif unpackaged_data = initial_data["response"]?.try &.as_h
elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 1).try &.as_h elsif unpackaged_data = initial_data.dig?("onResponseReceivedActions", 1).try &.as_h

View File

@ -83,5 +83,5 @@ end
def extract_selected_tab(tabs) def extract_selected_tab(tabs)
# Extract the selected tab from the array of tabs Youtube returns # Extract the selected tab from the array of tabs Youtube returns
return selected_target = tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"] return tabs.as_a.select(&.["tabRenderer"]?.try &.["selected"]?.try &.as_bool)[0]["tabRenderer"]
end end

View File

@ -272,7 +272,7 @@ module YoutubeAPI
# Return, as a Hash, the "context" data required to request the # Return, as a Hash, the "context" data required to request the
# youtube API endpoints. # youtube API endpoints.
# #
private def make_context(client_config : ClientConfig | Nil) : Hash private def make_context(client_config : ClientConfig | Nil, video_id = "dQw4w9WgXcQ") : Hash
# Use the default client config if nil is passed # Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG client_config ||= DEFAULT_CLIENT_CONFIG
@ -292,7 +292,7 @@ module YoutubeAPI
if client_config.screen == "EMBED" if client_config.screen == "EMBED"
client_context["thirdParty"] = { client_context["thirdParty"] = {
"embedUrl" => "https://www.youtube.com/embed/dQw4w9WgXcQ", "embedUrl" => "https://www.youtube.com/embed/#{video_id}",
} of String => String | Int64 } of String => String | Int64
end end
@ -320,6 +320,10 @@ module YoutubeAPI
client_context["client"]["platform"] = platform client_context["client"]["platform"] = platform
end end
if CONFIG.visitor_data.is_a?(String)
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
end
return client_context return client_context
end end
@ -453,19 +457,32 @@ module YoutubeAPI
params : String, params : String,
client_config : ClientConfig | Nil = nil client_config : ClientConfig | Nil = nil
) )
# Playback context, separate because it can be different between clients
playback_ctx = {
"html5Preference" => "HTML5_PREF_WANTS",
"referer" => "https://www.youtube.com/watch?v=#{video_id}",
} of String => String | Int64
if {"WEB", "TVHTML5"}.any? { |s| client_config.name.starts_with? s }
if sts = DECRYPT_FUNCTION.try &.get_sts
playback_ctx["signatureTimestamp"] = sts.to_i64
end
end
# JSON Request data, required by the API # JSON Request data, required by the API
data = { data = {
"contentCheckOk" => true, "contentCheckOk" => true,
"videoId" => video_id, "videoId" => video_id,
"context" => self.make_context(client_config), "context" => self.make_context(client_config, video_id),
"racyCheckOk" => true, "racyCheckOk" => true,
"user" => { "user" => {
"lockedSafetyMode" => false, "lockedSafetyMode" => false,
}, },
"playbackContext" => { "playbackContext" => {
"contentPlaybackContext" => { "contentPlaybackContext" => playback_ctx,
"html5Preference": "HTML5_PREF_WANTS",
}, },
"serviceIntegrityDimensions" => {
"poToken" => CONFIG.po_token,
}, },
} }
@ -599,6 +616,10 @@ module YoutubeAPI
headers["User-Agent"] = user_agent headers["User-Agent"] = user_agent
end end
if CONFIG.visitor_data.is_a?(String)
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
end
# Logging # Logging
LOGGER.debug("YoutubeAPI: Using endpoint: \"#{endpoint}\"") LOGGER.debug("YoutubeAPI: Using endpoint: \"#{endpoint}\"")
LOGGER.trace("YoutubeAPI: ClientConfig: #{client_config}") LOGGER.trace("YoutubeAPI: ClientConfig: #{client_config}")