mirror of
https://github.com/iv-org/invidious.git
synced 2024-12-22 16:19:53 +05:30
Compare commits
230 Commits
b3637f20a9
...
d3e6d7b6c5
Author | SHA1 | Date | |
---|---|---|---|
|
d3e6d7b6c5 | ||
|
f5244055a2 | ||
|
af86cdbd38 | ||
|
507bed6313 | ||
|
4a92dce449 | ||
|
419dbefd6c | ||
|
d7568ac45a | ||
|
a0cc807821 | ||
|
9060cc4e53 | ||
|
e2a6f5ddf2 | ||
|
ba43365acb | ||
|
f64e311dcd | ||
|
1647092b3c | ||
|
61a18e9894 | ||
|
d9ae22e979 | ||
|
8d6570d809 | ||
|
b4beae7418 | ||
|
0a2d799f6a | ||
|
59cc637c65 | ||
|
1255f5989b | ||
|
efce7c338e | ||
|
b13b7646b7 | ||
|
4645c58712 | ||
|
c46d867f17 | ||
|
75c4c0b349 | ||
|
16b8b6034f | ||
|
93559cbdd5 | ||
|
495ccdc221 | ||
|
e6f5fcbc4b | ||
|
df6b51f9c6 | ||
|
7a569d81ca | ||
|
875b8ea0c2 | ||
|
8e4833d21a | ||
|
c3a3f98014 | ||
|
b06c87ff8d | ||
|
69f23d95b8 | ||
|
3444117818 | ||
|
39ff94362e | ||
|
11ab6ffb32 | ||
|
9dd4195dd0 | ||
|
fcbd5106c3 | ||
|
836898754e | ||
|
d3b04ac68c | ||
|
b2b61ab0a9 | ||
|
62bd895562 | ||
|
8d2ab70cbc | ||
|
3024424ea2 | ||
|
5af87f97a3 | ||
|
96238d719d | ||
|
52c317f235 | ||
|
f954483eac | ||
|
a4ca460651 | ||
|
37bab74085 | ||
|
50d6a2afb9 | ||
|
daccbc2abb | ||
|
d250b4132b | ||
|
3690631cdd | ||
|
3b6474d72b | ||
|
fd3e2aa868 | ||
|
14a5751a47 | ||
|
fda8d2d4d3 | ||
|
46ea22f75c | ||
|
68c26563fc | ||
|
9cec83c1ff | ||
|
281c8ecbf5 | ||
|
1b942f4f0a | ||
|
e7bed765fe | ||
|
7556cb69f2 | ||
|
b5e30d66d4 | ||
|
d9521c82cf | ||
|
505a1566d1 | ||
|
19192b8be1 | ||
|
867d488931 | ||
|
45cc835694 | ||
|
233bd3f593 | ||
|
545a5937d8 | ||
|
35694cc7e3 | ||
|
372192eabc | ||
|
bc06c2fc27 | ||
|
7ea6ec1f52 | ||
|
042ad1f266 | ||
|
bbf16279bb | ||
|
16ac3be85b | ||
|
21f0b90354 | ||
|
928ea75dbc | ||
|
4414c9df70 | ||
|
898066407d | ||
|
381a0e326d | ||
|
193c510c65 | ||
|
f0c8477905 | ||
|
4379a3d873 | ||
|
df85265453 | ||
|
de78848039 | ||
|
e10f6b6626 | ||
|
634e913da9 | ||
|
1b25737b01 | ||
|
8dd1824869 | ||
|
c7876d564f | ||
|
5d176ad6de | ||
|
4b29f8254a | ||
|
c9eafb250f | ||
|
fe97b3d761 | ||
|
ed2d16c91d | ||
|
a727bb037f | ||
|
f0f6cb0d83 | ||
|
e8df08e41e | ||
|
fd06656d86 | ||
|
ea6db9c58a | ||
|
184bd3204f | ||
|
f0120bece1 | ||
|
7e3c685cd6 | ||
|
67a79faaeb | ||
|
11d45adcdc | ||
|
f2cc97b290 | ||
|
e656714542 | ||
|
a79b7ef170 | ||
|
ef4ff4e4b2 | ||
|
9c0c39baed | ||
|
6440ae0b5c | ||
|
e238c08be5 | ||
|
8d434ac06a | ||
|
1333e6db26 | ||
|
8bd2e60abc | ||
|
c713c32ceb | ||
|
12b4dd9191 | ||
|
b2a0e6f1ff | ||
|
d728599251 | ||
|
d6fb5c03b7 | ||
|
3a54e9556b | ||
|
6755e31b72 | ||
|
10fee9da61 | ||
|
b420de6977 | ||
|
febd14f703 | ||
|
92f6a4d546 | ||
|
544fc9f92e | ||
|
c385a944e6 | ||
|
ce1fb8d08c | ||
|
56ebb477ca | ||
|
cca8bcf2a8 | ||
|
f3d9db10a2 | ||
|
46a9ce811a | ||
|
36f7c99cfb | ||
|
720789b622 | ||
|
ce2649420f | ||
|
7aac401407 | ||
|
2d5145614b | ||
|
1eb1bae370 | ||
|
5017176e39 | ||
|
efda154ec8 | ||
|
c1fb320094 | ||
|
90914343ec | ||
|
384a8e200c | ||
|
28584f22c5 | ||
|
1b10446e5e | ||
|
d420741cc1 | ||
|
f298e225a1 | ||
|
97e3938f5f | ||
|
deed4d10f2 | ||
|
70a79f343d | ||
|
e6471feadc | ||
|
49e04192c0 | ||
|
1f12323ee6 | ||
|
732fb7c499 | ||
|
14053821ac | ||
|
346f32855a | ||
|
7d48b96173 | ||
|
b9932b113b | ||
|
72f83d4aa2 | ||
|
919997e41c | ||
|
6667bdcd92 | ||
|
cb0e837a5e | ||
|
e6ba3e3dab | ||
|
f81bc96da0 | ||
|
4c541489dd | ||
|
c60c14851b | ||
|
d857ee5a7c | ||
|
657486c19a | ||
|
9d52ddbf8d | ||
|
d8337252a8 | ||
|
66e671237f | ||
|
d5a516d76c | ||
|
231fb3481e | ||
|
7b4e3639cf | ||
|
778edf63cb | ||
|
fe1648e72e | ||
|
1825b8edb3 | ||
|
a3e587657f | ||
|
4078fc5818 | ||
|
a9fcfcf7c9 | ||
|
4aa2c406ff | ||
|
f46cc98654 | ||
|
ec1d6ee851 | ||
|
9eafbbdcbb | ||
|
5c24bf1322 | ||
|
b97b5b5859 | ||
|
d139334376 | ||
|
155f5fef97 | ||
|
17ecdbaf7d | ||
|
67859113fd | ||
|
eefc8bbbdd | ||
|
ff5e42d836 | ||
|
3cfbc19ccc | ||
|
7afa03d821 | ||
|
0107b774f2 | ||
|
9dfa268204 | ||
|
e24feab1f7 | ||
|
d1b51e57a2 | ||
|
5517a4eadb | ||
|
961cae2b9a | ||
|
adc605024f | ||
|
9a765418d1 | ||
|
b3c0afef02 | ||
|
c421f1f205 | ||
|
fffdaa1410 | ||
|
600da635b7 | ||
|
1da00bade3 | ||
|
73d2ed6f77 | ||
|
a3da03bee9 | ||
|
7755ed4ac8 | ||
|
49ddf8b6bd | ||
|
3341929060 | ||
|
025e755542 | ||
|
b3eea6ab3e | ||
|
20289a4d01 | ||
|
8445d3ae12 | ||
|
6f01d6eacf | ||
|
72d0c9e409 | ||
|
5c7bda66ae | ||
|
96344f28b4 | ||
|
855202e40e |
37
.github/workflows/auto-close-duplicate.yaml
vendored
Normal file
37
.github/workflows/auto-close-duplicate.yaml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
name: Close duplicates
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ubuntu-latest
|
||||
permissions: write-all
|
||||
steps:
|
||||
- uses: iv-org/close-potential-duplicates@v1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Issue title filter work with anymatch https://www.npmjs.com/package/anymatch.
|
||||
# Any matched issue will stop detection immediately.
|
||||
# You can specify multi filters in each line.
|
||||
filter: ''
|
||||
# Exclude keywords in title before detecting.
|
||||
exclude: ''
|
||||
# Label to set, when potential duplicates are detected.
|
||||
label: duplicate
|
||||
# Get issues with state to compare. Supported state: 'all', 'closed', 'open'.
|
||||
state: open
|
||||
# If similarity is higher than this threshold([0,1]), issue will be marked as duplicate.
|
||||
threshold: 0.9
|
||||
# Reactions to be add to comment when potential duplicates are detected.
|
||||
# Available reactions: "-1", "+1", "confused", "laugh", "heart", "hooray", "rocket", "eyes"
|
||||
reactions: ''
|
||||
close: true
|
||||
# Comment to post when potential duplicates are detected.
|
||||
comment: |
|
||||
Hello, your issue is a duplicate of this/these issue(s): {{#issues}}
|
||||
- #{{ number }} [accuracy: {{ accuracy }}%]
|
||||
{{/issues}}
|
||||
|
||||
If this is a mistake please explain why and ping @\unixfox, @\SamantazFox and @\TheFrenchGhosty.
|
||||
|
||||
Please refrain from opening new issues, it won't help in solving your problem.
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -38,10 +38,11 @@ jobs:
|
||||
matrix:
|
||||
stable: [true]
|
||||
crystal:
|
||||
- 1.3.2
|
||||
- 1.4.1
|
||||
- 1.5.1
|
||||
- 1.6.2
|
||||
- 1.7.3
|
||||
- 1.8.1
|
||||
include:
|
||||
- crystal: nightly
|
||||
stable: false
|
||||
|
14
Makefile
14
Makefile
@ -31,6 +31,10 @@ ifeq ($(DISABLE_QUIC), 1)
|
||||
FLAGS += -Ddisable_quic
|
||||
endif
|
||||
|
||||
ifeq ($(API_ONLY), 1)
|
||||
FLAGS += -Dapi_only
|
||||
endif
|
||||
|
||||
|
||||
# -----------------------
|
||||
# Main
|
||||
@ -82,6 +86,7 @@ clean:
|
||||
|
||||
distclean: clean
|
||||
rm -rf libs
|
||||
rm -rf ~/.cache/{crystal,shards}
|
||||
|
||||
|
||||
# -----------------------
|
||||
@ -106,11 +111,12 @@ help:
|
||||
@echo ""
|
||||
@echo "Build options available for this Makefile:"
|
||||
@echo ""
|
||||
@echo " RELEASE Make a release build (Default: 1)"
|
||||
@echo " STATIC Link libraries statically (Default: 0)"
|
||||
@echo " RELEASE Make a release build (Default: 1)"
|
||||
@echo " STATIC Link libraries statically (Default: 0)"
|
||||
@echo ""
|
||||
@echo " DISABLE_QUIC Disable support for QUIC (Default: 0)"
|
||||
@echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)"
|
||||
@echo " API_ONLY Build invidious without a GUI (Default: 0)"
|
||||
@echo " DISABLE_QUIC Disable support for QUIC (Default: 0)"
|
||||
@echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)"
|
||||
|
||||
|
||||
|
||||
|
@ -153,9 +153,9 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
|
||||
- [WatchTube](https://github.com/WatchTubeTeam/WatchTube): Powerful YouTube client for Apple Watch.
|
||||
- [Yattee](https://github.com/yattee/yattee): Alternative YouTube frontend for iPhone, iPad, Mac and Apple TV.
|
||||
- [TubiTui](https://codeberg.org/777/TubiTui): A lightweight, libre, TUI-based YouTube client.
|
||||
- [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API)
|
||||
- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV
|
||||
- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android
|
||||
- [Ytfzf](https://github.com/pystardust/ytfzf): A posix script to find and watch youtube videos from the terminal. (Without API).
|
||||
- [Playlet](https://github.com/iBicha/playlet): Unofficial Youtube client for Roku TV.
|
||||
- [Clipious](https://github.com/lamarios/clipious): Unofficial Invidious client for Android.
|
||||
|
||||
|
||||
## Liability
|
||||
|
@ -46,6 +46,7 @@ body a.channel-owner {
|
||||
}
|
||||
|
||||
.creator-heart {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@ -66,6 +67,7 @@ body a.channel-owner {
|
||||
}
|
||||
|
||||
.creator-heart-small-container {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
@ -119,13 +121,16 @@ body a.pure-button {
|
||||
|
||||
button.pure-button-primary,
|
||||
body a.pure-button-primary,
|
||||
.channel-owner:hover {
|
||||
.channel-owner:hover,
|
||||
.channel-owner:focus {
|
||||
background-color: #a0a0a0;
|
||||
color: rgba(35, 35, 35, 1);
|
||||
}
|
||||
|
||||
button.pure-button-primary:hover,
|
||||
body a.pure-button-primary:hover {
|
||||
body a.pure-button-primary:hover,
|
||||
button.pure-button-primary:focus,
|
||||
body a.pure-button-primary:focus {
|
||||
background-color: rgba(0, 182, 240, 1);
|
||||
color: #fff;
|
||||
}
|
||||
@ -227,6 +232,7 @@ div.watched-indicator {
|
||||
border-radius: 0;
|
||||
|
||||
box-shadow: none;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
@ -317,6 +323,30 @@ p.channel-name { margin: 0; }
|
||||
p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
|
||||
|
||||
|
||||
/*
|
||||
* Comments & community posts
|
||||
*/
|
||||
|
||||
#comments {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.video-iframe-wrapper {
|
||||
position: relative;
|
||||
height: 0;
|
||||
padding-bottom: 56.25%;
|
||||
}
|
||||
|
||||
.video-iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Footer
|
||||
*/
|
||||
@ -365,11 +395,14 @@ span > select {
|
||||
|
||||
.light-theme a:hover,
|
||||
.light-theme a:active,
|
||||
.light-theme summary:hover {
|
||||
.light-theme summary:hover,
|
||||
.light-theme a:focus,
|
||||
.light-theme summary:focus {
|
||||
color: #075A9E !important;
|
||||
}
|
||||
|
||||
.light-theme a.pure-button-primary:hover {
|
||||
.light-theme a.pure-button-primary:hover,
|
||||
.light-theme a.pure-button-primary:focus {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
@ -392,11 +425,14 @@ span > select {
|
||||
@media (prefers-color-scheme: light) {
|
||||
.no-theme a:hover,
|
||||
.no-theme a:active,
|
||||
.no-theme summary:hover {
|
||||
.no-theme summary:hover,
|
||||
.no-theme a:focus,
|
||||
.no-theme summary:focus {
|
||||
color: #075A9E !important;
|
||||
}
|
||||
|
||||
.no-theme a.pure-button-primary:hover {
|
||||
.no-theme a.pure-button-primary:hover,
|
||||
.no-theme a.pure-button-primary:focus {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
@ -423,7 +459,9 @@ span > select {
|
||||
|
||||
.dark-theme a:hover,
|
||||
.dark-theme a:active,
|
||||
.dark-theme summary:hover {
|
||||
.dark-theme summary:hover,
|
||||
.dark-theme a:focus,
|
||||
.dark-theme summary:focus {
|
||||
color: rgb(0, 182, 240);
|
||||
}
|
||||
|
||||
@ -462,7 +500,8 @@ body.dark-theme {
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.no-theme a:hover,
|
||||
.no-theme a:active {
|
||||
.no-theme a:active,
|
||||
.no-theme a:focus {
|
||||
color: rgb(0, 182, 240);
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@
|
||||
color: white;
|
||||
}
|
||||
|
||||
.watch-on-invidious > a:hover {
|
||||
.watch-on-invidious > a:hover,
|
||||
.watch-on-invidious > a:focus {
|
||||
color: rgba(0, 182, 240, 1);;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
Array.prototype.find = Array.prototype.find || function (condition) {
|
||||
return this.filter(condition)[0];
|
||||
};
|
||||
|
||||
Array.from = Array.from || function (source) {
|
||||
return Array.prototype.slice.call(source);
|
||||
};
|
||||
@ -201,15 +202,19 @@ window.helpers = window.helpers || {
|
||||
if (localStorageIsUsable) {
|
||||
return {
|
||||
get: function (key) {
|
||||
if (!localStorage[key]) return;
|
||||
let storageItem = localStorage.getItem(key)
|
||||
if (!storageItem) return;
|
||||
try {
|
||||
return JSON.parse(decodeURIComponent(localStorage[key]));
|
||||
return JSON.parse(decodeURIComponent(storageItem));
|
||||
} catch(e) {
|
||||
// Erase non parsable value
|
||||
helpers.storage.remove(key);
|
||||
}
|
||||
},
|
||||
set: function (key, value) { localStorage[key] = encodeURIComponent(JSON.stringify(value)); },
|
||||
set: function (key, value) {
|
||||
let encoded_value = encodeURIComponent(JSON.stringify(value))
|
||||
localStorage.setItem(key, encoded_value);
|
||||
},
|
||||
remove: function (key) { localStorage.removeItem(key); }
|
||||
};
|
||||
}
|
||||
|
@ -137,7 +137,7 @@
|
||||
if (focused_tag === 'textarea') return;
|
||||
if (focused_tag === 'input') {
|
||||
let focused_type = document.activeElement.type.toLowerCase();
|
||||
if (!focused_type.match(allowed)) return;
|
||||
if (!allowed.test(focused_type)) return;
|
||||
}
|
||||
|
||||
// Focus search bar on '/'
|
||||
|
@ -261,7 +261,7 @@ function updateCookie(newVolume, newSpeed) {
|
||||
var date = new Date();
|
||||
date.setFullYear(date.getFullYear() + 2);
|
||||
|
||||
var ipRegex = /^((\d+\.){3}\d+|[A-Fa-f0-9]*:[A-Fa-f0-9:]*:[A-Fa-f0-9:]+)$/;
|
||||
var ipRegex = /^((\d+\.){3}\d+|[\dA-Fa-f]*:[\d:A-Fa-f]*:[\d:A-Fa-f]+)$/;
|
||||
var domainUsed = location.hostname;
|
||||
|
||||
// Fix for a bug in FF where the leading dot in the FQDN is not ignored
|
||||
|
@ -282,7 +282,7 @@ function get_youtube_replies(target, load_more, load_replies) {
|
||||
if (load_more) {
|
||||
body = body.parentNode.parentNode;
|
||||
body.removeChild(body.lastElementChild);
|
||||
body.innerHTML += response.contentHtml;
|
||||
body.insertAdjacentHTML('beforeend', response.contentHtml);
|
||||
} else {
|
||||
body.removeChild(body.lastElementChild);
|
||||
|
||||
|
@ -255,8 +255,7 @@ https_only: false
|
||||
#registration_enabled: true
|
||||
|
||||
##
|
||||
## Allow/Forbid users to log-in. This setting affects the ability
|
||||
## to connect with BOTH Google and Invidious (local) accounts.
|
||||
## Allow/Forbid users to log-in.
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: true
|
||||
@ -456,13 +455,17 @@ jobs:
|
||||
#use_pubsub_feeds: false
|
||||
|
||||
##
|
||||
## HMAC signing key used for CSRF tokens and pubsub
|
||||
## HMAC signing key used for CSRF tokens, cookies and pubsub
|
||||
## subscriptions verification.
|
||||
##
|
||||
## Note: This parameter is mandatory and should be a random string.
|
||||
## Such random string can be generated on linux with the following
|
||||
## command: `pwgen 20 1`
|
||||
##
|
||||
## Accepted values: a string
|
||||
## Default: <none>
|
||||
##
|
||||
#hmac_key:
|
||||
hmac_key: "CHANGE_ME!!"
|
||||
|
||||
##
|
||||
## List of video IDs where the "download" widget must be
|
||||
@ -817,6 +820,16 @@ default_user_preferences:
|
||||
## Default: true
|
||||
##
|
||||
#vr_mode: true
|
||||
|
||||
##
|
||||
## Save the playback position
|
||||
## Allow to continue watching at the previous position when
|
||||
## watching the same video.
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: false
|
||||
##
|
||||
#save_player_pos: false
|
||||
|
||||
# -----------------------------
|
||||
# Subscription feed
|
||||
|
@ -30,6 +30,7 @@ services:
|
||||
# domain:
|
||||
# https_only: false
|
||||
# statistics_enabled: false
|
||||
hmac_key: "CHANGE_ME!!"
|
||||
healthcheck:
|
||||
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/comments/jNQXAC9IVRw || exit 1
|
||||
interval: 30s
|
||||
|
@ -1 +1,15 @@
|
||||
{}
|
||||
{
|
||||
"generic_views_count": "{{count}} kyk",
|
||||
"generic_views_count_plural": "{{count}} kyke",
|
||||
"generic_videos_count": "{{count}} video",
|
||||
"generic_videos_count_plural": "{{count}} videos",
|
||||
"generic_playlists_count": "{{count}} snitlys",
|
||||
"generic_playlists_count_plural": "{{count}} snitlyste",
|
||||
"generic_subscriptions_count": "{{count}} intekening",
|
||||
"generic_subscriptions_count_plural": "{{count}} intekeninge",
|
||||
"LIVE": "LEWENDIG",
|
||||
"generic_subscribers_count": "{{count}} intekenaar",
|
||||
"generic_subscribers_count_plural": "{{count}} intekenare",
|
||||
"Shared `x` ago": "`x` gelede gedeel",
|
||||
"New passwords must match": "Nuwe wagwoord moet ooreenstem"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "هل تريد محو سجل المشاهدة؟",
|
||||
"New password": "كلمة مرور جديدة",
|
||||
"New passwords must match": "يَجبُ أن تكون كلمتا المرور متطابقتين",
|
||||
"Cannot change password for Google accounts": "لا يُمكن تغيير كلمة المرور لِحسابات جوجل",
|
||||
"Authorize token?": "رمز التفويض؟",
|
||||
"Authorize token for `x`?": "السماح بالرمز المميز ل 'x'؟",
|
||||
"Yes": "نعم",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "المصدر",
|
||||
"Log in": "تسجيل الدخول",
|
||||
"Log in/register": "تسجيل الدخول \\ إنشاء حساب",
|
||||
"Log in with Google": "تسجيل الدخول باستخدام جوجل",
|
||||
"User ID": "مُعرِّف المُستخدم",
|
||||
"Password": "كلمة المرور",
|
||||
"Time (h:mm:ss):": "الوقت (h:mm:ss):",
|
||||
@ -46,13 +44,12 @@
|
||||
"Sign In": "تسجيل الدخول",
|
||||
"Register": "التسجيل",
|
||||
"E-mail": "البريد الإلكتروني",
|
||||
"Google verification code": "رمز تحقق جوجل",
|
||||
"Preferences": "الإعدادات",
|
||||
"preferences_category_player": "إعدادات المُشغِّل",
|
||||
"preferences_video_loop_label": "كرر المقطع المرئيّ دائما: ",
|
||||
"preferences_autoplay_label": "تشغيل تلقائي: ",
|
||||
"preferences_continue_label": "شغل المقطع التالي تلقائيًا: ",
|
||||
"preferences_continue_autoplay_label": "شغل المقطع التالي تلقائيًا: ",
|
||||
"preferences_continue_label": "تشغيل المقطع التالي تلقائيًا: ",
|
||||
"preferences_continue_autoplay_label": "شغل المقطع التالي تلقائيًا: . ",
|
||||
"preferences_listen_label": "تشغيل النسخة السمعية تلقائيًا: ",
|
||||
"preferences_local_label": "بروكسي المقاطع المرئيّة؟ ",
|
||||
"preferences_speed_label": "السرعة الافتراضية: ",
|
||||
@ -158,23 +155,18 @@
|
||||
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع ريديت",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات",
|
||||
"": "عرض `x` تعليقات"
|
||||
"": "عرض `x` تعليقات."
|
||||
},
|
||||
"View Reddit comments": "عرض تعليقات ريديت",
|
||||
"Hide replies": "إخفاء الردود",
|
||||
"Show replies": "عرض الردود",
|
||||
"Incorrect password": "كلمة السر غير صحيحة",
|
||||
"Quota exceeded, try again in a few hours": "تم تجاوز عدد المرات المسموح بها، حاول مجددًا بعد بضع ساعات",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "غير قادر على تسجيل الدخول، تأكد من تشغيل المصادقة الثنائية 2FA.",
|
||||
"Invalid TFA code": "كود مصادقة ثنائية 2FA غير صحيح",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "فشل تسجيل الدخول. قد يكون هذا بسبب أن المصادقة الثنائية 2FA معطلة في حسابك.",
|
||||
"Wrong answer": "إجابة خاطئة",
|
||||
"Erroneous CAPTCHA": "الكابتشا CAPTCHA غير صاحلة",
|
||||
"CAPTCHA is a required field": "مكان الكابتشا CAPTCHA مطلوب",
|
||||
"User ID is a required field": "مكان اسم المستخدم مطلوب",
|
||||
"Password is a required field": "مكان كلمة السر مطلوب",
|
||||
"Wrong username or password": "اسم المستخدم او كلمة السر غير صحيح",
|
||||
"Please sign in using 'Log in with Google'": "الرجاء تسجيل الدخول باستخدام \"تسجيل الدخول باستخدام Google\"",
|
||||
"Password cannot be empty": "لا يمكن أن تكون كلمة السر فارغة",
|
||||
"Password cannot be longer than 55 characters": "يجب أن لا تتعدى كلمة السر 55 حرفًا",
|
||||
"Please log in": "الرجاء تسجيل الدخول",
|
||||
@ -545,5 +537,8 @@
|
||||
"Album: ": "الألبوم: ",
|
||||
"Artist: ": "الفنان: ",
|
||||
"Song: ": "أغنية: ",
|
||||
"Channel Sponsor": "راعي القناة"
|
||||
"Channel Sponsor": "راعي القناة",
|
||||
"Standard YouTube license": "ترخيص YouTube القياسي",
|
||||
"Download is disabled": "تم تعطيل التحميلات",
|
||||
"Import YouTube playlist (.csv)": "استيراد قائمة تشغيل YouTube (.csv)"
|
||||
}
|
||||
|
@ -11,7 +11,6 @@
|
||||
"Clear watch history?": "দেখার ইতিহাস সাফ করবেন?",
|
||||
"New password": "নতুন পাসওয়ার্ড",
|
||||
"New passwords must match": "নতুন পাসওয়ার্ড অবশ্যই মিলতে হবে",
|
||||
"Cannot change password for Google accounts": "গুগল অ্যাকাউন্টগুলোর জন্য পাসওয়ার্ড পরিবর্তন করা যায় না",
|
||||
"Authorize token?": "টোকেন অনুমোদন করবেন?",
|
||||
"Authorize token for `x`?": "`x` -এর জন্য টোকেন অনুমোদন?",
|
||||
"Yes": "হ্যাঁ",
|
||||
@ -34,7 +33,6 @@
|
||||
"source": "সূত্র",
|
||||
"Log in": "লগ ইন",
|
||||
"Log in/register": "লগ ইন/রেজিস্টার",
|
||||
"Log in with Google": "গুগল দিয়ে লগ ইন করুন",
|
||||
"User ID": "ইউজার আইডি",
|
||||
"Password": "পাসওয়ার্ড",
|
||||
"Time (h:mm:ss):": "সময় (ঘণ্টা:মিনিট:সেকেন্ড):",
|
||||
@ -43,7 +41,6 @@
|
||||
"Sign In": "সাইন ইন",
|
||||
"Register": "নিবন্ধন",
|
||||
"E-mail": "ই-মেইল",
|
||||
"Google verification code": "গুগল যাচাইকরণ কোড",
|
||||
"Preferences": "পছন্দসমূহ",
|
||||
"preferences_category_player": "প্লেয়ারের পছন্দসমূহ",
|
||||
"preferences_video_loop_label": "সর্বদা লুপ: ",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "দেখার ইতিহাস সাফ করবেন?",
|
||||
"New password": "নতুন পাসওয়ার্ড",
|
||||
"New passwords must match": "নতুন পাসওয়ার্ড অবশ্যই মিলতে হবে",
|
||||
"Cannot change password for Google accounts": "গুগল অ্যাকাউন্টগুলোর জন্য পাসওয়ার্ড পরিবর্তন করা যায় না",
|
||||
"Authorize token?": "টোকেন অনুমোদন করবেন?",
|
||||
"Authorize token for `x`?": "`x` -এর জন্য টোকেন অনুমোদন?",
|
||||
"Yes": "হ্যাঁ",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "সূত্র",
|
||||
"Log in": "লগ ইন",
|
||||
"Log in/register": "লগ ইন/রেজিস্টার",
|
||||
"Log in with Google": "গুগল দিয়ে লগ ইন করুন",
|
||||
"User ID": "ইউজার আইডি",
|
||||
"Password": "পাসওয়ার্ড",
|
||||
"Time (h:mm:ss):": "সময় (ঘণ্টা:মিনিট:সেকেন্ড):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "সাইন ইন",
|
||||
"Register": "নিবন্ধন",
|
||||
"E-mail": "ই-মেইল",
|
||||
"Google verification code": "গুগল যাচাইকরণ কোড",
|
||||
"Preferences": "পছন্দসমূহ",
|
||||
"preferences_category_player": "প্লেয়ারের পছন্দসমূহ",
|
||||
"preferences_video_loop_label": "সর্বদা লুপ: ",
|
||||
|
@ -4,7 +4,6 @@
|
||||
"preferences_quality_label": "Qualitat de vídeo preferida: ",
|
||||
"newest": "més nou",
|
||||
"No": "No",
|
||||
"Google verification code": "Codi de verificació de Google",
|
||||
"User ID": "ID d'usuari",
|
||||
"Preferences": "Preferències",
|
||||
"Dark mode: ": "Mode fosc: ",
|
||||
@ -66,7 +65,7 @@
|
||||
"Malay": "Malai",
|
||||
"Persian": "Persa",
|
||||
"Slovak": "Eslovac",
|
||||
"Search": "Busca",
|
||||
"Search": "Cerca",
|
||||
"Show annotations": "Mostra anotacions",
|
||||
"preferences_region_label": "País del contingut: ",
|
||||
"preferences_sort_label": "Ordena vídeos per: ",
|
||||
@ -75,7 +74,7 @@
|
||||
"Title": "Títol",
|
||||
"Belarusian": "Bielorús",
|
||||
"Enable web notifications": "Activa notificacions web",
|
||||
"search": "cerca",
|
||||
"search": "Cerca",
|
||||
"Catalan": "Català",
|
||||
"Croatian": "Croat",
|
||||
"preferences_category_admin": "Preferències d'administrador",
|
||||
@ -122,8 +121,8 @@
|
||||
"search_filters_features_option_location": "Ubicació",
|
||||
"search_filters_apply_button": "Aplica els filtres seleccionats",
|
||||
"videoinfo_started_streaming_x_ago": "Ha començat el directe fa `x`",
|
||||
"next_steps_error_message_go_to_youtube": "Anar a YouTube",
|
||||
"footer_donate_page": "Donar",
|
||||
"next_steps_error_message_go_to_youtube": "Vés a YouTube",
|
||||
"footer_donate_page": "Feu un donatiu",
|
||||
"footer_original_source_code": "Codi font original",
|
||||
"videoinfo_watch_on_youTube": "Veure a YouTube",
|
||||
"user_saved_playlists": "`x` llistes de reproducció guardades",
|
||||
@ -137,7 +136,6 @@
|
||||
"channel_tab_channels_label": "Canals",
|
||||
"channel_tab_playlists_label": "Llistes de reproducció",
|
||||
"channel_tab_community_label": "Comunitat",
|
||||
"Invalid TFA code": "Codi TFA no vàlid",
|
||||
"Czech": "Txec",
|
||||
"Default": "Per defecte",
|
||||
"Amharic": "Amàric",
|
||||
@ -164,7 +162,7 @@
|
||||
"crash_page_report_issue": "Si cap de les anteriors no ha ajudat, <a href=\"`x`\">obre un nou issue a GitHub</a> (preferiblement en anglès) i inclou el text següent al missatge (NO tradueixis aquest text):",
|
||||
"generic_subscriptions_count": "{{count}} subscripció",
|
||||
"generic_subscriptions_count_plural": "{{count}} subscripcions",
|
||||
"error_video_not_in_playlist": "El vídeo sol·licitat no existeix en aquesta llista de reproducció. <a href=\"`x`\">Fes clic aquí per a la pàgina d'inici de la llista de reproducció.</a>",
|
||||
"error_video_not_in_playlist": "El vídeo sol·licitat no existeix en aquesta llista de reproducció. <a href=\"`x`\">Feu clic aquí per a la pàgina d'inici de la llista de reproducció.</a>",
|
||||
"comments_points_count": "{{count}} punt",
|
||||
"comments_points_count_plural": "{{count}} punts",
|
||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||
@ -175,7 +173,7 @@
|
||||
"preferences_unseen_only_label": "Mostra només no vistos: ",
|
||||
"preferences_listen_label": "Escolta per defecte: ",
|
||||
"Import": "Importar",
|
||||
"Token": "Senyal",
|
||||
"Token": "Testimoni",
|
||||
"Wilson score: ": "Puntuació de Wilson: ",
|
||||
"search_filters_date_label": "Data de càrrega",
|
||||
"search_filters_features_option_three_sixty": "360°",
|
||||
@ -184,10 +182,9 @@
|
||||
"preferences_comments_label": "Comentaris per defecte: ",
|
||||
"`x` uploaded a video": "`x` ha penjat un vídeo",
|
||||
"Released under the AGPLv3 on Github.": "Publicat sota l'AGPLv3 a GitHub.",
|
||||
"Token manager": "Gestor de tokens",
|
||||
"Token manager": "Gestor de testimonis",
|
||||
"Watch history": "Historial de reproduccions",
|
||||
"Cannot change password for Google accounts": "No es pot canviar la contrasenya dels comptes de Google",
|
||||
"Authorize token?": "Autoritzar senyal?",
|
||||
"Authorize token?": "Autoritzar testimoni?",
|
||||
"Source available here.": "Font disponible aquí.",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporta subscripcions com a OPML (per a NewPipe i FreeTube)",
|
||||
"Log in": "Inicia sessió",
|
||||
@ -197,7 +194,7 @@
|
||||
"Public": "Públic",
|
||||
"View all playlists": "Veure totes les llistes de reproducció",
|
||||
"reddit": "Reddit",
|
||||
"Manage tokens": "Gestiona senyals",
|
||||
"Manage tokens": "Gestiona testimonis",
|
||||
"Not a playlist.": "No és una llista de reproducció.",
|
||||
"preferences_local_label": "Vídeos de Proxy: ",
|
||||
"View channel on YouTube": "Veure canal a Youtube",
|
||||
@ -225,7 +222,6 @@
|
||||
},
|
||||
"View Reddit comments": "Veure comentaris de Reddit",
|
||||
"Incorrect password": "Contrasenya incorrecta",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No es pot iniciar la sessió, assegureu-vos que l'autenticació de dos factors (Autenticador o SMS) estigui activada.",
|
||||
"Erroneous CAPTCHA": "CAPTCHA erroni",
|
||||
"CAPTCHA is a required field": "El CAPTCHA és un camp obligatori",
|
||||
"Korean (auto-generated)": "Coreà (generat automàticament)",
|
||||
@ -272,7 +268,6 @@
|
||||
"Khmer": "Khmer",
|
||||
"This channel does not exist.": "Aquest canal no existeix.",
|
||||
"Song: ": "Cançó: ",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Error a l'iniciar sessió. Això pot ser perquè l'autenticació de dos factors no està activada per al vostre compte.",
|
||||
"channel:`x`": "canal: `x`",
|
||||
"Deleted or invalid channel": "Canal suprimit o no vàlid",
|
||||
"Could not get channel info.": "No s'ha pogut obtenir la informació del canal.",
|
||||
@ -291,17 +286,16 @@
|
||||
"User ID is a required field": "L'identificador d'usuari és un camp obligatori",
|
||||
"Password is a required field": "La contrasenya és un camp obligatori",
|
||||
"Wrong username or password": "Nom d'usuari o contrasenya incorrectes",
|
||||
"Please sign in using 'Log in with Google'": "Si us plau, inicieu la sessió amb 'Inicieu sessió amb Google'",
|
||||
"Password cannot be longer than 55 characters": "La contrasenya no pot tenir més de 55 caràcters",
|
||||
"Invidious Private Feed for `x`": "Feed privat Invidious per a `x`",
|
||||
"generic_views_count": "{{count}} visualització",
|
||||
"generic_views_count_plural": "{{count}} visualitzacions",
|
||||
"generic_videos_count": "{{count}} vídeo",
|
||||
"generic_videos_count_plural": "{{count}} vídeos",
|
||||
"Token is expired, please try again": "La senyal ha caducat, torna-ho a provar",
|
||||
"Token is expired, please try again": "El testimoni ha caducat, torna-ho a provar",
|
||||
"English": "Anglès",
|
||||
"Kannada": "Kanarès",
|
||||
"Erroneous token": "Senyal errònia",
|
||||
"Erroneous token": "Testimoni erroni",
|
||||
"`x` ago": "fa `x`",
|
||||
"Empty playlist": "Llista de reproducció buida",
|
||||
"Playlist does not exist.": "La llista de reproducció no existeix.",
|
||||
@ -376,7 +370,7 @@
|
||||
"Clear watch history": "Neteja l'historial de reproduccions",
|
||||
"Mongolian": "Mongol",
|
||||
"preferences_quality_dash_option_best": "Millor",
|
||||
"Authorize token for `x`?": "Autoritzar senyal per a `x`?",
|
||||
"Authorize token for `x`?": "Autoritzar testimoni per a `x`?",
|
||||
"Report statistics: ": "Estadístiques de l'informe: ",
|
||||
"Switch Invidious Instance": "Canvia la instància d'Invidious",
|
||||
"History": "Historial",
|
||||
@ -410,7 +404,7 @@
|
||||
"Export": "Exportar",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"JavaScript license information": "Informació de la llicència de JavaScript",
|
||||
"Hidden field \"token\" is a required field": "El camp ocult \"senyal\" és un camp obligatori",
|
||||
"Hidden field \"token\" is a required field": "El camp ocult \"testimoni\" és un camp obligatori",
|
||||
"Shona": "Xona",
|
||||
"Family friendly? ": "Apte per a tots els públics? ",
|
||||
"preferences_quality_dash_label": "Qualitat de vídeo DASH preferida: ",
|
||||
@ -436,16 +430,14 @@
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"preferences_quality_dash_option_720p": "720p",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
"Log in with Google": "Inicia sessió amb Google",
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"Previous page": "Pàgina anterior",
|
||||
"Only show latest video from channel: ": "Mostra només l'últim vídeo del canal: ",
|
||||
"unsubscribe": "cancel·la la subscripció",
|
||||
"View playlist on YouTube": "Veure llista de reproducció a YouTube",
|
||||
"Import NewPipe subscriptions (.json)": "Importar subscripcions de NewPipe (.json)",
|
||||
"crash_page_you_found_a_bug": "Sembla que has trobat un error a Invidious!",
|
||||
"crash_page_you_found_a_bug": "Heu trobat un error a Invidious!",
|
||||
"Subscribe": "Subscriu-me",
|
||||
"Quota exceeded, try again in a few hours": "S'ha superat la quota, torna-ho a provar d'aquí a unes hores",
|
||||
"generic_count_days": "{{count}} dia",
|
||||
"generic_count_days_plural": "{{count}} dies",
|
||||
"Trending": "Tendència",
|
||||
@ -468,8 +460,8 @@
|
||||
"revoke": "revocar",
|
||||
"English (United Kingdom)": "Anglès (Regne Unit)",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
"tokens_count": "{{count}} senyal",
|
||||
"tokens_count_plural": "{{count}} senyals",
|
||||
"tokens_count": "{{count}} testimoni",
|
||||
"tokens_count_plural": "{{count}} testimonis",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} notificació no vista",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} notificacions no vistes",
|
||||
"generic_subscribers_count": "{{count}} subscriptor",
|
||||
@ -481,5 +473,8 @@
|
||||
"Top": "Millors",
|
||||
"preferences_max_results_label": "Nombre de vídeos mostrats al feed: ",
|
||||
"Engagement: ": "Atracció: ",
|
||||
"Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: "
|
||||
"Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: ",
|
||||
"Standard YouTube license": "Llicència estàndard de YouTube",
|
||||
"Download is disabled": "Les baixades s'han inhabilitat",
|
||||
"Import YouTube playlist (.csv)": "Importar llista de reproducció de YouTube (.csv)"
|
||||
}
|
||||
|
@ -13,8 +13,7 @@
|
||||
"Previous page": "Předchozí strana",
|
||||
"Clear watch history?": "Smazat historii?",
|
||||
"New password": "Nové heslo",
|
||||
"New passwords must match": "Hesla se musí schodovat",
|
||||
"Cannot change password for Google accounts": "Nelze změnit heslo pro účty Google",
|
||||
"New passwords must match": "Hesla se musí shodovat",
|
||||
"Authorize token?": "Autorizovat token?",
|
||||
"Authorize token for `x`?": "Autorizovat token pro `x`?",
|
||||
"Yes": "Ano",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "zdrojový kód",
|
||||
"Log in": "Přihlásit se",
|
||||
"Log in/register": "Přihlásit se/vytvořit účet",
|
||||
"Log in with Google": "Přihlásit se s Googlem",
|
||||
"User ID": "ID uživatele",
|
||||
"Password": "Heslo",
|
||||
"Time (h:mm:ss):": "Čas (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Přihlásit se",
|
||||
"Register": "Vytvořit účet",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Verifikační číslo Google",
|
||||
"Preferences": "Nastavení",
|
||||
"preferences_category_player": "Nastavení přehravače",
|
||||
"preferences_video_loop_label": "Vždy opakovat: ",
|
||||
@ -335,7 +332,6 @@
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"invidious": "Invidious",
|
||||
"View more comments on Reddit": "Zobrazit více komentářů na Redditu",
|
||||
"Invalid TFA code": "Nesprávný TFA kód",
|
||||
"generic_playlists_count_0": "{{count}} playlist",
|
||||
"generic_playlists_count_1": "{{count}} playlisty",
|
||||
"generic_playlists_count_2": "{{count}} playlistů",
|
||||
@ -349,7 +345,6 @@
|
||||
"subscriptions_unseen_notifs_count_1": "{{count}} nezobrazená oznámení",
|
||||
"subscriptions_unseen_notifs_count_2": "{{count}} nezobrazených oznámení",
|
||||
"Show replies": "Zobrazit odpovědi",
|
||||
"Quota exceeded, try again in a few hours": "Kvóta překročena, zkuste to znovu za pár hodin",
|
||||
"Password cannot be longer than 55 characters": "Heslo nesmí být delší než 55 znaků",
|
||||
"comments_view_x_replies_0": "Zobrazit {{count}} odpověď",
|
||||
"comments_view_x_replies_1": "Zobrazit {{count}} odpovědi",
|
||||
@ -433,7 +428,6 @@
|
||||
"View YouTube comments": "Zobrazit YouTube komentáře",
|
||||
"Blacklisted regions: ": "Oblasti na černé listině: ",
|
||||
"Wrong username or password": "Nesprávné uživatelské jméno nebo heslo",
|
||||
"Please sign in using 'Log in with Google'": "Přihlaste se prosím pomocí Googlu",
|
||||
"Password cannot be empty": "Heslo nemůže být prázné",
|
||||
"preferences_category_misc": "Různá nastavení",
|
||||
"preferences_show_nick_label": "Zobrazit přezdívku na vrchu: ",
|
||||
@ -452,8 +446,6 @@
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Zobrazit `x` komentář",
|
||||
"": "Zobrazit `x` komentářů"
|
||||
},
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nepodařilo se přihlásit, ujistěte se, že je povoleno dvoufázové ověřování (autentifikátor nebo SMS).",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Přihlášení selhalo. Toto se může stát, když není na vašem účtu povolené dvoufázové ověřování.",
|
||||
"Could not get channel info.": "Nepodařilo se získat informace o kanálu.",
|
||||
"Could not fetch comments": "Nepodařilo se získat komentáře",
|
||||
"Could not create mix.": "Nepodařilo se vytvořit mix.",
|
||||
@ -497,5 +489,8 @@
|
||||
"Artist: ": "Umělec: ",
|
||||
"Album: ": "Album: ",
|
||||
"Channel Sponsor": "Sponzor kanálu",
|
||||
"Song: ": "Skladba: "
|
||||
"Song: ": "Skladba: ",
|
||||
"Standard YouTube license": "Standardní licence YouTube",
|
||||
"Download is disabled": "Stahování je zakázáno",
|
||||
"Import YouTube playlist (.csv)": "Importovat YouTube playlist (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Ryd afspilningshistorik?",
|
||||
"New password": "Nyt kodeord",
|
||||
"New passwords must match": "Nye kodeord skal matche",
|
||||
"Cannot change password for Google accounts": "Kan ikke skifte kodeord til Google-konti",
|
||||
"Authorize token?": "Godkend token?",
|
||||
"Authorize token for `x`?": "Godkend token til `x`?",
|
||||
"Yes": "Ja",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "kilde",
|
||||
"Log in": "Log på",
|
||||
"Log in/register": "Log på/registrer",
|
||||
"Log in with Google": "Log på med Google",
|
||||
"User ID": "Bruger ID",
|
||||
"Password": "Kodeord",
|
||||
"Time (h:mm:ss):": "Tid (t:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Log ind",
|
||||
"Register": "Registrer",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Google-verifikationskode",
|
||||
"Preferences": "Præferencer",
|
||||
"preferences_category_player": "Afspillerindstillinger",
|
||||
"preferences_video_loop_label": "Altid gentag: ",
|
||||
@ -159,17 +156,12 @@
|
||||
"Hide replies": "Skjul svar",
|
||||
"Show replies": "Vis svar",
|
||||
"Incorrect password": "Forkert adgangskode",
|
||||
"Quota exceeded, try again in a few hours": "Kvota overskredet, prøv igen om et par timer",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Login fejlet, tjek at totrinsbekræftelse (Authenticator eller SMS) er slået til.",
|
||||
"Invalid TFA code": "Ugyldig TFA kode",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Login fejlede. Dette kan skyldes, at to-faktor autentificering ikke er aktiveret for din konto.",
|
||||
"Wrong answer": "Forkert svar",
|
||||
"Erroneous CAPTCHA": "Fejlagtig CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA er et obligatorisk felt",
|
||||
"User ID is a required field": "Bruger ID er et krævet felt",
|
||||
"Password is a required field": "Adgangskode er et obligatorisk felt",
|
||||
"Wrong username or password": "Forkert brugernavn eller adgangskode",
|
||||
"Please sign in using 'Log in with Google'": "Log ind via 'Log ind med Google'",
|
||||
"Password cannot be empty": "Adgangskoden må ikke være tom",
|
||||
"Password cannot be longer than 55 characters": "Adgangskoden må ikke være længere end 55 tegn",
|
||||
"Please log in": "Venligst log ind",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Verlauf löschen?",
|
||||
"New password": "Neues Passwort",
|
||||
"New passwords must match": "Neue Passwörter müssen übereinstimmen",
|
||||
"Cannot change password for Google accounts": "Ich kann das Passwort deines Google Kontos nicht ändern",
|
||||
"Authorize token?": "Token autorisieren?",
|
||||
"Authorize token for `x`?": "Token für `x` autorisieren?",
|
||||
"Yes": "Ja",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "Quelle",
|
||||
"Log in": "Anmelden",
|
||||
"Log in/register": "Anmelden/registrieren",
|
||||
"Log in with Google": "Mit Google anmelden",
|
||||
"User ID": "Benutzer-ID",
|
||||
"Password": "Passwort",
|
||||
"Time (h:mm:ss):": "Zeit (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Anmelden",
|
||||
"Register": "Registrieren",
|
||||
"E-mail": "E-Mail",
|
||||
"Google verification code": "Google-Bestätigungscode",
|
||||
"Preferences": "Einstellungen",
|
||||
"preferences_category_player": "Wiedergabeeinstellungen",
|
||||
"preferences_video_loop_label": "Immer wiederholen: ",
|
||||
@ -164,17 +161,12 @@
|
||||
"Hide replies": "Antworten verstecken",
|
||||
"Show replies": "Antworten anzeigen",
|
||||
"Incorrect password": "Falsches Passwort",
|
||||
"Quota exceeded, try again in a few hours": "Kontingent überschritten, versuche es in ein paar Stunden erneut",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Anmeldung nicht möglich, stellen Sie sicher, dass die Zwei-Faktor-Authentisierung (Authenticator oder SMS) aktiviert ist.",
|
||||
"Invalid TFA code": "Ungültiger TFA Code",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Die Anmeldung ist fehlgeschlagen. Dies kann daran liegen, dass die Zwei-Faktor-Authentisierung für Ihr Konto nicht aktiviert ist.",
|
||||
"Wrong answer": "Ungültige Antwort",
|
||||
"Erroneous CAPTCHA": "Ungültiges CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA ist eine erforderliche Eingabe",
|
||||
"User ID is a required field": "Benutzer ID ist eine erforderliche Eingabe",
|
||||
"Password is a required field": "Passwort ist eine erforderliche Eingabe",
|
||||
"Wrong username or password": "Ungültiger Benutzername oder Passwort",
|
||||
"Please sign in using 'Log in with Google'": "Bitte melden Sie sich mit „Mit Google anmelden“ an",
|
||||
"Password cannot be empty": "Passwort darf nicht leer sein",
|
||||
"Password cannot be longer than 55 characters": "Passwort darf nicht länger als 55 Zeichen sein",
|
||||
"Please log in": "Bitte anmelden",
|
||||
@ -433,7 +425,7 @@
|
||||
"comments_points_count_plural": "{{count}} Punkte",
|
||||
"crash_page_you_found_a_bug": "Anscheinend haben Sie einen Fehler in Invidious gefunden!",
|
||||
"generic_count_months": "{{count}} Monat",
|
||||
"generic_count_months_plural": "{{count}} Monate",
|
||||
"generic_count_months_plural": "{{count}} Monaten",
|
||||
"Cantonese (Hong Kong)": "Kantonesisch (Hong Kong)",
|
||||
"Chinese (Hong Kong)": "Chinesisch (Hong Kong)",
|
||||
"generic_playlists_count": "{{count}} Wiedergabeliste",
|
||||
@ -479,5 +471,10 @@
|
||||
"Artist: ": "Künstler: ",
|
||||
"Album: ": "Album: ",
|
||||
"channel_tab_playlists_label": "Wiedergabelisten",
|
||||
"channel_tab_channels_label": "Kanäle"
|
||||
"channel_tab_channels_label": "Kanäle",
|
||||
"Channel Sponsor": "Kanalsponsor",
|
||||
"Standard YouTube license": "Standard YouTube-Lizenz",
|
||||
"Song: ": "Musik: ",
|
||||
"Download is disabled": "Herunterladen ist deaktiviert",
|
||||
"Import YouTube playlist (.csv)": "YouTube Playlist Importieren (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Διαγραφή ιστορικού προβολής;",
|
||||
"New password": "Νέος κωδικός πρόσβασης",
|
||||
"New passwords must match": "Οι νέοι κωδικοί πρόσβασης πρέπει να ταιριάζουν",
|
||||
"Cannot change password for Google accounts": "Δεν επιτρέπεται η αλλαγή κωδικού πρόσβασης λογαριασμών Google",
|
||||
"Authorize token?": "Εξουσιοδότηση διασύνδεσης;",
|
||||
"Authorize token for `x`?": "Εξουσιοδότηση διασύνδεσης με `x`;",
|
||||
"Yes": "Ναι",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "πηγή",
|
||||
"Log in": "Σύνδεση",
|
||||
"Log in/register": "Σύνδεση/εγγραφή",
|
||||
"Log in with Google": "Σύνδεση με Google",
|
||||
"User ID": "Ταυτότητα χρήστη",
|
||||
"Password": "Κωδικός πρόσβασης",
|
||||
"Time (h:mm:ss):": "Ώρα (ω:λλ:δδ):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Σύνδεση",
|
||||
"Register": "Εγγραφή",
|
||||
"E-mail": "Ηλεκτρονικό ταχυδρομείο",
|
||||
"Google verification code": "Κωδικός επαλήθευσης Google",
|
||||
"Preferences": "Προτιμήσεις",
|
||||
"preferences_category_player": "Προτιμήσεις αναπαραγωγής",
|
||||
"preferences_video_loop_label": "Αυτόματη επανάληψη: ",
|
||||
@ -155,17 +152,12 @@
|
||||
"Hide replies": "Απόκρυψη απαντήσεων",
|
||||
"Show replies": "Προβολή απαντήσεων",
|
||||
"Incorrect password": "Λανθασμένος κωδικός πρόσβασης",
|
||||
"Quota exceeded, try again in a few hours": "Έχετε υπερβεί το όριο προσπαθειών, δοκιμάστε ξανα σε λίγες ώρες",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Αδυναμία σύνδεσης, βεβαιωθείτε πως ο έλεγχος ταυτότητας δύο παραγόντων (με Authenticator ή SMS) είναι ενεργοποιημένος.",
|
||||
"Invalid TFA code": "Μη έγκυρος κωδικός ελέγχου ταυτότητας δύο παραγόντων",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Αποτυχία σύνδεσης. Ίσως ευθύνεται η έλλειψη ελέγχου ταυτότητας δύο παραγόντων για το λογαριασμό σας.",
|
||||
"Wrong answer": "Λανθασμένη απάντηση",
|
||||
"Erroneous CAPTCHA": "Λανθασμένο CAPTCHA",
|
||||
"CAPTCHA is a required field": "Το CAPTCHA είναι απαιτούμενο πεδίο",
|
||||
"User ID is a required field": "Η ταυτότητα χρήστη είναι απαιτούμενο πεδίο",
|
||||
"Password is a required field": "Ο κωδικός πρόσβασης είναι απαιτούμενο πεδίο",
|
||||
"Wrong username or password": "Λανθασμένο όνομα χρήστη ή κωδικός πρόσβασης",
|
||||
"Please sign in using 'Log in with Google'": "Συνδεθείτε με την επιλογή 'Σύνδεση με Google'",
|
||||
"Password cannot be empty": "Ο κωδικός πρόσβασης δεν γίνεται να είναι κενός",
|
||||
"Password cannot be longer than 55 characters": "Ο κωδικός πρόσβασης δεν γίνεται να υπερβαίνει τους 55 χαρακτήρες",
|
||||
"Please log in": "Συνδεθείτε",
|
||||
|
@ -24,7 +24,6 @@
|
||||
"Clear watch history?": "Clear watch history?",
|
||||
"New password": "New password",
|
||||
"New passwords must match": "New passwords must match",
|
||||
"Cannot change password for Google accounts": "Cannot change password for Google accounts",
|
||||
"Authorize token?": "Authorize token?",
|
||||
"Authorize token for `x`?": "Authorize token for `x`?",
|
||||
"Yes": "Yes",
|
||||
@ -33,6 +32,7 @@
|
||||
"Import": "Import",
|
||||
"Import Invidious data": "Import Invidious JSON data",
|
||||
"Import YouTube subscriptions": "Import YouTube/OPML subscriptions",
|
||||
"Import YouTube playlist (.csv)": "Import YouTube playlist (.csv)",
|
||||
"Import FreeTube subscriptions (.db)": "Import FreeTube subscriptions (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Import NewPipe subscriptions (.json)",
|
||||
"Import NewPipe data (.zip)": "Import NewPipe data (.zip)",
|
||||
@ -47,7 +47,6 @@
|
||||
"source": "source",
|
||||
"Log in": "Log in",
|
||||
"Log in/register": "Log in/register",
|
||||
"Log in with Google": "Log in with Google",
|
||||
"User ID": "User ID",
|
||||
"Password": "Password",
|
||||
"Time (h:mm:ss):": "Time (h:mm:ss):",
|
||||
@ -56,7 +55,6 @@
|
||||
"Sign In": "Sign In",
|
||||
"Register": "Register",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Google verification code",
|
||||
"Preferences": "Preferences",
|
||||
"preferences_category_player": "Player preferences",
|
||||
"preferences_video_loop_label": "Always loop: ",
|
||||
@ -207,17 +205,12 @@
|
||||
"Hide replies": "Hide replies",
|
||||
"Show replies": "Show replies",
|
||||
"Incorrect password": "Incorrect password",
|
||||
"Quota exceeded, try again in a few hours": "Quota exceeded, try again in a few hours",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.",
|
||||
"Invalid TFA code": "Invalid TFA code",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Login failed. This may be because two-factor authentication is not turned on for your account.",
|
||||
"Wrong answer": "Wrong answer",
|
||||
"Erroneous CAPTCHA": "Erroneous CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA is a required field",
|
||||
"User ID is a required field": "User ID is a required field",
|
||||
"Password is a required field": "Password is a required field",
|
||||
"Wrong username or password": "Wrong username or password",
|
||||
"Please sign in using 'Log in with Google'": "Please sign in using 'Log in with Google'",
|
||||
"Password cannot be empty": "Password cannot be empty",
|
||||
"Password cannot be longer than 55 characters": "Password cannot be longer than 55 characters",
|
||||
"Please log in": "Please log in",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Ĉu forigi vidohistorion?",
|
||||
"New password": "Nova pasvorto",
|
||||
"New passwords must match": "Novaj pasvortoj devas kongrui",
|
||||
"Cannot change password for Google accounts": "Ne eblas ŝanĝi pasvorton por kontoj de Google",
|
||||
"Authorize token?": "Ĉu rajtigi ĵetonon?",
|
||||
"Authorize token for `x`?": "Ĉu rajtigi ĵetonon por `x`?",
|
||||
"Yes": "Jes",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "fonto",
|
||||
"Log in": "Ensaluti",
|
||||
"Log in/register": "Ensaluti/Registriĝi",
|
||||
"Log in with Google": "Ensaluti al Google",
|
||||
"User ID": "Uzula identigilo",
|
||||
"Password": "Pasvorto",
|
||||
"Time (h:mm:ss):": "Horo (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Ensaluti",
|
||||
"Register": "Registriĝi",
|
||||
"E-mail": "Retpoŝto",
|
||||
"Google verification code": "Kontrolkodo de Google",
|
||||
"Preferences": "Agordoj",
|
||||
"preferences_category_player": "Spektilaj agordoj",
|
||||
"preferences_video_loop_label": "Ĉiam ripeti: ",
|
||||
@ -164,17 +161,12 @@
|
||||
"Hide replies": "Kaŝi respondojn",
|
||||
"Show replies": "Montri respondojn",
|
||||
"Incorrect password": "Malbona pasvorto",
|
||||
"Quota exceeded, try again in a few hours": "Kvoto transpasita, provu denove post iuj horoj",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ne povas ensaluti, certigu, ke dufaktora aŭtentigo (Authenticator aŭ SMS) estas ebligita.",
|
||||
"Invalid TFA code": "Nevalida TFA-kodo",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Ensalutado fiaskis. Eble ĉar la dufaktora aŭtentigo estas malebligita en via konto.",
|
||||
"Wrong answer": "Nevalida respondo",
|
||||
"Erroneous CAPTCHA": "Nevalida CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA estas deviga kampo",
|
||||
"User ID is a required field": "Uzula identigilo estas deviga kampo",
|
||||
"Password is a required field": "Pasvorto estas deviga kampo",
|
||||
"Wrong username or password": "Nevalida uzantnomo aŭ pasvorto",
|
||||
"Please sign in using 'Log in with Google'": "Bonvolu ensaluti per 'Ensaluti per Google'",
|
||||
"Password cannot be empty": "Pasvorto ne povas esti malplena",
|
||||
"Password cannot be longer than 55 characters": "Pasvorto ne povas esti pli longa ol 55 signoj",
|
||||
"Please log in": "Bonvolu ensaluti",
|
||||
@ -479,5 +471,10 @@
|
||||
"channel_tab_shorts_label": "Mallongaj",
|
||||
"Music in this video": "Muziko en ĉi tiu video",
|
||||
"Artist: ": "Artisto: ",
|
||||
"Album: ": "Albumo: "
|
||||
"Album: ": "Albumo: ",
|
||||
"Channel Sponsor": "Kanala sponsoro",
|
||||
"Song: ": "Muzikaĵo: ",
|
||||
"Standard YouTube license": "Implicita YouTube-licenco",
|
||||
"Download is disabled": "Elŝuto estas malebligita",
|
||||
"Import YouTube playlist (.csv)": "Importi YouTube-ludliston (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "¿Quiere borrar el historial de reproducción?",
|
||||
"New password": "Nueva contraseña",
|
||||
"New passwords must match": "Las nuevas contraseñas deben coincidir",
|
||||
"Cannot change password for Google accounts": "No se puede cambiar la contraseña de la cuenta de Google",
|
||||
"Authorize token?": "¿Autorizar el token?",
|
||||
"Authorize token for `x`?": "¿Autorizar el token para `x`?",
|
||||
"Yes": "Sí",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "código fuente",
|
||||
"Log in": "Iniciar sesión",
|
||||
"Log in/register": "Iniciar sesión/Registrarse",
|
||||
"Log in with Google": "Iniciar sesión en Google",
|
||||
"User ID": "Nombre",
|
||||
"Password": "Contraseña",
|
||||
"Time (h:mm:ss):": "Hora (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Iniciar sesión",
|
||||
"Register": "Registrarse",
|
||||
"E-mail": "Correo",
|
||||
"Google verification code": "Código de verificación de Google",
|
||||
"Preferences": "Preferencias",
|
||||
"preferences_category_player": "Preferencias del reproductor",
|
||||
"preferences_video_loop_label": "Repetir siempre: ",
|
||||
@ -164,17 +161,12 @@
|
||||
"Hide replies": "Ocultar las respuestas",
|
||||
"Show replies": "Mostrar las respuestas",
|
||||
"Incorrect password": "Contraseña incorrecta",
|
||||
"Quota exceeded, try again in a few hours": "Cuota excedida, prueba otra vez en unas horas",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "No se puede iniciar sesión, asegúrese de que la autentificación de dos factores (autentificador o SMS) esté habilitada.",
|
||||
"Invalid TFA code": "Código TFA no válido",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Error de inicio de sesion. Puede deberse a que la autentificación de dos factores no está habilitada en su cuenta.",
|
||||
"Wrong answer": "Respuesta no válida",
|
||||
"Erroneous CAPTCHA": "CAPTCHA no válido",
|
||||
"CAPTCHA is a required field": "El CAPTCHA es un campo obligatorio",
|
||||
"User ID is a required field": "El nombre es un campo obligatorio",
|
||||
"Password is a required field": "La contraseña es un campo obligatorio",
|
||||
"Wrong username or password": "Nombre o contraseña incorrecto",
|
||||
"Please sign in using 'Log in with Google'": "Inicie sesión con «Iniciar sesión con Google»",
|
||||
"Password cannot be empty": "La contraseña no puede estar en blanco",
|
||||
"Password cannot be longer than 55 characters": "La contraseña no debe tener más de 55 caracteres",
|
||||
"Please log in": "Inicie sesión, por favor",
|
||||
@ -413,10 +405,9 @@
|
||||
"generic_count_weeks": "{{count}} semana",
|
||||
"generic_count_weeks_plural": "{{count}} semanas",
|
||||
"generic_playlists_count": "{{count}} lista de reproducción",
|
||||
"generic_playlists_count_plural": "{{count}} listas de reproducción",
|
||||
"generic_videos_count_0": "{{count}} video",
|
||||
"generic_videos_count_1": "{{count}} videos",
|
||||
"generic_videos_count_2": "{{count}} videos",
|
||||
"generic_playlists_count_plural": "{{count}} listas de reproducciones",
|
||||
"generic_videos_count": "{{count}} video",
|
||||
"generic_videos_count_plural": "{{count}} video",
|
||||
"generic_count_months": "{{count}} mes",
|
||||
"generic_count_months_plural": "{{count}} meses",
|
||||
"comments_points_count": "{{count}} punto",
|
||||
@ -469,8 +460,8 @@
|
||||
"search_filters_duration_option_none": "Cualquier duración",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_apply_button": "Aplicar filtros",
|
||||
"tokens_count": "{{count}} ficha",
|
||||
"tokens_count_plural": "{{count}} fichas",
|
||||
"tokens_count": "{{count}} token",
|
||||
"tokens_count_plural": "{{count}} tokens",
|
||||
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
|
||||
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
||||
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
|
||||
@ -482,5 +473,8 @@
|
||||
"Artist: ": "Artista: ",
|
||||
"Album: ": "Álbum: ",
|
||||
"Song: ": "Canción: ",
|
||||
"Channel Sponsor": "Patrocinador del canal"
|
||||
"Channel Sponsor": "Patrocinador del canal",
|
||||
"Standard YouTube license": "Licencia de YouTube estándar",
|
||||
"Download is disabled": "La descarga está deshabilitada",
|
||||
"Import YouTube playlist (.csv)": "Importar lista de reproducción de YouTube (.csv)"
|
||||
}
|
||||
|
@ -25,7 +25,6 @@
|
||||
"Clear watch history?": "Kustuta vaatamiste ajalugu?",
|
||||
"New password": "Uus salasõna",
|
||||
"New passwords must match": "Uued salasõnad peavad ühtima",
|
||||
"Cannot change password for Google accounts": "Google'i kasutaja salasõna ei saa muuta",
|
||||
"Import and Export Data": "Impordi ja ekspordi andmed",
|
||||
"Import": "Impordi",
|
||||
"Import YouTube subscriptions": "Impordi tellimused Youtube'ist/OPML-ist",
|
||||
@ -38,7 +37,6 @@
|
||||
"History": "Ajalugu",
|
||||
"JavaScript license information": "JavaScripti litsentsi info",
|
||||
"source": "allikas",
|
||||
"Log in with Google": "Logi sisse Google'iga",
|
||||
"User ID": "Kasutada ID",
|
||||
"Password": "Salasõna",
|
||||
"Time (h:mm:ss):": "Aeg (h:mm:ss):",
|
||||
@ -118,12 +116,10 @@
|
||||
"Hide replies": "Peida vastused",
|
||||
"Show replies": "Näita vastuseid",
|
||||
"Incorrect password": "Vale salasõna",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Sisselogimine ei õnnestunud. Asi võib olla selles, et",
|
||||
"Wrong answer": "Vale vastus",
|
||||
"User ID is a required field": "Kasutaja ID on kohustuslik väli",
|
||||
"Password is a required field": "Salasõna on kohustuslik väli",
|
||||
"Wrong username or password": "Vale kasutajanimi või salasõna",
|
||||
"Please sign in using 'Log in with Google'": "Palun kasutage 'Logi sisse Google'iga'",
|
||||
"Password cannot be longer than 55 characters": "Salasõna ei tohi olla pikem kui 55 tähemärki",
|
||||
"Password cannot be empty": "Salasõna ei tohi olla tühi",
|
||||
"Please log in": "Palun logige sisse",
|
||||
@ -290,8 +286,6 @@
|
||||
"": "Vaata `x` kommentaare"
|
||||
},
|
||||
"Khmer": "Khmeeri",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sisselogimine ei õnnestunud. Kontrollige, kas two-factor authentication (Authenticator või SMS) on sisselülitatud.",
|
||||
"Invalid TFA code": "Vale TFA-kood",
|
||||
"Bosnian": "Bosnia",
|
||||
"Corsican": "Korsika",
|
||||
"Javanese": "Jaava",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Garbitu ikusitakoen historia?",
|
||||
"New password": "Pasahitz berria",
|
||||
"New passwords must match": "Pasahitza berriek bat egin behar dute",
|
||||
"Cannot change password for Google accounts": "Ezin da pasahitza aldatu Google kontuetan",
|
||||
"Authorize token?": "Baimendu tokena?",
|
||||
"Yes": "Bai",
|
||||
"No": "Ez",
|
||||
@ -36,7 +35,6 @@
|
||||
"source": "iturburua",
|
||||
"Log in": "Saioa hasi",
|
||||
"Log in/register": "Hasi saioa / Eman izena",
|
||||
"Log in with Google": "Hasi saioa Googlekin",
|
||||
"User ID": "Erabiltzaile IDa",
|
||||
"Password": "Pasahitza",
|
||||
"Time (h:mm:ss):": "Denbora (h:mm:ss):",
|
||||
@ -93,7 +91,6 @@
|
||||
"Import/export data": "Inportatu/exportatu data",
|
||||
"Create playlist": "Zerrenda sortu",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Aditu! JavaScript itzalita dakazula ematen du. Hemen sakatu iruzkinak ikusteko. Denbora luza leikeela kontuan hartu.",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ezinezkoa izena eman. Ziurtatu berresteko bi faktoreak (Authenticator edo SMS) piztuta daudela.",
|
||||
"generic_views_count": "{{count}}ikusia",
|
||||
"generic_views_count_plural": "{{count}}ikusiak",
|
||||
"generic_playlists_count": "{{count}}zerrenda",
|
||||
@ -136,7 +133,6 @@
|
||||
"License: ": "Lizentzia: ",
|
||||
"Family friendly? ": "Adeikorra familiarekin? ",
|
||||
"Wilson score: ": "Wilsonen puntuazioa: ",
|
||||
"Quota exceeded, try again in a few hours": "Kuota gaindituta, ordu batzuren bueltan berriro saiatu",
|
||||
"comments_view_x_replies": "{{count}} erantzuna ikusi",
|
||||
"comments_view_x_replies_plural": "{{count}} erantzunak ikusi",
|
||||
"Catalan": "Katalaniera",
|
||||
@ -204,7 +200,6 @@
|
||||
"preferences_category_data": "Dataren lehentasunak",
|
||||
"preferences_default_home_label": "Homepage lehenetsia: ",
|
||||
"preferences_automatic_instance_redirect_label": "berbideratze adibide automatikoa (atzera egin berbideratzeko: invidious.io) ",
|
||||
"Please sign in using 'Log in with Google'": "'Log in Googlerekin' erabili",
|
||||
"`x` uploaded a video": "' x'(e)k bideo bat igo du",
|
||||
"published - reverse": "argitaratuta - alderantziz",
|
||||
"Could not get channel info.": "Kanalaren adierazpena ezin lortu.",
|
||||
@ -220,7 +215,6 @@
|
||||
"Premieres in `x`": "'x'eko estrenaldiak",
|
||||
"Delete playlist `x`?": "'x' zerrenda ezabatu nahi?",
|
||||
"Token is expired, please try again": "Token kadukatua, saiatu berriro",
|
||||
"Invalid TFA code": "TFA kodea ez da zuzena",
|
||||
"CAPTCHA enabled: ": "CAPTCHA gaitu: ",
|
||||
"Released under the AGPLv3 on Github.": "GitHubeko AGPLv3pean argitaratuta.",
|
||||
"channel:`x`": "Kanal: 'x'",
|
||||
@ -242,9 +236,7 @@
|
||||
"preferences_category_subscription": "Harpidetzaren lehentasunak",
|
||||
"Hidden field \"challenge\" is a required field": "\"challenge\" eremu ezkutua beharrezkoa da",
|
||||
"German": "Alemaniarra",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Ezin izena eman. Izan leike zure konturako berresteko bi faktoreak piztuta ez daudela.",
|
||||
"View YouTube comments": "YouTubeko iruzkinak ikusi",
|
||||
"Google verification code": "Googleren berresteko kodea",
|
||||
"`x` is live": "'x' bizirik darrai",
|
||||
"Password cannot be empty": "Pasahitza ezin da hutsik utzi",
|
||||
"preferences_video_loop_label": "Beti begiztatu: ",
|
||||
|
@ -19,7 +19,6 @@
|
||||
"Clear watch history?": "پاک کردن تاریخچه نمایش؟",
|
||||
"New password": "گذرواژه تازه",
|
||||
"New passwords must match": "گذارواژه های تازه باید باهم همخوانی داشته باشند",
|
||||
"Cannot change password for Google accounts": "نمیتوان گذرواژه را برای حساب های کاربری گوگل تغییر داد",
|
||||
"Authorize token?": "توکن دسترسی؟",
|
||||
"Authorize token for `x`?": "توکن دسترسی برای `x`؟",
|
||||
"Yes": "بله",
|
||||
@ -42,7 +41,6 @@
|
||||
"source": "منبع",
|
||||
"Log in": "ورود",
|
||||
"Log in/register": "ورود/ثبت نام",
|
||||
"Log in with Google": "ورود با گوگل",
|
||||
"User ID": "شناسه کاربری",
|
||||
"Password": "گذرواژه",
|
||||
"Time (h:mm:ss):": "زمان (h:mm:ss):",
|
||||
@ -51,7 +49,6 @@
|
||||
"Sign In": "ورود",
|
||||
"Register": "ثبت نام",
|
||||
"E-mail": "ایمیل",
|
||||
"Google verification code": "کد تایید گوگل",
|
||||
"Preferences": "ترجیحات",
|
||||
"preferences_category_player": "ترجیحات نمایشدهنده",
|
||||
"preferences_video_loop_label": "همواره ویدئو را بازپخش کن ",
|
||||
@ -171,17 +168,12 @@
|
||||
"Hide replies": "مخفی کردن پاسخ ها",
|
||||
"Show replies": "نمایش پاسخ ها",
|
||||
"Incorrect password": "گذرواژه نا درست",
|
||||
"Quota exceeded, try again in a few hours": "سهمیه بیشتر شده است، چند ساعت بعد دوباره تلاش کنید",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "قادر به ورود نیستید، مطمئن شوید احراز تایید-دومرحله (Authenticator یا پیامکوتاه) خاموش باشد.",
|
||||
"Invalid TFA code": "کد TFA نادرست است",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "ورود با خطا مواجه شد. این ممکن است به خاطر احراز تایید-دومرحله باشد که برای حساب کاربری شما فعال نشده است.",
|
||||
"Wrong answer": "پاسخ غلط",
|
||||
"Erroneous CAPTCHA": "CAPTCHA نا درست",
|
||||
"CAPTCHA is a required field": "CAPTCHA یک فیلد ضروری است",
|
||||
"User ID is a required field": "شناسه کاربری یک فیلد ضروری است",
|
||||
"Password is a required field": "گذرواژه یک فیلد ضروری است",
|
||||
"Wrong username or password": "نام کاربری یا گذرواژه غلط است",
|
||||
"Please sign in using 'Log in with Google'": "لطفا با استفاده از 'ورود توسط گوگل' وارد شوید",
|
||||
"Password cannot be empty": "گذرواژه نمیتواند خالی باشد",
|
||||
"Password cannot be longer than 55 characters": "گذر واژه نمیتواند از ۵۵ کاراکتر بیشتر باشد",
|
||||
"Please log in": "لطفا وارد شوید",
|
||||
@ -450,5 +442,8 @@
|
||||
"Music in this video": "آهنگ در این ویدیو",
|
||||
"Artist: ": "هنرمند: ",
|
||||
"Album: ": "آلبوم: ",
|
||||
"Song: ": "آهنگ: "
|
||||
"Song: ": "آهنگ: ",
|
||||
"Channel Sponsor": "اسپانسر کانال",
|
||||
"Standard YouTube license": "پروانه استاندارد YouTube",
|
||||
"search_message_use_another_instance": " شما همچنین میتوانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>."
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Tyhjennä katseluhistoria?",
|
||||
"New password": "Uusi salasana",
|
||||
"New passwords must match": "Uusien salasanojen täytyy täsmätä",
|
||||
"Cannot change password for Google accounts": "Google-tilien salasanaa ei voi vaihtaa",
|
||||
"Authorize token?": "Valuutetaanko tunnus?",
|
||||
"Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?",
|
||||
"Yes": "Kyllä",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "lähde",
|
||||
"Log in": "Kirjaudu sisään",
|
||||
"Log in/register": "Kirjaudu sisään/rekisteröidy",
|
||||
"Log in with Google": "Kirjaudu sisään Googlella",
|
||||
"User ID": "Käyttäjätunnus",
|
||||
"Password": "Salasana",
|
||||
"Time (h:mm:ss):": "Aika (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Kirjaudu sisään",
|
||||
"Register": "Rekisteröidy",
|
||||
"E-mail": "Sähköposti",
|
||||
"Google verification code": "Google-vahvistuskoodi",
|
||||
"Preferences": "Asetukset",
|
||||
"preferences_category_player": "Soittimen asetukset",
|
||||
"preferences_video_loop_label": "Toista jatkuvasti aina: ",
|
||||
@ -163,17 +160,12 @@
|
||||
"Hide replies": "Piilota vastaukset",
|
||||
"Show replies": "Näytä vastaukset",
|
||||
"Incorrect password": "Väärä salasana",
|
||||
"Quota exceeded, try again in a few hours": "Kiintiö ylitetty, yritä parin tunnin kuluttua uudestaan",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Sisäänkirjautuminen epäonnistui. Varmista, että kaksivaiheinen tunnistautuminen (Authenticator tai tekstiviesti) on käytössä.",
|
||||
"Invalid TFA code": "Virheellinen turvakoodi",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Sisäänkirjautuminen epäonnistui. Tämä voi johtua siitä, että kaksivaiheinen tunnistautuminen on pois käytöstä tunnuksellasi.",
|
||||
"Wrong answer": "Väärä vastaus",
|
||||
"Erroneous CAPTCHA": "Virheellinen CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA-kenttä vaaditaan",
|
||||
"User ID is a required field": "Käyttäjätunnus vaaditaan",
|
||||
"Password is a required field": "Salasana vaaditaan",
|
||||
"Wrong username or password": "Väärä käyttäjänimi tai salasana",
|
||||
"Please sign in using 'Log in with Google'": "Ole hyvä ja kirjaudu sisään Google-tunnuksella",
|
||||
"Password cannot be empty": "Salasana ei voi olla tyhjä",
|
||||
"Password cannot be longer than 55 characters": "Salasana ei voi olla yli 55 merkkiä pitkä",
|
||||
"Please log in": "Kirjaudu sisään, ole hyvä",
|
||||
|
@ -24,7 +24,6 @@
|
||||
"Clear watch history?": "Êtes-vous sûr de vouloir supprimer l'historique des vidéos regardées ?",
|
||||
"New password": "Nouveau mot de passe",
|
||||
"New passwords must match": "Les nouveaux mots de passe doivent correspondre",
|
||||
"Cannot change password for Google accounts": "Le mot de passe d'un compte Google ne peut pas être changé depuis Invidious",
|
||||
"Authorize token?": "Autoriser le token ?",
|
||||
"Authorize token for `x`?": "Autoriser le token pour `x` ?",
|
||||
"Yes": "Oui",
|
||||
@ -47,7 +46,6 @@
|
||||
"source": "source",
|
||||
"Log in": "Se connecter",
|
||||
"Log in/register": "Se connecter/S'inscrire",
|
||||
"Log in with Google": "Se connecter avec Google",
|
||||
"User ID": "Identifiant utilisateur",
|
||||
"Password": "Mot de passe",
|
||||
"Time (h:mm:ss):": "Heure (h:mm:ss) :",
|
||||
@ -56,7 +54,6 @@
|
||||
"Sign In": "Se connecter",
|
||||
"Register": "S'inscrire",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Code de vérification Google",
|
||||
"Preferences": "Préférences",
|
||||
"preferences_category_player": "Préférences du lecteur",
|
||||
"preferences_video_loop_label": "Lire en boucle : ",
|
||||
@ -179,17 +176,12 @@
|
||||
"Hide replies": "Masquer les réponses",
|
||||
"Show replies": "Afficher les réponses",
|
||||
"Incorrect password": "Mot de passe incorrect",
|
||||
"Quota exceeded, try again in a few hours": "Nombre de tentatives de connexion dépassé, réessayez dans quelques heures",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Impossible de se connecter, si après plusieurs tentative vous ne parvenez toujours pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.",
|
||||
"Invalid TFA code": "Code d'authentification à deux facteurs invalide",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.",
|
||||
"Wrong answer": "Réponse invalide",
|
||||
"Erroneous CAPTCHA": "CAPTCHA invalide",
|
||||
"CAPTCHA is a required field": "Veuillez entrer un CAPTCHA",
|
||||
"User ID is a required field": "Veuillez entrer un Identifiant Utilisateur",
|
||||
"Password is a required field": "Veuillez entrer un Mot de passe",
|
||||
"Wrong username or password": "Nom d'utilisateur ou mot de passe invalide",
|
||||
"Please sign in using 'Log in with Google'": "Veuillez vous connecter en utilisant \"Se connecter avec Google\"",
|
||||
"Password cannot be empty": "Le mot de passe ne peut pas être vide",
|
||||
"Password cannot be longer than 55 characters": "Le mot de passe ne doit pas comporter plus de 55 caractères",
|
||||
"Please log in": "Veuillez vous connecter",
|
||||
@ -473,8 +465,16 @@
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_duration_option_none": "Toutes les durées",
|
||||
"error_video_not_in_playlist": "La vidéo demandée n'existe pas dans cette liste de lecture. <a href=\"`x`\">Cliquez ici pour retourner à la liste de lecture.</a>",
|
||||
"channel_tab_shorts_label": "Clips",
|
||||
"channel_tab_streams_label": "En direct",
|
||||
"channel_tab_shorts_label": "Vidéos courtes",
|
||||
"channel_tab_streams_label": "Vidéos en direct",
|
||||
"channel_tab_playlists_label": "Listes de lecture",
|
||||
"channel_tab_channels_label": "Chaînes"
|
||||
"channel_tab_channels_label": "Chaînes",
|
||||
"Song: ": "Chanson : ",
|
||||
"Artist: ": "Artiste : ",
|
||||
"Album: ": "Album : ",
|
||||
"Standard YouTube license": "Licence YouTube Standard",
|
||||
"Music in this video": "Musique dans cette vidéo",
|
||||
"Channel Sponsor": "Soutien de la chaîne",
|
||||
"Download is disabled": "Le téléchargement est désactivé",
|
||||
"Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "לנקות את היסטוריית הצפייה?",
|
||||
"New password": "סיסמה חדשה",
|
||||
"New passwords must match": "על הסיסמאות החדשות להתאים",
|
||||
"Cannot change password for Google accounts": "לא ניתן לשנות את הסיסמה לחשבונות Google",
|
||||
"Authorize token?": "לאשר את האסימון?",
|
||||
"Authorize token for `x`?": "האם לאשר את האסימון עבור `x`?",
|
||||
"Yes": "כן",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "source",
|
||||
"Log in": "כניסה",
|
||||
"Log in/register": "כניסה/הרשמה",
|
||||
"Log in with Google": "כניסה עם Google",
|
||||
"User ID": "שם משתמש",
|
||||
"Password": "סיסמה",
|
||||
"Time (h:mm:ss):": "זמן (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "התחברות",
|
||||
"Register": "הרשמה",
|
||||
"E-mail": "דוא״ל",
|
||||
"Google verification code": "קוד האימות של Google",
|
||||
"Preferences": "העדפות",
|
||||
"preferences_category_player": "העדפות הנגן",
|
||||
"preferences_autoplay_label": "ניגון אוטומטי: ",
|
||||
@ -137,7 +134,6 @@
|
||||
"User ID is a required field": "חובה למלא את שדה שם המשתמש",
|
||||
"Password is a required field": "חובה למלא את שדה הסיסמה",
|
||||
"Wrong username or password": "שם משתמש שגוי או סיסמה שגויה",
|
||||
"Please sign in using 'Log in with Google'": "נא להתחבר בעזרת \"התחברות עם Google\"",
|
||||
"Password cannot be longer than 55 characters": "על אורך הסיסמה להיות 55 תווים לכל היותר",
|
||||
"Please log in": "נא להתחבר",
|
||||
"channel:`x`": "ערוץ:`x`",
|
||||
|
@ -4,7 +4,6 @@
|
||||
"No": "नहीं",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML के रूप में सदस्यताएँ निर्यात करें (NewPipe और FreeTube के लिए)",
|
||||
"Log in/register": "लॉग-इन/पंजीकृत करें",
|
||||
"Log in with Google": "Google के साथ लॉग-इन करें",
|
||||
"preferences_autoplay_label": "अपने आप चलाने की सुविधा: ",
|
||||
"preferences_dark_mode_label": "थीम: ",
|
||||
"preferences_default_home_label": "डिफ़ॉल्ट मुखपृष्ठ: ",
|
||||
@ -58,7 +57,6 @@
|
||||
"Clear watch history?": "देखने का इतिहास मिटाएँ?",
|
||||
"New password": "नया पासवर्ड",
|
||||
"New passwords must match": "पासवर्ड्स को मेल खाना होगा",
|
||||
"Cannot change password for Google accounts": "Google खातों के लिए पासवर्ड नहीं बदल सकते",
|
||||
"Authorize token?": "टोकन को प्रमाणित करें?",
|
||||
"Authorize token for `x`?": "`x` के लिए टोकन को प्रमाणित करें?",
|
||||
"Import and Export Data": "डेटा को आयात और निर्यात करें",
|
||||
@ -81,7 +79,6 @@
|
||||
"Password": "पासवर्ड",
|
||||
"Register": "पंजीकृत करें",
|
||||
"E-mail": "ईमेल",
|
||||
"Google verification code": "Google प्रमाणीकरण कोड",
|
||||
"Time (h:mm:ss):": "समय (घं:मिमि:सेसे):",
|
||||
"Text CAPTCHA": "टेक्स्ट CAPTCHA",
|
||||
"Image CAPTCHA": "चित्र CAPTCHA",
|
||||
@ -224,15 +221,10 @@
|
||||
"Hide replies": "जवाब छिपाएँ",
|
||||
"Show replies": "जवाब दिखाएँ",
|
||||
"Incorrect password": "गलत पासवर्ड",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "लॉग-इन नहीं किया जा सका, सुनिश्चित करें कि दो-कारक प्रमाणीकरण (Authenticator या SMS) सक्षम है।",
|
||||
"Invalid TFA code": "अमान्य TFA कोड",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "लॉग-इन नाकाम रहा। ऐसा इसलिए हो सकता है कि दो-कारक प्रमाणीकरण आपके खाते पर सक्षम नहीं है।",
|
||||
"Quota exceeded, try again in a few hours": "कोटा पार हो चुका है, कृपया कुछ घंटों में फिर कोशिश करें",
|
||||
"CAPTCHA is a required field": "CAPTCHA एक ज़रूरी फ़ील्ड है",
|
||||
"User ID is a required field": "सदस्य ID एक ज़रूरी फ़ील्ड है",
|
||||
"Password is a required field": "पासवर्ड एक ज़रूरी फ़ील्ड है",
|
||||
"Wrong username or password": "गलत सदस्यनाम या पासवर्ड",
|
||||
"Please sign in using 'Log in with Google'": "कृपया 'Google के साथ लॉग-इन करें' के साथ साइन-इन करें",
|
||||
"Password cannot be empty": "पासवर्ड खाली नहीं हो सकता",
|
||||
"Password cannot be longer than 55 characters": "पासवर्ड में अधिकतम 55 अक्षर हो सकते हैं",
|
||||
"Invidious Private Feed for `x`": "`x` के लिए Invidious निजी फ़ीड",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Izbrisati povijest gledanja?",
|
||||
"New password": "Nova lozinka",
|
||||
"New passwords must match": "Nove lozinke se moraju poklapati",
|
||||
"Cannot change password for Google accounts": "Nije moguće promijeniti lozinku za Google račune",
|
||||
"Authorize token?": "Autorizirati token?",
|
||||
"Authorize token for `x`?": "Autorizirati token za `x`?",
|
||||
"Yes": "Da",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "izvor",
|
||||
"Log in": "Prijavi se",
|
||||
"Log in/register": "Prijavi se/registriraj se",
|
||||
"Log in with Google": "Prijavi se pomoću Googlea",
|
||||
"User ID": "Korisnički ID",
|
||||
"Password": "Lozinka",
|
||||
"Time (h:mm:ss):": "Vrijeme (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Prijavi se",
|
||||
"Register": "Registriraj se",
|
||||
"E-mail": "E-mail adresa",
|
||||
"Google verification code": "Googleov potvrdni kod",
|
||||
"Preferences": "Postavke",
|
||||
"preferences_category_player": "Postavke playera",
|
||||
"preferences_video_loop_label": "Uvijek ponavljaj: ",
|
||||
@ -164,17 +161,12 @@
|
||||
"Hide replies": "Sakrij odgovore",
|
||||
"Show replies": "Prikaži odgovore",
|
||||
"Incorrect password": "Neispravna lozinka",
|
||||
"Quota exceeded, try again in a few hours": "Kvota je prekoračena. Pokušaj ponovo za par sati",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Prijava neuspjela. Provjeri da je dvofaktorska autentifikacija uključena (Authenticator ili SMS).",
|
||||
"Invalid TFA code": "Neispravan TFA kod",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Prijava neuspjela. Možda zato što za tvoj račun nije uključena dvofaktorska autentifikacija.",
|
||||
"Wrong answer": "Krivi odgovor",
|
||||
"Erroneous CAPTCHA": "Neispravan CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA je obavezno polje",
|
||||
"User ID is a required field": "Korisnički ID je obavezno polje",
|
||||
"Password is a required field": "Polje lozinke je obavezno polje",
|
||||
"Wrong username or password": "Krivo korisničko ime ili lozinka",
|
||||
"Please sign in using 'Log in with Google'": "Za prijavu koristi „Prijavi se pomoću Googlea”",
|
||||
"Password cannot be empty": "Polje lozinke ne smije ostati prazno",
|
||||
"Password cannot be longer than 55 characters": "Lozinka ne može biti duža od 55 znakova",
|
||||
"Please log in": "Prijavi se",
|
||||
@ -497,5 +489,8 @@
|
||||
"Album: ": "Album: ",
|
||||
"Artist: ": "Izvođač: ",
|
||||
"Channel Sponsor": "Sponzor kanala",
|
||||
"Song: ": "Pjesma: "
|
||||
"Song: ": "Pjesma: ",
|
||||
"Standard YouTube license": "Standardna YouTube licenca",
|
||||
"Download is disabled": "Preuzimanje je deaktivirano",
|
||||
"Import YouTube playlist (.csv)": "Uvezi YouTube zbirku (.csv)"
|
||||
}
|
||||
|
@ -24,7 +24,6 @@
|
||||
"Clear watch history?": "Törölve legyen a megnézett videók naplója?",
|
||||
"New password": "Új jelszó",
|
||||
"New passwords must match": "Az új jelszavaknak egyezniük kell.",
|
||||
"Cannot change password for Google accounts": "A Google-fiók jelszavát nem lehet megváltoztatni.",
|
||||
"Authorize token?": "Engedélyezve legyen a token?",
|
||||
"Authorize token for `x`?": "Engedélyezve legyen a token erre? „`x`”",
|
||||
"Yes": "Igen",
|
||||
@ -47,7 +46,6 @@
|
||||
"source": "forrás",
|
||||
"Log in": "Bejelentkezés",
|
||||
"Log in/register": "Bejelentkezés/Regisztrálás",
|
||||
"Log in with Google": "Bejelentkezés Google-fiókkal",
|
||||
"User ID": "Felhasználói azonosító",
|
||||
"Password": "Jelszó",
|
||||
"Time (h:mm:ss):": "A pontos idő (ó:pp:mm):",
|
||||
@ -56,7 +54,6 @@
|
||||
"Sign In": "Bejelentkezés",
|
||||
"Register": "Regisztrálás",
|
||||
"E-mail": "E-mail-cím",
|
||||
"Google verification code": "A Google ellenőrző kódja",
|
||||
"Preferences": "Beállítások",
|
||||
"preferences_category_player": "Lejátszó beállításai",
|
||||
"preferences_video_loop_label": "Videó állandó ismétlése: ",
|
||||
@ -173,16 +170,12 @@
|
||||
"Hide replies": "Válaszok elrejtése",
|
||||
"Show replies": "Válaszok mutatása",
|
||||
"Incorrect password": "A jelszó nem megfelelő",
|
||||
"Quota exceeded, try again in a few hours": "A kvótát meghaladták. Néhány órával később próbáld meg újból betölteni.",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nem sikerült bejelentkezni. A kétlépcsős (hitelesítő vagy szöveges üzenet általi) hitelesítésnek bekapcsolva kell lennie.",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Nem sikerült bejelentkezni. Ennek oka lehet, hogy a kétlépcsős hitelesítés nincs bekapcsolva a fiók beállításaiban.",
|
||||
"Wrong answer": "Nem jól válaszoltál.",
|
||||
"Erroneous CAPTCHA": "A CAPTCHA hibás.",
|
||||
"CAPTCHA is a required field": "A CAPTCHA-mezőt ki kell tölteni.",
|
||||
"User ID is a required field": "A felhasználói azonosítót meg kell adni.",
|
||||
"Password is a required field": "Meg kell adni egy jelszót.",
|
||||
"Wrong username or password": "Vagy a felhasználói név, vagy pedig a jelszó nem megfelelő.",
|
||||
"Please sign in using 'Log in with Google'": "A „Bejelentkezés Google-el” gombbal jelentkezz be.",
|
||||
"Password cannot be empty": "A jelszót nem lehet kihagyni.",
|
||||
"Password cannot be longer than 55 characters": "A jelszó nem lehet hosszabb 55 karakternél.",
|
||||
"Please log in": "Kérjük, jelentkezz be.",
|
||||
@ -419,7 +412,6 @@
|
||||
"Switch Invidious Instance": "Váltás másik Invidious-oldalra",
|
||||
"Urdu": "urdu",
|
||||
"search_filters_date_option_week": "Ezen a héten",
|
||||
"Invalid TFA code": "A kétlépéses hitelesítés kódja nem megfelelő",
|
||||
"footer_documentation": "Dokumentáció",
|
||||
"search_filters_features_option_hd": "HD",
|
||||
"next_steps_error_message_go_to_youtube": "Ugrás a YouTube-ra",
|
||||
|
@ -19,7 +19,6 @@
|
||||
"Clear watch history?": "Bersihkan riwayat tontonan?",
|
||||
"New password": "Kata sandi baru",
|
||||
"New passwords must match": "Kata sandi baru harus cocok",
|
||||
"Cannot change password for Google accounts": "Tidak dapat mengganti kata sandi untuk akun Google",
|
||||
"Authorize token?": "Otorisasi token?",
|
||||
"Authorize token for `x`?": "Otorisasi token untuk `x`?",
|
||||
"Yes": "Ya",
|
||||
@ -42,7 +41,6 @@
|
||||
"source": "sumber",
|
||||
"Log in": "Masuk",
|
||||
"Log in/register": "Masuk/Daftar",
|
||||
"Log in with Google": "Masuk dengan Google",
|
||||
"User ID": "ID Pengguna",
|
||||
"Password": "Kata Sandi",
|
||||
"Time (h:mm:ss):": "Waktu (j:mm:dd):",
|
||||
@ -51,7 +49,6 @@
|
||||
"Sign In": "Masuk",
|
||||
"Register": "Daftar",
|
||||
"E-mail": "Surel",
|
||||
"Google verification code": "Kode verifikasi Google",
|
||||
"Preferences": "Preferensi",
|
||||
"preferences_category_player": "Preferensi pemutar",
|
||||
"preferences_video_loop_label": "Selalu ulangi: ",
|
||||
@ -171,17 +168,12 @@
|
||||
"Hide replies": "Sembunyikan balasan",
|
||||
"Show replies": "Lihat balasan",
|
||||
"Incorrect password": "Kata sandi salah",
|
||||
"Quota exceeded, try again in a few hours": "Kuota penuh, coba lagi dalam beberapa jam",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Tidak dapat masuk, pastikan autentikasi dua-faktor (autentikator atau SMS) sudah nyala.",
|
||||
"Invalid TFA code": "Kode TFA tidak valid",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Gagal masuk. Ini mungkin disebabkan autentikasi dua-faktor tidak dinyalakan untuk akun Anda.",
|
||||
"Wrong answer": "Jawaban salah",
|
||||
"Erroneous CAPTCHA": "CAPTCHA salah",
|
||||
"CAPTCHA is a required field": "CAPTCHA perlu diisi",
|
||||
"User ID is a required field": "ID pengguna perlu diisi",
|
||||
"Password is a required field": "Kata sandi perlu diisi",
|
||||
"Wrong username or password": "Nama pengguna atau kata sandi salah",
|
||||
"Please sign in using 'Log in with Google'": "Harap masuk menggunakan 'Masuk dengan Google'",
|
||||
"Password cannot be empty": "Kata sandi tidak boleh kosong",
|
||||
"Password cannot be longer than 55 characters": "Kata sandi tidak boleh lebih dari 55 karakter",
|
||||
"Please log in": "Harap masuk",
|
||||
@ -453,5 +445,6 @@
|
||||
"crash_page_switch_instance": "mencoba untuk <a href=\"`x`\">menggunakan peladen lainnya</a>",
|
||||
"crash_page_read_the_faq": "baca <a href=\"`x`\">Soal Sering Ditanya (SSD/FAQ)</a>",
|
||||
"crash_page_search_issue": "mencari <a href=\"`x`\">isu yang ada di GitHub</a>",
|
||||
"crash_page_report_issue": "Jika yang di atas tidak membantu, <a href=\"`x`\">buka isu baru di GitHub</a> (sebaiknya dalam bahasa Inggris) dan sertakan teks berikut dalam pesan Anda (JANGAN terjemahkan teks tersebut):"
|
||||
"crash_page_report_issue": "Jika yang di atas tidak membantu, <a href=\"`x`\">buka isu baru di GitHub</a> (sebaiknya dalam bahasa Inggris) dan sertakan teks berikut dalam pesan Anda (JANGAN terjemahkan teks tersebut):",
|
||||
"Popular enabled: ": "Populer diaktifkan: "
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Hreinsa áhorfssögu?",
|
||||
"New password": "Nýtt lykilorð",
|
||||
"New passwords must match": "Nýtt lykilorð verður að passa",
|
||||
"Cannot change password for Google accounts": "Ekki er hægt að breyta lykilorði fyrir Google reikninga",
|
||||
"Authorize token?": "Leyfa tákn?",
|
||||
"Authorize token for `x`?": "Leyfa tákn fyrir `x`?",
|
||||
"Yes": "Já",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "uppspretta",
|
||||
"Log in": "Skrá inn",
|
||||
"Log in/register": "Innskráning/nýskráning",
|
||||
"Log in with Google": "Skrá inn með Google",
|
||||
"User ID": "Notandakenni",
|
||||
"Password": "Lykilorð",
|
||||
"Time (h:mm:ss):": "Tími (h:mm: ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Skrá inn",
|
||||
"Register": "Nýskrá",
|
||||
"E-mail": "Tölvupóstur",
|
||||
"Google verification code": "Google staðfestingarkóði",
|
||||
"Preferences": "Kjörstillingar",
|
||||
"preferences_category_player": "Kjörstillingar spilara",
|
||||
"preferences_video_loop_label": "Alltaf lykkja: ",
|
||||
@ -155,17 +152,12 @@
|
||||
"Hide replies": "Fela svör",
|
||||
"Show replies": "Sýna svör",
|
||||
"Incorrect password": "Rangt lykilorð",
|
||||
"Quota exceeded, try again in a few hours": "Kvóti fór yfir, reyndu aftur eftir nokkrar klukkustundir",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ekki er hægt að skrá þig inn, vertu viss um að tvíþætt staðfesting (Authenticator eða SMS) sé kveikt á.",
|
||||
"Invalid TFA code": "Ógildur TFA kóði",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Innskráning mistókst. Þetta gæti verið vegna þess að tvíþátta staðfesting er ekki kveikt á reikningnum þínum.",
|
||||
"Wrong answer": "Rangt svar",
|
||||
"Erroneous CAPTCHA": "Rangt CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA er nauðsynlegur reitur",
|
||||
"User ID is a required field": "Notandakenni er nauðsynlegur reitur",
|
||||
"Password is a required field": "Lykilorð er nauðsynlegur reitur",
|
||||
"Wrong username or password": "Rangt notandanafn eða lykilorð",
|
||||
"Please sign in using 'Log in with Google'": "Vinsamlegast skráðu þig inn með því að nota 'Innskráning með Google'",
|
||||
"Password cannot be empty": "Lykilorð má ekki vera autt",
|
||||
"Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir",
|
||||
"Please log in": "Vinsamlegast skráðu þig inn",
|
||||
|
@ -20,7 +20,6 @@
|
||||
"Clear watch history?": "Eliminare la cronologia dei video guardati?",
|
||||
"New password": "Nuova password",
|
||||
"New passwords must match": "Le nuove password devono corrispondere",
|
||||
"Cannot change password for Google accounts": "Non è possibile modificare la password per gli account Google",
|
||||
"Authorize token?": "Autorizzare gettone?",
|
||||
"Authorize token for `x`?": "Autorizzare gettone per `x`?",
|
||||
"Yes": "Sì",
|
||||
@ -43,7 +42,6 @@
|
||||
"source": "sorgente",
|
||||
"Log in": "Accedi",
|
||||
"Log in/register": "Accedi/Registrati",
|
||||
"Log in with Google": "Accedi con Google",
|
||||
"User ID": "ID utente",
|
||||
"Password": "Password",
|
||||
"Time (h:mm:ss):": "Orario (h:mm:ss):",
|
||||
@ -52,7 +50,6 @@
|
||||
"Sign In": "Accedi",
|
||||
"Register": "Registrati",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Codice di verifica Google",
|
||||
"Preferences": "Preferenze",
|
||||
"preferences_category_player": "Preferenze del riproduttore",
|
||||
"preferences_video_loop_label": "Ripeti sempre: ",
|
||||
@ -169,17 +166,12 @@
|
||||
"Hide replies": "Nascondi le risposte",
|
||||
"Show replies": "Mostra le risposte",
|
||||
"Incorrect password": "Password sbagliata",
|
||||
"Quota exceeded, try again in a few hours": "Limite superato, prova di nuovo fra qualche ora",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Impossibile autenticarsi, controlla che l'autenticazione in due passaggi (Authenticator o SMS) sia attiva.",
|
||||
"Invalid TFA code": "Codice di autenticazione a due fattori non valido",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Login fallito. L'errore potrebbe essere causato dal fatto che la verifica in due passaggi non è attiva sul tuo account.",
|
||||
"Wrong answer": "Risposta errata",
|
||||
"Erroneous CAPTCHA": "CAPTCHA errato",
|
||||
"CAPTCHA is a required field": "Il CAPTCHA è un campo obbligatorio",
|
||||
"User ID is a required field": "L'ID utente è obbligatorio",
|
||||
"Password is a required field": "La password è un campo obbligatorio",
|
||||
"Wrong username or password": "Nome utente o password errati",
|
||||
"Please sign in using 'Log in with Google'": "Per favore accedi con «Entra con Google»",
|
||||
"Password cannot be empty": "La password non può essere vuota",
|
||||
"Password cannot be longer than 55 characters": "La password non può contenere più di 55 caratteri",
|
||||
"Please log in": "Per favore, accedi",
|
||||
@ -479,5 +471,10 @@
|
||||
"channel_tab_community_label": "Comunità",
|
||||
"Music in this video": "Musica in questo video",
|
||||
"Artist: ": "Artista: ",
|
||||
"Album: ": "Album: "
|
||||
"Album: ": "Album: ",
|
||||
"Download is disabled": "Il download è disabilitato",
|
||||
"Song: ": "Canzone: ",
|
||||
"Standard YouTube license": "Licenza standard di YouTube",
|
||||
"Channel Sponsor": "Sponsor del canale",
|
||||
"Import YouTube playlist (.csv)": "Importa playlist di YouTube (.csv)"
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
{
|
||||
"generic_views_count_0": "{{count}} 回視聴",
|
||||
"generic_videos_count_0": "{{count}} 個の動画",
|
||||
"generic_playlists_count_0": "{{count}} 個の再生リスト",
|
||||
"generic_videos_count_0": "{{count}}本の動画",
|
||||
"generic_playlists_count_0": "{{count}}個の再生リスト",
|
||||
"generic_subscribers_count_0": "{{count}} 人の登録者",
|
||||
"generic_subscriptions_count_0": "{{count}} 個の登録チャンネル",
|
||||
"generic_subscriptions_count_0": "{{count}}個の登録チャンネル",
|
||||
"LIVE": "ライブ",
|
||||
"Shared `x` ago": "`x`前に公開",
|
||||
"Unsubscribe": "登録解除",
|
||||
"Subscribe": "登録",
|
||||
"View channel on YouTube": "YouTube でチャンネルを見る",
|
||||
"View playlist on YouTube": "YouTube で再生リストを見る",
|
||||
"View channel on YouTube": "YouTube でチャンネルを表示",
|
||||
"View playlist on YouTube": "YouTube で再生リストを表示",
|
||||
"newest": "新しい順",
|
||||
"oldest": "古い順",
|
||||
"popular": "人気順",
|
||||
@ -19,7 +19,6 @@
|
||||
"Clear watch history?": "再生履歴を削除しますか?",
|
||||
"New password": "新しいパスワード",
|
||||
"New passwords must match": "新しいパスワードが一致していません",
|
||||
"Cannot change password for Google accounts": "Google アカウントのパスワードは変更できません",
|
||||
"Authorize token?": "トークンを認証しますか?",
|
||||
"Authorize token for `x`?": "トークン `x` を認証しますか?",
|
||||
"Yes": "はい",
|
||||
@ -42,7 +41,6 @@
|
||||
"source": "ソース",
|
||||
"Log in": "ログイン",
|
||||
"Log in/register": "ログイン/登録",
|
||||
"Log in with Google": "Google でログイン",
|
||||
"User ID": "ユーザー ID",
|
||||
"Password": "パスワード",
|
||||
"Time (h:mm:ss):": "時間 (時:分分:秒秒):",
|
||||
@ -51,16 +49,15 @@
|
||||
"Sign In": "サインイン",
|
||||
"Register": "登録",
|
||||
"E-mail": "メールアドレス",
|
||||
"Google verification code": "Google 認証コード",
|
||||
"Preferences": "設定",
|
||||
"preferences_category_player": "プレイヤーの設定",
|
||||
"preferences_video_loop_label": "常にループ: ",
|
||||
"preferences_autoplay_label": "自動再生: ",
|
||||
"preferences_continue_label": "次の動画を再生: ",
|
||||
"preferences_continue_label": "次の動画を自動再生: ",
|
||||
"preferences_continue_autoplay_label": "次の動画を自動再生: ",
|
||||
"preferences_listen_label": "デフォルトで音声モードを使用: ",
|
||||
"preferences_local_label": "動画視聴にプロキシーを経由: ",
|
||||
"preferences_speed_label": "標準の再生速度: ",
|
||||
"preferences_listen_label": "音声モードを使用: ",
|
||||
"preferences_local_label": "動画視聴にプロキシを経由: ",
|
||||
"preferences_speed_label": "再生速度の初期値: ",
|
||||
"preferences_quality_label": "優先する画質: ",
|
||||
"preferences_volume_label": "プレイヤーの音量: ",
|
||||
"preferences_comments_label": "デフォルトのコメント: ",
|
||||
@ -69,7 +66,7 @@
|
||||
"preferences_captions_label": "優先する字幕: ",
|
||||
"Fallback captions: ": "フォールバック時の字幕: ",
|
||||
"preferences_related_videos_label": "関連動画を表示: ",
|
||||
"preferences_annotations_label": "デフォルトでアノテーションを表示: ",
|
||||
"preferences_annotations_label": "最初からアノテーションを表示: ",
|
||||
"preferences_extend_desc_label": "動画の説明文を自動的に拡張: ",
|
||||
"preferences_vr_mode_label": "対話的な360°動画 (WebGL が必要): ",
|
||||
"preferences_category_visual": "外観設定",
|
||||
@ -82,7 +79,7 @@
|
||||
"preferences_category_misc": "ほかの設定",
|
||||
"preferences_automatic_instance_redirect_label": "インスタンスの自動転送 (redirect.invidious.ioにフォールバック): ",
|
||||
"preferences_category_subscription": "登録チャンネル設定",
|
||||
"preferences_annotations_subscribed_label": "デフォルトで登録チャンネルのアノテーションを表示しますか? ",
|
||||
"preferences_annotations_subscribed_label": "最初から登録チャンネルのアノテーションを表示 ",
|
||||
"Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ",
|
||||
"preferences_max_results_label": "フィードに表示する動画の量: ",
|
||||
"preferences_sort_label": "動画を並び替え: ",
|
||||
@ -109,8 +106,8 @@
|
||||
"Delete account": "アカウントを削除",
|
||||
"preferences_category_admin": "管理者設定",
|
||||
"preferences_default_home_label": "ホームに表示するページ: ",
|
||||
"preferences_feed_menu_label": "フィードメニュー: ",
|
||||
"preferences_show_nick_label": "ニックネームを一番上に表示する: ",
|
||||
"preferences_feed_menu_label": "フィードのメニュー: ",
|
||||
"preferences_show_nick_label": "ログイン名を上部に表示: ",
|
||||
"Top enabled: ": "トップページを有効化: ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA を有効化: ",
|
||||
"Login enabled: ": "ログインを有効化: ",
|
||||
@ -120,18 +117,18 @@
|
||||
"Subscription manager": "登録チャンネルの管理",
|
||||
"Token manager": "トークンの管理",
|
||||
"Token": "トークン",
|
||||
"tokens_count_0": "{{count}} 個のトークン",
|
||||
"tokens_count_0": "{{count}}個のトークン",
|
||||
"Import/export": "インポート/エクスポート",
|
||||
"unsubscribe": "登録解除",
|
||||
"revoke": "取り消す",
|
||||
"Subscriptions": "登録チャンネル",
|
||||
"subscriptions_unseen_notifs_count_0": "{{count}} 個の未読通知",
|
||||
"subscriptions_unseen_notifs_count_0": "{{count}}件の未読通知",
|
||||
"search": "検索",
|
||||
"Log out": "ログアウト",
|
||||
"Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開",
|
||||
"Source available here.": "ソースはここで閲覧可能です。",
|
||||
"View JavaScript license information.": "JavaScript ライセンス情報",
|
||||
"View privacy policy.": "プライバシーポリシー",
|
||||
"View privacy policy.": "個人情報保護方針",
|
||||
"Trending": "急上昇",
|
||||
"Public": "公開",
|
||||
"Unlisted": "限定公開",
|
||||
@ -142,11 +139,11 @@
|
||||
"Delete playlist": "再生リストを削除",
|
||||
"Create playlist": "再生リストを作成",
|
||||
"Title": "タイトル",
|
||||
"Playlist privacy": "再生リストの公開設定",
|
||||
"Playlist privacy": "再生リストの公開状態",
|
||||
"Editing playlist `x`": "再生リスト `x` を編集中",
|
||||
"Show more": "もっと見る",
|
||||
"Show less": "表示を少なく",
|
||||
"Watch on YouTube": "YouTube で視聴",
|
||||
"Watch on YouTube": "YouTubeで視聴",
|
||||
"Switch Invidious Instance": "Invidious インスタンスの変更",
|
||||
"Hide annotations": "アノテーションを隠す",
|
||||
"Show annotations": "アノテーションを表示",
|
||||
@ -161,27 +158,22 @@
|
||||
"Premieres in `x`": "`x`後にプレミア公開",
|
||||
"Premieres `x`": "`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.": "やあ!君は JavaScript を無効にしているのかな?ここをクリックしてコメントを見れるけど、読み込みには少し時間がかかることがあるのを覚えておいてね。",
|
||||
"View YouTube comments": "YouTube のコメントを見る",
|
||||
"View YouTube comments": "YouTube のコメントを表示",
|
||||
"View more comments on Reddit": "Reddit でコメントをもっと見る",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 件のコメントを見る",
|
||||
"": "`x` 件のコメントを見る"
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` 件のコメントを表示",
|
||||
"": "`x` 件のコメントを表示"
|
||||
},
|
||||
"View Reddit comments": "Reddit のコメントを見る",
|
||||
"View Reddit comments": "Reddit のコメントを表示",
|
||||
"Hide replies": "返信を非表示",
|
||||
"Show replies": "返信を表示",
|
||||
"Incorrect password": "パスワードが間違っています",
|
||||
"Quota exceeded, try again in a few hours": "試行を制限中です。数時間後にやり直してください",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "ログインできませんでした。2段階認証 (認証アプリまたは SMS) が有効になっていることを確認してください。",
|
||||
"Invalid TFA code": "TFA (2段階認証) コードが無効です",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "ログインに失敗しました。あなたのアカウントで2段階認証が有効になっていない可能性があります。",
|
||||
"Wrong answer": "回答が間違っています",
|
||||
"Erroneous CAPTCHA": "CAPTCHA が間違っています",
|
||||
"CAPTCHA is a required field": "CAPTCHA は必須項目です",
|
||||
"User ID is a required field": "ユーザー ID は必須項目です",
|
||||
"Password is a required field": "パスワードは必須項目です",
|
||||
"Wrong username or password": "ユーザー名またはパスワードが間違っています",
|
||||
"Please sign in using 'Log in with Google'": "「Google でログイン」を使用してログインしてください",
|
||||
"Password cannot be empty": "パスワードは空にできません",
|
||||
"Password cannot be longer than 55 characters": "パスワードは55文字より長くできません",
|
||||
"Please log in": "ログインしてください",
|
||||
@ -314,7 +306,7 @@
|
||||
"Zulu": "ズール語",
|
||||
"generic_count_years_0": "{{count}}年",
|
||||
"generic_count_months_0": "{{count}}か月",
|
||||
"generic_count_weeks_0": "{{count}}週",
|
||||
"generic_count_weeks_0": "{{count}}週間",
|
||||
"generic_count_days_0": "{{count}}日",
|
||||
"generic_count_hours_0": "{{count}}時間",
|
||||
"generic_count_minutes_0": "{{count}}分",
|
||||
@ -326,8 +318,8 @@
|
||||
"About": "このサービスについて",
|
||||
"Rating: ": "評価: ",
|
||||
"preferences_locale_label": "言語: ",
|
||||
"View as playlist": "再生リストで見る",
|
||||
"Default": "デフォルト",
|
||||
"View as playlist": "再生リストとして閲覧",
|
||||
"Default": "標準",
|
||||
"Music": "音楽",
|
||||
"Gaming": "ゲーム",
|
||||
"News": "ニュース",
|
||||
@ -347,7 +339,7 @@
|
||||
"search_filters_sort_option_relevance": "関連度",
|
||||
"search_filters_sort_option_rating": "評価",
|
||||
"search_filters_sort_option_date": "アップロード日",
|
||||
"search_filters_sort_option_views": "再生回数",
|
||||
"search_filters_sort_option_views": "視聴回数",
|
||||
"search_filters_type_label": "種類",
|
||||
"search_filters_duration_label": "再生時間",
|
||||
"search_filters_features_label": "特徴",
|
||||
@ -375,7 +367,7 @@
|
||||
"next_steps_error_message_refresh": "再読込",
|
||||
"next_steps_error_message_go_to_youtube": "YouTubeへ",
|
||||
"search_filters_duration_option_short": "4 分未満",
|
||||
"footer_documentation": "文書",
|
||||
"footer_documentation": "説明書",
|
||||
"footer_source_code": "ソースコード",
|
||||
"footer_original_source_code": "元のソースコード",
|
||||
"footer_modfied_source_code": "改変して使用",
|
||||
@ -383,7 +375,7 @@
|
||||
"search_filters_duration_option_long": "20 分以上",
|
||||
"preferences_region_label": "地域: ",
|
||||
"footer_donate_page": "寄付する",
|
||||
"preferences_quality_dash_label": "優先するDash画質 : ",
|
||||
"preferences_quality_dash_label": "優先するDASH画質: ",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"preferences_quality_dash_option_144p": "144p",
|
||||
@ -403,11 +395,11 @@
|
||||
"none": "なし",
|
||||
"download_subtitles": "字幕 - `x` (.vtt)",
|
||||
"search_filters_features_option_purchased": "購入済み",
|
||||
"preferences_quality_option_dash": "DASH (適応品質)",
|
||||
"preferences_quality_option_dash": "DASH (適応的画質)",
|
||||
"preferences_quality_dash_option_worst": "最悪",
|
||||
"preferences_quality_dash_option_best": "最高",
|
||||
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
|
||||
"videoinfo_watch_on_youTube": "YouTube上で見る",
|
||||
"videoinfo_watch_on_youTube": "YouTubeで視聴",
|
||||
"user_created_playlists": "`x`個の作成した再生リスト",
|
||||
"Video unavailable": "動画は利用できません",
|
||||
"Chinese": "中国語",
|
||||
@ -442,9 +434,9 @@
|
||||
"crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>を試す",
|
||||
"crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む",
|
||||
"Popular enabled: ": "人気動画を有効化 ",
|
||||
"search_message_use_another_instance": " <a href=\"`x`\">別のインスタンス上でも検索</a>できます。",
|
||||
"search_message_use_another_instance": " <a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
|
||||
"search_filters_apply_button": "選択したフィルターを適用",
|
||||
"user_saved_playlists": "`x` 個の保存した再生リスト",
|
||||
"user_saved_playlists": "`x`個の保存済みの再生リスト",
|
||||
"crash_page_you_found_a_bug": "Invidious のバグのようです!",
|
||||
"crash_page_refresh": "<a href=\"`x`\">ページを更新</a>を試す",
|
||||
"preferences_watch_history_label": "再生履歴を有効化 ",
|
||||
@ -465,5 +457,8 @@
|
||||
"Artist: ": "アーティスト: ",
|
||||
"Album: ": "アルバム: ",
|
||||
"Song: ": "曲: ",
|
||||
"Channel Sponsor": "チャンネルのスポンサー"
|
||||
"Channel Sponsor": "チャンネルのスポンサー",
|
||||
"Standard YouTube license": "標準 Youtube ライセンス",
|
||||
"Download is disabled": "ダウンロード: このインスタンスでは未対応",
|
||||
"Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)"
|
||||
}
|
||||
|
@ -32,7 +32,6 @@
|
||||
"preferences_video_loop_label": "항상 반복: ",
|
||||
"preferences_category_player": "플레이어 설정",
|
||||
"Preferences": "설정",
|
||||
"Google verification code": "구글 인증 코드",
|
||||
"E-mail": "이메일",
|
||||
"Register": "회원가입",
|
||||
"Sign In": "로그인",
|
||||
@ -42,11 +41,10 @@
|
||||
"Time (h:mm:ss):": "시각 (h:mm:ss):",
|
||||
"Password": "비밀번호",
|
||||
"User ID": "사용자 ID",
|
||||
"Log in with Google": "구글로 로그인",
|
||||
"Log in/register": "로그인/회원가입",
|
||||
"Log in": "로그인",
|
||||
"source": "출처",
|
||||
"JavaScript license information": "자바스크립트 라이센스 정보",
|
||||
"JavaScript license information": "자바스크립트 라이선스 정보",
|
||||
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
||||
"History": "역사",
|
||||
"Delete account?": "계정을 삭제 하시겠습니까?",
|
||||
@ -65,7 +63,6 @@
|
||||
"Yes": "예",
|
||||
"Authorize token for `x`?": "`x` 에 대한 토큰을 승인하시겠습니까?",
|
||||
"Authorize token?": "토큰을 승인하시겠습니까?",
|
||||
"Cannot change password for Google accounts": "구글 계정의 비밀번호를 변경할 수 없습니다",
|
||||
"New passwords must match": "새 비밀번호는 일치해야 합니다",
|
||||
"New password": "새 비밀번호",
|
||||
"Clear watch history?": "재생 기록을 삭제 하시겠습니까?",
|
||||
@ -112,11 +109,10 @@
|
||||
"This channel does not exist.": "이 채널은 존재하지 않습니다.",
|
||||
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
|
||||
"channel:`x`": "채널:`x`",
|
||||
"Invalid TFA code": "유효하지 않은 TFA 코드",
|
||||
"Show replies": "댓글 보기",
|
||||
"Hide replies": "댓글 숨기기",
|
||||
"Incorrect password": "잘못된 비밀번호",
|
||||
"License: ": "라이센스: ",
|
||||
"License: ": "라이선스: ",
|
||||
"Genre: ": "장르: ",
|
||||
"Editing playlist `x`": "재생목록 `x` 수정하기",
|
||||
"Playlist privacy": "재생목록 공개 범위",
|
||||
@ -135,7 +131,7 @@
|
||||
"Unlisted": "목록에 없음",
|
||||
"Public": "공개",
|
||||
"View privacy policy.": "개인정보 처리방침 보기.",
|
||||
"View JavaScript license information.": "자바스크립트 라이센스 정보 보기.",
|
||||
"View JavaScript license information.": "자바스크립트 라이선스 정보 보기.",
|
||||
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
|
||||
"Log out": "로그아웃",
|
||||
"search": "검색",
|
||||
@ -249,7 +245,6 @@
|
||||
"Engagement: ": "약속: ",
|
||||
"Wilson score: ": "Wilson Score: ",
|
||||
"Family friendly? ": "전연령 영상입니까? ",
|
||||
"Quota exceeded, try again in a few hours": "한도량을 초과했습니다. 몇 시간 후에 다시 시도하세요",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x`개의 댓글 보기",
|
||||
"": "`x`개의 댓글 보기"
|
||||
@ -272,7 +267,6 @@
|
||||
"Bulgarian": "불가리아어",
|
||||
"Bosnian": "보스니아어",
|
||||
"Belarusian": "벨라루스어",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "로그인할 수 없습니다. 이중 인증(Authenticator 또는 SMS)이 켜져 있는지 확인하세요.",
|
||||
"View more comments on Reddit": "레딧에서 더 많은 댓글 보기",
|
||||
"View YouTube comments": "유튜브 댓글 보기",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
|
||||
@ -282,13 +276,11 @@
|
||||
"Please log in": "로그인하세요",
|
||||
"Password cannot be longer than 55 characters": "비밀번호는 55자 이하여야 합니다",
|
||||
"Password cannot be empty": "비밀번호는 비워둘 수 없습니다",
|
||||
"Please sign in using 'Log in with Google'": "'구글로 로그인'을 사용하여 로그인하세요",
|
||||
"Wrong username or password": "잘못된 사용자 이름 또는 비밀번호",
|
||||
"Password is a required field": "비밀번호는 필수 입력란입니다",
|
||||
"User ID is a required field": "사용자 ID는 필수 입력란입니다",
|
||||
"CAPTCHA is a required field": "캡차는 필수 입력란입니다",
|
||||
"Erroneous CAPTCHA": "잘못된 캡차",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "로그인 실패. 계정에 이중 인증이 설정되어 있지 않기 때문일 수 있습니다.",
|
||||
"Blacklisted regions: ": "차단된 지역: ",
|
||||
"Playlists": "재생목록",
|
||||
"View as playlist": "재생목록으로 보기",
|
||||
@ -460,5 +452,13 @@
|
||||
"channel_tab_shorts_label": "쇼츠",
|
||||
"channel_tab_streams_label": "실시간 스트리밍",
|
||||
"channel_tab_channels_label": "채널",
|
||||
"channel_tab_playlists_label": "재생목록"
|
||||
"channel_tab_playlists_label": "재생목록",
|
||||
"Standard YouTube license": "표준 유튜브 라이선스",
|
||||
"Song: ": "제목: ",
|
||||
"Channel Sponsor": "채널 스폰서",
|
||||
"Album: ": "앨범: ",
|
||||
"Music in this video": "동영상 속 음악",
|
||||
"Artist: ": "아티스트: ",
|
||||
"Download is disabled": "다운로드가 비활성화 되어있음",
|
||||
"Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Išvalyti žiūrėjimo istoriją?",
|
||||
"New password": "Naujas slaptažodis",
|
||||
"New passwords must match": "Naujas slaptažodis turi sutapti",
|
||||
"Cannot change password for Google accounts": "Negalima pakeisti Google paskyros slaptažodžio",
|
||||
"Authorize token?": "Autorizuoti žetoną?",
|
||||
"Authorize token for `x`?": "Autorizuoti žetoną `x`?",
|
||||
"Yes": "Taip",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "šaltinis",
|
||||
"Log in": "Prisijungti",
|
||||
"Log in/register": "Prisijungti/ registruotis",
|
||||
"Log in with Google": "Prisijungti naudojantis Google",
|
||||
"User ID": "Naudotojo ID",
|
||||
"Password": "Slaptažodis",
|
||||
"Time (h:mm:ss):": "Laikas (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Prisijungti",
|
||||
"Register": "Registruotis",
|
||||
"E-mail": "El. paštas",
|
||||
"Google verification code": "Google patvirtinimo kodas",
|
||||
"Preferences": "Pasirinktys",
|
||||
"preferences_category_player": "Grotuvo pasirinktys",
|
||||
"preferences_video_loop_label": "Visada kartoti: ",
|
||||
@ -164,17 +161,12 @@
|
||||
"Hide replies": "Slėpti atsakymus",
|
||||
"Show replies": "Rodyti atsakymus",
|
||||
"Incorrect password": "Slaptažodis neteisingas",
|
||||
"Quota exceeded, try again in a few hours": "Viršyta kvota, bandykite dar kartą po keleto valandų",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nepavyko prisijungti, įsitikinkite, kad yra įjungta dviejų etapų autentifikacija (Autentifikatorius arba SMS).",
|
||||
"Invalid TFA code": "Neteisingas TFA kodas",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Prisijungimas nepavyko. Tai gali būti todėl, kad jūsų paskyroje nėra įjungta dviejų etapų autentifikacija.",
|
||||
"Wrong answer": "Atsakymas neteisingas",
|
||||
"Erroneous CAPTCHA": "Klaidinga CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA yra reikalinga šiam laukeliui",
|
||||
"User ID is a required field": "Vartotojo ID yra reikalingas šiam laukeliui",
|
||||
"Password is a required field": "Slaptažodis yra reikalingas šiam laukeliui",
|
||||
"Wrong username or password": "Neteisingas vartotojo vardas arba slaptažodis",
|
||||
"Please sign in using 'Log in with Google'": "Prašome prisijungti naudojant \"Prisijungti su\" Google \"",
|
||||
"Password cannot be empty": "Slaptažodžio laukelis negali būti tuščias",
|
||||
"Password cannot be longer than 55 characters": "Slaptažodis negali būti ilgesnis nei 55 simboliai",
|
||||
"Please log in": "Prašome prisijungti",
|
||||
@ -488,5 +480,6 @@
|
||||
"preferences_save_player_pos_label": "Išsaugoti atkūrimo padėtį: ",
|
||||
"videoinfo_youTube_embed_link": "Įterpti",
|
||||
"videoinfo_invidious_embed_link": "Įterpti nuorodą",
|
||||
"crash_page_refresh": "pabandėte <a href=\"`x`\">atnaujinti puslapį</a>"
|
||||
"crash_page_refresh": "pabandėte <a href=\"`x`\">atnaujinti puslapį</a>",
|
||||
"Album: ": "Albumas "
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Tøm visningshistorikk?",
|
||||
"New password": "Nytt passord",
|
||||
"New passwords must match": "Nye passordfelter må stemme overens",
|
||||
"Cannot change password for Google accounts": "Kan ikke endre passord for Google-kontoer",
|
||||
"Authorize token?": "Identitetsbekreft symbol?",
|
||||
"Authorize token for `x`?": "Identitetsbekreft symbol for `x`?",
|
||||
"Yes": "Ja",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "kilde",
|
||||
"Log in": "Logg inn",
|
||||
"Log in/register": "Logg inn/registrer",
|
||||
"Log in with Google": "Logg inn med Google",
|
||||
"User ID": "Bruker-ID",
|
||||
"Password": "Passord",
|
||||
"Time (h:mm:ss):": "Tid (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Innlogging",
|
||||
"Register": "Registrer",
|
||||
"E-mail": "E-post",
|
||||
"Google verification code": "Google-bekreftelseskode",
|
||||
"Preferences": "Innstillinger",
|
||||
"preferences_category_player": "Avspillerinnstillinger",
|
||||
"preferences_video_loop_label": "Alltid gjenta: ",
|
||||
@ -164,17 +161,12 @@
|
||||
"Hide replies": "Skjul svar",
|
||||
"Show replies": "Vis svar",
|
||||
"Incorrect password": "Feil passord",
|
||||
"Quota exceeded, try again in a few hours": "Kvote overskredet, prøv igjen om et par timer",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Kunne ikke logge inn, forsikre deg om at tofaktor-identitetsbekreftelse (Authenticator eller SMS) er skrudd på.",
|
||||
"Invalid TFA code": "Ugyldig tofaktorkode",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Innlogging mislyktes. Dette kan være fordi tofaktor-identitetsbekreftelse er skrudd av på kontoen din.",
|
||||
"Wrong answer": "Ugyldig svar",
|
||||
"Erroneous CAPTCHA": "Ugyldig CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA er et påkrevd felt",
|
||||
"User ID is a required field": "Bruker-ID er et påkrevd felt",
|
||||
"Password is a required field": "Passord er et påkrevd felt",
|
||||
"Wrong username or password": "Ugyldig brukernavn eller passord",
|
||||
"Please sign in using 'Log in with Google'": "Logg inn ved bruk av \"Google-innlogging\"",
|
||||
"Password cannot be empty": "Passordet kan ikke være tomt",
|
||||
"Password cannot be longer than 55 characters": "Passordet kan ikke være lengre enn 55 tegn",
|
||||
"Please log in": "Logg inn",
|
||||
@ -472,5 +464,17 @@
|
||||
"search_filters_apply_button": "Bruk valgte filtre",
|
||||
"search_filters_date_option_none": "Siden begynnelsen",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"error_video_not_in_playlist": "Forespurt video finnes ikke i denne spillelisten. <a href=\"`x`\">Trykk her for spillelistens hjemmeside.</a>"
|
||||
"error_video_not_in_playlist": "Forespurt video finnes ikke i denne spillelisten. <a href=\"`x`\">Trykk her for spillelistens hjemmeside.</a>",
|
||||
"Standard YouTube license": "Standard YouTube-lisens",
|
||||
"Song: ": "Sang: ",
|
||||
"channel_tab_streams_label": "Direktesendinger",
|
||||
"channel_tab_shorts_label": "Kortvideoer",
|
||||
"channel_tab_playlists_label": "Spillelister",
|
||||
"Music in this video": "Musikk i denne videoen",
|
||||
"channel_tab_channels_label": "Kanaler",
|
||||
"Artist: ": "Artist: ",
|
||||
"Album: ": "Album: ",
|
||||
"Download is disabled": "Nedlasting er avskrudd",
|
||||
"Channel Sponsor": "Kanalsponsor",
|
||||
"Import YouTube playlist (.csv)": "Importer YouTube-spilleliste (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Wil je de kijkgeschiedenis wissen?",
|
||||
"New password": "Nieuw wachtwoord",
|
||||
"New passwords must match": "De nieuwe wachtwoorden moeten overeenkomen",
|
||||
"Cannot change password for Google accounts": "Kan het wachtwoord van Google-accounts niet wijzigen",
|
||||
"Authorize token?": "Wil je de toegangssleutel machtigen?",
|
||||
"Authorize token for `x`?": "Wil je de toegangssleutel machtigen voor `x`?",
|
||||
"Yes": "Ja",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "bron",
|
||||
"Log in": "Inloggen",
|
||||
"Log in/register": "Inloggen/Registreren",
|
||||
"Log in with Google": "Inloggen met Google",
|
||||
"User ID": "Gebruikers-id",
|
||||
"Password": "Wachtwoord",
|
||||
"Time (h:mm:ss):": "Tijd (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Inloggen",
|
||||
"Register": "Registreren",
|
||||
"E-mail": "E-mailadres",
|
||||
"Google verification code": "Google-verificatiecode",
|
||||
"Preferences": "Instellingen",
|
||||
"preferences_category_player": "Spelerinstellingen",
|
||||
"preferences_video_loop_label": "Altijd herhalen: ",
|
||||
@ -159,17 +156,12 @@
|
||||
"Hide replies": "Antwoorden verbergen",
|
||||
"Show replies": "Antwoorden tonen",
|
||||
"Incorrect password": "Wachtwoord is onjuist",
|
||||
"Quota exceeded, try again in a few hours": "Quota overschreden; probeer het over een paar uur opnieuw",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Kan niet inloggen. Zorg ervoor dat authenticatie in twee stappen (Authenticator of sms) is ingeschakeld.",
|
||||
"Invalid TFA code": "Onjuiste TFA-code",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Inloggen mislukt. Wellicht is authenticatie in twee stappen niet ingeschakeld op je account.",
|
||||
"Wrong answer": "Onjuist antwoord",
|
||||
"Erroneous CAPTCHA": "Onjuiste CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA is vereist",
|
||||
"User ID is a required field": "Gebruikers-id is vereist",
|
||||
"Password is a required field": "Wachtwoord is vereist",
|
||||
"Wrong username or password": "Onjuiste gebruikersnaam of wachtwoord",
|
||||
"Please sign in using 'Log in with Google'": "Log in via 'Inloggen met Google'",
|
||||
"Password cannot be empty": "Het wachtwoordveld mag niet leeg zijn",
|
||||
"Password cannot be longer than 55 characters": "Het wachtwoord mag niet langer dan 55 tekens zijn",
|
||||
"Please log in": "Log in",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Wyczyścić historię?",
|
||||
"New password": "Nowe hasło",
|
||||
"New passwords must match": "Nowe hasła muszą być identyczne",
|
||||
"Cannot change password for Google accounts": "Nie można zmienić hasła do konta Google",
|
||||
"Authorize token?": "Autoryzować token?",
|
||||
"Authorize token for `x`?": "Autoryzować token dla `x`?",
|
||||
"Yes": "Tak",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "źródło",
|
||||
"Log in": "Zaloguj",
|
||||
"Log in/register": "Zaloguj/Zarejestruj",
|
||||
"Log in with Google": "Zaloguj do Google",
|
||||
"User ID": "ID użytkownika",
|
||||
"Password": "Hasło",
|
||||
"Time (h:mm:ss):": "Godzina (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Zaloguj się",
|
||||
"Register": "Zarejestruj się",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Kod weryfikacyjny Google",
|
||||
"Preferences": "Preferencje",
|
||||
"preferences_category_player": "Ustawienia odtwarzacza",
|
||||
"preferences_video_loop_label": "Zawsze zapętlaj: ",
|
||||
@ -163,17 +160,12 @@
|
||||
"Hide replies": "Ukryj odpowiedzi",
|
||||
"Show replies": "Pokaż odpowiedzi",
|
||||
"Incorrect password": "Niepoprawne hasło",
|
||||
"Quota exceeded, try again in a few hours": "Przekroczony limit zapytań, spróbuj ponownie za kilka godzin",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Nie udało się zalogować, upewnij się, że dwuetapowe uwierzytelnianie (Autentykator lub SMS) jest aktywne.",
|
||||
"Invalid TFA code": "Niepoprawny kod TFA",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Nie udało się zalogować. To może być spowodowane wyłączoną dwustopniową autoryzacją na twoim koncie.",
|
||||
"Wrong answer": "Niepoprawna odpowiedź",
|
||||
"Erroneous CAPTCHA": "CAPTCHA wykonane błędnie",
|
||||
"CAPTCHA is a required field": "CAPTCHA jest polem wymaganym",
|
||||
"User ID is a required field": "ID użytkownika jest polem wymaganym",
|
||||
"Password is a required field": "Hasło jest polem wymaganym",
|
||||
"Wrong username or password": "Niepoprawny login lub hasło",
|
||||
"Please sign in using 'Log in with Google'": "Zaloguj się używając \"Zaloguj się przez Google\"",
|
||||
"Password cannot be empty": "Hasło nie może być puste",
|
||||
"Password cannot be longer than 55 characters": "Hasło nie może być dłuższe niż 55 znaków",
|
||||
"Please log in": "Proszę się zalogować",
|
||||
@ -498,5 +490,7 @@
|
||||
"Artist: ": "Wykonawca: ",
|
||||
"Album: ": "Album: ",
|
||||
"Song: ": "Piosenka: ",
|
||||
"Channel Sponsor": "Sponsor kanału"
|
||||
"Channel Sponsor": "Sponsor kanału",
|
||||
"Standard YouTube license": "Standardowa licencja YouTube",
|
||||
"Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Limpar histórico de reprodução?",
|
||||
"New password": "Nova senha",
|
||||
"New passwords must match": "Nova senha deve ser igual",
|
||||
"Cannot change password for Google accounts": "Não é possível alterar sua senha de contas do Google",
|
||||
"Authorize token?": "Autorizar o token?",
|
||||
"Authorize token for `x`?": "Autorizar o token para `x`?",
|
||||
"Yes": "Sim",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "código-fonte",
|
||||
"Log in": "Entrar",
|
||||
"Log in/register": "Entrar/Registrar",
|
||||
"Log in with Google": "Entrar com conta Google",
|
||||
"User ID": "Usuário",
|
||||
"Password": "Senha",
|
||||
"Time (h:mm:ss):": "Hora (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Entrar",
|
||||
"Register": "Registrar",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Código de verificação do Google",
|
||||
"Preferences": "Preferências",
|
||||
"preferences_category_player": "Preferências do reprodutor",
|
||||
"preferences_video_loop_label": "Repetir sempre: ",
|
||||
@ -166,17 +163,12 @@
|
||||
"Hide replies": "Ocultar respostas",
|
||||
"Show replies": "Mostrar respostas",
|
||||
"Incorrect password": "Senha incorreta",
|
||||
"Quota exceeded, try again in a few hours": "Cota excedida, tente novamente em algumas horas",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não foi possível fazer login, sua autenticação em dois passos (app autenticador ou sms) deve estar ativada.",
|
||||
"Invalid TFA code": "Código TFA inválido",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Falha no login. Isso pode acontecer porque a autenticação em dois passos está desativada para sua conta.",
|
||||
"Wrong answer": "Resposta incorreta",
|
||||
"Erroneous CAPTCHA": "CAPTCHA inválido",
|
||||
"CAPTCHA is a required field": "O CAPTCHA é um campo obrigatório",
|
||||
"User ID is a required field": "O nome de usuário é um campo obrigatório",
|
||||
"Password is a required field": "A senha é um campo obrigatório",
|
||||
"Wrong username or password": "Nome de usuário ou senha inválidos",
|
||||
"Please sign in using 'Log in with Google'": "Por favor, entre usando 'Entrar com conta Google'",
|
||||
"Password cannot be empty": "A senha não pode ficar em branco",
|
||||
"Password cannot be longer than 55 characters": "A senha não pode ter mais que 55 caracteres",
|
||||
"Please log in": "Por favor, inicie sua sessão",
|
||||
@ -479,5 +471,10 @@
|
||||
"channel_tab_streams_label": "Ao Vivo",
|
||||
"Music in this video": "Música neste vídeo",
|
||||
"Artist: ": "Artista: ",
|
||||
"Album: ": "Álbum: "
|
||||
"Album: ": "Álbum: ",
|
||||
"Standard YouTube license": "Licença padrão do YouTube",
|
||||
"Song: ": "Música: ",
|
||||
"Channel Sponsor": "Patrocinador do Canal",
|
||||
"Download is disabled": "Download está desativado",
|
||||
"Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Limpar histórico de reprodução?",
|
||||
"New password": "Nova palavra-chave",
|
||||
"New passwords must match": "As novas palavra-chaves devem corresponder",
|
||||
"Cannot change password for Google accounts": "Não é possível alterar a palavra-chave para contas do Google",
|
||||
"Authorize token?": "Autorizar token?",
|
||||
"Authorize token for `x`?": "Autorizar token para `x`?",
|
||||
"Yes": "Sim",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "código-fonte",
|
||||
"Log in": "Iniciar sessão",
|
||||
"Log in/register": "Iniciar sessão/registar",
|
||||
"Log in with Google": "Iniciar sessão com o Google",
|
||||
"User ID": "Utilizador",
|
||||
"Password": "Palavra-chave",
|
||||
"Time (h:mm:ss):": "Tempo (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Iniciar sessão",
|
||||
"Register": "Registar",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Código de verificação do Google",
|
||||
"Preferences": "Preferências",
|
||||
"preferences_category_player": "Preferências do reprodutor",
|
||||
"preferences_video_loop_label": "Repetir sempre: ",
|
||||
@ -166,17 +163,12 @@
|
||||
"Hide replies": "Ocultar respostas",
|
||||
"Show replies": "Mostrar respostas",
|
||||
"Incorrect password": "Palavra-chave incorreta",
|
||||
"Quota exceeded, try again in a few hours": "Cota excedida. Tente novamente dentro de algumas horas",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não é possível iniciar a sessão, certifique-se que a autenticação de dois fatores (Autenticador ou SMS) está ativada.",
|
||||
"Invalid TFA code": "Código TFA inválido",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Falhou o início de sessão. Isto pode ser devido a não ter ativado na sua conta a autenticação de dois fatores (2FA).",
|
||||
"Wrong answer": "Resposta errada",
|
||||
"Erroneous CAPTCHA": "CAPTCHA inválido",
|
||||
"CAPTCHA is a required field": "CAPTCHA é um campo obrigatório",
|
||||
"User ID is a required field": "O nome de utilizador é um campo obrigatório",
|
||||
"Password is a required field": "Palavra-chave é um campo obrigatório",
|
||||
"Wrong username or password": "Nome de utilizador ou palavra-chave incorreto",
|
||||
"Please sign in using 'Log in with Google'": "Por favor, inicie sessão usando 'Iniciar sessão com o Google'",
|
||||
"Password cannot be empty": "A palavra-chave não pode estar vazia",
|
||||
"Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres",
|
||||
"Please log in": "Por favor, inicie sessão",
|
||||
|
@ -63,8 +63,6 @@
|
||||
"Could not pull trending pages.": "Não foi possível obter as páginas de tendências.",
|
||||
"Could not create mix.": "Não foi possível criar a mistura.",
|
||||
"Deleted or invalid channel": "Canal eliminado ou inválido",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Falhou o início de sessão. Isto pode ser devido a não ter ativado na sua conta a autenticação de dois fatores (2FA).",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Não é possível iniciar a sessão, certifique-se que a autenticação de dois fatores (Autenticador ou SMS) está ativada.",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.",
|
||||
"Delete playlist": "Eliminar lista de reprodução",
|
||||
"Delete playlist `x`?": "Eliminar a lista de reprodução 'x'?",
|
||||
@ -81,7 +79,6 @@
|
||||
"Log in/register": "Iniciar sessão/registar",
|
||||
"Delete account?": "Eliminar conta?",
|
||||
"Import and Export Data": "Importar e exportar dados",
|
||||
"Cannot change password for Google accounts": "Não é possível alterar a palavra-chave para contas do Google",
|
||||
"Filipino": "Filipino",
|
||||
"Estonian": "Estónio",
|
||||
"Esperanto": "Esperanto",
|
||||
@ -125,15 +122,12 @@
|
||||
"Please log in": "Por favor, inicie sessão",
|
||||
"Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres",
|
||||
"Password cannot be empty": "A palavra-chave não pode estar vazia",
|
||||
"Please sign in using 'Log in with Google'": "Por favor, inicie sessão usando 'Iniciar sessão com o Google'",
|
||||
"Wrong username or password": "Nome de utilizador ou palavra-chave incorreto",
|
||||
"Password is a required field": "Palavra-chave é um campo obrigatório",
|
||||
"User ID is a required field": "O nome de utilizador é um campo obrigatório",
|
||||
"CAPTCHA is a required field": "CAPTCHA é um campo obrigatório",
|
||||
"Erroneous CAPTCHA": "CAPTCHA inválido",
|
||||
"Wrong answer": "Resposta errada",
|
||||
"Invalid TFA code": "Código TFA inválido",
|
||||
"Quota exceeded, try again in a few hours": "Cota excedida. Tente novamente dentro de algumas horas",
|
||||
"Incorrect password": "Palavra-chave incorreta",
|
||||
"Show replies": "Mostrar respostas",
|
||||
"Hide replies": "Ocultar respostas",
|
||||
@ -232,7 +226,6 @@
|
||||
"preferences_video_loop_label": "Repetir sempre: ",
|
||||
"preferences_category_player": "Preferências do reprodutor",
|
||||
"Preferences": "Preferências",
|
||||
"Google verification code": "Código de verificação do Google",
|
||||
"E-mail": "E-mail",
|
||||
"Register": "Registar",
|
||||
"Image CAPTCHA": "Imagem CAPTCHA",
|
||||
@ -240,7 +233,6 @@
|
||||
"Time (h:mm:ss):": "Tempo (h:mm:ss):",
|
||||
"Password": "Palavra-chave",
|
||||
"User ID": "Utilizador",
|
||||
"Log in with Google": "Iniciar sessão com o Google",
|
||||
"Log in": "Iniciar sessão",
|
||||
"source": "código-fonte",
|
||||
"JavaScript license information": "Informação de licença do JavaScript",
|
||||
@ -481,5 +473,8 @@
|
||||
"Artist: ": "Artista: ",
|
||||
"Album: ": "Álbum: ",
|
||||
"Song: ": "Canção: ",
|
||||
"Channel Sponsor": "Patrocinador do canal"
|
||||
"Channel Sponsor": "Patrocinador do canal",
|
||||
"Standard YouTube license": "Licença padrão do YouTube",
|
||||
"Download is disabled": "A descarga está desativada",
|
||||
"Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Doriți să ștergeți istoricul?",
|
||||
"New password": "Parola nouă",
|
||||
"New passwords must match": "Câmpurile \"Parolă nouă\" trebuie să fie identice",
|
||||
"Cannot change password for Google accounts": "Parola pentru un cont Google nu poate fi schimbată de pe Invidious",
|
||||
"Authorize token?": "Autorizați token-ul?",
|
||||
"Authorize token for `x`?": "Autorizați token-ul pentru `x` ?",
|
||||
"Yes": "Da",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "sursă",
|
||||
"Log in": "Conectați-vă",
|
||||
"Log in/register": "Conectați-vă/Creați-vă un cont",
|
||||
"Log in with Google": "Conectați-vă cu Google",
|
||||
"User ID": "ID Utilizator",
|
||||
"Password": "Parolă",
|
||||
"Time (h:mm:ss):": "Ora (h:mm:ss) :",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Conectați-vă",
|
||||
"Register": "Înregistrați-vă",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Cod de verificare Google",
|
||||
"Preferences": "Preferințe",
|
||||
"preferences_category_player": "Setări de redare",
|
||||
"preferences_video_loop_label": "Reluați videoclipul la nesfârșit: ",
|
||||
@ -155,17 +152,12 @@
|
||||
"Hide replies": "Ascundeți replicile",
|
||||
"Show replies": "Afișați replicile",
|
||||
"Incorrect password": "Parolă incorectă",
|
||||
"Quota exceeded, try again in a few hours": "Numărul de tentative de conectare a fost depășit. Va rugăm să încercați din nou în câteva ore",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Conectare eșuată. Dacă nu reușiți să vă conectați, verificați dacă ați activat autentificarea cu doi factori (Autentificator sau SMS).",
|
||||
"Invalid TFA code": "Codul de autentificare cu doi factori este invalid",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Conectare eșuată. Acest lucru ar putea fi cauzat de faptul că nu ați activat autentificarea cu doi factori.",
|
||||
"Wrong answer": "Răspuns invalid",
|
||||
"Erroneous CAPTCHA": "CAPTCHA invalid",
|
||||
"CAPTCHA is a required field": "Câmpul CAPTCHA este obligatoriu",
|
||||
"User ID is a required field": "Câmpul ID Utilizator este obligatoriu",
|
||||
"Password is a required field": "Câmpul Parolă este obligatoriu",
|
||||
"Wrong username or password": "Nume de utilizator sau parolă invalidă",
|
||||
"Please sign in using 'Log in with Google'": "Vă rog conectați-vă folosind \"Conectați-vă cu Google\"",
|
||||
"Password cannot be empty": "Parola nu poate fi goală",
|
||||
"Password cannot be longer than 55 characters": "Parola nu poate să conțină mai mult de 55 de caractere",
|
||||
"Please log in": "Vă rog conectați-vă",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Очистить историю просмотров?",
|
||||
"New password": "Новый пароль",
|
||||
"New passwords must match": "Новые пароли не совпадают",
|
||||
"Cannot change password for Google accounts": "Изменить пароль аккаунта Google невозможно",
|
||||
"Authorize token?": "Авторизовать токен?",
|
||||
"Authorize token for `x`?": "Авторизовать токен для `x`?",
|
||||
"Yes": "Да",
|
||||
@ -30,23 +29,21 @@
|
||||
"Export subscriptions as OPML": "Экспортировать подписки в формате OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)",
|
||||
"Export data as JSON": "Экспортировать данные Invidious в формате JSON",
|
||||
"Delete account?": "Удалить аккаунт?",
|
||||
"Delete account?": "Удалить учётку?",
|
||||
"History": "История",
|
||||
"An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube",
|
||||
"JavaScript license information": "Информация о лицензиях JavaScript",
|
||||
"source": "источник",
|
||||
"Log in": "Войти",
|
||||
"Log in/register": "Войти или зарегистрироваться",
|
||||
"Log in with Google": "Войти через Google",
|
||||
"User ID": "ID пользователя",
|
||||
"User ID": "ИД пользователя",
|
||||
"Password": "Пароль",
|
||||
"Time (h:mm:ss):": "Время (ч:мм:сс):",
|
||||
"Text CAPTCHA": "Текстовая капча (англ.)",
|
||||
"Image CAPTCHA": "Капча-картинка",
|
||||
"Sign In": "Войти",
|
||||
"Register": "Зарегистрироваться",
|
||||
"E-mail": "Электронная почта",
|
||||
"Google verification code": "Код подтверждения Google",
|
||||
"E-mail": "Эл. почта",
|
||||
"Preferences": "Настройки",
|
||||
"preferences_category_player": "Настройки проигрывателя",
|
||||
"preferences_video_loop_label": "Всегда повторять: ",
|
||||
@ -69,11 +66,11 @@
|
||||
"preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ",
|
||||
"preferences_category_visual": "Настройки сайта",
|
||||
"preferences_player_style_label": "Стиль проигрывателя: ",
|
||||
"Dark mode: ": "Темное оформление: ",
|
||||
"Dark mode: ": "Тёмное оформление: ",
|
||||
"preferences_dark_mode_label": "Тема: ",
|
||||
"dark": "темная",
|
||||
"dark": "тёмная",
|
||||
"light": "светлая",
|
||||
"preferences_thin_mode_label": "Облегченное оформление: ",
|
||||
"preferences_thin_mode_label": "Облегчённое оформление: ",
|
||||
"preferences_category_misc": "Прочие настройки",
|
||||
"preferences_automatic_instance_redirect_label": "Автоматическая смена зеркала (переход на redirect.invidious.io): ",
|
||||
"preferences_category_subscription": "Настройки подписок",
|
||||
@ -147,13 +144,13 @@
|
||||
"License: ": "Лицензия: ",
|
||||
"Family friendly? ": "Семейный просмотр: ",
|
||||
"Wilson score: ": "Оценка Уилсона: ",
|
||||
"Engagement: ": "Вовлеченность: ",
|
||||
"Engagement: ": "Вовлечённость: ",
|
||||
"Whitelisted regions: ": "Доступно в регионах: ",
|
||||
"Blacklisted regions: ": "Недоступно в регионах: ",
|
||||
"Shared `x`": "Опубликовано `x`",
|
||||
"Premieres in `x`": "Премьера через `x`",
|
||||
"Premieres `x`": "Премьера `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.": "Похоже, у вас отключен JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Похоже, у вас отключён JavaScript. Нажмите сюда, чтобы увидеть комментарии. Но учтите: они могут загружаться немного медленнее.",
|
||||
"View YouTube comments": "Показать комментарии с YouTube",
|
||||
"View more comments on Reddit": "Посмотреть больше комментариев на Reddit",
|
||||
"View `x` comments": {
|
||||
@ -164,39 +161,34 @@
|
||||
"Hide replies": "Скрыть ответы",
|
||||
"Show replies": "Показать ответы",
|
||||
"Incorrect password": "Неправильный пароль",
|
||||
"Quota exceeded, try again in a few hours": "Лимит превышен, попробуйте снова через несколько часов",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не удалось войти. Проверьте, не включена ли двухфакторная аутентификация (по коду или смс).",
|
||||
"Invalid TFA code": "Неправильный код двухфакторной аутентификации",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Не удалось войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.",
|
||||
"Wrong answer": "Неправильный ответ",
|
||||
"Erroneous CAPTCHA": "Неправильная капча",
|
||||
"CAPTCHA is a required field": "Необходимо решить капчу",
|
||||
"User ID is a required field": "Необходимо ввести ID пользователя",
|
||||
"User ID is a required field": "Необходимо ввести идентификатор пользователя",
|
||||
"Password is a required field": "Необходимо ввести пароль",
|
||||
"Wrong username or password": "Неправильный логин или пароль",
|
||||
"Please sign in using 'Log in with Google'": "Пожалуйста, нажмите «Войти через Google»",
|
||||
"Password cannot be empty": "Пароль не может быть пустым",
|
||||
"Password cannot be longer than 55 characters": "Пароль не может быть длиннее 55 символов",
|
||||
"Please log in": "Пожалуйста, войдите",
|
||||
"Invidious Private Feed for `x`": "Приватная лента Invidious для `x`",
|
||||
"channel:`x`": "канал: `x`",
|
||||
"Deleted or invalid channel": "Канал удален или не найден",
|
||||
"Deleted or invalid channel": "Канал удалён или не найден",
|
||||
"This channel does not exist.": "Такого канала не существует.",
|
||||
"Could not get channel info.": "Не удается получить информацию об этом канале.",
|
||||
"Could not fetch comments": "Не удается загрузить комментарии",
|
||||
"Could not get channel info.": "Не удаётся получить информацию об этом канале.",
|
||||
"Could not fetch comments": "Не удаётся загрузить комментарии",
|
||||
"`x` ago": "`x` назад",
|
||||
"Load more": "Загрузить еще",
|
||||
"Load more": "Загрузить ещё",
|
||||
"Could not create mix.": "Не удалось создать микс.",
|
||||
"Empty playlist": "Плейлист пуст",
|
||||
"Not a playlist.": "Это не плейлист.",
|
||||
"Playlist does not exist.": "Плейлист не существует.",
|
||||
"Could not pull trending pages.": "Не удается загрузить страницы «в тренде».",
|
||||
"Could not pull trending pages.": "Не удаётся загрузить страницы «в тренде».",
|
||||
"Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле «challenge»",
|
||||
"Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле «токен»",
|
||||
"Erroneous challenge": "Неправильный ответ в «challenge»",
|
||||
"Erroneous token": "Неправильный токен",
|
||||
"No such user": "Пользователь не найден",
|
||||
"Token is expired, please try again": "Срок действия токена истек, попробуйте позже",
|
||||
"Token is expired, please try again": "Срок действия токена истёк, попробуйте позже",
|
||||
"English": "Английский",
|
||||
"English (auto-generated)": "Английский (созданы автоматически)",
|
||||
"Afrikaans": "Африкаанс",
|
||||
@ -213,7 +205,7 @@
|
||||
"Burmese": "Бирманский",
|
||||
"Catalan": "Каталонский",
|
||||
"Cebuano": "Себуанский",
|
||||
"Chinese (Simplified)": "Китайский (упрощенный)",
|
||||
"Chinese (Simplified)": "Китайский (упрощённый)",
|
||||
"Chinese (Traditional)": "Китайский (традиционный)",
|
||||
"Corsican": "Корсиканский",
|
||||
"Croatian": "Хорватский",
|
||||
@ -328,14 +320,14 @@
|
||||
"channel_tab_videos_label": "Видео",
|
||||
"Playlists": "Плейлисты",
|
||||
"channel_tab_community_label": "Сообщество",
|
||||
"search_filters_sort_option_relevance": "по актуальности",
|
||||
"search_filters_sort_option_rating": "по рейтингу",
|
||||
"search_filters_sort_option_date": "по дате загрузки",
|
||||
"search_filters_sort_option_views": "по просмотрам",
|
||||
"search_filters_sort_option_relevance": "актуальности",
|
||||
"search_filters_sort_option_rating": "рейтингу",
|
||||
"search_filters_sort_option_date": "дате загрузки",
|
||||
"search_filters_sort_option_views": "просмотрам",
|
||||
"search_filters_type_label": "Тип",
|
||||
"search_filters_duration_label": "Длительность",
|
||||
"search_filters_features_label": "Дополнительно",
|
||||
"search_filters_sort_label": "Сортировать",
|
||||
"search_filters_sort_label": "Сортировать по",
|
||||
"search_filters_date_option_hour": "Последний час",
|
||||
"search_filters_date_option_today": "Сегодня",
|
||||
"search_filters_date_option_week": "Эта неделя",
|
||||
@ -379,7 +371,7 @@
|
||||
"Turkish (auto-generated)": "Турецкий (созданы автоматически)",
|
||||
"Vietnamese (auto-generated)": "Вьетнамский (созданы автоматически)",
|
||||
"footer_documentation": "Документация",
|
||||
"adminprefs_modified_source_code_url_label": "URL-адрес репозитория измененного исходного кода",
|
||||
"adminprefs_modified_source_code_url_label": "Ссылка на репозиторий с измененными исходными кодами",
|
||||
"none": "ничего",
|
||||
"videoinfo_watch_on_youTube": "Смотреть на YouTube",
|
||||
"videoinfo_youTube_embed_link": "Версия для встраивания",
|
||||
@ -453,8 +445,8 @@
|
||||
"Portuguese (Brazil)": "Португальский (Бразилия)",
|
||||
"footer_source_code": "Исходный код",
|
||||
"footer_original_source_code": "Оригинальный исходный код",
|
||||
"footer_modfied_source_code": "Измененный исходный код",
|
||||
"user_saved_playlists": "`x` сохраненных плейлистов",
|
||||
"footer_modfied_source_code": "Изменённый исходный код",
|
||||
"user_saved_playlists": "`x` сохранённых плейлистов",
|
||||
"crash_page_search_issue": "поискали <a href=\"`x`\">похожую проблему на GitHub</a>",
|
||||
"comments_points_count_0": "{{count}} плюс",
|
||||
"comments_points_count_1": "{{count}} плюса",
|
||||
@ -491,9 +483,14 @@
|
||||
"error_video_not_in_playlist": "Запрошенного видео нет в этом плейлисте. <a href=\"`x`\">Нажмите тут, чтобы вернуться к странице плейлиста.</a>",
|
||||
"channel_tab_playlists_label": "Плейлисты",
|
||||
"channel_tab_channels_label": "Каналы",
|
||||
"channel_tab_streams_label": "Живое вещание",
|
||||
"channel_tab_streams_label": "Стримы",
|
||||
"channel_tab_shorts_label": "Shorts",
|
||||
"Music in this video": "Музыка в этом видео",
|
||||
"Artist: ": "Исполнитель: ",
|
||||
"Album: ": "Альбом: "
|
||||
"Album: ": "Альбом: ",
|
||||
"Song: ": "Композиция: ",
|
||||
"Standard YouTube license": "Стандартная лицензия YouTube",
|
||||
"Channel Sponsor": "Спонсор канала",
|
||||
"Download is disabled": "Загрузка отключена",
|
||||
"Import YouTube playlist (.csv)": "Импорт плейлиста YouTube (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"oldest": "පැරණිතම",
|
||||
"popular": "ජනප්රිය",
|
||||
"last": "අවසන්",
|
||||
"Cannot change password for Google accounts": "Google ගිණුම් සඳහා මුරපදය වෙනස් කළ නොහැක",
|
||||
"Authorize token?": "ටෝකනය අනුමත කරනවා ද?",
|
||||
"Authorize token for `x`?": "`x` සඳහා ටෝකනය අනුමත කරනවා ද?",
|
||||
"Yes": "ඔව්",
|
||||
@ -31,7 +30,6 @@
|
||||
"An alternative front-end to YouTube": "YouTube සඳහා විකල්ප ඉදිරිපස අන්තයක්",
|
||||
"source": "මූලාශ්රය",
|
||||
"Log in/register": "පුරන්න/ලියාපදිංචිවන්න",
|
||||
"Log in with Google": "Google සමඟ පුරන්න",
|
||||
"Password": "මුරපදය",
|
||||
"Time (h:mm:ss):": "වේලාව (h:mm:ss):",
|
||||
"Sign In": "පුරන්න",
|
||||
@ -86,7 +84,6 @@
|
||||
"User ID": "පරිශීලක කේතය",
|
||||
"Text CAPTCHA": "CAPTCHA පෙල",
|
||||
"Image CAPTCHA": "CAPTCHA රූපය",
|
||||
"Google verification code": "Google සත්යාපන කේතය",
|
||||
"E-mail": "විද්යුත් තැපෑල",
|
||||
"preferences_quality_label": "කැමති වීඩියෝ ගුණත්වය: ",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
|
@ -12,7 +12,6 @@
|
||||
"Clear watch history?": "Vymazať históriu sledovania?",
|
||||
"New password": "Nové heslo",
|
||||
"New passwords must match": "Nové heslá sa musia zhodovať",
|
||||
"Cannot change password for Google accounts": "Heslo pre účty Google sa nedá zmeniť",
|
||||
"Authorize token?": "Autorizovať token?",
|
||||
"Yes": "Áno",
|
||||
"No": "Nie",
|
||||
@ -34,7 +33,6 @@
|
||||
"source": "zdroj",
|
||||
"Log in": "Prihlásiť sa",
|
||||
"Log in/register": "Prihlásiť sa/Registrovať",
|
||||
"Log in with Google": "Prihlásiť sa pomocou účtu Google",
|
||||
"User ID": "ID používateľa",
|
||||
"Password": "Heslo",
|
||||
"Time (h:mm:ss):": "Čas (h:mm:ss):",
|
||||
@ -43,7 +41,6 @@
|
||||
"Sign In": "Prihlásiť sa",
|
||||
"Register": "Registrovať",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Overovací kód Google",
|
||||
"Preferences": "Nastavenia",
|
||||
"preferences_category_player": "Nastavenia prehrávača",
|
||||
"preferences_video_loop_label": "Vždy opakovať: ",
|
||||
|
@ -8,7 +8,6 @@
|
||||
"Clear watch history?": "Izbrisati zgodovino ogledov?",
|
||||
"New password": "Novo geslo",
|
||||
"New passwords must match": "Nova gesla se morajo ujemati",
|
||||
"Cannot change password for Google accounts": "Ni mogoče spremeniti gesla za račune Google",
|
||||
"Authorize token?": "Naj odobrim žeton?",
|
||||
"Yes": "Da",
|
||||
"Import and Export Data": "Uvoz in izvoz podatkov",
|
||||
@ -22,7 +21,6 @@
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Izvozi naročnine kot OPML (za NewPipe in FreeTube)",
|
||||
"Log in": "Prijava",
|
||||
"Log in/register": "Prijava/registracija",
|
||||
"Log in with Google": "Prijavi se z Googlom",
|
||||
"User ID": "ID uporabnika",
|
||||
"Password": "Geslo",
|
||||
"Time (h:mm:ss):": "Čas (h:mm:ss):",
|
||||
@ -32,7 +30,6 @@
|
||||
"Sign In": "Prijavi se",
|
||||
"Register": "Registriraj se",
|
||||
"E-mail": "E-pošta",
|
||||
"Google verification code": "Googlova koda za preverjanje",
|
||||
"Preferences": "Nastavitve",
|
||||
"preferences_video_loop_label": "Vedno v zanki: ",
|
||||
"preferences_autoplay_label": "Samodejno predvajanje: ",
|
||||
@ -120,9 +117,6 @@
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Poglej `x` komentar",
|
||||
"": "Poglej `x` komentarjev"
|
||||
},
|
||||
"Quota exceeded, try again in a few hours": "Kvota je presežena, poskusi znova čez nekaj ur",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Ne morem se prijaviti, preveri, ali je vklopljeno dvofaktorsko preverjanje pristnosti (avtentikator ali SMS).",
|
||||
"Please sign in using 'Log in with Google'": "Prijavi se z uporabo »Prijava z Googlom«",
|
||||
"Password cannot be empty": "Geslo ne sme biti prazno",
|
||||
"`x` ago": "`x` nazaj",
|
||||
"Load more": "Naloži več",
|
||||
@ -348,8 +342,6 @@
|
||||
"View Reddit comments": "Oglej si komentarje na Redditu",
|
||||
"This channel does not exist.": "Ta kanal ne obstaja.",
|
||||
"Hide replies": "Skrij odgovore",
|
||||
"Invalid TFA code": "Neveljavna koda TFA",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Prijava ni uspela. To je lahko zato, ker za tvoj račun ni vklopljeno dvofaktorsko preverjanje pristnosti.",
|
||||
"Invidious Private Feed for `x`": "Invidious zasebni vir za `x`",
|
||||
"Deleted or invalid channel": "Izbrisan ali neveljaven kanal",
|
||||
"Empty playlist": "Prazen seznam predvajanja",
|
||||
@ -511,5 +503,10 @@
|
||||
"channel_tab_streams_label": "Prenosi v živo",
|
||||
"Artist: ": "Umetnik/ca: ",
|
||||
"Music in this video": "Glasba v tem videoposnetku",
|
||||
"Album: ": "Album: "
|
||||
"Album: ": "Album: ",
|
||||
"Song: ": "Pesem: ",
|
||||
"Standard YouTube license": "Standardna licenca YouTube",
|
||||
"Channel Sponsor": "Sponzor kanala",
|
||||
"Download is disabled": "Prenos je onemogočen",
|
||||
"Import YouTube playlist (.csv)": "Uvoz seznama predvajanja YouTube (.csv)"
|
||||
}
|
||||
|
@ -35,12 +35,10 @@
|
||||
"videoinfo_youTube_embed_link": "Trupëzojeni",
|
||||
"videoinfo_invidious_embed_link": "Lidhje Trupëzimi",
|
||||
"oldest": "më të vjetrat",
|
||||
"Cannot change password for Google accounts": "S’mund të ndryshojë fjalëkalimin për llogari Google",
|
||||
"New passwords must match": "Fjalëkalimet e rinj duhet të përputhen me njëri-tjetrin",
|
||||
"Authorize token?": "Të autorizohet token-i?",
|
||||
"Authorize token for `x`?": "Të autorizohet token-i për `x`?",
|
||||
"Log in/register": "Hyni/regjistrohuni",
|
||||
"Log in with Google": "Hyni me Google",
|
||||
"User ID": "ID Përdoruesi",
|
||||
"Password": "Fjalëkalim",
|
||||
"Time (h:mm:ss):": "Kohë (h:mm:ss):",
|
||||
@ -156,19 +154,14 @@
|
||||
"Whitelisted regions: ": "Rajone të lejuara: ",
|
||||
"Premieres `x`": "Premiera `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.": "Njatjeta! Duket sikur keni JavaScript-in të çaktivizuar. Klikoni këtu që të shihni komentet, mbani parasysh se mund të duhet pak më tepër kohë që të ngarkohen.",
|
||||
"Quota exceeded, try again in a few hours": "Janë tejkaluar kuotat, riprovoni pas pak orësh",
|
||||
"Blacklisted regions: ": "Rajone të palejuara: ",
|
||||
"Premieres in `x`": "Premiera në `x`",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "S’arrihet të bëhet hyrja, sigurohuni se mirëfilltësimi dyfaktorësh (me Mirëfilltësues apo SMS) është i aktivizuar.",
|
||||
"Wrong answer": "Përgjigje e gabuar",
|
||||
"Invalid TFA code": "Kod MDF i pavlefshëm",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Dështoi hyrja. Kjo mund të vijë ngaqë për llogarinë tuaj s’është aktivizuar mirëfilltësimi dyfaktorësh.",
|
||||
"Erroneous CAPTCHA": "CAPTCHA e gabuar",
|
||||
"CAPTCHA is a required field": "CAPTCHA është fushë e domosdoshme",
|
||||
"User ID is a required field": "ID-ja e përdoruesit është fushë e domosdoshme",
|
||||
"Password is a required field": "Fusha e fjalëkalimit është e domosdoshme",
|
||||
"Wrong username or password": "Emër përdoruesi ose fjalëkalim i gabuar",
|
||||
"Please sign in using 'Log in with Google'": "Ju lutemi, bëni hyrjen duke përdorur “Bëni hyrjen me Google”",
|
||||
"Password cannot be empty": "Fjalëkalimi s’mund të jetë i zbrazët",
|
||||
"Password cannot be longer than 55 characters": "Fjalëkalimi s’mund të jetë më i gjatë se 55 shenja",
|
||||
"Please log in": "Ju lutemi, bëni hyrjen",
|
||||
@ -303,7 +296,6 @@
|
||||
"Previous page": "Faqja e mëparshme",
|
||||
"Clear watch history?": "Të spastrohet historiku i parjeve?",
|
||||
"New password": "Fjalëkalim i ri",
|
||||
"Google verification code": "Kod verifikimi Google",
|
||||
"preferences_related_videos_label": "Shfaq video të afërta: ",
|
||||
"preferences_annotations_label": "Si parazgjedhje, shfaqi shënimet: ",
|
||||
"preferences_show_nick_label": "Shfaqe nofkën në krye: ",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Izbrisati povest pregledanja?",
|
||||
"New password": "Nova lozinka",
|
||||
"New passwords must match": "Nove lozinke moraju biti istovetne",
|
||||
"Cannot change password for Google accounts": "Nije moguće promeniti lozinku za Google naloge",
|
||||
"Authorize token?": "Ovlasti žeton?",
|
||||
"Authorize token for `x`?": "Ovlasti žeton za `x`?",
|
||||
"Yes": "Da",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "izvor",
|
||||
"Log in": "Prijavi se",
|
||||
"Log in/register": "Prijavi se/Otvori nalog",
|
||||
"Log in with Google": "Prijavi se pomoću Google-a",
|
||||
"User ID": "Korisnički ID",
|
||||
"Password": "Lozinka",
|
||||
"Time (h:mm:ss):": "Vreme (č:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Prijava",
|
||||
"Register": "Otvori nalog",
|
||||
"E-mail": "E-pošta",
|
||||
"Google verification code": "Google-ova overna koda",
|
||||
"Preferences": "Podešavanja",
|
||||
"preferences_category_player": "Podešavanja reproduktora",
|
||||
"preferences_video_loop_label": "Uvek ponavljaj: ",
|
||||
@ -57,13 +54,11 @@
|
||||
"preferences_local_label": "Prikaz video zapisa preko posrednika: ",
|
||||
"Playlist privacy": "Podešavanja privatnosti plej liste",
|
||||
"Editing playlist `x`": "Izmena plej liste `x`",
|
||||
"Please sign in using 'Log in with Google'": "Molimo Vas da se prijavite pomoću 'Log in with Google'",
|
||||
"Playlist does not exist.": "Nepostojeća plej lista.",
|
||||
"Erroneous challenge": "Pogrešan izazov",
|
||||
"Maltese": "Malteški",
|
||||
"Download": "Preuzmi",
|
||||
"Download as: ": "Preuzmi kao: ",
|
||||
"Quota exceeded, try again in a few hours": "Kvota je premašena, molimo vas da pokušate ponovo za par sati",
|
||||
"Bangla": "Bangla/Bengalski",
|
||||
"preferences_quality_dash_label": "Preferirani kvalitet DASH video formata: ",
|
||||
"Token manager": "Upravljanje žetonima",
|
||||
@ -182,7 +177,6 @@
|
||||
"": "Prikaži `x` komentara"
|
||||
},
|
||||
"View Reddit comments": "Prikaži Reddit komentare",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Neuspešna prijava, proverite da li ste upalili dvofaktornu autentikaciju (Autentikator ili SMS).",
|
||||
"CAPTCHA is a required field": "CAPTCHA je obavezno polje",
|
||||
"Croatian": "Hrvatski",
|
||||
"Estonian": "Estonski",
|
||||
@ -283,8 +277,6 @@
|
||||
"Wrong answer": "Pogrešan odgovor",
|
||||
"preferences_quality_label": "Preferirani video kvalitet: ",
|
||||
"Hide replies": "Sakrij odgovore",
|
||||
"Invalid TFA code": "Nevažeća TFA koda",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Neuspešna prijava! Ovo se možda dešava jer dvofaktorna autentikacija nije omogućena na vašem nalogu.",
|
||||
"Erroneous CAPTCHA": "Pogrešna CAPTCHA",
|
||||
"Erroneous token": "Pogrešan žeton",
|
||||
"Czech": "Češki",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Избрисати повест прегледања?",
|
||||
"New password": "Нова лозинка",
|
||||
"New passwords must match": "Нове лозинке морају бити истоветне",
|
||||
"Cannot change password for Google accounts": "Није могуће променити лозинку за Google налоге",
|
||||
"Authorize token?": "Овласти жетон?",
|
||||
"Authorize token for `x`?": "Овласти жетон за `x`?",
|
||||
"Yes": "Да",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "извор",
|
||||
"Log in": "Пријави се",
|
||||
"Log in/register": "Пријави се/Отворите налог",
|
||||
"Log in with Google": "Пријави се помоћу Google-а",
|
||||
"User ID": "Кориснички ИД",
|
||||
"Password": "Лозинка",
|
||||
"Time (h:mm:ss):": "Време (ч:мм:сс):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Пријава",
|
||||
"Register": "Отвори налог",
|
||||
"E-mail": "Е-пошта",
|
||||
"Google verification code": "Google-ова оверна кода",
|
||||
"Preferences": "Подешавања",
|
||||
"preferences_category_player": "Подешавања репродуктора",
|
||||
"preferences_video_loop_label": "Увек понављај: ",
|
||||
@ -150,8 +147,6 @@
|
||||
"Burmese": "Бурмански",
|
||||
"preferences_quality_dash_label": "Преферирани квалитет DASH видео формата: ",
|
||||
"Erroneous token": "Погрешан жетон",
|
||||
"Quota exceeded, try again in a few hours": "Квота је премашена, молимо вас да покушате поново за пар сати",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Неуспешна пријава, проверите да ли сте упалили двофакторну аутентикацију (Аутентикатор или СМС).",
|
||||
"CAPTCHA is a required field": "CAPTCHA је обавезно поље",
|
||||
"No such user": "Непостојећи корисник",
|
||||
"Chinese (Traditional)": "Кинески (Традиционални)",
|
||||
@ -164,7 +159,6 @@
|
||||
"preferences_show_nick_label": "Прикажи надимке на врху: ",
|
||||
"Report statistics: ": "Извештавај о статистици: ",
|
||||
"Show more": "Прикажи више",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Неуспешна пријава! Ово се можда дешава јер двофакторна аутентикација није омогућена на vашем налогу.",
|
||||
"Wrong answer": "Погрешан одговор",
|
||||
"Hidden field \"token\" is a required field": "Сакривено \"token\" поље је обавезно",
|
||||
"English": "Енглески",
|
||||
@ -198,7 +192,6 @@
|
||||
"User ID is a required field": "Кориснички ИД је обавезно поље",
|
||||
"Password is a required field": "Лозинка је обавезно поље",
|
||||
"Wrong username or password": "Погрешно корисничко име или лозинка",
|
||||
"Please sign in using 'Log in with Google'": "Молимо Вас да се пријавите помоћу 'Log in with Google'",
|
||||
"Password cannot be empty": "Лозинка не може бити празна",
|
||||
"Password cannot be longer than 55 characters": "Лозинка не може бити дужа од 55 карактера",
|
||||
"Invidious Private Feed for `x`": "Инвидиоус Приватни Довод за `x`",
|
||||
@ -324,7 +317,6 @@
|
||||
"Released under the AGPLv3 on Github.": "Избачено под лиценцом AGPLv3 на GitHub-у.",
|
||||
"Afrikaans": "Африканс",
|
||||
"preferences_automatic_instance_redirect_label": "Аутоматско пребацивање на другу инстанцу у случају отказивања (пречи ће назад на редирецт.инвидиоус.ио): ",
|
||||
"Invalid TFA code": "Неважећа TFA кода",
|
||||
"Please log in": "Молимо вас да се пријавите",
|
||||
"English (auto-generated)": "Енглески (аутоматски генерисано)",
|
||||
"Hindi": "Хинди",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Töm visningshistorik?",
|
||||
"New password": "Nytt lösenord",
|
||||
"New passwords must match": "Nya lösenord måste stämma överens",
|
||||
"Cannot change password for Google accounts": "Kan inte ändra lösenord på Google-konton",
|
||||
"Authorize token?": "Auktorisera åtkomsttoken?",
|
||||
"Authorize token for `x`?": "Auktorisera åtkomsttoken för `x`?",
|
||||
"Yes": "Ja",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "källa",
|
||||
"Log in": "Logga in",
|
||||
"Log in/register": "Logga in/registrera",
|
||||
"Log in with Google": "Logga in med Google",
|
||||
"User ID": "Användar-ID",
|
||||
"Password": "Lösenord",
|
||||
"Time (h:mm:ss):": "Tid (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Inloggning",
|
||||
"Register": "Registrera",
|
||||
"E-mail": "E-post",
|
||||
"Google verification code": "Google-bekräftelsekod",
|
||||
"Preferences": "Inställningar",
|
||||
"preferences_category_player": "Spelarinställningar",
|
||||
"preferences_video_loop_label": "Loopa alltid: ",
|
||||
@ -162,17 +159,12 @@
|
||||
"Hide replies": "Dölj svar",
|
||||
"Show replies": "Visa svar",
|
||||
"Incorrect password": "Fel lösenord",
|
||||
"Quota exceeded, try again in a few hours": "Kvoten överskriden, försök igen om ett par timmar",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Kunde inte logga in, försäkra dig om att tvåfaktors-autentisering (Authenticator eller SMS) är påslagen.",
|
||||
"Invalid TFA code": "Ogiltig tvåfaktor-kod",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Inloggning misslyckades. Detta kan vara för att tvåfaktors-autentisering inte är påslaget på ditt konto.",
|
||||
"Wrong answer": "Fel svar",
|
||||
"Erroneous CAPTCHA": "Ogiltig CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA är ett obligatoriskt fält",
|
||||
"User ID is a required field": "Användar-ID är ett obligatoriskt fält",
|
||||
"Password is a required field": "Lösenord är ett obligatoriskt fält",
|
||||
"Wrong username or password": "Ogiltigt användarnamn eller lösenord",
|
||||
"Please sign in using 'Log in with Google'": "Logga in genom \"Google-inloggning\"",
|
||||
"Password cannot be empty": "Lösenordet kan inte vara tomt",
|
||||
"Password cannot be longer than 55 characters": "Lösenordet kan inte vara längre än 55 tecken",
|
||||
"Please log in": "Logga in",
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "İzleme geçmişi temizlensin mi?",
|
||||
"New password": "Yeni Parola",
|
||||
"New passwords must match": "Yeni Parolalar Eşleşmek Zorunda",
|
||||
"Cannot change password for Google accounts": "Google Hesapları İçin Parola Değiştirilemez",
|
||||
"Authorize token?": "Belirteç yetkilendirilsin mi?",
|
||||
"Authorize token for `x`?": "`x` için belirteç yetkilendirilsin mi?",
|
||||
"Yes": "Evet",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "Kaynak",
|
||||
"Log in": "Oturum Aç",
|
||||
"Log in/register": "Oturum Aç/Kayıt Ol",
|
||||
"Log in with Google": "Google İle Oturum Aç",
|
||||
"User ID": "Kullanıcı Kimliği",
|
||||
"Password": "Parola",
|
||||
"Time (h:mm:ss):": "Zaman (h:mm:ss):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Oturum Aç",
|
||||
"Register": "Kayıt Ol",
|
||||
"E-mail": "E-Posta",
|
||||
"Google verification code": "Google Doğrulama Kodu",
|
||||
"Preferences": "Tercihler",
|
||||
"preferences_category_player": "Oynatıcı Tercihleri",
|
||||
"preferences_video_loop_label": "Sürekli Döngü: ",
|
||||
@ -164,17 +161,12 @@
|
||||
"Hide replies": "Cevapları Gizle",
|
||||
"Show replies": "Cevapları Göster",
|
||||
"Incorrect password": "Yanlış Parola",
|
||||
"Quota exceeded, try again in a few hours": "Kota aşıldı, birkaç saat içinde tekrar deneyin.",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Oturum açılamadı, iki faktörlü kimlik doğrulamanın (Kimlik Doğrulayıcı ya da SMS) açık olduğundan emin olun.",
|
||||
"Invalid TFA code": "Geçersiz TFA Kodu",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Giriş başarısız. Bunun nedeni, hesabınız için iki faktörlü kimlik doğrulamanın açık olmaması olabilir.",
|
||||
"Wrong answer": "Yanlış Cevap",
|
||||
"Erroneous CAPTCHA": "Hatalı CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA Zorunlu Bir Alandır",
|
||||
"User ID is a required field": "Kullanıcı Kimliği Zorunlu Bir Alandır",
|
||||
"Password is a required field": "Parola Zorunlu Bir Alandır",
|
||||
"Wrong username or password": "Yanlış Kullanıcı Adı ya da Parola",
|
||||
"Please sign in using 'Log in with Google'": "Lütfen 'Google İle Giriş Yap' Seçeneğini Kullanarak Oturum Açın",
|
||||
"Password cannot be empty": "Parola Boş Olamaz",
|
||||
"Password cannot be longer than 55 characters": "Parola 55 Karakterden Uzun Olamaz",
|
||||
"Please log in": "Lütfen Oturum Açın",
|
||||
@ -481,5 +473,8 @@
|
||||
"Music in this video": "Bu videodaki müzik",
|
||||
"Artist: ": "Sanatçı: ",
|
||||
"Channel Sponsor": "Kanal Sponsoru",
|
||||
"Song: ": "Şarkı: "
|
||||
"Song: ": "Şarkı: ",
|
||||
"Standard YouTube license": "Standart YouTube lisansı",
|
||||
"Download is disabled": "İndirme devre dışı",
|
||||
"Import YouTube playlist (.csv)": "YouTube Oynatma Listesini İçe Aktar (.csv)"
|
||||
}
|
||||
|
@ -14,7 +14,6 @@
|
||||
"Clear watch history?": "Очистити історію переглядів?",
|
||||
"New password": "Новий пароль",
|
||||
"New passwords must match": "Нові паролі не співпадають",
|
||||
"Cannot change password for Google accounts": "Змінити пароль обліківки Google неможливо",
|
||||
"Authorize token?": "Авторизувати токен?",
|
||||
"Authorize token for `x`?": "Авторизувати токен для `x`?",
|
||||
"Yes": "Так",
|
||||
@ -37,7 +36,6 @@
|
||||
"source": "джерело",
|
||||
"Log in": "Увійти",
|
||||
"Log in/register": "Увійти або зареєструватися",
|
||||
"Log in with Google": "Увійти через Google",
|
||||
"User ID": "ID користувача",
|
||||
"Password": "Пароль",
|
||||
"Time (h:mm:ss):": "Час (г:хх:сс):",
|
||||
@ -46,7 +44,6 @@
|
||||
"Sign In": "Увійти",
|
||||
"Register": "Зареєструватися",
|
||||
"E-mail": "Електронна пошта",
|
||||
"Google verification code": "Код підтвердження Google",
|
||||
"Preferences": "Налаштування",
|
||||
"preferences_category_player": "Налаштування програвача",
|
||||
"preferences_video_loop_label": "Завжди повторювати: ",
|
||||
@ -155,17 +152,12 @@
|
||||
"Hide replies": "Сховати відповіді",
|
||||
"Show replies": "Показати відповіді",
|
||||
"Incorrect password": "Неправильний пароль",
|
||||
"Quota exceeded, try again in a few hours": "Ліміт перевищено, спробуйте знову за декілька годин",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Не вдається увійти. Перевірте, чи не ввімкнена двофакторна аутентифікація (за кодом чи смс).",
|
||||
"Invalid TFA code": "Неправильний код двофакторної автентифікації",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Не вдається увійти. Це може бути через те, що у вашій обліківці не ввімкнена двофакторна аутентифікація.",
|
||||
"Wrong answer": "Неправильна відповідь",
|
||||
"Erroneous CAPTCHA": "Неправильна капча",
|
||||
"CAPTCHA is a required field": "Необхідно пройти CAPTCHA",
|
||||
"User ID is a required field": "Необхідно ввести ID користувача",
|
||||
"Password is a required field": "Необхідно ввести пароль",
|
||||
"Wrong username or password": "Неправильний логін чи пароль",
|
||||
"Please sign in using 'Log in with Google'": "Будь ласка, натисніть «Увійти через Google»",
|
||||
"Password cannot be empty": "Пароль не може бути порожнім",
|
||||
"Password cannot be longer than 55 characters": "Пароль не може бути довшим за 55 знаків",
|
||||
"Please log in": "Будь ласка, увійдіть",
|
||||
@ -497,5 +489,8 @@
|
||||
"Artist: ": "Виконавець: ",
|
||||
"Album: ": "Альбом: ",
|
||||
"Song: ": "Пісня: ",
|
||||
"Channel Sponsor": "Спонсор каналу"
|
||||
"Channel Sponsor": "Спонсор каналу",
|
||||
"Standard YouTube license": "Стандартна ліцензія YouTube",
|
||||
"Download is disabled": "Завантаження вимкнено",
|
||||
"Import YouTube playlist (.csv)": "Імпорт списку відтворення YouTube (.csv)"
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"generic_videos_count_0": "{{count}} video",
|
||||
"generic_subscribers_count_0": "{{count}} subscribers",
|
||||
"generic_subscribers_count_0": "{{count}} người theo dõi",
|
||||
"LIVE": "TRỰC TIẾP",
|
||||
"Shared `x` ago": "Đã chia sẻ` x` trước",
|
||||
"Unsubscribe": "Hủy đăng ký",
|
||||
"Subscribe": "Đăng ký",
|
||||
"Unsubscribe": "Hủy theo dõi",
|
||||
"Subscribe": "Theo dõi",
|
||||
"View channel on YouTube": "Xem kênh trên YouTube",
|
||||
"View playlist on YouTube": "Xem danh sách phát trên YouTube",
|
||||
"newest": "mới nhất",
|
||||
@ -16,22 +16,21 @@
|
||||
"Clear watch history?": "Xóa lịch sử xem?",
|
||||
"New password": "Mật khẩu mới",
|
||||
"New passwords must match": "Mật khẩu mới phải khớp",
|
||||
"Cannot change password for Google accounts": "Không thể thay đổi mật khẩu cho tài khoản Google",
|
||||
"Authorize token?": "Cấp phép mã thông báo?",
|
||||
"Authorize token for `x`?": "Cấp phép mã thông báo cho` x`?",
|
||||
"Yes": "Đúng",
|
||||
"No": "Không",
|
||||
"Import and Export Data": "Nhập và xuất dữ liệu",
|
||||
"Import": "Nhập",
|
||||
"Import Invidious data": "Nhập dữ liệu sống động",
|
||||
"Import YouTube subscriptions": "Nhập đăng ký YouTube",
|
||||
"Import Invidious data": "Nhập dữ liệu Invidious JSON",
|
||||
"Import YouTube subscriptions": "Nhập dữ liệu thuê bao YouTube/OPML",
|
||||
"Import FreeTube subscriptions (.db)": "Nhập đăng ký FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Nhập đăng ký NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Nhập dữ liệu NewPipe (.zip)",
|
||||
"Export": "Xuất",
|
||||
"Export subscriptions as OPML": "Xuất đăng ký dưới dạng OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Xuất đăng ký dưới dạng OPML (cho NewPipe & FreeTube)",
|
||||
"Export data as JSON": "Xuất dữ liệu dưới dạng JSON",
|
||||
"Export data as JSON": "Xuất dữ liệu Invidious dưới dạng JSON",
|
||||
"Delete account?": "Xóa tài khoản?",
|
||||
"History": "Lịch sử",
|
||||
"An alternative front-end to YouTube": "Giao diện người dùng thay thế cho YouTube",
|
||||
@ -39,7 +38,6 @@
|
||||
"source": "nguồn",
|
||||
"Log in": "Đăng nhập",
|
||||
"Log in/register": "Đăng nhập / đăng ký",
|
||||
"Log in with Google": "Đăng nhập bằng Google",
|
||||
"User ID": "Tên người dùng",
|
||||
"Password": "Mật khẩu",
|
||||
"Time (h:mm:ss):": "Thời gian (h: mm: ss):",
|
||||
@ -48,36 +46,35 @@
|
||||
"Sign In": "Đăng nhập",
|
||||
"Register": "Đăng ký",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Mã xác minh của Google",
|
||||
"Preferences": "Sở thích",
|
||||
"preferences_category_player": "Tùy chọn người chơi",
|
||||
"preferences_category_player": "Tùy chọn trình phát video",
|
||||
"preferences_video_loop_label": "Luôn lặp lại: ",
|
||||
"preferences_autoplay_label": "Tự chạy: ",
|
||||
"preferences_continue_label": "Phát tiếp theo theo mặc định: ",
|
||||
"preferences_continue_label": "Phát kế tiếp theo mặc định: ",
|
||||
"preferences_continue_autoplay_label": "Tự động phát video tiếp theo: ",
|
||||
"preferences_listen_label": "Nghe theo mặc định: ",
|
||||
"preferences_local_label": "Video proxy: ",
|
||||
"preferences_speed_label": "Tốc độ mặc định: ",
|
||||
"preferences_quality_label": "Chất lượng video ưa thích: ",
|
||||
"preferences_volume_label": "Khối lượng trình phát: ",
|
||||
"preferences_volume_label": "Âm lượng trình phát video: ",
|
||||
"preferences_comments_label": "Nhận xét mặc định: ",
|
||||
"youtube": "YouTube",
|
||||
"reddit": "reddit",
|
||||
"reddit": "Reddit",
|
||||
"preferences_captions_label": "Phụ đề mặc định: ",
|
||||
"Fallback captions: ": "Phụ đề dự phòng: ",
|
||||
"preferences_related_videos_label": "Hiển thị các video có liên quan: ",
|
||||
"preferences_annotations_label": "Hiển thị chú thích theo mặc định: ",
|
||||
"preferences_extend_desc_label": "Tự động mở rộng mô tả video: ",
|
||||
"preferences_vr_mode_label": "Video 360 độ tương tác: ",
|
||||
"preferences_vr_mode_label": "Video 360 độ tương tác (yêu cầu WebGL): ",
|
||||
"preferences_category_visual": "Tùy chọn hình ảnh",
|
||||
"preferences_player_style_label": "Phong cách người chơi: ",
|
||||
"preferences_player_style_label": "Phong cách trình phát: ",
|
||||
"Dark mode: ": "Chế độ tối: ",
|
||||
"preferences_dark_mode_label": "Chủ đề: ",
|
||||
"dark": "tối",
|
||||
"light": "ánh sáng",
|
||||
"preferences_thin_mode_label": "Chế độ mỏng: ",
|
||||
"preferences_category_misc": "Tùy chọn khác",
|
||||
"preferences_automatic_instance_redirect_label": "Chuyển hướng phiên bản tự động (dự phòng thành redirect.invidious.io): ",
|
||||
"preferences_automatic_instance_redirect_label": "Tự động chuyển hướng phiên bản (dự phòng về redirect.invidious.io): ",
|
||||
"preferences_category_subscription": "Tùy chọn đăng ký",
|
||||
"preferences_annotations_subscribed_label": "Hiển thị chú thích theo mặc định cho các kênh đã đăng ký: ",
|
||||
"Redirect homepage to feed: ": "Chuyển hướng trang chủ đến nguồn cấp dữ liệu: ",
|
||||
@ -117,14 +114,14 @@
|
||||
"Subscription manager": "Người quản lý đăng ký",
|
||||
"Token manager": "Trình quản lý mã thông báo",
|
||||
"Token": "Mã thông báo",
|
||||
"search": "Tìm kiếm",
|
||||
"search": "tìm kiếm",
|
||||
"Log out": "Đăng xuất",
|
||||
"Source available here.": "Nguồn có sẵn ở đây.",
|
||||
"View JavaScript license information.": "Xem thông tin giấy phép JavaScript.",
|
||||
"View privacy policy.": "Xem chính sách bảo mật.",
|
||||
"Trending": "Xu hướng",
|
||||
"Public": "Công cộng",
|
||||
"Unlisted": "Riêng tư",
|
||||
"Unlisted": "Không hiển thị",
|
||||
"Private": "Riêng tư",
|
||||
"View all playlists": "Xem tất cả danh sách phát",
|
||||
"Updated `x` ago": "Đã cập nhật` x` trước",
|
||||
@ -152,17 +149,12 @@
|
||||
"Hide replies": "Ẩn câu trả lời",
|
||||
"Show replies": "Hiển thị câu trả lời",
|
||||
"Incorrect password": "Mật khẩu không đúng",
|
||||
"Quota exceeded, try again in a few hours": "Đã vượt quá hạn ngạch, hãy thử lại sau vài giờ nữa",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "Không thể đăng nhập, hãy đảm bảo rằng xác thực hai yếu tố (Authenticator hoặc SMS) được bật.",
|
||||
"Invalid TFA code": "Mã TFA không hợp lệ",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "Đăng nhập không thành công. Điều này có thể là do xác thực hai yếu tố chưa được bật cho tài khoản của bạn.",
|
||||
"Wrong answer": "Câu trả lời sai",
|
||||
"Erroneous CAPTCHA": "CAPTCHA bị lỗi",
|
||||
"CAPTCHA is a required field": "CAPTCHA là trường bắt buộc",
|
||||
"User ID is a required field": "User ID là trường bắt buộc",
|
||||
"Password is a required field": "Mật khẩu là trường bắt buộc",
|
||||
"Wrong username or password": "Tên người dùng hoặc mật khẩu sai",
|
||||
"Please sign in using 'Log in with Google'": "Vui lòng đăng nhập bằng 'Đăng nhập bằng Google'",
|
||||
"Password cannot be empty": "Mật khẩu không được để trống",
|
||||
"Password cannot be longer than 55 characters": "Mật khẩu không được dài hơn 55 ký tự",
|
||||
"Please log in": "Xin vui lòng đăng nhập",
|
||||
@ -345,6 +337,51 @@
|
||||
"generic_playlists_count": "{{count}} danh sách phát",
|
||||
"generic_views_count": "{{count}} lượt xem",
|
||||
"View `x` comments": {
|
||||
"": "Xem `x` bình luận"
|
||||
}
|
||||
"": "Xem `x` bình luận",
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Hiển thị `x`bình luận"
|
||||
},
|
||||
"Song: ": "Ca khúc: ",
|
||||
"Premieres in `x`": "Trình chiếu lần đầu vào `x`",
|
||||
"preferences_quality_dash_option_worst": "Thấp nhất",
|
||||
"preferences_watch_history_label": "Bật lịch sử video đã xem ",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
"unsubscribe": "hủy đăng kí",
|
||||
"revoke": "gỡ bỏ",
|
||||
"preferences_quality_dash_label": "Chất lượng video DASH ưa thích ",
|
||||
"preferences_quality_dash_option_auto": "Tự động",
|
||||
"Subscriptions": "Thuê bao",
|
||||
"View YouTube comments": "Hiển thị bình luận trên YouTube",
|
||||
"View more comments on Reddit": "Hiển thị thêm bình luận từ Reddit",
|
||||
"Music in this video": "Nhạc trong video này",
|
||||
"Artist: ": "Nghệ sĩ: ",
|
||||
"Premieres `x`": "Phát lần đầu `x`",
|
||||
"preferences_region_label": "Nội dung theo quốc gia ",
|
||||
"search_message_change_filters_or_query": "Thử mở rộng nội dung tìm kiếm hoặc thay đổi bộ lọc.",
|
||||
"preferences_quality_option_small": "Nhỏ",
|
||||
"preferences_quality_dash_option_144p": "144p",
|
||||
"invidious": "Invidious",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"Import/export": "Xuất/nhập dữ liệu",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)",
|
||||
"generic_subscriptions_count_0": "{{count}} thuê bao",
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
"preferences_quality_dash_option_2160p": "2160p",
|
||||
"search_message_no_results": "Tìm kiếm không có kết quả.",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"preferences_quality_dash_option_720p": "720p",
|
||||
"preferences_quality_option_medium": "Trung bình",
|
||||
"Load more": "Hiển thị thêm",
|
||||
"comments_points_count_0": "{{count}} điểm",
|
||||
"Import YouTube playlist (.csv)": "Nhập danh sách phát YouTube (.csv)",
|
||||
"preferences_quality_dash_option_best": "Tốt nhất",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"subscriptions_unseen_notifs_count_0": "{{count}} thông báo chưa đọc",
|
||||
"Released under the AGPLv3 on Github.": "Phát hành dưới giấy phép AGPLv3 trên GitHub.",
|
||||
"search_message_use_another_instance": " Bạn cũng có thể tìm kiếm <a href=\"`x`\"> ở một phiên bản khác</a>.",
|
||||
"Standard YouTube license": "Giấy phép YouTube thông thường",
|
||||
"Album: ": "Album: ",
|
||||
"preferences_save_player_pos_label": "Lưu vị trí xem cuối cùng ",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn."
|
||||
}
|
||||
|
@ -19,7 +19,6 @@
|
||||
"Clear watch history?": "清除观看历史?",
|
||||
"New password": "新密码",
|
||||
"New passwords must match": "新密码必须匹配",
|
||||
"Cannot change password for Google accounts": "无法为 Google 账户更改密码",
|
||||
"Authorize token?": "授权令牌?",
|
||||
"Authorize token for `x`?": "`x` 的授权令牌?",
|
||||
"Yes": "是",
|
||||
@ -42,7 +41,6 @@
|
||||
"source": "source",
|
||||
"Log in": "登录",
|
||||
"Log in/register": "登录/注册",
|
||||
"Log in with Google": "使用 Google 账户登录",
|
||||
"User ID": "用户 ID",
|
||||
"Password": "密码",
|
||||
"Time (h:mm:ss):": "时间 (h:mm:ss):",
|
||||
@ -51,7 +49,6 @@
|
||||
"Sign In": "登录",
|
||||
"Register": "注册",
|
||||
"E-mail": "E-mail",
|
||||
"Google verification code": "Google 验证代码",
|
||||
"Preferences": "偏好设置",
|
||||
"preferences_category_player": "播放器偏好设置",
|
||||
"preferences_video_loop_label": "始终循环: ",
|
||||
@ -171,17 +168,12 @@
|
||||
"Hide replies": "隐藏回复",
|
||||
"Show replies": "显示回复",
|
||||
"Incorrect password": "密码错误",
|
||||
"Quota exceeded, try again in a few hours": "已超出限额,请于几小时后重试",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "无法登录。请确认你的短信或验证器的二步验证已打开。",
|
||||
"Invalid TFA code": "无效的二步验证码",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "登录失败。可能是因为二步验证未打开。",
|
||||
"Wrong answer": "错误的回复",
|
||||
"Erroneous CAPTCHA": "验证码错误",
|
||||
"CAPTCHA is a required field": "验证码必填",
|
||||
"User ID is a required field": "用户名必填",
|
||||
"Password is a required field": "密码必填",
|
||||
"Wrong username or password": "用户名或密码错误",
|
||||
"Please sign in using 'Log in with Google'": "请通过谷歌账户登录",
|
||||
"Password cannot be empty": "密码不能为空",
|
||||
"Password cannot be longer than 55 characters": "密码长度不能大于 55",
|
||||
"Please log in": "请登录",
|
||||
@ -465,5 +457,8 @@
|
||||
"channel_tab_shorts_label": "短视频",
|
||||
"channel_tab_channels_label": "频道",
|
||||
"Song: ": "歌曲: ",
|
||||
"Channel Sponsor": "频道赞助者"
|
||||
"Channel Sponsor": "频道赞助者",
|
||||
"Standard YouTube license": "标准 YouTube 许可证",
|
||||
"Download is disabled": "已禁用下载",
|
||||
"Import YouTube playlist (.csv)": "导入 YouTube 播放列表(.csv)"
|
||||
}
|
||||
|
@ -19,7 +19,6 @@
|
||||
"Clear watch history?": "清除觀看歷史?",
|
||||
"New password": "新密碼",
|
||||
"New passwords must match": "新密碼必須符合",
|
||||
"Cannot change password for Google accounts": "無法變更 Google 帳號的密碼",
|
||||
"Authorize token?": "授權 token?",
|
||||
"Authorize token for `x`?": "`x` 的授權 token?",
|
||||
"Yes": "是",
|
||||
@ -42,7 +41,6 @@
|
||||
"source": "來源",
|
||||
"Log in": "登入",
|
||||
"Log in/register": "登入/註冊",
|
||||
"Log in with Google": "使用 Google 登入",
|
||||
"User ID": "使用者 ID",
|
||||
"Password": "密碼",
|
||||
"Time (h:mm:ss):": "時間 (h:mm:ss):",
|
||||
@ -51,7 +49,6 @@
|
||||
"Sign In": "登入",
|
||||
"Register": "註冊",
|
||||
"E-mail": "電子郵件",
|
||||
"Google verification code": "Google 驗證碼",
|
||||
"Preferences": "偏好設定",
|
||||
"preferences_category_player": "播放器偏好設定",
|
||||
"preferences_video_loop_label": "總是循環播放: ",
|
||||
@ -171,17 +168,12 @@
|
||||
"Hide replies": "隱藏回覆",
|
||||
"Show replies": "顯示回覆",
|
||||
"Incorrect password": "不正確的密碼",
|
||||
"Quota exceeded, try again in a few hours": "超過限額,請在幾個小時後再試一次",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "無法登入,請確定雙因素驗證(驗證器或簡訊)已開啟。",
|
||||
"Invalid TFA code": "無效的 TFA 代碼",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "登入失敗。這可能是因為您的帳號未開啟雙因素驗證的關係。",
|
||||
"Wrong answer": "錯誤的答案",
|
||||
"Erroneous CAPTCHA": "錯誤的 CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA 為必填欄位",
|
||||
"User ID is a required field": "使用者 ID 為必填欄位",
|
||||
"Password is a required field": "密碼為必填欄位",
|
||||
"Wrong username or password": "錯誤的使用者名稱或密碼",
|
||||
"Please sign in using 'Log in with Google'": "請使用「以 Google 登入」來登入",
|
||||
"Password cannot be empty": "密碼不能為空",
|
||||
"Password cannot be longer than 55 characters": "密碼不能長於55個字元",
|
||||
"Please log in": "請登入",
|
||||
@ -465,5 +457,8 @@
|
||||
"Album: ": "專輯: ",
|
||||
"Music in this video": "此影片中的音樂",
|
||||
"Channel Sponsor": "頻道贊助者",
|
||||
"Song: ": "歌曲: "
|
||||
"Song: ": "歌曲: ",
|
||||
"Standard YouTube license": "標準 YouTube 授權條款",
|
||||
"Download is disabled": "已停用下載",
|
||||
"Import YouTube playlist (.csv)": "匯入 YouTube 播放清單 (.csv)"
|
||||
}
|
||||
|
2
mocks
2
mocks
@ -1 +1 @@
|
||||
Subproject commit cb16e0343c8f94182615610bfe3c503db89717a7
|
||||
Subproject commit 11ec372f72747c09d48ffef04843f72be67d5b54
|
@ -23,18 +23,6 @@ Spectator.describe "Helper" do
|
||||
end
|
||||
end
|
||||
|
||||
describe "#produce_comment_continuation" do
|
||||
it "correctly produces a continuation token for comments" do
|
||||
expect(produce_comment_continuation("_cE8xSu6swE", "ADSJ_i2qvJeFtL0htmS5_K5Ctj3eGFVBMWL9Wd42o3kmUL6_mAzdLp85-liQZL0mYr_16BhaggUqX652Sv9JqV6VXinShSP-ZT6rL4NolPBaPXVtJsO5_rA_qE3GubAuLFw9uzIIXU2-HnpXbdgPLWTFavfX206hqWmmpHwUOrmxQV_OX6tYkM3ux3rPAKCDrT8eWL7MU3bLiNcnbgkW8o0h8KYLL_8BPa8LcHbTv8pAoNkjerlX1x7K4pqxaXPoyz89qNlnh6rRx6AXgAzzoHH1dmcyQ8CIBeOHg-m4i8ZxdX4dP88XWrIFg-jJGhpGP8JUMDgZgavxVx225hUEYZMyrLGler5em4FgbG62YWC51moLDLeYEA")).to eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyjAMK9gJBRFNKX2kycXZKZUZ0TDBodG1TNV9LNUN0ajNlR0ZWQk1XTDlXZDQybzNrbVVMNl9tQXpkTHA4NS1saVFaTDBtWXJfMTZCaGFnZ1VxWDY1MlN2OUpxVjZWWGluU2hTUC1aVDZyTDROb2xQQmFQWFZ0SnNPNV9yQV9xRTNHdWJBdUxGdzl1eklJWFUyLUhucFhiZGdQTFdURmF2ZlgyMDZocVdtbXBId1VPcm14UVZfT1g2dFlrTTN1eDNyUEFLQ0RyVDhlV0w3TVUzYkxpTmNuYmdrVzhvMGg4S1lMTF84QlBhOExjSGJUdjhwQW9Oa2plcmxYMXg3SzRwcXhhWFBveXo4OXFObG5oNnJSeDZBWGdBenpvSEgxZG1jeVE4Q0lCZU9IZy1tNGk4WnhkWDRkUDg4WFdySUZnLWpKR2hwR1A4SlVNRGdaZ2F2eFZ4MjI1aFVFWVpNeXJMR2xlcjVlbTRGZ2JHNjJZV0M1MW1vTERMZVlFQSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")
|
||||
|
||||
expect(produce_comment_continuation("_cE8xSu6swE", "ADSJ_i1yz21HI4xrtsYXVC-2_kfZ6kx1yjYQumXAAxqH3CAd7ZxKxfLdZS1__fqhCtOASRbbpSBGH_tH1J96Dxux-Qfjk-lUbupMqv08Q3aHzGu7p70VoUMHhI2-GoJpnbpmcOxkGzeIuenRS_ym2Y8fkDowhqLPFgsS0n4djnZ2UmC17F3Ch3N1S1UYf1ZVOc991qOC1iW9kJDzyvRQTWCPsJUPneSaAKW-Rr97pdesOkR4i8cNvHZRnQKe2HEfsvlJOb2C3lF1dJBfJeNfnQYeh5hv6_fZN7bt3-JL1Xk3Qc9NXNxmmbDpwAC_yFR8dthFfUJdyIO9Nu1D79MLYeR-H5HxqUJokkJiGIz4lTE_CXXbhAI")).to eq("EkMSC19jRTh4U3U2c3dFyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyiQMK8wJBRFNKX2kxeXoyMUhJNHhydHNZWFZDLTJfa2ZaNmt4MXlqWVF1bVhBQXhxSDNDQWQ3WnhLeGZMZFpTMV9fZnFoQ3RPQVNSYmJwU0JHSF90SDFKOTZEeHV4LVFmamstbFVidXBNcXYwOFEzYUh6R3U3cDcwVm9VTUhoSTItR29KcG5icG1jT3hrR3plSXVlblJTX3ltMlk4ZmtEb3docUxQRmdzUzBuNGRqbloyVW1DMTdGM0NoM04xUzFVWWYxWlZPYzk5MXFPQzFpVzlrSkR6eXZSUVRXQ1BzSlVQbmVTYUFLVy1Scjk3cGRlc09rUjRpOGNOdkhaUm5RS2UySEVmc3ZsSk9iMkMzbEYxZEpCZkplTmZuUVllaDVodjZfZlpON2J0My1KTDFYazNRYzlOWE54bW1iRHB3QUNfeUZSOGR0aEZmVUpkeUlPOU51MUQ3OU1MWWVSLUg1SHhxVUpva2tKaUdJejRsVEVfQ1hYYmhBSSIPIgtfY0U4eFN1NnN3RTAAKBQ%3D")
|
||||
|
||||
expect(produce_comment_continuation("29-q7YnyUmY", "")).to eq("EkMSCzI5LXE3WW55VW1ZyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyFQoAIg8iCzI5LXE3WW55VW1ZMAAoFA%3D%3D")
|
||||
|
||||
expect(produce_comment_continuation("CvFH_6DNRCY", "")).to eq("EkMSC0N2RkhfNkROUkNZyAEA4AEBogINKP___________wFAAMICHQgEGhdodHRwczovL3d3dy55b3V0dWJlLmNvbSIAGAYyFQoAIg8iC0N2RkhfNkROUkNZMAAoFA%3D%3D")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#produce_channel_community_continuation" do
|
||||
it "correctly produces a continuation token for a channel community" do
|
||||
expect(produce_channel_community_continuation("UCCj956IF62FbT7Gouszaj9w", "Egljb21tdW5pdHm4")).to eq("4qmFsgIsEhhVQ0NqOTU2SUY2MkZiVDdHb3VzemFqOXcaEEVnbGpiMjF0ZFc1cGRIbTQ%3D")
|
||||
|
46
spec/invidious/utils_spec.cr
Normal file
46
spec/invidious/utils_spec.cr
Normal file
@ -0,0 +1,46 @@
|
||||
require "../spec_helper"
|
||||
|
||||
Spectator.describe "Utils" do
|
||||
describe "decode_date" do
|
||||
it "parses short dates (en-US)" do
|
||||
expect(decode_date("1s ago")).to be_close(Time.utc - 1.second, 500.milliseconds)
|
||||
expect(decode_date("2min ago")).to be_close(Time.utc - 2.minutes, 500.milliseconds)
|
||||
expect(decode_date("3h ago")).to be_close(Time.utc - 3.hours, 500.milliseconds)
|
||||
expect(decode_date("4d ago")).to be_close(Time.utc - 4.days, 500.milliseconds)
|
||||
expect(decode_date("5w ago")).to be_close(Time.utc - 5.weeks, 500.milliseconds)
|
||||
expect(decode_date("6mo ago")).to be_close(Time.utc - 6.months, 500.milliseconds)
|
||||
expect(decode_date("7y ago")).to be_close(Time.utc - 7.years, 500.milliseconds)
|
||||
end
|
||||
|
||||
it "parses short dates (en-GB)" do
|
||||
expect(decode_date("55s ago")).to be_close(Time.utc - 55.seconds, 500.milliseconds)
|
||||
expect(decode_date("44min ago")).to be_close(Time.utc - 44.minutes, 500.milliseconds)
|
||||
expect(decode_date("22hr ago")).to be_close(Time.utc - 22.hours, 500.milliseconds)
|
||||
expect(decode_date("1day ago")).to be_close(Time.utc - 1.day, 500.milliseconds)
|
||||
expect(decode_date("2days ago")).to be_close(Time.utc - 2.days, 500.milliseconds)
|
||||
expect(decode_date("3wk ago")).to be_close(Time.utc - 3.weeks, 500.milliseconds)
|
||||
expect(decode_date("11mo ago")).to be_close(Time.utc - 11.months, 500.milliseconds)
|
||||
expect(decode_date("11yr ago")).to be_close(Time.utc - 11.years, 500.milliseconds)
|
||||
end
|
||||
|
||||
it "parses long forms (singular)" do
|
||||
expect(decode_date("1 second ago")).to be_close(Time.utc - 1.second, 500.milliseconds)
|
||||
expect(decode_date("1 minute ago")).to be_close(Time.utc - 1.minute, 500.milliseconds)
|
||||
expect(decode_date("1 hour ago")).to be_close(Time.utc - 1.hour, 500.milliseconds)
|
||||
expect(decode_date("1 day ago")).to be_close(Time.utc - 1.day, 500.milliseconds)
|
||||
expect(decode_date("1 week ago")).to be_close(Time.utc - 1.week, 500.milliseconds)
|
||||
expect(decode_date("1 month ago")).to be_close(Time.utc - 1.month, 500.milliseconds)
|
||||
expect(decode_date("1 year ago")).to be_close(Time.utc - 1.year, 500.milliseconds)
|
||||
end
|
||||
|
||||
it "parses long forms (plural)" do
|
||||
expect(decode_date("5 seconds ago")).to be_close(Time.utc - 5.seconds, 500.milliseconds)
|
||||
expect(decode_date("17 minutes ago")).to be_close(Time.utc - 17.minutes, 500.milliseconds)
|
||||
expect(decode_date("23 hours ago")).to be_close(Time.utc - 23.hours, 500.milliseconds)
|
||||
expect(decode_date("3 days ago")).to be_close(Time.utc - 3.days, 500.milliseconds)
|
||||
expect(decode_date("2 weeks ago")).to be_close(Time.utc - 2.weeks, 500.milliseconds)
|
||||
expect(decode_date("9 months ago")).to be_close(Time.utc - 9.months, 500.milliseconds)
|
||||
expect(decode_date("8 years ago")).to be_close(Time.utc - 8.years, 500.milliseconds)
|
||||
end
|
||||
end
|
||||
end
|
@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do
|
||||
# Basic video infos
|
||||
|
||||
expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island")
|
||||
expect(info["views"].as_i).to eq(115_784_415)
|
||||
expect(info["likes"].as_i).to eq(4_932_790)
|
||||
expect(info["views"].as_i).to eq(126_573_823)
|
||||
expect(info["likes"].as_i).to eq(5_157_654)
|
||||
|
||||
# For some reason the video length from VideoDetails and the
|
||||
# one from microformat differs by 1s...
|
||||
@ -48,12 +48,12 @@ Spectator.describe "parse_video_info" do
|
||||
|
||||
expect(info["relatedVideos"].as_a.size).to eq(20)
|
||||
|
||||
expect(info["relatedVideos"][0]["id"]).to eq("iogcY_4xGjo")
|
||||
expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $1,000,000 Hotel Room!")
|
||||
expect(info["relatedVideos"][0]["id"]).to eq("Hwybp38GnZw")
|
||||
expect(info["relatedVideos"][0]["title"]).to eq("I Built Willy Wonka's Chocolate Factory!")
|
||||
expect(info["relatedVideos"][0]["author"]).to eq("MrBeast")
|
||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
||||
expect(info["relatedVideos"][0]["view_count"]).to eq("172972109")
|
||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("172M")
|
||||
expect(info["relatedVideos"][0]["view_count"]).to eq("179877630")
|
||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("179M")
|
||||
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
|
||||
|
||||
# Description
|
||||
@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do
|
||||
expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
||||
|
||||
expect(info["authorThumbnail"].as_s).to eq(
|
||||
"https://yt3.ggpht.com/ytc/AL5GRJUfhQdJS6n-YJtsAf-ouS2myDavDOq_zXBfebal3Q=s48-c-k-c0x00ffffff-no-rj"
|
||||
"https://yt3.ggpht.com/ytc/AL5GRJVuqw82ERvHzsmBxL7avr1dpBtsVIXcEzBPZaloFg=s48-c-k-c0x00ffffff-no-rj"
|
||||
)
|
||||
|
||||
expect(info["authorVerified"].as_bool).to be_true
|
||||
expect(info["subCountText"].as_s).to eq("135M")
|
||||
expect(info["subCountText"].as_s).to eq("143M")
|
||||
end
|
||||
|
||||
it "parses a regular video with no descrition/comments" do
|
||||
@ -99,7 +99,7 @@ Spectator.describe "parse_video_info" do
|
||||
# Basic video infos
|
||||
|
||||
expect(info["title"].as_s).to eq("Chris Rea - Auberge")
|
||||
expect(info["views"].as_i).to eq(10_698_554)
|
||||
expect(info["views"].as_i).to eq(10_943_126)
|
||||
expect(info["likes"].as_i).to eq(0)
|
||||
expect(info["lengthSeconds"].as_i).to eq(283_i64)
|
||||
expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
|
||||
@ -132,21 +132,21 @@ Spectator.describe "parse_video_info" do
|
||||
|
||||
# Related videos
|
||||
|
||||
expect(info["relatedVideos"].as_a.size).to eq(18)
|
||||
expect(info["relatedVideos"].as_a.size).to eq(19)
|
||||
|
||||
expect(info["relatedVideos"][0]["id"]).to eq("rfyZrJUmzxU")
|
||||
expect(info["relatedVideos"][0]["title"]).to eq("cheb mami - bekatni")
|
||||
expect(info["relatedVideos"][0]["author"]).to eq("pelitovic")
|
||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCsp6vFyJeGoLxgn-AsHp1tw")
|
||||
expect(info["relatedVideos"][0]["view_count"]).to eq("13863619")
|
||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("13M")
|
||||
expect(info["relatedVideos"][0]["id"]).to eq("Ww3KeZ2_Yv4")
|
||||
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea")
|
||||
expect(info["relatedVideos"][0]["author"]).to eq("PanMusic")
|
||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCsKAPSuh1iNbLWUga_igPyA")
|
||||
expect(info["relatedVideos"][0]["view_count"]).to eq("31581")
|
||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("31K")
|
||||
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
|
||||
|
||||
# Description
|
||||
|
||||
expect(info["description"].as_s).to eq(" ")
|
||||
expect(info["shortDescription"].as_s).to be_empty
|
||||
expect(info["descriptionHtml"].as_s).to eq("<p></p>")
|
||||
expect(info["descriptionHtml"].as_s).to eq("")
|
||||
|
||||
# Video metadata
|
||||
|
||||
|
@ -86,9 +86,10 @@ Spectator.describe "parse_video_info" do
|
||||
expect(info["description"].as_s).to start_with(description_start_text)
|
||||
expect(info["shortDescription"].as_s).to start_with(description_start_text)
|
||||
|
||||
expect(info["descriptionHtml"].as_s).to start_with(
|
||||
"PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - <a href=\"https://aura.com/pbd\">aura.com/pbd</a>"
|
||||
)
|
||||
# TODO: Update mocks right before the start of PDB podcast, either on friday or saturday (time unknown)
|
||||
# expect(info["descriptionHtml"].as_s).to start_with(
|
||||
# "PBD Podcast Episode 241. The home team is ready and at it again with the latest news, interesting topics and trending conversations on topics that matter. Try our sponsor Aura for 14 days free - <a href=\"https://aura.com/pbd\">aura.com/pbd</a>"
|
||||
# )
|
||||
|
||||
# Video metadata
|
||||
|
||||
|
@ -13,7 +13,7 @@ require "../src/invidious/helpers/utils"
|
||||
|
||||
require "../src/invidious/videos"
|
||||
require "../src/invidious/videos/*"
|
||||
require "../src/invidious/comments"
|
||||
require "../src/invidious/comments/content"
|
||||
|
||||
require "../src/invidious/helpers/serialized_yt_data"
|
||||
require "../src/invidious/yt_backend/extractors"
|
||||
|
@ -7,7 +7,6 @@ require "../src/invidious/helpers/*"
|
||||
require "../src/invidious/channels/*"
|
||||
require "../src/invidious/videos/caption"
|
||||
require "../src/invidious/videos"
|
||||
require "../src/invidious/comments"
|
||||
require "../src/invidious/playlists"
|
||||
require "../src/invidious/search/ctoken"
|
||||
require "../src/invidious/trending"
|
||||
|
@ -43,6 +43,7 @@ require "./invidious/videos/*"
|
||||
require "./invidious/jsonify/**"
|
||||
|
||||
require "./invidious/*"
|
||||
require "./invidious/comments/*"
|
||||
require "./invidious/channels/*"
|
||||
require "./invidious/user/*"
|
||||
require "./invidious/search/*"
|
||||
@ -57,11 +58,10 @@ end
|
||||
alias IV = Invidious
|
||||
|
||||
CONFIG = Config.load
|
||||
HMAC_KEY = CONFIG.hmac_key || Random::Secure.hex(32)
|
||||
HMAC_KEY = CONFIG.hmac_key
|
||||
|
||||
PG_DB = DB.open CONFIG.database_url
|
||||
ARCHIVE_URL = URI.parse("https://archive.org")
|
||||
LOGIN_URL = URI.parse("https://accounts.google.com")
|
||||
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
||||
REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||
YT_URL = URI.parse("https://www.youtube.com")
|
||||
|
@ -159,12 +159,18 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||
LOGGER.debug("fetch_channel: #{ucid}")
|
||||
LOGGER.trace("fetch_channel: #{ucid} : pull_all_videos = #{pull_all_videos}")
|
||||
|
||||
namespaces = {
|
||||
"yt" => "http://www.youtube.com/xml/schemas/2015",
|
||||
"media" => "http://search.yahoo.com/mrss/",
|
||||
"default" => "http://www.w3.org/2005/Atom",
|
||||
}
|
||||
|
||||
LOGGER.trace("fetch_channel: #{ucid} : Downloading RSS feed")
|
||||
rss = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}").body
|
||||
LOGGER.trace("fetch_channel: #{ucid} : Parsing RSS feed")
|
||||
rss = XML.parse_html(rss)
|
||||
rss = XML.parse(rss)
|
||||
|
||||
author = rss.xpath_node(%q(//feed/title))
|
||||
author = rss.xpath_node("//default:feed/default:title", namespaces)
|
||||
if !author
|
||||
raise InfoException.new("Deleted or invalid channel")
|
||||
end
|
||||
@ -192,15 +198,23 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||
videos, continuation = IV::Channel::Tabs.get_videos(channel)
|
||||
|
||||
LOGGER.trace("fetch_channel: #{ucid} : Extracting videos from channel RSS feed")
|
||||
rss.xpath_nodes("//feed/entry").each do |entry|
|
||||
video_id = entry.xpath_node("videoid").not_nil!.content
|
||||
title = entry.xpath_node("title").not_nil!.content
|
||||
published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content)
|
||||
updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content)
|
||||
author = entry.xpath_node("author/name").not_nil!.content
|
||||
ucid = entry.xpath_node("channelid").not_nil!.content
|
||||
views = entry.xpath_node("group/community/statistics").try &.["views"]?.try &.to_i64?
|
||||
views ||= 0_i64
|
||||
rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry|
|
||||
video_id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content
|
||||
title = entry.xpath_node("default:title", namespaces).not_nil!.content
|
||||
|
||||
published = Time.parse_rfc3339(
|
||||
entry.xpath_node("default:published", namespaces).not_nil!.content
|
||||
)
|
||||
updated = Time.parse_rfc3339(
|
||||
entry.xpath_node("default:updated", namespaces).not_nil!.content
|
||||
)
|
||||
|
||||
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
|
||||
ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
|
||||
|
||||
views = entry
|
||||
.xpath_node("media:group/media:community/media:statistics", namespaces)
|
||||
.try &.["views"]?.try &.to_i64? || 0_i64
|
||||
|
||||
channel_video = videos
|
||||
.select(SearchVideo)
|
||||
|
@ -31,18 +31,16 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
|
||||
session_token: session_token,
|
||||
}
|
||||
|
||||
response = YT_POOL.client &.post("/comment_service_ajax?action_get_comments=1&ctoken=#{continuation}&continuation=#{continuation}&hl=en&gl=US", headers, form: post_req)
|
||||
body = JSON.parse(response.body)
|
||||
body = YoutubeAPI.browse(continuation)
|
||||
|
||||
body = body["response"]["continuationContents"]["itemSectionContinuation"]? ||
|
||||
body["response"]["continuationContents"]["backstageCommentsContinuation"]?
|
||||
body = body.dig?("continuationContents", "itemSectionContinuation") ||
|
||||
body.dig?("continuationContents", "backstageCommentsContinuation")
|
||||
|
||||
if !body
|
||||
raise InfoException.new("Could not extract continuation.")
|
||||
end
|
||||
end
|
||||
|
||||
continuation = body["continuations"]?.try &.[0]["nextContinuationData"]["continuation"].as_s
|
||||
posts = body["contents"].as_a
|
||||
|
||||
if message = posts[0]["messageRenderer"]?
|
||||
@ -125,49 +123,13 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
|
||||
|
||||
if attachment = post["backstageAttachment"]?
|
||||
json.field "attachment" do
|
||||
json.object do
|
||||
case attachment.as_h
|
||||
when .has_key?("videoRenderer")
|
||||
attachment = attachment["videoRenderer"]
|
||||
json.field "type", "video"
|
||||
|
||||
if !attachment["videoId"]?
|
||||
error_message = (attachment["title"]["simpleText"]? ||
|
||||
attachment["title"]["runs"]?.try &.[0]?.try &.["text"]?)
|
||||
|
||||
json.field "error", error_message
|
||||
else
|
||||
video_id = attachment["videoId"].as_s
|
||||
|
||||
video_title = attachment["title"]["simpleText"]? || attachment["title"]["runs"]?.try &.[0]?.try &.["text"]?
|
||||
json.field "title", video_title
|
||||
json.field "videoId", video_id
|
||||
json.field "videoThumbnails" do
|
||||
Invidious::JSONify::APIv1.thumbnails(json, video_id)
|
||||
end
|
||||
|
||||
json.field "lengthSeconds", decode_length_seconds(attachment["lengthText"]["simpleText"].as_s)
|
||||
|
||||
author_info = attachment["ownerText"]["runs"][0].as_h
|
||||
|
||||
json.field "author", author_info["text"].as_s
|
||||
json.field "authorId", author_info["navigationEndpoint"]["browseEndpoint"]["browseId"]
|
||||
json.field "authorUrl", author_info["navigationEndpoint"]["commandMetadata"]["webCommandMetadata"]["url"]
|
||||
|
||||
# TODO: json.field "authorThumbnails", "channelThumbnailSupportedRenderers"
|
||||
# TODO: json.field "authorVerified", "ownerBadges"
|
||||
|
||||
published = decode_date(attachment["publishedTimeText"]["simpleText"].as_s)
|
||||
|
||||
json.field "published", published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
|
||||
|
||||
view_count = attachment["viewCountText"]?.try &.["simpleText"].as_s.gsub(/\D/, "").to_i64? || 0_i64
|
||||
|
||||
json.field "viewCount", view_count
|
||||
json.field "viewCountText", translate_count(locale, "generic_views_count", view_count, NumberFormatting::Short)
|
||||
end
|
||||
when .has_key?("backstageImageRenderer")
|
||||
case attachment.as_h
|
||||
when .has_key?("videoRenderer")
|
||||
parse_item(attachment)
|
||||
.as(SearchVideo)
|
||||
.to_json(locale, json)
|
||||
when .has_key?("backstageImageRenderer")
|
||||
json.object do
|
||||
attachment = attachment["backstageImageRenderer"]
|
||||
json.field "type", "image"
|
||||
|
||||
@ -188,7 +150,9 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
|
||||
end
|
||||
end
|
||||
end
|
||||
when .has_key?("pollRenderer")
|
||||
end
|
||||
when .has_key?("pollRenderer")
|
||||
json.object do
|
||||
attachment = attachment["pollRenderer"]
|
||||
json.field "type", "poll"
|
||||
json.field "totalVotes", short_text_to_number(attachment["totalVotes"]["simpleText"].as_s.split(" ")[0])
|
||||
@ -221,7 +185,9 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
|
||||
end
|
||||
end
|
||||
end
|
||||
when .has_key?("postMultiImageRenderer")
|
||||
end
|
||||
when .has_key?("postMultiImageRenderer")
|
||||
json.object do
|
||||
attachment = attachment["postMultiImageRenderer"]
|
||||
json.field "type", "multiImage"
|
||||
json.field "images" do
|
||||
@ -245,7 +211,13 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
end
|
||||
when .has_key?("playlistRenderer")
|
||||
parse_item(attachment)
|
||||
.as(SearchPlaylist)
|
||||
.to_json(locale, json)
|
||||
else
|
||||
json.object do
|
||||
json.field "type", "unknown"
|
||||
json.field "error", "Unrecognized attachment type."
|
||||
end
|
||||
@ -270,17 +242,15 @@ def fetch_channel_community(ucid, continuation, locale, format, thin_mode)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if body["continuations"]?
|
||||
continuation = body["continuations"][0]["nextContinuationData"]["continuation"].as_s
|
||||
json.field "continuation", extract_channel_community_cursor(continuation)
|
||||
if cont = posts.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token")
|
||||
json.field "continuation", extract_channel_community_cursor(cont.as_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if format == "html"
|
||||
response = JSON.parse(response)
|
||||
content_html = template_youtube_comments(response, locale, thin_mode)
|
||||
content_html = IV::Frontend::Comments.template_youtube(response, locale, thin_mode)
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
|
@ -20,7 +20,7 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so
|
||||
case sort_by
|
||||
when "newest" then 1_i64
|
||||
when "popular" then 2_i64
|
||||
when "oldest" then 3_i64 # Broken as of 10/2022 :c
|
||||
when "oldest" then 4_i64
|
||||
else 1_i64 # Fallback to "newest"
|
||||
end
|
||||
|
||||
|
@ -1,762 +0,0 @@
|
||||
class RedditThing
|
||||
include JSON::Serializable
|
||||
|
||||
property kind : String
|
||||
property data : RedditComment | RedditLink | RedditMore | RedditListing
|
||||
end
|
||||
|
||||
class RedditComment
|
||||
include JSON::Serializable
|
||||
|
||||
property author : String
|
||||
property body_html : String
|
||||
property replies : RedditThing | String
|
||||
property score : Int32
|
||||
property depth : Int32
|
||||
property permalink : String
|
||||
|
||||
@[JSON::Field(converter: RedditComment::TimeConverter)]
|
||||
property created_utc : Time
|
||||
|
||||
module TimeConverter
|
||||
def self.from_json(value : JSON::PullParser) : Time
|
||||
Time.unix(value.read_float.to_i)
|
||||
end
|
||||
|
||||
def self.to_json(value : Time, json : JSON::Builder)
|
||||
json.number(value.to_unix)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
struct RedditLink
|
||||
include JSON::Serializable
|
||||
|
||||
property author : String
|
||||
property score : Int32
|
||||
property subreddit : String
|
||||
property num_comments : Int32
|
||||
property id : String
|
||||
property permalink : String
|
||||
property title : String
|
||||
end
|
||||
|
||||
struct RedditMore
|
||||
include JSON::Serializable
|
||||
|
||||
property children : Array(String)
|
||||
property count : Int32
|
||||
property depth : Int32
|
||||
end
|
||||
|
||||
class RedditListing
|
||||
include JSON::Serializable
|
||||
|
||||
property children : Array(RedditThing)
|
||||
property modhash : String
|
||||
end
|
||||
|
||||
def fetch_youtube_comments(id, cursor, format, locale, thin_mode, region, sort_by = "top")
|
||||
case cursor
|
||||
when nil, ""
|
||||
ctoken = produce_comment_continuation(id, cursor: "", sort_by: sort_by)
|
||||
when .starts_with? "ADSJ"
|
||||
ctoken = produce_comment_continuation(id, cursor: cursor, sort_by: sort_by)
|
||||
else
|
||||
ctoken = cursor
|
||||
end
|
||||
|
||||
client_config = YoutubeAPI::ClientConfig.new(region: region)
|
||||
response = YoutubeAPI.next(continuation: ctoken, client_config: client_config)
|
||||
contents = nil
|
||||
|
||||
if on_response_received_endpoints = response["onResponseReceivedEndpoints"]?
|
||||
header = nil
|
||||
on_response_received_endpoints.as_a.each do |item|
|
||||
if item["reloadContinuationItemsCommand"]?
|
||||
case item["reloadContinuationItemsCommand"]["slot"]
|
||||
when "RELOAD_CONTINUATION_SLOT_HEADER"
|
||||
header = item["reloadContinuationItemsCommand"]["continuationItems"][0]
|
||||
when "RELOAD_CONTINUATION_SLOT_BODY"
|
||||
# continuationItems is nil when video has no comments
|
||||
contents = item["reloadContinuationItemsCommand"]["continuationItems"]?
|
||||
end
|
||||
elsif item["appendContinuationItemsAction"]?
|
||||
contents = item["appendContinuationItemsAction"]["continuationItems"]
|
||||
end
|
||||
end
|
||||
elsif response["continuationContents"]?
|
||||
response = response["continuationContents"]
|
||||
if response["commentRepliesContinuation"]?
|
||||
body = response["commentRepliesContinuation"]
|
||||
else
|
||||
body = response["itemSectionContinuation"]
|
||||
end
|
||||
contents = body["contents"]?
|
||||
header = body["header"]?
|
||||
else
|
||||
raise NotFoundException.new("Comments not found.")
|
||||
end
|
||||
|
||||
if !contents
|
||||
if format == "json"
|
||||
return {"comments" => [] of String}.to_json
|
||||
else
|
||||
return {"contentHtml" => "", "commentCount" => 0}.to_json
|
||||
end
|
||||
end
|
||||
|
||||
continuation_item_renderer = nil
|
||||
contents.as_a.reject! do |item|
|
||||
if item["continuationItemRenderer"]?
|
||||
continuation_item_renderer = item["continuationItemRenderer"]
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
if header
|
||||
count_text = header["commentsHeaderRenderer"]["countText"]
|
||||
comment_count = (count_text["simpleText"]? || count_text["runs"]?.try &.[0]?.try &.["text"]?)
|
||||
.try &.as_s.gsub(/\D/, "").to_i? || 0
|
||||
json.field "commentCount", comment_count
|
||||
end
|
||||
|
||||
json.field "videoId", id
|
||||
|
||||
json.field "comments" do
|
||||
json.array do
|
||||
contents.as_a.each do |node|
|
||||
json.object do
|
||||
if node["commentThreadRenderer"]?
|
||||
node = node["commentThreadRenderer"]
|
||||
end
|
||||
|
||||
if node["replies"]?
|
||||
node_replies = node["replies"]["commentRepliesRenderer"]
|
||||
end
|
||||
|
||||
if node["comment"]?
|
||||
node_comment = node["comment"]["commentRenderer"]
|
||||
else
|
||||
node_comment = node["commentRenderer"]
|
||||
end
|
||||
|
||||
content_html = node_comment["contentText"]?.try { |t| parse_content(t, id) } || ""
|
||||
author = node_comment["authorText"]?.try &.["simpleText"]? || ""
|
||||
|
||||
json.field "verified", (node_comment["authorCommentBadge"]? != nil)
|
||||
|
||||
json.field "author", author
|
||||
json.field "authorThumbnails" do
|
||||
json.array do
|
||||
node_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail|
|
||||
json.object do
|
||||
json.field "url", thumbnail["url"]
|
||||
json.field "width", thumbnail["width"]
|
||||
json.field "height", thumbnail["height"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if node_comment["authorEndpoint"]?
|
||||
json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"]
|
||||
json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
|
||||
else
|
||||
json.field "authorId", ""
|
||||
json.field "authorUrl", ""
|
||||
end
|
||||
|
||||
published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s
|
||||
published = decode_date(published_text.rchop(" (edited)"))
|
||||
|
||||
if published_text.includes?(" (edited)")
|
||||
json.field "isEdited", true
|
||||
else
|
||||
json.field "isEdited", false
|
||||
end
|
||||
|
||||
json.field "content", html_to_content(content_html)
|
||||
json.field "contentHtml", content_html
|
||||
|
||||
json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil)
|
||||
json.field "isSponsor", (node_comment["sponsorCommentBadge"]? != nil)
|
||||
if node_comment["sponsorCommentBadge"]?
|
||||
# Sponsor icon thumbnails always have one object and there's only ever the url property in it
|
||||
json.field "sponsorIconUrl", node_comment.dig("sponsorCommentBadge", "sponsorCommentBadgeRenderer", "customBadge", "thumbnails", 0, "url").to_s
|
||||
end
|
||||
json.field "published", published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
|
||||
|
||||
comment_action_buttons_renderer = node_comment["actionButtons"]["commentActionButtonsRenderer"]
|
||||
|
||||
json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i
|
||||
json.field "commentId", node_comment["commentId"]
|
||||
json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"]
|
||||
|
||||
if comment_action_buttons_renderer["creatorHeart"]?
|
||||
hearth_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"]
|
||||
json.field "creatorHeart" do
|
||||
json.object do
|
||||
json.field "creatorThumbnail", hearth_data["thumbnails"][-1]["url"]
|
||||
json.field "creatorName", hearth_data["accessibility"]["accessibilityData"]["label"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if node_replies && !response["commentRepliesContinuation"]?
|
||||
if node_replies["continuations"]?
|
||||
continuation = node_replies["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s
|
||||
elsif node_replies["contents"]?
|
||||
continuation = node_replies["contents"]?.try &.as_a[0]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"].as_s
|
||||
end
|
||||
continuation ||= ""
|
||||
|
||||
json.field "replies" do
|
||||
json.object do
|
||||
json.field "replyCount", node_comment["replyCount"]? || 1
|
||||
json.field "continuation", continuation
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if continuation_item_renderer
|
||||
if continuation_item_renderer["continuationEndpoint"]?
|
||||
continuation_endpoint = continuation_item_renderer["continuationEndpoint"]
|
||||
elsif continuation_item_renderer["button"]?
|
||||
continuation_endpoint = continuation_item_renderer["button"]["buttonRenderer"]["command"]
|
||||
end
|
||||
if continuation_endpoint
|
||||
json.field "continuation", continuation_endpoint["continuationCommand"]["token"].as_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if format == "html"
|
||||
response = JSON.parse(response)
|
||||
content_html = template_youtube_comments(response, locale, thin_mode)
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
json.field "contentHtml", content_html
|
||||
|
||||
if response["commentCount"]?
|
||||
json.field "commentCount", response["commentCount"]
|
||||
else
|
||||
json.field "commentCount", 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return response
|
||||
end
|
||||
|
||||
def fetch_reddit_comments(id, sort_by = "confidence")
|
||||
client = make_client(REDDIT_URL)
|
||||
headers = HTTP::Headers{"User-Agent" => "web:invidious:v#{CURRENT_VERSION} (by github.com/iv-org/invidious)"}
|
||||
|
||||
# TODO: Use something like #479 for a static list of instances to use here
|
||||
query = URI::Params.encode({q: "(url:3D#{id} OR url:#{id}) AND (site:invidio.us OR site:youtube.com OR site:youtu.be)"})
|
||||
search_results = client.get("/search.json?#{query}", headers)
|
||||
|
||||
if search_results.status_code == 200
|
||||
search_results = RedditThing.from_json(search_results.body)
|
||||
|
||||
# For videos that have more than one thread, choose the one with the highest score
|
||||
threads = search_results.data.as(RedditListing).children
|
||||
thread = threads.max_by?(&.data.as(RedditLink).score).try(&.data.as(RedditLink))
|
||||
result = thread.try do |t|
|
||||
body = client.get("/r/#{t.subreddit}/comments/#{t.id}.json?limit=100&sort=#{sort_by}", headers).body
|
||||
Array(RedditThing).from_json(body)
|
||||
end
|
||||
result ||= [] of RedditThing
|
||||
elsif search_results.status_code == 302
|
||||
# Previously, if there was only one result then the API would redirect to that result.
|
||||
# Now, it appears it will still return a listing so this section is likely unnecessary.
|
||||
|
||||
result = client.get(search_results.headers["Location"], headers).body
|
||||
result = Array(RedditThing).from_json(result)
|
||||
|
||||
thread = result[0].data.as(RedditListing).children[0].data.as(RedditLink)
|
||||
else
|
||||
raise NotFoundException.new("Comments not found.")
|
||||
end
|
||||
|
||||
client.close
|
||||
|
||||
comments = result[1]?.try(&.data.as(RedditListing).children)
|
||||
comments ||= [] of RedditThing
|
||||
return comments, thread
|
||||
end
|
||||
|
||||
def template_youtube_comments(comments, locale, thin_mode, is_replies = false)
|
||||
String.build do |html|
|
||||
root = comments["comments"].as_a
|
||||
root.each do |child|
|
||||
if child["replies"]?
|
||||
replies_count_text = translate_count(locale,
|
||||
"comments_view_x_replies",
|
||||
child["replies"]["replyCount"].as_i64 || 0,
|
||||
NumberFormatting::Separator
|
||||
)
|
||||
|
||||
replies_html = <<-END_HTML
|
||||
<div id="replies" class="pure-g">
|
||||
<div class="pure-u-1-24"></div>
|
||||
<div class="pure-u-23-24">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-replies>#{replies_count_text}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
if !thin_mode
|
||||
author_thumbnail = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).request_target}"
|
||||
else
|
||||
author_thumbnail = ""
|
||||
end
|
||||
|
||||
author_name = HTML.escape(child["author"].as_s)
|
||||
sponsor_icon = ""
|
||||
if child["verified"]?.try &.as_bool && child["authorIsChannelOwner"]?.try &.as_bool
|
||||
author_name += " <i class=\"icon ion ion-md-checkmark-circle\"></i>"
|
||||
elsif child["verified"]?.try &.as_bool
|
||||
author_name += " <i class=\"icon ion ion-md-checkmark\"></i>"
|
||||
end
|
||||
|
||||
if child["isSponsor"]?.try &.as_bool
|
||||
sponsor_icon = String.build do |str|
|
||||
str << %(<img alt="" )
|
||||
str << %(src="/ggpht) << URI.parse(child["sponsorIconUrl"].as_s).request_target << "\" "
|
||||
str << %(title=") << translate(locale, "Channel Sponsor") << "\" "
|
||||
str << %(width="16" height="16" />)
|
||||
end
|
||||
end
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g" style="width:100%">
|
||||
<div class="channel-profile pure-u-4-24 pure-u-md-2-24">
|
||||
<img loading="lazy" style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}">
|
||||
</div>
|
||||
<div class="pure-u-20-24 pure-u-md-22-24">
|
||||
<p>
|
||||
<b>
|
||||
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{author_name}</a>
|
||||
</b>
|
||||
#{sponsor_icon}
|
||||
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
|
||||
END_HTML
|
||||
|
||||
if child["attachment"]?
|
||||
attachment = child["attachment"]
|
||||
|
||||
case attachment["type"]
|
||||
when "image"
|
||||
attachment = attachment["imageThumbnails"][1]
|
||||
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-2">
|
||||
<img loading="lazy" style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}">
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
when "video"
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-2">
|
||||
<div style="position:relative;width:100%;height:0;padding-bottom:56.25%;margin-bottom:5px">
|
||||
END_HTML
|
||||
|
||||
if attachment["error"]?
|
||||
html << <<-END_HTML
|
||||
<p>#{attachment["error"]}</p>
|
||||
END_HTML
|
||||
else
|
||||
html << <<-END_HTML
|
||||
<iframe id='ivplayer' style='position:absolute;width:100%;height:100%;left:0;top:0' src='/embed/#{attachment["videoId"]?}?autoplay=0' style='border:none;'></iframe>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
else nil # Ignore
|
||||
end
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
<span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span>
|
||||
|
|
||||
END_HTML
|
||||
|
||||
if comments["videoId"]?
|
||||
html << <<-END_HTML
|
||||
<a href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
elsif comments["authorId"]?
|
||||
html << <<-END_HTML
|
||||
<a 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_HTML
|
||||
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
|
||||
END_HTML
|
||||
|
||||
if child["creatorHeart"]?
|
||||
if !thin_mode
|
||||
creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).request_target}"
|
||||
else
|
||||
creator_thumbnail = ""
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
<span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
|
||||
<div class="creator-heart">
|
||||
<img loading="lazy" class="creator-heart-background-hearted" src="#{creator_thumbnail}"></img>
|
||||
<div class="creator-heart-small-hearted">
|
||||
<div class="icon ion-ios-heart creator-heart-small-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
</p>
|
||||
#{replies_html}
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
if comments["continuation"]?
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def template_reddit_comments(root, locale)
|
||||
String.build do |html|
|
||||
root.each do |child|
|
||||
if child.data.is_a?(RedditComment)
|
||||
child = child.data.as(RedditComment)
|
||||
body_html = HTML.unescape(child.body_html)
|
||||
|
||||
replies_html = ""
|
||||
if child.replies.is_a?(RedditThing)
|
||||
replies = child.replies.as(RedditThing)
|
||||
replies_html = template_reddit_comments(replies.data.as(RedditListing).children, locale)
|
||||
end
|
||||
|
||||
if child.depth > 0
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-24">
|
||||
</div>
|
||||
<div class="pure-u-23-24">
|
||||
END_HTML
|
||||
else
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
|
||||
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
|
||||
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
|
||||
<span title="#{child.created_utc.to_s(translate(locale, "%a %B %-d %T %Y UTC"))}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
|
||||
<a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a>
|
||||
</p>
|
||||
<div>
|
||||
#{body_html}
|
||||
#{replies_html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def replace_links(html)
|
||||
# Check if the document is empty
|
||||
# Prevents edge-case bug with Reddit comments, see issue #3115
|
||||
if html.nil? || html.empty?
|
||||
return html
|
||||
end
|
||||
|
||||
html = XML.parse_html(html)
|
||||
|
||||
html.xpath_nodes(%q(//a)).each do |anchor|
|
||||
url = URI.parse(anchor["href"])
|
||||
|
||||
if url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") || url.host.not_nil!.ends_with?("youtu.be")
|
||||
if url.host.try &.ends_with? "youtu.be"
|
||||
url = "/watch?v=#{url.path.lstrip('/')}#{url.query_params}"
|
||||
else
|
||||
if url.path == "/redirect"
|
||||
params = HTTP::Params.parse(url.query.not_nil!)
|
||||
anchor["href"] = params["q"]?
|
||||
else
|
||||
anchor["href"] = url.request_target
|
||||
end
|
||||
end
|
||||
elsif url.to_s == "#"
|
||||
begin
|
||||
length_seconds = decode_length_seconds(anchor.content)
|
||||
rescue ex
|
||||
length_seconds = decode_time(anchor.content)
|
||||
end
|
||||
|
||||
if length_seconds > 0
|
||||
anchor["href"] = "javascript:void(0)"
|
||||
anchor["onclick"] = "player.currentTime(#{length_seconds})"
|
||||
else
|
||||
anchor["href"] = url.request_target
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
html = html.xpath_node(%q(//body)).not_nil!
|
||||
if node = html.xpath_node(%q(./p))
|
||||
html = node
|
||||
end
|
||||
|
||||
return html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
end
|
||||
|
||||
def fill_links(html, scheme, host)
|
||||
# Check if the document is empty
|
||||
# Prevents edge-case bug with Reddit comments, see issue #3115
|
||||
if html.nil? || html.empty?
|
||||
return html
|
||||
end
|
||||
|
||||
html = XML.parse_html(html)
|
||||
|
||||
html.xpath_nodes("//a").each do |match|
|
||||
url = URI.parse(match["href"])
|
||||
# Reddit links don't have host
|
||||
if !url.host && !match["href"].starts_with?("javascript") && !url.to_s.ends_with? "#"
|
||||
url.scheme = scheme
|
||||
url.host = host
|
||||
match["href"] = url
|
||||
end
|
||||
end
|
||||
|
||||
if host == "www.youtube.com"
|
||||
html = html.xpath_node(%q(//body/p)).not_nil!
|
||||
end
|
||||
|
||||
return html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
end
|
||||
|
||||
def text_to_parsed_content(text : String) : JSON::Any
|
||||
nodes = [] of JSON::Any
|
||||
# For each line convert line to array of nodes
|
||||
text.split('\n').each do |line|
|
||||
# In first case line is just a simple node before
|
||||
# check patterns inside line
|
||||
# { 'text': line }
|
||||
currentNodes = [] of JSON::Any
|
||||
initialNode = {"text" => line}
|
||||
currentNodes << (JSON.parse(initialNode.to_json))
|
||||
|
||||
# For each match with url pattern, get last node and preserve
|
||||
# last node before create new node with url information
|
||||
# { 'text': match, 'navigationEndpoint': { 'urlEndpoint' : 'url': match } }
|
||||
line.scan(/https?:\/\/[^ ]*/).each do |urlMatch|
|
||||
# Retrieve last node and update node without match
|
||||
lastNode = currentNodes[currentNodes.size - 1].as_h
|
||||
splittedLastNode = lastNode["text"].as_s.split(urlMatch[0])
|
||||
lastNode["text"] = JSON.parse(splittedLastNode[0].to_json)
|
||||
currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json)
|
||||
# Create new node with match and navigation infos
|
||||
currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}}
|
||||
currentNodes << (JSON.parse(currentNode.to_json))
|
||||
# If text remain after match create new simple node with text after match
|
||||
afterNode = {"text" => splittedLastNode.size > 0 ? splittedLastNode[1] : ""}
|
||||
currentNodes << (JSON.parse(afterNode.to_json))
|
||||
end
|
||||
|
||||
# After processing of matches inside line
|
||||
# Add \n at end of last node for preserve carriage return
|
||||
lastNode = currentNodes[currentNodes.size - 1].as_h
|
||||
lastNode["text"] = JSON.parse("#{currentNodes[currentNodes.size - 1]["text"]}\n".to_json)
|
||||
currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json)
|
||||
|
||||
# Finally add final nodes to nodes returned
|
||||
currentNodes.each do |node|
|
||||
nodes << (node)
|
||||
end
|
||||
end
|
||||
return JSON.parse({"runs" => nodes}.to_json)
|
||||
end
|
||||
|
||||
def parse_content(content : JSON::Any, video_id : String? = "") : String
|
||||
content["simpleText"]?.try &.as_s.rchop('\ufeff').try { |b| HTML.escape(b) }.to_s ||
|
||||
content["runs"]?.try &.as_a.try { |r| content_to_comment_html(r, video_id).try &.to_s.gsub("\n", "<br>") } || ""
|
||||
end
|
||||
|
||||
def content_to_comment_html(content, video_id : String? = "")
|
||||
html_array = content.map do |run|
|
||||
# Sometimes, there is an empty element.
|
||||
# See: https://github.com/iv-org/invidious/issues/3096
|
||||
next if run.as_h.empty?
|
||||
|
||||
text = HTML.escape(run["text"].as_s)
|
||||
|
||||
if run["navigationEndpoint"]?
|
||||
if url = run["navigationEndpoint"]["urlEndpoint"]?.try &.["url"].as_s
|
||||
url = URI.parse(url)
|
||||
displayed_url = text
|
||||
|
||||
if url.host == "youtu.be"
|
||||
url = "/watch?v=#{url.request_target.lstrip('/')}"
|
||||
elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com")
|
||||
if url.path == "/redirect"
|
||||
# Sometimes, links can be corrupted (why?) so make sure to fallback
|
||||
# nicely. See https://github.com/iv-org/invidious/issues/2682
|
||||
url = url.query_params["q"]? || ""
|
||||
displayed_url = url
|
||||
else
|
||||
url = url.request_target
|
||||
displayed_url = "youtube.com#{url}"
|
||||
end
|
||||
end
|
||||
|
||||
text = %(<a href="#{url}">#{reduce_uri(displayed_url)}</a>)
|
||||
elsif watch_endpoint = run["navigationEndpoint"]["watchEndpoint"]?
|
||||
start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i
|
||||
link_video_id = watch_endpoint["videoId"].as_s
|
||||
|
||||
url = "/watch?v=#{link_video_id}"
|
||||
url += "&t=#{start_time}" if !start_time.nil?
|
||||
|
||||
# If the current video ID (passed through from the caller function)
|
||||
# is the same as the video ID in the link, add HTML attributes for
|
||||
# the JS handler function that bypasses page reload.
|
||||
#
|
||||
# See: https://github.com/iv-org/invidious/issues/3063
|
||||
if link_video_id == video_id
|
||||
start_time ||= 0
|
||||
text = %(<a href="#{url}" data-onclick="jump_to_time" data-jump-time="#{start_time}">#{reduce_uri(text)}</a>)
|
||||
else
|
||||
text = %(<a href="#{url}">#{text}</a>)
|
||||
end
|
||||
elsif url = run.dig?("navigationEndpoint", "commandMetadata", "webCommandMetadata", "url").try &.as_s
|
||||
if text.starts_with?(/\s?[@#]/)
|
||||
# Handle "pings" in comments and hasthags differently
|
||||
# See:
|
||||
# - https://github.com/iv-org/invidious/issues/3038
|
||||
# - https://github.com/iv-org/invidious/issues/3062
|
||||
text = %(<a href="#{url}">#{text}</a>)
|
||||
else
|
||||
text = %(<a href="#{url}">#{reduce_uri(url)}</a>)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
text = "<b>#{text}</b>" if run["bold"]?
|
||||
text = "<s>#{text}</s>" if run["strikethrough"]?
|
||||
text = "<i>#{text}</i>" if run["italics"]?
|
||||
|
||||
# check for custom emojis
|
||||
if run["emoji"]?
|
||||
if run["emoji"]["isCustomEmoji"]?.try &.as_bool
|
||||
if emojiImage = run.dig?("emoji", "image")
|
||||
emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text
|
||||
emojiThumb = emojiImage["thumbnails"][0]
|
||||
text = String.build do |str|
|
||||
str << %(<img alt=") << emojiAlt << "\" "
|
||||
str << %(src="/ggpht) << URI.parse(emojiThumb["url"].as_s).request_target << "\" "
|
||||
str << %(title=") << emojiAlt << "\" "
|
||||
str << %(width=") << emojiThumb["width"] << "\" "
|
||||
str << %(height=") << emojiThumb["height"] << "\" "
|
||||
str << %(class="channel-emoji"/>)
|
||||
end
|
||||
else
|
||||
# Hide deleted channel emoji
|
||||
text = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
return html_array.join("").delete('\ufeff')
|
||||
end
|
||||
|
||||
def produce_comment_continuation(video_id, cursor = "", sort_by = "top")
|
||||
object = {
|
||||
"2:embedded" => {
|
||||
"2:string" => video_id,
|
||||
"25:varint" => 0_i64,
|
||||
"28:varint" => 1_i64,
|
||||
"36:embedded" => {
|
||||
"5:varint" => -1_i64,
|
||||
"8:varint" => 0_i64,
|
||||
},
|
||||
"40:embedded" => {
|
||||
"1:varint" => 4_i64,
|
||||
"3:string" => "https://www.youtube.com",
|
||||
"4:string" => "",
|
||||
},
|
||||
},
|
||||
"3:varint" => 6_i64,
|
||||
"6:embedded" => {
|
||||
"1:string" => cursor,
|
||||
"4:embedded" => {
|
||||
"4:string" => video_id,
|
||||
"6:varint" => 0_i64,
|
||||
},
|
||||
"5:varint" => 20_i64,
|
||||
},
|
||||
}
|
||||
|
||||
case sort_by
|
||||
when "top"
|
||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64
|
||||
when "new", "newest"
|
||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 1_i64
|
||||
else # top
|
||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64
|
||||
end
|
||||
|
||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
return continuation
|
||||
end
|
89
src/invidious/comments/content.cr
Normal file
89
src/invidious/comments/content.cr
Normal file
@ -0,0 +1,89 @@
|
||||
def text_to_parsed_content(text : String) : JSON::Any
|
||||
nodes = [] of JSON::Any
|
||||
# For each line convert line to array of nodes
|
||||
text.split('\n').each do |line|
|
||||
# In first case line is just a simple node before
|
||||
# check patterns inside line
|
||||
# { 'text': line }
|
||||
currentNodes = [] of JSON::Any
|
||||
initialNode = {"text" => line}
|
||||
currentNodes << (JSON.parse(initialNode.to_json))
|
||||
|
||||
# For each match with url pattern, get last node and preserve
|
||||
# last node before create new node with url information
|
||||
# { 'text': match, 'navigationEndpoint': { 'urlEndpoint' : 'url': match } }
|
||||
line.scan(/https?:\/\/[^ ]*/).each do |urlMatch|
|
||||
# Retrieve last node and update node without match
|
||||
lastNode = currentNodes[currentNodes.size - 1].as_h
|
||||
splittedLastNode = lastNode["text"].as_s.split(urlMatch[0])
|
||||
lastNode["text"] = JSON.parse(splittedLastNode[0].to_json)
|
||||
currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json)
|
||||
# Create new node with match and navigation infos
|
||||
currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}}
|
||||
currentNodes << (JSON.parse(currentNode.to_json))
|
||||
# If text remain after match create new simple node with text after match
|
||||
afterNode = {"text" => splittedLastNode.size > 1 ? splittedLastNode[1] : ""}
|
||||
currentNodes << (JSON.parse(afterNode.to_json))
|
||||
end
|
||||
|
||||
# After processing of matches inside line
|
||||
# Add \n at end of last node for preserve carriage return
|
||||
lastNode = currentNodes[currentNodes.size - 1].as_h
|
||||
lastNode["text"] = JSON.parse("#{currentNodes[currentNodes.size - 1]["text"]}\n".to_json)
|
||||
currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json)
|
||||
|
||||
# Finally add final nodes to nodes returned
|
||||
currentNodes.each do |node|
|
||||
nodes << (node)
|
||||
end
|
||||
end
|
||||
return JSON.parse({"runs" => nodes}.to_json)
|
||||
end
|
||||
|
||||
def parse_content(content : JSON::Any, video_id : String? = "") : String
|
||||
content["simpleText"]?.try &.as_s.rchop('\ufeff').try { |b| HTML.escape(b) }.to_s ||
|
||||
content["runs"]?.try &.as_a.try { |r| content_to_comment_html(r, video_id).try &.to_s.gsub("\n", "<br>") } || ""
|
||||
end
|
||||
|
||||
def content_to_comment_html(content, video_id : String? = "")
|
||||
html_array = content.map do |run|
|
||||
# Sometimes, there is an empty element.
|
||||
# See: https://github.com/iv-org/invidious/issues/3096
|
||||
next if run.as_h.empty?
|
||||
|
||||
text = HTML.escape(run["text"].as_s)
|
||||
|
||||
if navigationEndpoint = run.dig?("navigationEndpoint")
|
||||
text = parse_link_endpoint(navigationEndpoint, text, video_id)
|
||||
end
|
||||
|
||||
text = "<b>#{text}</b>" if run["bold"]?
|
||||
text = "<s>#{text}</s>" if run["strikethrough"]?
|
||||
text = "<i>#{text}</i>" if run["italics"]?
|
||||
|
||||
# check for custom emojis
|
||||
if run["emoji"]?
|
||||
if run["emoji"]["isCustomEmoji"]?.try &.as_bool
|
||||
if emojiImage = run.dig?("emoji", "image")
|
||||
emojiAlt = emojiImage.dig?("accessibility", "accessibilityData", "label").try &.as_s || text
|
||||
emojiThumb = emojiImage["thumbnails"][0]
|
||||
text = String.build do |str|
|
||||
str << %(<img alt=") << emojiAlt << "\" "
|
||||
str << %(src="/ggpht) << URI.parse(emojiThumb["url"].as_s).request_target << "\" "
|
||||
str << %(title=") << emojiAlt << "\" "
|
||||
str << %(width=") << emojiThumb["width"] << "\" "
|
||||
str << %(height=") << emojiThumb["height"] << "\" "
|
||||
str << %(class="channel-emoji" />)
|
||||
end
|
||||
else
|
||||
# Hide deleted channel emoji
|
||||
text = ""
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
text
|
||||
end
|
||||
|
||||
return html_array.join("").delete('\ufeff')
|
||||
end
|
76
src/invidious/comments/links_util.cr
Normal file
76
src/invidious/comments/links_util.cr
Normal file
@ -0,0 +1,76 @@
|
||||
module Invidious::Comments
|
||||
extend self
|
||||
|
||||
def replace_links(html)
|
||||
# Check if the document is empty
|
||||
# Prevents edge-case bug with Reddit comments, see issue #3115
|
||||
if html.nil? || html.empty?
|
||||
return html
|
||||
end
|
||||
|
||||
html = XML.parse_html(html)
|
||||
|
||||
html.xpath_nodes(%q(//a)).each do |anchor|
|
||||
url = URI.parse(anchor["href"])
|
||||
|
||||
if url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") || url.host.not_nil!.ends_with?("youtu.be")
|
||||
if url.host.try &.ends_with? "youtu.be"
|
||||
url = "/watch?v=#{url.path.lstrip('/')}#{url.query_params}"
|
||||
else
|
||||
if url.path == "/redirect"
|
||||
params = HTTP::Params.parse(url.query.not_nil!)
|
||||
anchor["href"] = params["q"]?
|
||||
else
|
||||
anchor["href"] = url.request_target
|
||||
end
|
||||
end
|
||||
elsif url.to_s == "#"
|
||||
begin
|
||||
length_seconds = decode_length_seconds(anchor.content)
|
||||
rescue ex
|
||||
length_seconds = decode_time(anchor.content)
|
||||
end
|
||||
|
||||
if length_seconds > 0
|
||||
anchor["href"] = "javascript:void(0)"
|
||||
anchor["onclick"] = "player.currentTime(#{length_seconds})"
|
||||
else
|
||||
anchor["href"] = url.request_target
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
html = html.xpath_node(%q(//body)).not_nil!
|
||||
if node = html.xpath_node(%q(./p))
|
||||
html = node
|
||||
end
|
||||
|
||||
return html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
end
|
||||
|
||||
def fill_links(html, scheme, host)
|
||||
# Check if the document is empty
|
||||
# Prevents edge-case bug with Reddit comments, see issue #3115
|
||||
if html.nil? || html.empty?
|
||||
return html
|
||||
end
|
||||
|
||||
html = XML.parse_html(html)
|
||||
|
||||
html.xpath_nodes("//a").each do |match|
|
||||
url = URI.parse(match["href"])
|
||||
# Reddit links don't have host
|
||||
if !url.host && !match["href"].starts_with?("javascript") && !url.to_s.ends_with? "#"
|
||||
url.scheme = scheme
|
||||
url.host = host
|
||||
match["href"] = url
|
||||
end
|
||||
end
|
||||
|
||||
if host == "www.youtube.com"
|
||||
html = html.xpath_node(%q(//body/p)).not_nil!
|
||||
end
|
||||
|
||||
return html.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
end
|
||||
end
|
41
src/invidious/comments/reddit.cr
Normal file
41
src/invidious/comments/reddit.cr
Normal file
@ -0,0 +1,41 @@
|
||||
module Invidious::Comments
|
||||
extend self
|
||||
|
||||
def fetch_reddit(id, sort_by = "confidence")
|
||||
client = make_client(REDDIT_URL)
|
||||
headers = HTTP::Headers{"User-Agent" => "web:invidious:v#{CURRENT_VERSION} (by github.com/iv-org/invidious)"}
|
||||
|
||||
# TODO: Use something like #479 for a static list of instances to use here
|
||||
query = URI::Params.encode({q: "(url:3D#{id} OR url:#{id}) AND (site:invidio.us OR site:youtube.com OR site:youtu.be)"})
|
||||
search_results = client.get("/search.json?#{query}", headers)
|
||||
|
||||
if search_results.status_code == 200
|
||||
search_results = RedditThing.from_json(search_results.body)
|
||||
|
||||
# For videos that have more than one thread, choose the one with the highest score
|
||||
threads = search_results.data.as(RedditListing).children
|
||||
thread = threads.max_by?(&.data.as(RedditLink).score).try(&.data.as(RedditLink))
|
||||
result = thread.try do |t|
|
||||
body = client.get("/r/#{t.subreddit}/comments/#{t.id}.json?limit=100&sort=#{sort_by}", headers).body
|
||||
Array(RedditThing).from_json(body)
|
||||
end
|
||||
result ||= [] of RedditThing
|
||||
elsif search_results.status_code == 302
|
||||
# Previously, if there was only one result then the API would redirect to that result.
|
||||
# Now, it appears it will still return a listing so this section is likely unnecessary.
|
||||
|
||||
result = client.get(search_results.headers["Location"], headers).body
|
||||
result = Array(RedditThing).from_json(result)
|
||||
|
||||
thread = result[0].data.as(RedditListing).children[0].data.as(RedditLink)
|
||||
else
|
||||
raise NotFoundException.new("Comments not found.")
|
||||
end
|
||||
|
||||
client.close
|
||||
|
||||
comments = result[1]?.try(&.data.as(RedditListing).children)
|
||||
comments ||= [] of RedditThing
|
||||
return comments, thread
|
||||
end
|
||||
end
|
57
src/invidious/comments/reddit_types.cr
Normal file
57
src/invidious/comments/reddit_types.cr
Normal file
@ -0,0 +1,57 @@
|
||||
class RedditThing
|
||||
include JSON::Serializable
|
||||
|
||||
property kind : String
|
||||
property data : RedditComment | RedditLink | RedditMore | RedditListing
|
||||
end
|
||||
|
||||
class RedditComment
|
||||
include JSON::Serializable
|
||||
|
||||
property author : String
|
||||
property body_html : String
|
||||
property replies : RedditThing | String
|
||||
property score : Int32
|
||||
property depth : Int32
|
||||
property permalink : String
|
||||
|
||||
@[JSON::Field(converter: RedditComment::TimeConverter)]
|
||||
property created_utc : Time
|
||||
|
||||
module TimeConverter
|
||||
def self.from_json(value : JSON::PullParser) : Time
|
||||
Time.unix(value.read_float.to_i)
|
||||
end
|
||||
|
||||
def self.to_json(value : Time, json : JSON::Builder)
|
||||
json.number(value.to_unix)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
struct RedditLink
|
||||
include JSON::Serializable
|
||||
|
||||
property author : String
|
||||
property score : Int32
|
||||
property subreddit : String
|
||||
property num_comments : Int32
|
||||
property id : String
|
||||
property permalink : String
|
||||
property title : String
|
||||
end
|
||||
|
||||
struct RedditMore
|
||||
include JSON::Serializable
|
||||
|
||||
property children : Array(String)
|
||||
property count : Int32
|
||||
property depth : Int32
|
||||
end
|
||||
|
||||
class RedditListing
|
||||
include JSON::Serializable
|
||||
|
||||
property children : Array(RedditThing)
|
||||
property modhash : String
|
||||
end
|
250
src/invidious/comments/youtube.cr
Normal file
250
src/invidious/comments/youtube.cr
Normal file
@ -0,0 +1,250 @@
|
||||
module Invidious::Comments
|
||||
extend self
|
||||
|
||||
def fetch_youtube(id, cursor, format, locale, thin_mode, region, sort_by = "top")
|
||||
case cursor
|
||||
when nil, ""
|
||||
ctoken = Comments.produce_continuation(id, cursor: "", sort_by: sort_by)
|
||||
when .starts_with? "ADSJ"
|
||||
ctoken = Comments.produce_continuation(id, cursor: cursor, sort_by: sort_by)
|
||||
else
|
||||
ctoken = cursor
|
||||
end
|
||||
|
||||
client_config = YoutubeAPI::ClientConfig.new(region: region)
|
||||
response = YoutubeAPI.next(continuation: ctoken, client_config: client_config)
|
||||
contents = nil
|
||||
|
||||
if on_response_received_endpoints = response["onResponseReceivedEndpoints"]?
|
||||
header = nil
|
||||
on_response_received_endpoints.as_a.each do |item|
|
||||
if item["reloadContinuationItemsCommand"]?
|
||||
case item["reloadContinuationItemsCommand"]["slot"]
|
||||
when "RELOAD_CONTINUATION_SLOT_HEADER"
|
||||
header = item["reloadContinuationItemsCommand"]["continuationItems"][0]
|
||||
when "RELOAD_CONTINUATION_SLOT_BODY"
|
||||
# continuationItems is nil when video has no comments
|
||||
contents = item["reloadContinuationItemsCommand"]["continuationItems"]?
|
||||
end
|
||||
elsif item["appendContinuationItemsAction"]?
|
||||
contents = item["appendContinuationItemsAction"]["continuationItems"]
|
||||
end
|
||||
end
|
||||
elsif response["continuationContents"]?
|
||||
response = response["continuationContents"]
|
||||
if response["commentRepliesContinuation"]?
|
||||
body = response["commentRepliesContinuation"]
|
||||
else
|
||||
body = response["itemSectionContinuation"]
|
||||
end
|
||||
contents = body["contents"]?
|
||||
header = body["header"]?
|
||||
else
|
||||
raise NotFoundException.new("Comments not found.")
|
||||
end
|
||||
|
||||
if !contents
|
||||
if format == "json"
|
||||
return {"comments" => [] of String}.to_json
|
||||
else
|
||||
return {"contentHtml" => "", "commentCount" => 0}.to_json
|
||||
end
|
||||
end
|
||||
|
||||
continuation_item_renderer = nil
|
||||
contents.as_a.reject! do |item|
|
||||
if item["continuationItemRenderer"]?
|
||||
continuation_item_renderer = item["continuationItemRenderer"]
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
if header
|
||||
count_text = header["commentsHeaderRenderer"]["countText"]
|
||||
comment_count = (count_text["simpleText"]? || count_text["runs"]?.try &.[0]?.try &.["text"]?)
|
||||
.try &.as_s.gsub(/\D/, "").to_i? || 0
|
||||
json.field "commentCount", comment_count
|
||||
end
|
||||
|
||||
json.field "videoId", id
|
||||
|
||||
json.field "comments" do
|
||||
json.array do
|
||||
contents.as_a.each do |node|
|
||||
json.object do
|
||||
if node["commentThreadRenderer"]?
|
||||
node = node["commentThreadRenderer"]
|
||||
end
|
||||
|
||||
if node["replies"]?
|
||||
node_replies = node["replies"]["commentRepliesRenderer"]
|
||||
end
|
||||
|
||||
if node["comment"]?
|
||||
node_comment = node["comment"]["commentRenderer"]
|
||||
else
|
||||
node_comment = node["commentRenderer"]
|
||||
end
|
||||
|
||||
content_html = node_comment["contentText"]?.try { |t| parse_content(t, id) } || ""
|
||||
author = node_comment["authorText"]?.try &.["simpleText"]? || ""
|
||||
|
||||
json.field "verified", (node_comment["authorCommentBadge"]? != nil)
|
||||
|
||||
json.field "author", author
|
||||
json.field "authorThumbnails" do
|
||||
json.array do
|
||||
node_comment["authorThumbnail"]["thumbnails"].as_a.each do |thumbnail|
|
||||
json.object do
|
||||
json.field "url", thumbnail["url"]
|
||||
json.field "width", thumbnail["width"]
|
||||
json.field "height", thumbnail["height"]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if node_comment["authorEndpoint"]?
|
||||
json.field "authorId", node_comment["authorEndpoint"]["browseEndpoint"]["browseId"]
|
||||
json.field "authorUrl", node_comment["authorEndpoint"]["browseEndpoint"]["canonicalBaseUrl"]
|
||||
else
|
||||
json.field "authorId", ""
|
||||
json.field "authorUrl", ""
|
||||
end
|
||||
|
||||
published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s
|
||||
published = decode_date(published_text.rchop(" (edited)"))
|
||||
|
||||
if published_text.includes?(" (edited)")
|
||||
json.field "isEdited", true
|
||||
else
|
||||
json.field "isEdited", false
|
||||
end
|
||||
|
||||
json.field "content", html_to_content(content_html)
|
||||
json.field "contentHtml", content_html
|
||||
|
||||
json.field "isPinned", (node_comment["pinnedCommentBadge"]? != nil)
|
||||
json.field "isSponsor", (node_comment["sponsorCommentBadge"]? != nil)
|
||||
if node_comment["sponsorCommentBadge"]?
|
||||
# Sponsor icon thumbnails always have one object and there's only ever the url property in it
|
||||
json.field "sponsorIconUrl", node_comment.dig("sponsorCommentBadge", "sponsorCommentBadgeRenderer", "customBadge", "thumbnails", 0, "url").to_s
|
||||
end
|
||||
json.field "published", published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
|
||||
|
||||
comment_action_buttons_renderer = node_comment["actionButtons"]["commentActionButtonsRenderer"]
|
||||
|
||||
json.field "likeCount", comment_action_buttons_renderer["likeButton"]["toggleButtonRenderer"]["accessibilityData"]["accessibilityData"]["label"].as_s.scan(/\d/).map(&.[0]).join.to_i
|
||||
json.field "commentId", node_comment["commentId"]
|
||||
json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"]
|
||||
|
||||
if comment_action_buttons_renderer["creatorHeart"]?
|
||||
hearth_data = comment_action_buttons_renderer["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"]
|
||||
json.field "creatorHeart" do
|
||||
json.object do
|
||||
json.field "creatorThumbnail", hearth_data["thumbnails"][-1]["url"]
|
||||
json.field "creatorName", hearth_data["accessibility"]["accessibilityData"]["label"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if node_replies && !response["commentRepliesContinuation"]?
|
||||
if node_replies["continuations"]?
|
||||
continuation = node_replies["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s
|
||||
elsif node_replies["contents"]?
|
||||
continuation = node_replies["contents"]?.try &.as_a[0]["continuationItemRenderer"]["continuationEndpoint"]["continuationCommand"]["token"].as_s
|
||||
end
|
||||
continuation ||= ""
|
||||
|
||||
json.field "replies" do
|
||||
json.object do
|
||||
json.field "replyCount", node_comment["replyCount"]? || 1
|
||||
json.field "continuation", continuation
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if continuation_item_renderer
|
||||
if continuation_item_renderer["continuationEndpoint"]?
|
||||
continuation_endpoint = continuation_item_renderer["continuationEndpoint"]
|
||||
elsif continuation_item_renderer["button"]?
|
||||
continuation_endpoint = continuation_item_renderer["button"]["buttonRenderer"]["command"]
|
||||
end
|
||||
if continuation_endpoint
|
||||
json.field "continuation", continuation_endpoint["continuationCommand"]["token"].as_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if format == "html"
|
||||
response = JSON.parse(response)
|
||||
content_html = Frontend::Comments.template_youtube(response, locale, thin_mode)
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
json.field "contentHtml", content_html
|
||||
|
||||
if response["commentCount"]?
|
||||
json.field "commentCount", response["commentCount"]
|
||||
else
|
||||
json.field "commentCount", 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return response
|
||||
end
|
||||
|
||||
def produce_continuation(video_id, cursor = "", sort_by = "top")
|
||||
object = {
|
||||
"2:embedded" => {
|
||||
"2:string" => video_id,
|
||||
"25:varint" => 0_i64,
|
||||
"28:varint" => 1_i64,
|
||||
"36:embedded" => {
|
||||
"5:varint" => -1_i64,
|
||||
"8:varint" => 0_i64,
|
||||
},
|
||||
"40:embedded" => {
|
||||
"1:varint" => 4_i64,
|
||||
"3:string" => "https://www.youtube.com",
|
||||
"4:string" => "",
|
||||
},
|
||||
},
|
||||
"3:varint" => 6_i64,
|
||||
"6:embedded" => {
|
||||
"1:string" => cursor,
|
||||
"4:embedded" => {
|
||||
"4:string" => video_id,
|
||||
"6:varint" => 0_i64,
|
||||
},
|
||||
"5:varint" => 20_i64,
|
||||
},
|
||||
}
|
||||
|
||||
case sort_by
|
||||
when "top"
|
||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64
|
||||
when "new", "newest"
|
||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 1_i64
|
||||
else # top
|
||||
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64
|
||||
end
|
||||
|
||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
return continuation
|
||||
end
|
||||
end
|
@ -86,7 +86,7 @@ class Config
|
||||
property https_only : Bool?
|
||||
property login_only : Bool?
|
||||
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
||||
property hmac_key : String?
|
||||
property hmac_key : String = ""
|
||||
# Domain to be used for links to resources on the site where an absolute URL is required
|
||||
property domain : String?
|
||||
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
|
||||
@ -205,6 +205,16 @@ class Config
|
||||
end
|
||||
{% end %}
|
||||
|
||||
# HMAC_key is mandatory
|
||||
# See: https://github.com/iv-org/invidious/issues/3854
|
||||
if config.hmac_key.empty?
|
||||
puts "Config: 'hmac_key' is required/can't be empty"
|
||||
exit(1)
|
||||
elsif config.hmac_key == "CHANGE_ME!!"
|
||||
puts "Config: The value of 'hmac_key' needs to be changed!!"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
# Build database_url from db.* if it's not set directly
|
||||
if config.database_url.to_s.empty?
|
||||
if db = config.db
|
||||
@ -217,7 +227,7 @@ class Config
|
||||
path: db.dbname,
|
||||
)
|
||||
else
|
||||
puts "Config : Either database_url or db.* is required"
|
||||
puts "Config: Either database_url or db.* is required"
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
@ -52,7 +52,7 @@ module Invidious::Database::Users
|
||||
def mark_watched(user : User, vid : String)
|
||||
request = <<-SQL
|
||||
UPDATE users
|
||||
SET watched = array_append(watched, $1)
|
||||
SET watched = array_append(array_remove(watched, $1), $1)
|
||||
WHERE email = $2
|
||||
SQL
|
||||
|
||||
|
50
src/invidious/frontend/comments_reddit.cr
Normal file
50
src/invidious/frontend/comments_reddit.cr
Normal file
@ -0,0 +1,50 @@
|
||||
module Invidious::Frontend::Comments
|
||||
extend self
|
||||
|
||||
def template_reddit(root, locale)
|
||||
String.build do |html|
|
||||
root.each do |child|
|
||||
if child.data.is_a?(RedditComment)
|
||||
child = child.data.as(RedditComment)
|
||||
body_html = HTML.unescape(child.body_html)
|
||||
|
||||
replies_html = ""
|
||||
if child.replies.is_a?(RedditThing)
|
||||
replies = child.replies.as(RedditThing)
|
||||
replies_html = self.template_reddit(replies.data.as(RedditListing).children, locale)
|
||||
end
|
||||
|
||||
if child.depth > 0
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-24">
|
||||
</div>
|
||||
<div class="pure-u-23-24">
|
||||
END_HTML
|
||||
else
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
|
||||
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
|
||||
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
|
||||
<span title="#{child.created_utc.to_s(translate(locale, "%a %B %-d %T %Y UTC"))}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
|
||||
<a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a>
|
||||
</p>
|
||||
<div>
|
||||
#{body_html}
|
||||
#{replies_html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
160
src/invidious/frontend/comments_youtube.cr
Normal file
160
src/invidious/frontend/comments_youtube.cr
Normal file
@ -0,0 +1,160 @@
|
||||
module Invidious::Frontend::Comments
|
||||
extend self
|
||||
|
||||
def template_youtube(comments, locale, thin_mode, is_replies = false)
|
||||
String.build do |html|
|
||||
root = comments["comments"].as_a
|
||||
root.each do |child|
|
||||
if child["replies"]?
|
||||
replies_count_text = translate_count(locale,
|
||||
"comments_view_x_replies",
|
||||
child["replies"]["replyCount"].as_i64 || 0,
|
||||
NumberFormatting::Separator
|
||||
)
|
||||
|
||||
replies_html = <<-END_HTML
|
||||
<div id="replies" class="pure-g">
|
||||
<div class="pure-u-1-24"></div>
|
||||
<div class="pure-u-23-24">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-replies>#{replies_count_text}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
if !thin_mode
|
||||
author_thumbnail = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).request_target}"
|
||||
else
|
||||
author_thumbnail = ""
|
||||
end
|
||||
|
||||
author_name = HTML.escape(child["author"].as_s)
|
||||
sponsor_icon = ""
|
||||
if child["verified"]?.try &.as_bool && child["authorIsChannelOwner"]?.try &.as_bool
|
||||
author_name += " <i class=\"icon ion ion-md-checkmark-circle\"></i>"
|
||||
elsif child["verified"]?.try &.as_bool
|
||||
author_name += " <i class=\"icon ion ion-md-checkmark\"></i>"
|
||||
end
|
||||
|
||||
if child["isSponsor"]?.try &.as_bool
|
||||
sponsor_icon = String.build do |str|
|
||||
str << %(<img alt="" )
|
||||
str << %(src="/ggpht) << URI.parse(child["sponsorIconUrl"].as_s).request_target << "\" "
|
||||
str << %(title=") << translate(locale, "Channel Sponsor") << "\" "
|
||||
str << %(width="16" height="16" />)
|
||||
end
|
||||
end
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g" style="width:100%">
|
||||
<div class="channel-profile pure-u-4-24 pure-u-md-2-24">
|
||||
<img loading="lazy" style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}" alt="" />
|
||||
</div>
|
||||
<div class="pure-u-20-24 pure-u-md-22-24">
|
||||
<p>
|
||||
<b>
|
||||
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{author_name}</a>
|
||||
</b>
|
||||
#{sponsor_icon}
|
||||
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
|
||||
END_HTML
|
||||
|
||||
if child["attachment"]?
|
||||
attachment = child["attachment"]
|
||||
|
||||
case attachment["type"]
|
||||
when "image"
|
||||
attachment = attachment["imageThumbnails"][1]
|
||||
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-2">
|
||||
<img loading="lazy" style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
when "video"
|
||||
if attachment["error"]?
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g video-iframe-wrapper">
|
||||
<p>#{attachment["error"]}</p>
|
||||
</div>
|
||||
END_HTML
|
||||
else
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g video-iframe-wrapper">
|
||||
<iframe class="video-iframe" src='/embed/#{attachment["videoId"]?}?autoplay=0'></iframe>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
else nil # Ignore
|
||||
end
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
<p>
|
||||
<span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span>
|
||||
|
|
||||
END_HTML
|
||||
|
||||
if comments["videoId"]?
|
||||
html << <<-END_HTML
|
||||
<a href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
elsif comments["authorId"]?
|
||||
html << <<-END_HTML
|
||||
<a 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_HTML
|
||||
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
|
||||
END_HTML
|
||||
|
||||
if child["creatorHeart"]?
|
||||
if !thin_mode
|
||||
creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).request_target}"
|
||||
else
|
||||
creator_thumbnail = ""
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
|
||||
<span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
|
||||
<span class="creator-heart">
|
||||
<img loading="lazy" class="creator-heart-background-hearted" src="#{creator_thumbnail}" alt="" />
|
||||
<span class="creator-heart-small-hearted">
|
||||
<span class="icon ion-ios-heart creator-heart-small-container"></span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
</p>
|
||||
#{replies_html}
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
if comments["continuation"]?
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -17,21 +17,18 @@ module Invidious::Hashtag
|
||||
"80226972:embedded" => {
|
||||
"2:string" => "FEhashtag",
|
||||
"3:base64" => {
|
||||
"1:varint" => cursor.to_i64,
|
||||
},
|
||||
"7:base64" => {
|
||||
"325477796:embedded" => {
|
||||
"1:embedded" => {
|
||||
"2:0:embedded" => {
|
||||
"2:string" => '#' + hashtag,
|
||||
"4:varint" => 0_i64,
|
||||
"11:string" => "",
|
||||
},
|
||||
"4:string" => "browse-feedFEhashtag",
|
||||
},
|
||||
"2:string" => hashtag,
|
||||
"1:varint" => 60_i64, # result count
|
||||
"15:base64" => {
|
||||
"1:varint" => cursor.to_i64,
|
||||
"2:varint" => 0_i64,
|
||||
},
|
||||
"93:2:embedded" => {
|
||||
"1:string" => hashtag,
|
||||
"2:varint" => 0_i64,
|
||||
"3:varint" => 1_i64,
|
||||
},
|
||||
},
|
||||
"35:string" => "browse-feedFEhashtag",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -22,31 +22,6 @@ struct Annotation
|
||||
property annotations : String
|
||||
end
|
||||
|
||||
def login_req(f_req)
|
||||
data = {
|
||||
# Unfortunately there's not much information available on `bgRequest`; part of Google's BotGuard
|
||||
# Generally this is much longer (>1250 characters), see also
|
||||
# https://github.com/ytdl-org/youtube-dl/commit/baf67a604d912722b0fe03a40e9dc5349a2208cb .
|
||||
# For now this can be empty.
|
||||
"bgRequest" => %|["identifier",""]|,
|
||||
"pstMsg" => "1",
|
||||
"checkConnection" => "youtube",
|
||||
"checkedDomains" => "youtube",
|
||||
"hl" => "en",
|
||||
"deviceinfo" => %|[null,null,null,[],null,"US",null,null,[],"GlifWebSignIn",null,[null,null,[]]]|,
|
||||
"f.req" => f_req,
|
||||
"flowName" => "GlifWebSignIn",
|
||||
"flowEntry" => "ServiceLogin",
|
||||
# "cookiesDisabled" => "false",
|
||||
# "gmscoreversion" => "undefined",
|
||||
# "continue" => "https://accounts.google.com/ManageAccount",
|
||||
# "azt" => "",
|
||||
# "bgHash" => "",
|
||||
}
|
||||
|
||||
return HTTP::Params.encode(data)
|
||||
end
|
||||
|
||||
def html_to_content(description_html : String)
|
||||
description = description_html.gsub(/(<br>)|(<br\/>)/, {
|
||||
"<br>": "\n",
|
||||
|
@ -84,6 +84,7 @@ struct SearchVideo
|
||||
json.field "descriptionHtml", self.description_html
|
||||
|
||||
json.field "viewCount", self.views
|
||||
json.field "viewCountText", translate_count(locale, "generic_views_count", self.views, NumberFormatting::Short)
|
||||
json.field "published", self.published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
||||
json.field "lengthSeconds", self.length_seconds
|
||||
|
@ -111,24 +111,27 @@ def decode_date(string : String)
|
||||
else nil # Continue
|
||||
end
|
||||
|
||||
# String matches format "20 hours ago", "4 months ago"...
|
||||
date = string.split(" ")[-3, 3]
|
||||
delta = date[0].to_i
|
||||
# String matches format "20 hours ago", "4 months ago", "20s ago", "15min ago"...
|
||||
match = string.match(/(?<count>\d+) ?(?<span>[smhdwy]\w*) ago/)
|
||||
|
||||
case date[1]
|
||||
when .includes? "second"
|
||||
raise "Could not parse #{string}" if match.nil?
|
||||
|
||||
delta = match["count"].to_i
|
||||
|
||||
case match["span"]
|
||||
when .starts_with? "s" # second(s)
|
||||
delta = delta.seconds
|
||||
when .includes? "minute"
|
||||
when .starts_with? "mi" # minute(s)
|
||||
delta = delta.minutes
|
||||
when .includes? "hour"
|
||||
when .starts_with? "h" # hour(s)
|
||||
delta = delta.hours
|
||||
when .includes? "day"
|
||||
when .starts_with? "d" # day(s)
|
||||
delta = delta.days
|
||||
when .includes? "week"
|
||||
when .starts_with? "w" # week(s)
|
||||
delta = delta.weeks
|
||||
when .includes? "month"
|
||||
when .starts_with? "mo" # month(s)
|
||||
delta = delta.months
|
||||
when .includes? "year"
|
||||
when .starts_with? "y" # year(s)
|
||||
delta = delta.years
|
||||
else
|
||||
raise "Could not parse #{string}"
|
||||
@ -389,3 +392,56 @@ def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
# Get the html link from a NavigationEndpoint or an innertubeCommand
|
||||
def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
|
||||
if url = endpoint.dig?("urlEndpoint", "url").try &.as_s
|
||||
url = URI.parse(url)
|
||||
displayed_url = text
|
||||
|
||||
if url.host == "youtu.be"
|
||||
url = "/watch?v=#{url.request_target.lstrip('/')}"
|
||||
elsif url.host.nil? || url.host.not_nil!.ends_with?("youtube.com")
|
||||
if url.path == "/redirect"
|
||||
# Sometimes, links can be corrupted (why?) so make sure to fallback
|
||||
# nicely. See https://github.com/iv-org/invidious/issues/2682
|
||||
url = url.query_params["q"]? || ""
|
||||
displayed_url = url
|
||||
else
|
||||
url = url.request_target
|
||||
displayed_url = "youtube.com#{url}"
|
||||
end
|
||||
end
|
||||
|
||||
text = %(<a href="#{url}">#{reduce_uri(displayed_url)}</a>)
|
||||
elsif watch_endpoint = endpoint.dig?("watchEndpoint")
|
||||
start_time = watch_endpoint["startTimeSeconds"]?.try &.as_i
|
||||
link_video_id = watch_endpoint["videoId"].as_s
|
||||
|
||||
url = "/watch?v=#{link_video_id}"
|
||||
url += "&t=#{start_time}" if !start_time.nil?
|
||||
|
||||
# If the current video ID (passed through from the caller function)
|
||||
# is the same as the video ID in the link, add HTML attributes for
|
||||
# the JS handler function that bypasses page reload.
|
||||
#
|
||||
# See: https://github.com/iv-org/invidious/issues/3063
|
||||
if link_video_id == video_id
|
||||
start_time ||= 0
|
||||
text = %(<a href="#{url}" data-onclick="jump_to_time" data-jump-time="#{start_time}">#{reduce_uri(text)}</a>)
|
||||
else
|
||||
text = %(<a href="#{url}">#{text}</a>)
|
||||
end
|
||||
elsif url = endpoint.dig?("commandMetadata", "webCommandMetadata", "url").try &.as_s
|
||||
if text.starts_with?(/\s?[@#]/)
|
||||
# Handle "pings" in comments and hasthags differently
|
||||
# See:
|
||||
# - https://github.com/iv-org/invidious/issues/3038
|
||||
# - https://github.com/iv-org/invidious/issues/3062
|
||||
text = %(<a href="#{url}">#{text}</a>)
|
||||
else
|
||||
text = %(<a href="#{url}">#{reduce_uri(text)}</a>)
|
||||
end
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
@ -2,7 +2,7 @@ module Invidious::Jobs
|
||||
JOBS = [] of BaseJob
|
||||
|
||||
# Automatically generate a structure that wraps the various
|
||||
# jobs' configs, so that the follwing YAML config can be used:
|
||||
# jobs' configs, so that the following YAML config can be used:
|
||||
#
|
||||
# jobs:
|
||||
# job_name:
|
||||
|
@ -97,7 +97,7 @@ def template_mix(mix)
|
||||
<li class="pure-menu-item">
|
||||
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
|
||||
<div class="thumbnail">
|
||||
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
|
||||
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
||||
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||
</div>
|
||||
<p style="width:100%">#{video["title"]}</p>
|
||||
|
@ -507,7 +507,7 @@ def template_playlist(playlist)
|
||||
<li class="pure-menu-item" id="#{video["videoId"]}">
|
||||
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}">
|
||||
<div class="thumbnail">
|
||||
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg">
|
||||
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
||||
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||
</div>
|
||||
<p style="width:100%">#{video["title"]}</p>
|
||||
|
@ -42,11 +42,6 @@ module Invidious::Routes::Account
|
||||
sid = sid.as(String)
|
||||
token = env.params.body["csrf_token"]?
|
||||
|
||||
# We don't store passwords for Google accounts
|
||||
if !user.password
|
||||
return error_template(400, "Cannot change password for Google accounts")
|
||||
end
|
||||
|
||||
begin
|
||||
validate_request(token, sid, env.request, HMAC_KEY, locale)
|
||||
rescue ex
|
||||
@ -54,7 +49,7 @@ module Invidious::Routes::Account
|
||||
end
|
||||
|
||||
password = env.params.body["password"]?
|
||||
if !password
|
||||
if password.nil? || password.empty?
|
||||
return error_template(401, "Password is a required field")
|
||||
end
|
||||
|
||||
|
@ -178,10 +178,6 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
Invidious::Database::Users.subscribe_channel(user, ucid)
|
||||
end
|
||||
|
||||
# For Google accounts, access tokens don't have enough information to
|
||||
# make a request on the user's behalf, which is why we don't sync with
|
||||
# YouTube.
|
||||
|
||||
env.response.status_code = 204
|
||||
end
|
||||
|
||||
|
@ -55,4 +55,32 @@ module Invidious::Routes::API::V1::Search
|
||||
return error_json(500, ex)
|
||||
end
|
||||
end
|
||||
|
||||
def self.hashtag(env)
|
||||
hashtag = env.params.url["hashtag"]
|
||||
|
||||
page = env.params.query["page"]?.try &.to_i? || 1
|
||||
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
region = env.params.query["region"]?
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
begin
|
||||
results = Invidious::Hashtag.fetch(hashtag, page, region)
|
||||
rescue ex
|
||||
return error_json(400, ex)
|
||||
end
|
||||
|
||||
JSON.build do |json|
|
||||
json.object do
|
||||
json.field "results" do
|
||||
json.array do
|
||||
results.each do |item|
|
||||
item.to_json(locale, json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -333,7 +333,7 @@ module Invidious::Routes::API::V1::Videos
|
||||
sort_by ||= "top"
|
||||
|
||||
begin
|
||||
comments = fetch_youtube_comments(id, continuation, format, locale, thin_mode, region, sort_by: sort_by)
|
||||
comments = Comments.fetch_youtube(id, continuation, format, locale, thin_mode, region, sort_by: sort_by)
|
||||
rescue ex : NotFoundException
|
||||
return error_json(404, ex)
|
||||
rescue ex
|
||||
@ -345,7 +345,7 @@ module Invidious::Routes::API::V1::Videos
|
||||
sort_by ||= "confidence"
|
||||
|
||||
begin
|
||||
comments, reddit_thread = fetch_reddit_comments(id, sort_by: sort_by)
|
||||
comments, reddit_thread = Comments.fetch_reddit(id, sort_by: sort_by)
|
||||
rescue ex
|
||||
comments = nil
|
||||
reddit_thread = nil
|
||||
@ -361,9 +361,9 @@ module Invidious::Routes::API::V1::Videos
|
||||
|
||||
return reddit_thread.to_json
|
||||
else
|
||||
content_html = template_reddit_comments(comments, locale)
|
||||
content_html = fill_links(content_html, "https", "www.reddit.com")
|
||||
content_html = replace_links(content_html)
|
||||
content_html = Frontend::Comments.template_reddit(comments, locale)
|
||||
content_html = Comments.fill_links(content_html, "https", "www.reddit.com")
|
||||
content_html = Comments.replace_links(content_html)
|
||||
response = {
|
||||
"title" => reddit_thread.title,
|
||||
"permalink" => reddit_thread.permalink,
|
||||
|
@ -61,6 +61,39 @@ module Invidious::Routes::BeforeAll
|
||||
env.response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
|
||||
end
|
||||
|
||||
if env.request.cookies.has_key? "SID"
|
||||
sid = env.request.cookies["SID"].value
|
||||
|
||||
if sid.starts_with? "v1:"
|
||||
raise "Cannot use token as SID"
|
||||
end
|
||||
|
||||
if email = Database::SessionIDs.select_email(sid)
|
||||
user = Database::Users.select!(email: email)
|
||||
csrf_token = generate_response(sid, {
|
||||
":authorize_token",
|
||||
":playlist_ajax",
|
||||
":signout",
|
||||
":subscription_ajax",
|
||||
":token_ajax",
|
||||
":watch_ajax",
|
||||
}, HMAC_KEY, 1.week)
|
||||
|
||||
preferences = user.preferences
|
||||
env.set "preferences", preferences
|
||||
|
||||
env.set "sid", sid
|
||||
env.set "csrf_token", csrf_token
|
||||
env.set "user", user
|
||||
end
|
||||
end
|
||||
|
||||
unregistered_path_whitelist = {"/", "/login", "/licenses", "/privacy"}
|
||||
if CONFIG.login_only && !env.get?("user") && !unregistered_path_whitelist.includes?(env.request.path)
|
||||
env.response.headers["Location"] = "/login"
|
||||
haltf env, status_code: 302
|
||||
end
|
||||
|
||||
return if {
|
||||
"/sb/",
|
||||
"/vi/",
|
||||
@ -73,59 +106,6 @@ module Invidious::Routes::BeforeAll
|
||||
"/download",
|
||||
}.any? { |r| env.request.resource.starts_with? r }
|
||||
|
||||
if env.request.cookies.has_key? "SID"
|
||||
sid = env.request.cookies["SID"].value
|
||||
|
||||
if sid.starts_with? "v1:"
|
||||
raise "Cannot use token as SID"
|
||||
end
|
||||
|
||||
# Invidious users only have SID
|
||||
if !env.request.cookies.has_key? "SSID"
|
||||
if email = Invidious::Database::SessionIDs.select_email(sid)
|
||||
user = Invidious::Database::Users.select!(email: email)
|
||||
csrf_token = generate_response(sid, {
|
||||
":authorize_token",
|
||||
":playlist_ajax",
|
||||
":signout",
|
||||
":subscription_ajax",
|
||||
":token_ajax",
|
||||
":watch_ajax",
|
||||
}, HMAC_KEY, 1.week)
|
||||
|
||||
preferences = user.preferences
|
||||
env.set "preferences", preferences
|
||||
|
||||
env.set "sid", sid
|
||||
env.set "csrf_token", csrf_token
|
||||
env.set "user", user
|
||||
end
|
||||
else
|
||||
headers = HTTP::Headers.new
|
||||
headers["Cookie"] = env.request.headers["Cookie"]
|
||||
|
||||
begin
|
||||
user, sid = get_user(sid, headers, false)
|
||||
csrf_token = generate_response(sid, {
|
||||
":authorize_token",
|
||||
":playlist_ajax",
|
||||
":signout",
|
||||
":subscription_ajax",
|
||||
":token_ajax",
|
||||
":watch_ajax",
|
||||
}, HMAC_KEY, 1.week)
|
||||
|
||||
preferences = user.preferences
|
||||
env.set "preferences", preferences
|
||||
|
||||
env.set "sid", sid
|
||||
env.set "csrf_token", csrf_token
|
||||
env.set "user", user
|
||||
rescue ex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
dark_mode = convert_theme(env.params.query["dark_mode"]?) || preferences.dark_mode.to_s
|
||||
thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s
|
||||
thin_mode = thin_mode == "true"
|
||||
@ -148,11 +128,5 @@ module Invidious::Routes::BeforeAll
|
||||
end
|
||||
|
||||
env.set "current_page", URI.encode_www_form(current_page)
|
||||
|
||||
unregistered_path_whitelist = {"/", "/login", "/licenses", "/privacy"}
|
||||
if CONFIG.login_only && !env.get?("user") && !unregistered_path_whitelist.includes?(env.request.path)
|
||||
env.response.headers["Location"] = "/login"
|
||||
haltf env, status_code: 302
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -278,6 +278,7 @@ module Invidious::Routes::Channels
|
||||
return error_template(500, ex)
|
||||
end
|
||||
|
||||
env.set "search", "channel:#{ucid} "
|
||||
return {locale, user, subscriptions, continuation, ucid, channel}
|
||||
end
|
||||
end
|
||||
|
@ -83,10 +83,6 @@ module Invidious::Routes::Feeds
|
||||
headers = HTTP::Headers.new
|
||||
headers["Cookie"] = env.request.headers["Cookie"]
|
||||
|
||||
if !user.password
|
||||
user, sid = get_user(sid, headers)
|
||||
end
|
||||
|
||||
max_results = env.params.query["max_results"]?.try &.to_i?.try &.clamp(0, MAX_ITEMS_PER_PAGE)
|
||||
max_results ||= user.preferences.max_results
|
||||
max_results ||= CONFIG.default_user_preferences.max_results
|
||||
|
@ -24,9 +24,6 @@ module Invidious::Routes::Login
|
||||
captcha_type = env.params.query["captcha"]?
|
||||
captcha_type ||= "image"
|
||||
|
||||
tfa = env.params.query["tfa"]?
|
||||
prompt = nil
|
||||
|
||||
templated "user/login"
|
||||
end
|
||||
|
||||
@ -47,283 +44,18 @@ module Invidious::Routes::Login
|
||||
account_type ||= "invidious"
|
||||
|
||||
case account_type
|
||||
when "google"
|
||||
tfa_code = env.params.body["tfa"]?.try &.lchop("G-")
|
||||
traceback = IO::Memory.new
|
||||
|
||||
# See https://github.com/ytdl-org/youtube-dl/blob/2019.04.07/youtube_dl/extractor/youtube.py#L82
|
||||
begin
|
||||
client = nil # Declare variable
|
||||
{% unless flag?(:disable_quic) %}
|
||||
client = CONFIG.use_quic ? QUIC::Client.new(LOGIN_URL) : HTTP::Client.new(LOGIN_URL)
|
||||
{% else %}
|
||||
client = HTTP::Client.new(LOGIN_URL)
|
||||
{% end %}
|
||||
|
||||
headers = HTTP::Headers.new
|
||||
|
||||
login_page = client.get("/ServiceLogin")
|
||||
headers = login_page.cookies.add_request_headers(headers)
|
||||
|
||||
lookup_req = {
|
||||
email, nil, [] of String, nil, "US", nil, nil, 2, false, true,
|
||||
{nil, nil,
|
||||
{2, 1, nil, 1,
|
||||
"https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn",
|
||||
nil, [] of String, 4},
|
||||
1,
|
||||
{nil, nil, [] of String},
|
||||
nil, nil, nil, true,
|
||||
},
|
||||
email,
|
||||
}.to_json
|
||||
|
||||
traceback << "Getting lookup..."
|
||||
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded;charset=utf-8"
|
||||
headers["Google-Accounts-XSRF"] = "1"
|
||||
|
||||
response = client.post("/_/signin/sl/lookup", headers, login_req(lookup_req))
|
||||
lookup_results = JSON.parse(response.body[5..-1])
|
||||
|
||||
traceback << "done, returned #{response.status_code}.<br/>"
|
||||
|
||||
user_hash = lookup_results[0][2]
|
||||
|
||||
if token = env.params.body["token"]?
|
||||
answer = env.params.body["answer"]?
|
||||
captcha = {token, answer}
|
||||
else
|
||||
captcha = nil
|
||||
end
|
||||
|
||||
challenge_req = {
|
||||
user_hash, nil, 1, nil,
|
||||
{1, nil, nil, nil,
|
||||
{password, captcha, true},
|
||||
},
|
||||
{nil, nil,
|
||||
{2, 1, nil, 1,
|
||||
"https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn",
|
||||
nil, [] of String, 4},
|
||||
1,
|
||||
{nil, nil, [] of String},
|
||||
nil, nil, nil, true,
|
||||
},
|
||||
}.to_json
|
||||
|
||||
traceback << "Getting challenge..."
|
||||
|
||||
response = client.post("/_/signin/sl/challenge", headers, login_req(challenge_req))
|
||||
headers = response.cookies.add_request_headers(headers)
|
||||
challenge_results = JSON.parse(response.body[5..-1])
|
||||
|
||||
traceback << "done, returned #{response.status_code}.<br/>"
|
||||
|
||||
headers["Cookie"] = URI.decode_www_form(headers["Cookie"])
|
||||
|
||||
if challenge_results[0][3]?.try &.== 7
|
||||
return error_template(423, "Account has temporarily been disabled")
|
||||
end
|
||||
|
||||
if token = challenge_results[0][-1]?.try &.[-1]?.try &.as_h?.try &.["5001"]?.try &.[-1].as_a?.try &.[-1].as_s
|
||||
account_type = "google"
|
||||
captcha_type = "image"
|
||||
prompt = nil
|
||||
tfa = tfa_code
|
||||
captcha = {tokens: [token], question: ""}
|
||||
|
||||
return templated "user/login"
|
||||
end
|
||||
|
||||
if challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED"
|
||||
return error_template(401, "Incorrect password")
|
||||
end
|
||||
|
||||
prompt_type = challenge_results[0][-1]?.try &.[0].as_a?.try &.[0][2]?
|
||||
if {"TWO_STEP_VERIFICATION", "LOGIN_CHALLENGE"}.includes? prompt_type
|
||||
traceback << "Handling prompt #{prompt_type}.<br/>"
|
||||
case prompt_type
|
||||
when "TWO_STEP_VERIFICATION"
|
||||
prompt_type = 2
|
||||
else # "LOGIN_CHALLENGE"
|
||||
prompt_type = 4
|
||||
end
|
||||
|
||||
# Prefer Authenticator app and SMS over unsupported protocols
|
||||
if !{6, 9, 12, 15}.includes?(challenge_results[0][-1][0][0][8].as_i) && prompt_type == 2
|
||||
tfa = challenge_results[0][-1][0].as_a.select { |auth_type| {6, 9, 12, 15}.includes? auth_type[8] }[0]
|
||||
|
||||
traceback << "Selecting challenge #{tfa[8]}..."
|
||||
select_challenge = {prompt_type, nil, nil, nil, {tfa[8]}}.to_json
|
||||
|
||||
tl = challenge_results[1][2]
|
||||
|
||||
tfa = client.post("/_/signin/selectchallenge?TL=#{tl}", headers, login_req(select_challenge)).body
|
||||
tfa = tfa[5..-1]
|
||||
tfa = JSON.parse(tfa)[0][-1]
|
||||
|
||||
traceback << "done.<br/>"
|
||||
else
|
||||
traceback << "Using challenge #{challenge_results[0][-1][0][0][8]}.<br/>"
|
||||
tfa = challenge_results[0][-1][0][0]
|
||||
end
|
||||
|
||||
if tfa[5] == "QUOTA_EXCEEDED"
|
||||
return error_template(423, "Quota exceeded, try again in a few hours")
|
||||
end
|
||||
|
||||
if !tfa_code
|
||||
account_type = "google"
|
||||
captcha_type = "image"
|
||||
|
||||
case tfa[8]
|
||||
when 6, 9
|
||||
prompt = "Google verification code"
|
||||
when 12
|
||||
prompt = "Login verification, recovery email: #{tfa[-1][tfa[-1].as_h.keys[0]][0]}"
|
||||
when 15
|
||||
prompt = "Login verification, security question: #{tfa[-1][tfa[-1].as_h.keys[0]][0]}"
|
||||
else
|
||||
prompt = "Google verification code"
|
||||
end
|
||||
|
||||
tfa = nil
|
||||
captcha = nil
|
||||
return templated "user/login"
|
||||
end
|
||||
|
||||
tl = challenge_results[1][2]
|
||||
|
||||
request_type = tfa[8]
|
||||
case request_type
|
||||
when 6 # Authenticator app
|
||||
tfa_req = {
|
||||
user_hash, nil, 2, nil,
|
||||
{6, nil, nil, nil, nil,
|
||||
{tfa_code, false},
|
||||
},
|
||||
}.to_json
|
||||
when 9 # Voice or text message
|
||||
tfa_req = {
|
||||
user_hash, nil, 2, nil,
|
||||
{9, nil, nil, nil, nil, nil, nil, nil,
|
||||
{nil, tfa_code, false, 2},
|
||||
},
|
||||
}.to_json
|
||||
when 12 # Recovery email
|
||||
tfa_req = {
|
||||
user_hash, nil, 4, nil,
|
||||
{12, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
{tfa_code},
|
||||
},
|
||||
}.to_json
|
||||
when 15 # Security question
|
||||
tfa_req = {
|
||||
user_hash, nil, 5, nil,
|
||||
{15, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil,
|
||||
{tfa_code},
|
||||
},
|
||||
}.to_json
|
||||
else
|
||||
return error_template(500, "Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.")
|
||||
end
|
||||
|
||||
traceback << "Submitting challenge..."
|
||||
|
||||
response = client.post("/_/signin/challenge?hl=en&TL=#{tl}", headers, login_req(tfa_req))
|
||||
headers = response.cookies.add_request_headers(headers)
|
||||
challenge_results = JSON.parse(response.body[5..-1])
|
||||
|
||||
if (challenge_results[0][-1]?.try &.[5] == "INCORRECT_ANSWER_ENTERED") ||
|
||||
(challenge_results[0][-1]?.try &.[5] == "INVALID_INPUT")
|
||||
return error_template(401, "Invalid TFA code")
|
||||
end
|
||||
|
||||
traceback << "done.<br/>"
|
||||
end
|
||||
|
||||
traceback << "Logging in..."
|
||||
|
||||
location = URI.parse(challenge_results[0][-1][2].to_s)
|
||||
cookies = HTTP::Cookies.from_client_headers(headers)
|
||||
|
||||
headers.delete("Content-Type")
|
||||
headers.delete("Google-Accounts-XSRF")
|
||||
|
||||
loop do
|
||||
if !location || location.path == "/ManageAccount"
|
||||
break
|
||||
end
|
||||
|
||||
# Occasionally there will be a second page after login confirming
|
||||
# the user's phone number ("/b/0/SmsAuthInterstitial"), which we currently don't handle.
|
||||
|
||||
if location.path.starts_with? "/b/0/SmsAuthInterstitial"
|
||||
traceback << "Unhandled dialog /b/0/SmsAuthInterstitial."
|
||||
end
|
||||
|
||||
login = client.get(location.request_target, headers)
|
||||
|
||||
headers = login.cookies.add_request_headers(headers)
|
||||
location = login.headers["Location"]?.try { |u| URI.parse(u) }
|
||||
end
|
||||
|
||||
cookies = HTTP::Cookies.from_client_headers(headers)
|
||||
sid = cookies["SID"]?.try &.value
|
||||
if !sid
|
||||
raise "Couldn't get SID."
|
||||
end
|
||||
|
||||
user, sid = get_user(sid, headers)
|
||||
|
||||
# We are now logged in
|
||||
traceback << "done.<br/>"
|
||||
|
||||
host = URI.parse(env.request.headers["Host"]).host
|
||||
|
||||
cookies.each do |cookie|
|
||||
cookie.secure = Invidious::User::Cookies::SECURE
|
||||
|
||||
if cookie.extension
|
||||
cookie.extension = cookie.extension.not_nil!.gsub(".youtube.com", host)
|
||||
cookie.extension = cookie.extension.not_nil!.gsub("Secure; ", "")
|
||||
end
|
||||
env.response.cookies << cookie
|
||||
end
|
||||
|
||||
if env.request.cookies["PREFS"]?
|
||||
user.preferences = env.get("preferences").as(Preferences)
|
||||
Invidious::Database::Users.update_preferences(user)
|
||||
|
||||
cookie = env.request.cookies["PREFS"]
|
||||
cookie.expires = Time.utc(1990, 1, 1)
|
||||
env.response.cookies << cookie
|
||||
end
|
||||
|
||||
env.redirect referer
|
||||
rescue ex
|
||||
traceback.rewind
|
||||
# error_message = translate(locale, "Login failed. This may be because two-factor authentication is not turned on for your account.")
|
||||
error_message = %(#{ex.message}<br/>Traceback:<br/><div style="padding-left:2em" id="traceback">#{traceback.gets_to_end}</div>)
|
||||
return error_template(500, error_message)
|
||||
end
|
||||
when "invidious"
|
||||
if !email
|
||||
if email.nil? || email.empty?
|
||||
return error_template(401, "User ID is a required field")
|
||||
end
|
||||
|
||||
if !password
|
||||
if password.nil? || password.empty?
|
||||
return error_template(401, "Password is a required field")
|
||||
end
|
||||
|
||||
user = Invidious::Database::Users.select(email: email)
|
||||
|
||||
if user
|
||||
if !user.password
|
||||
return error_template(400, "Please sign in using 'Log in with Google'")
|
||||
end
|
||||
|
||||
if Crypto::Bcrypt::Password.new(user.password.not_nil!).verify(password.byte_slice(0, 55))
|
||||
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||
Invidious::Database::SessionIDs.insert(sid, email)
|
||||
@ -367,8 +99,6 @@ module Invidious::Routes::Login
|
||||
captcha_type ||= "image"
|
||||
|
||||
account_type = "invidious"
|
||||
tfa = false
|
||||
prompt = ""
|
||||
|
||||
if captcha_type == "image"
|
||||
captcha = Invidious::User::Captcha.generate_image(HMAC_KEY)
|
||||
@ -481,11 +211,4 @@ module Invidious::Routes::Login
|
||||
|
||||
env.redirect referer
|
||||
end
|
||||
|
||||
def self.captcha(env)
|
||||
headers = HTTP::Headers{":authority" => "accounts.google.com"}
|
||||
response = YT_POOL.client &.get(env.request.resource, headers)
|
||||
env.response.headers["Content-Type"] = response.headers["Content-Type"]
|
||||
response.body
|
||||
end
|
||||
end
|
||||
|
@ -24,50 +24,6 @@ module Invidious::Routes::Notifications
|
||||
|
||||
user = user.as(User)
|
||||
|
||||
if !user.password
|
||||
channel_req = {} of String => String
|
||||
|
||||
channel_req["receive_all_updates"] = env.params.query["receive_all_updates"]? || "true"
|
||||
channel_req["receive_no_updates"] = env.params.query["receive_no_updates"]? || ""
|
||||
channel_req["receive_post_updates"] = env.params.query["receive_post_updates"]? || "true"
|
||||
|
||||
channel_req.reject! { |k, v| v != "true" && v != "false" }
|
||||
|
||||
headers = HTTP::Headers.new
|
||||
headers["Cookie"] = env.request.headers["Cookie"]
|
||||
|
||||
html = YT_POOL.client &.get("/subscription_manager?disable_polymer=1", headers)
|
||||
|
||||
cookies = HTTP::Cookies.from_client_headers(headers)
|
||||
html.cookies.each do |cookie|
|
||||
if {"VISITOR_INFO1_LIVE", "YSC", "SIDCC"}.includes? cookie.name
|
||||
if cookies[cookie.name]?
|
||||
cookies[cookie.name] = cookie
|
||||
else
|
||||
cookies << cookie
|
||||
end
|
||||
end
|
||||
end
|
||||
headers = cookies.add_request_headers(headers)
|
||||
|
||||
if match = html.body.match(/'XSRF_TOKEN': "(?<session_token>[^"]+)"/)
|
||||
session_token = match["session_token"]
|
||||
else
|
||||
return env.redirect referer
|
||||
end
|
||||
|
||||
headers["content-type"] = "application/x-www-form-urlencoded"
|
||||
channel_req["session_token"] = session_token
|
||||
|
||||
subs = XML.parse_html(html.body)
|
||||
subs.xpath_nodes(%q(//a[@class="subscription-title yt-uix-sessionlink"]/@href)).each do |channel|
|
||||
channel_id = channel.content.lstrip("/channel/").not_nil!
|
||||
channel_req["channel_id"] = channel_id
|
||||
|
||||
YT_POOL.client &.post("/subscription_ajax?action_update_subscription_preferences=1", headers, form: channel_req)
|
||||
end
|
||||
end
|
||||
|
||||
if redirect
|
||||
env.redirect referer
|
||||
else
|
||||
|
@ -320,10 +320,6 @@ module Invidious::Routes::Playlists
|
||||
end
|
||||
end
|
||||
|
||||
if !user.password
|
||||
# TODO: Playlist stub, sync with YouTube for Google accounts
|
||||
# playlist_ajax(playlist_id, action, env.request.headers)
|
||||
end
|
||||
email = user.email
|
||||
|
||||
case action
|
||||
@ -410,8 +406,8 @@ module Invidious::Routes::Playlists
|
||||
return error_template(500, ex)
|
||||
end
|
||||
|
||||
page_count = (playlist.video_count / 100).to_i
|
||||
page_count += 1 if (playlist.video_count % 100) > 0
|
||||
page_count = (playlist.video_count / 200).to_i
|
||||
page_count += 1 if (playlist.video_count % 200) > 0
|
||||
|
||||
if page > page_count
|
||||
return env.redirect "/playlist?list=#{plid}&page=#{page_count}"
|
||||
@ -422,7 +418,7 @@ module Invidious::Routes::Playlists
|
||||
end
|
||||
|
||||
begin
|
||||
videos = get_playlist_videos(playlist, offset: (page - 1) * 100)
|
||||
videos = get_playlist_videos(playlist, offset: (page - 1) * 200)
|
||||
rescue ex
|
||||
return error_template(500, "Error encountered while retrieving playlist videos.<br>#{ex.message}")
|
||||
end
|
||||
|
@ -310,6 +310,15 @@ module Invidious::Routes::PreferencesRoute
|
||||
response: error_template(415, "Invalid subscription file uploaded")
|
||||
)
|
||||
end
|
||||
when "import_youtube_pl"
|
||||
filename = part.filename || ""
|
||||
success = Invidious::User::Import.from_youtube_pl(user, body, filename, type)
|
||||
|
||||
if !success
|
||||
haltf(env, status_code: 415,
|
||||
response: error_template(415, "Invalid playlist file uploaded")
|
||||
)
|
||||
end
|
||||
when "import_freetube"
|
||||
Invidious::User::Import.from_freetube(user, body)
|
||||
when "import_newpipe_subscriptions"
|
||||
|
@ -65,7 +65,11 @@ module Invidious::Routes::Search
|
||||
|
||||
redirect_url = Invidious::Frontend::Misc.redirect_url(env)
|
||||
|
||||
env.set "search", query.text
|
||||
if query.type == Invidious::Search::Query::Type::Channel
|
||||
env.set "search", "channel:#{query.channel} #{query.text}"
|
||||
else
|
||||
env.set "search", query.text
|
||||
end
|
||||
templated "search"
|
||||
end
|
||||
end
|
||||
|
@ -43,11 +43,6 @@ module Invidious::Routes::Subscriptions
|
||||
channel_id = env.params.query["c"]?
|
||||
channel_id ||= ""
|
||||
|
||||
if !user.password
|
||||
# Sync subscriptions with YouTube
|
||||
subscribe_ajax(channel_id, action, env.request.headers)
|
||||
end
|
||||
|
||||
case action
|
||||
when "action_create_subscription_to_channel"
|
||||
if !user.subscriptions.includes? channel_id
|
||||
@ -82,14 +77,6 @@ module Invidious::Routes::Subscriptions
|
||||
user = user.as(User)
|
||||
sid = sid.as(String)
|
||||
|
||||
if !user.password
|
||||
# Refresh account
|
||||
headers = HTTP::Headers.new
|
||||
headers["Cookie"] = env.request.headers["Cookie"]
|
||||
|
||||
user, sid = get_user(sid, headers)
|
||||
end
|
||||
|
||||
action_takeout = env.params.query["action_takeout"]?.try &.to_i?
|
||||
action_takeout ||= 0
|
||||
action_takeout = action_takeout == 1
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user