mirror of
https://gitea.invidious.io/iv-org/invidious.git
synced 2024-08-15 00:53:41 +00:00
Merge branch 'master' into feature/sorted-channels
This commit is contained in:
commit
389e6a7659
84 changed files with 1343 additions and 1015 deletions
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -38,16 +38,16 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
stable: [true]
|
stable: [true]
|
||||||
crystal:
|
crystal:
|
||||||
- 1.6.2
|
|
||||||
- 1.7.3
|
- 1.7.3
|
||||||
- 1.8.2
|
- 1.8.2
|
||||||
- 1.9.2
|
- 1.9.2
|
||||||
|
- 1.10.1
|
||||||
include:
|
include:
|
||||||
- crystal: nightly
|
- crystal: nightly
|
||||||
stable: false
|
stable: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Build Docker
|
- name: Build Docker
|
||||||
run: docker-compose build --build-arg release=0
|
run: docker-compose build --build-arg release=0
|
||||||
|
@ -103,18 +103,18 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build Docker ARM64 image
|
- name: Build Docker ARM64 image
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
|
|
47
.github/workflows/container-release.yml
vendored
47
.github/workflows/container-release.yml
vendored
|
@ -11,7 +11,6 @@ on:
|
||||||
- invidious.service
|
- invidious.service
|
||||||
- .git*
|
- .git*
|
||||||
- .editorconfig
|
- .editorconfig
|
||||||
|
|
||||||
- screenshots/*
|
- screenshots/*
|
||||||
- .github/ISSUE_TEMPLATE/*
|
- .github/ISSUE_TEMPLATE/*
|
||||||
- kubernetes/**
|
- kubernetes/**
|
||||||
|
@ -22,7 +21,7 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.8.0
|
uses: crystal-lang/install-crystal@v1.8.0
|
||||||
|
@ -38,42 +37,64 @@ jobs:
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
uses: docker/login-action@v2
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: quay.io
|
registry: quay.io
|
||||||
username: ${{ secrets.QUAY_USERNAME }}
|
username: ${{ secrets.QUAY_USERNAME }}
|
||||||
password: ${{ secrets.QUAY_PASSWORD }}
|
password: ${{ secrets.QUAY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: quay.io/invidious/invidious
|
||||||
|
tags: |
|
||||||
|
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
labels: |
|
||||||
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker AMD64 image for Push Event
|
- name: Build and push Docker AMD64 image for Push Event
|
||||||
if: github.ref == 'refs/heads/master'
|
uses: docker/build-push-action@v5
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
labels: quay.expires-after=12w
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
push: true
|
push: true
|
||||||
tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
build-args: |
|
build-args: |
|
||||||
"release=1"
|
"release=1"
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta-arm64
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: quay.io/invidious/invidious
|
||||||
|
flavor: |
|
||||||
|
suffix=-arm64
|
||||||
|
tags: |
|
||||||
|
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
labels: |
|
||||||
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker ARM64 image for Push Event
|
- name: Build and push Docker ARM64 image for Push Event
|
||||||
if: github.ref == 'refs/heads/master'
|
uses: docker/build-push-action@v5
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
platforms: linux/arm64/v8
|
platforms: linux/arm64/v8
|
||||||
labels: quay.expires-after=12w
|
labels: ${{ steps.meta-arm64.outputs.labels }}
|
||||||
push: true
|
push: true
|
||||||
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64
|
tags: ${{ steps.meta-arm64.outputs.tags }}
|
||||||
build-args: |
|
build-args: |
|
||||||
"release=1"
|
"release=1"
|
||||||
|
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
|
@ -10,13 +10,13 @@ jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v5
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 365
|
days-before-stale: 365
|
||||||
days-before-pr-stale: 90
|
days-before-pr-stale: 90
|
||||||
days-before-close: 30
|
days-before-close: 30
|
||||||
exempt-pr-labels: blocked
|
exempt-pr-labels: blocked,exempt-stale
|
||||||
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
||||||
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
||||||
stale-issue-label: "stale"
|
stale-issue-label: "stale"
|
||||||
|
|
13
README.md
13
README.md
|
@ -145,18 +145,7 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
|
||||||
|
|
||||||
## Projects using Invidious
|
## Projects using Invidious
|
||||||
|
|
||||||
- [FreeTube](https://github.com/FreeTubeApp/FreeTube): A libre software YouTube app for privacy.
|
A list of projects and extensions for or utilizing Invidious can be found in the documentation: https://docs.invidious.io/applications/
|
||||||
- [CloudTube](https://sr.ht/~cadence/tube/): A JavaScript-rich alternate YouTube player.
|
|
||||||
- [PeerTubeify](https://gitlab.com/Cha_de_L/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists.
|
|
||||||
- [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube.
|
|
||||||
- [HoloPlay](https://github.com/stephane-r/holoplay-pwa): Progressive Web App connecting on Invidious API's with search, playlists and favorites.
|
|
||||||
- [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.
|
|
||||||
|
|
||||||
|
|
||||||
## Liability
|
## Liability
|
||||||
|
|
||||||
|
|
119
assets/css/carousel.css
Normal file
119
assets/css/carousel.css
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
Copyright (c) 2024 by Jennifer (https://codepen.io/jwjertzoch/pen/JjyGeRy)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person
|
||||||
|
obtaining a copy of this software and associated documentation
|
||||||
|
files (the "Software"), to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to use, copy, modify,
|
||||||
|
merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall
|
||||||
|
be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||||
|
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||||
|
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.carousel {
|
||||||
|
margin: 0 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slides {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: scroll;
|
||||||
|
scrollbar-width: none;
|
||||||
|
scroll-snap-type: x mandatory;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slides::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slides-item {
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 100px;
|
||||||
|
height: 600px;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 1rem;
|
||||||
|
position: relative;
|
||||||
|
scroll-snap-align: start;
|
||||||
|
transform: scale(1);
|
||||||
|
transform-origin: center center;
|
||||||
|
transition: transform .5s;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel__nav {
|
||||||
|
padding: 1.25rem .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-nav {
|
||||||
|
align-items: center;
|
||||||
|
background-color: #ddd;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #000;
|
||||||
|
display: inline-flex;
|
||||||
|
height: 1.5rem;
|
||||||
|
justify-content: center;
|
||||||
|
padding: .5rem;
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
top: auto;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
align-items: center;
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
font-size: 30px;
|
||||||
|
height: 30px;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: .8;
|
||||||
|
text-decoration: none;
|
||||||
|
width: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light-theme .slider-nav {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .slider-nav {
|
||||||
|
background-color: #0005;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.no-theme .slider-nav {
|
||||||
|
background-color: #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.no-theme .slider-nav {
|
||||||
|
background-color: #0005;
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-box {
|
.h-box {
|
||||||
|
@ -197,6 +198,7 @@ img.thumbnail {
|
||||||
display: block; /* See: https://stackoverflow.com/a/11635197 */
|
display: block; /* See: https://stackoverflow.com/a/11635197 */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumbnail-placeholder {
|
.thumbnail-placeholder {
|
||||||
|
|
|
@ -10,7 +10,7 @@ var notifications, delivered;
|
||||||
var notifications_mock = { close: function () { } };
|
var notifications_mock = { close: function () { } };
|
||||||
|
|
||||||
function get_subscriptions() {
|
function get_subscriptions() {
|
||||||
helpers.xhr('GET', '/api/v1/auth/subscriptions?fields=authorId', {
|
helpers.xhr('GET', '/api/v1/auth/subscriptions', {
|
||||||
retries: 5,
|
retries: 5,
|
||||||
entity_name: 'subscriptions'
|
entity_name: 'subscriptions'
|
||||||
}, {
|
}, {
|
||||||
|
@ -22,7 +22,7 @@ function create_notification_stream(subscriptions) {
|
||||||
// sse.js can't be replaced to EventSource in place as it lack support of payload and headers
|
// sse.js can't be replaced to EventSource in place as it lack support of payload and headers
|
||||||
// see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource
|
// see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource
|
||||||
notifications = new SSE(
|
notifications = new SSE(
|
||||||
'/api/v1/auth/notifications?fields=videoId,title,author,authorId,publishedText,published,authorThumbnails,liveNow', {
|
'/api/v1/auth/notifications', {
|
||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','),
|
payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','),
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
||||||
|
|
|
@ -747,6 +747,17 @@ if (navigator.vendor === 'Apple Computer, Inc.' && video_data.params.listen) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Safari screen timeout on looped video playback fix
|
||||||
|
if (navigator.vendor === 'Apple Computer, Inc.' && !video_data.params.listen && video_data.params.video_loop) {
|
||||||
|
player.loop(false);
|
||||||
|
player.ready(function () {
|
||||||
|
player.on('ended', function () {
|
||||||
|
player.currentTime(0);
|
||||||
|
player.play();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Watch on Invidious link
|
// Watch on Invidious link
|
||||||
if (location.pathname.startsWith('/embed/')) {
|
if (location.pathname.startsWith('/embed/')) {
|
||||||
const Button = videojs.getComponent('Button');
|
const Button = videojs.getComponent('Button');
|
||||||
|
|
|
@ -392,27 +392,6 @@ jobs:
|
||||||
enable: true
|
enable: true
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Captcha API
|
|
||||||
# -----------------------------
|
|
||||||
|
|
||||||
##
|
|
||||||
## URL of the captcha solving service.
|
|
||||||
##
|
|
||||||
## Accepted values: any URL
|
|
||||||
## Default: https://api.anti-captcha.com
|
|
||||||
##
|
|
||||||
#captcha_api_url: https://api.anti-captcha.com
|
|
||||||
|
|
||||||
##
|
|
||||||
## API key for the captcha solving service.
|
|
||||||
##
|
|
||||||
## Accepted values: a string
|
|
||||||
## Default: <none>
|
|
||||||
##
|
|
||||||
#captcha_key:
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Miscellaneous
|
# Miscellaneous
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
|
|
@ -36,8 +36,6 @@ services:
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 2
|
retries: 2
|
||||||
depends_on:
|
|
||||||
- invidious-db
|
|
||||||
|
|
||||||
invidious-db:
|
invidious-db:
|
||||||
image: docker.io/library/postgres:14
|
image: docker.io/library/postgres:14
|
||||||
|
|
|
@ -33,7 +33,7 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.18
|
||||||
RUN apk add --no-cache librsvg ttf-opensans tini
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
adduser -u 1000 -S invidious -G invidious
|
adduser -u 1000 -S invidious -G invidious
|
||||||
|
|
|
@ -33,7 +33,7 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.18
|
||||||
RUN apk add --no-cache librsvg ttf-opensans tini
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
adduser -u 1000 -S invidious -G invidious
|
adduser -u 1000 -S invidious -G invidious
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
"Time (h:mm:ss):": "الوقت (h:mm:ss):",
|
"Time (h:mm:ss):": "الوقت (h:mm:ss):",
|
||||||
"Text CAPTCHA": "نص الكابتشا",
|
"Text CAPTCHA": "نص الكابتشا",
|
||||||
"Image CAPTCHA": "صورة الكابتشا",
|
"Image CAPTCHA": "صورة الكابتشا",
|
||||||
"Sign In": "تسجيل الدخول",
|
"Sign In": "إنشاء حساب",
|
||||||
"Register": "التسجيل",
|
"Register": "التسجيل",
|
||||||
"E-mail": "البريد الإلكتروني",
|
"E-mail": "البريد الإلكتروني",
|
||||||
"Preferences": "الإعدادات",
|
"Preferences": "الإعدادات",
|
||||||
|
@ -554,5 +554,7 @@
|
||||||
"generic_channels_count_2": "{{count}} قناتان",
|
"generic_channels_count_2": "{{count}} قناتان",
|
||||||
"generic_channels_count_3": "{{count}} قنوات",
|
"generic_channels_count_3": "{{count}} قنوات",
|
||||||
"generic_channels_count_4": "{{count}} قنوات",
|
"generic_channels_count_4": "{{count}} قنوات",
|
||||||
"generic_channels_count_5": "{{count}} قناة"
|
"generic_channels_count_5": "{{count}} قناة",
|
||||||
|
"Import YouTube watch history (.json)": "استيراد سجل مشاهدة YouTube بصيغة (.json)",
|
||||||
|
"toggle_theme": "تبديل الموضوع"
|
||||||
}
|
}
|
||||||
|
|
|
@ -486,5 +486,6 @@
|
||||||
"preferences_annotations_label": "Покажи анотаций по подразбиране: ",
|
"preferences_annotations_label": "Покажи анотаций по подразбиране: ",
|
||||||
"generic_views_count": "{{count}} гледане",
|
"generic_views_count": "{{count}} гледане",
|
||||||
"generic_views_count_plural": "{{count}} гледания",
|
"generic_views_count_plural": "{{count}} гледания",
|
||||||
"Next page": "Следваща страница"
|
"Next page": "Следваща страница",
|
||||||
|
"Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -486,5 +486,6 @@
|
||||||
"generic_channels_count_plural": "{{count}} canals",
|
"generic_channels_count_plural": "{{count}} canals",
|
||||||
"generic_button_edit": "Edita",
|
"generic_button_edit": "Edita",
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
"generic_button_delete": "Suprimeix"
|
"generic_button_delete": "Suprimeix",
|
||||||
|
"Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -503,5 +503,7 @@
|
||||||
"playlist_button_add_items": "Přidat videa",
|
"playlist_button_add_items": "Přidat videa",
|
||||||
"generic_channels_count_0": "{{count}} kanál",
|
"generic_channels_count_0": "{{count}} kanál",
|
||||||
"generic_channels_count_1": "{{count}} kanály",
|
"generic_channels_count_1": "{{count}} kanály",
|
||||||
"generic_channels_count_2": "{{count}} kanálů"
|
"generic_channels_count_2": "{{count}} kanálů",
|
||||||
|
"Import YouTube watch history (.json)": "Importovat historii sledování z YouTube (.json)",
|
||||||
|
"toggle_theme": "Přepnout motiv"
|
||||||
}
|
}
|
||||||
|
|
|
@ -452,5 +452,40 @@
|
||||||
"crash_page_you_found_a_bug": "Det ser ud til, at du har fundet en fejl i Invidious!",
|
"crash_page_you_found_a_bug": "Det ser ud til, at du har fundet en fejl i Invidious!",
|
||||||
"crash_page_read_the_faq": "læs <a href=\"`x`\">Ofte stillede spørgsmål (FAQ)</a>",
|
"crash_page_read_the_faq": "læs <a href=\"`x`\">Ofte stillede spørgsmål (FAQ)</a>",
|
||||||
"crash_page_search_issue": "søgte efter <a href=\"`x`\">eksisterende problemer på GitHub</a>",
|
"crash_page_search_issue": "søgte efter <a href=\"`x`\">eksisterende problemer på GitHub</a>",
|
||||||
"search_filters_title": "Filter"
|
"search_filters_title": "Filter",
|
||||||
|
"playlist_button_add_items": "Tilføj videoer",
|
||||||
|
"search_message_no_results": "Ingen resultater fundet.",
|
||||||
|
"Import YouTube watch history (.json)": "Importer YouTube afspilningshistorik (.json)",
|
||||||
|
"search_message_change_filters_or_query": "Prøv at udvide din søgeforspørgsel og/eller ændre filtrene.",
|
||||||
|
"search_message_use_another_instance": " Du kan også <a href=\"`x`\">søge på en anden instans</a>.",
|
||||||
|
"Music in this video": "Musik i denne video",
|
||||||
|
"search_filters_date_option_none": "Enhver dato",
|
||||||
|
"search_filters_type_option_all": "Enhver type",
|
||||||
|
"search_filters_duration_option_none": "Enhver varighed",
|
||||||
|
"search_filters_duration_option_medium": "Medium (4 - 20 minutter)",
|
||||||
|
"search_filters_features_option_vr180": "VR180",
|
||||||
|
"generic_channels_count": "{{count}} kanal",
|
||||||
|
"generic_channels_count_plural": "{{count}} kanaler",
|
||||||
|
"Import YouTube playlist (.csv)": "Importer YouTube playliste (.csv)",
|
||||||
|
"Standard YouTube license": "Standard Youtube-licens",
|
||||||
|
"Album: ": "Album: ",
|
||||||
|
"Channel Sponsor": "Kanal-sponsor",
|
||||||
|
"Song: ": "Sang: ",
|
||||||
|
"channel_tab_playlists_label": "Playlister",
|
||||||
|
"channel_tab_channels_label": "Kanaler",
|
||||||
|
"Artist: ": "Kunstner: ",
|
||||||
|
"search_filters_date_label": "Uploaddato",
|
||||||
|
"generic_button_delete": "Slet",
|
||||||
|
"generic_button_edit": "Rediger",
|
||||||
|
"generic_button_save": "Gem",
|
||||||
|
"generic_button_cancel": "Afbryd",
|
||||||
|
"generic_button_rss": "RSS",
|
||||||
|
"Popular enabled: ": "Populær aktiveret: ",
|
||||||
|
"search_filters_apply_button": "Anvend udvalgte filtre",
|
||||||
|
"channel_tab_shorts_label": "Shorts",
|
||||||
|
"channel_tab_streams_label": "Livestreams",
|
||||||
|
"channel_tab_podcasts_label": "Podcasts",
|
||||||
|
"channel_tab_releases_label": "Udgivelser",
|
||||||
|
"Download is disabled": "Download er slået fra",
|
||||||
|
"error_video_not_in_playlist": "Den ønskede video findes ikke i denne playliste. <a href=\"`x`\">Klik her for playlistens startside.</a>"
|
||||||
}
|
}
|
||||||
|
|
|
@ -148,7 +148,7 @@
|
||||||
"Whitelisted regions: ": "Erlaubte Regionen: ",
|
"Whitelisted regions: ": "Erlaubte Regionen: ",
|
||||||
"Blacklisted regions: ": "Unerlaubte Regionen: ",
|
"Blacklisted regions: ": "Unerlaubte Regionen: ",
|
||||||
"Shared `x`": "Geteilt `x`",
|
"Shared `x`": "Geteilt `x`",
|
||||||
"Premieres in `x`": "Zuerst gesehen in `x`",
|
"Premieres in `x`": "Premiere in `x`",
|
||||||
"Premieres `x`": "Erster Start `x`",
|
"Premieres `x`": "Erster Start `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.": "Hallo! Anscheinend haben Sie JavaScript deaktiviert. Klicken Sie hier um Kommentare anzuzeigen, beachten sie dass es etwas länger dauern kann um sie zu laden.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hallo! Anscheinend haben Sie JavaScript deaktiviert. Klicken Sie hier um Kommentare anzuzeigen, beachten sie dass es etwas länger dauern kann um sie zu laden.",
|
||||||
"View YouTube comments": "YouTube Kommentare anzeigen",
|
"View YouTube comments": "YouTube Kommentare anzeigen",
|
||||||
|
@ -486,5 +486,6 @@
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
"channel_tab_podcasts_label": "Podcasts",
|
||||||
"channel_tab_releases_label": "Veröffentlichungen",
|
"channel_tab_releases_label": "Veröffentlichungen",
|
||||||
"generic_channels_count": "{{count}} Kanal",
|
"generic_channels_count": "{{count}} Kanal",
|
||||||
"generic_channels_count_plural": "{{count}} Kanäle"
|
"generic_channels_count_plural": "{{count}} Kanäle",
|
||||||
|
"Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
{
|
{
|
||||||
|
"Add to playlist": "Add to playlist",
|
||||||
|
"Add to playlist: ": "Add to playlist: ",
|
||||||
|
"Answer": "Answer",
|
||||||
|
"Search for videos": "Search for videos",
|
||||||
|
"The Popular feed has been disabled by the administrator.": "The Popular feed has been disabled by the administrator.",
|
||||||
"generic_channels_count": "{{count}} channel",
|
"generic_channels_count": "{{count}} channel",
|
||||||
"generic_channels_count_plural": "{{count}} channels",
|
"generic_channels_count_plural": "{{count}} channels",
|
||||||
"generic_views_count": "{{count}} view",
|
"generic_views_count": "{{count}} view",
|
||||||
|
@ -487,5 +492,9 @@
|
||||||
"channel_tab_releases_label": "Releases",
|
"channel_tab_releases_label": "Releases",
|
||||||
"channel_tab_playlists_label": "Playlists",
|
"channel_tab_playlists_label": "Playlists",
|
||||||
"channel_tab_community_label": "Community",
|
"channel_tab_community_label": "Community",
|
||||||
"channel_tab_channels_label": "Channels"
|
"channel_tab_channels_label": "Channels",
|
||||||
|
"toggle_theme": "Toggle Theme",
|
||||||
|
"carousel_slide": "Slide {{current}} of {{total}}",
|
||||||
|
"carousel_skip": "Skip the Carousel",
|
||||||
|
"carousel_go_to": "Go to slide `x`"
|
||||||
}
|
}
|
||||||
|
|
109
locales/es.json
109
locales/es.json
|
@ -90,7 +90,7 @@
|
||||||
"preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ",
|
"preferences_notifications_only_label": "Mostrar solo notificaciones (si hay alguna): ",
|
||||||
"Enable web notifications": "Habilitar notificaciones web",
|
"Enable web notifications": "Habilitar notificaciones web",
|
||||||
"`x` uploaded a video": "`x` subió un video",
|
"`x` uploaded a video": "`x` subió un video",
|
||||||
"`x` is live": "`x` esta en vivo",
|
"`x` is live": "`x` está en directo",
|
||||||
"preferences_category_data": "Preferencias de los datos",
|
"preferences_category_data": "Preferencias de los datos",
|
||||||
"Clear watch history": "Borrar el historial de reproducción",
|
"Clear watch history": "Borrar el historial de reproducción",
|
||||||
"Import/export data": "Importar/Exportar datos",
|
"Import/export data": "Importar/Exportar datos",
|
||||||
|
@ -102,7 +102,7 @@
|
||||||
"preferences_category_admin": "Preferencias de administrador",
|
"preferences_category_admin": "Preferencias de administrador",
|
||||||
"preferences_default_home_label": "Página de inicio por defecto: ",
|
"preferences_default_home_label": "Página de inicio por defecto: ",
|
||||||
"preferences_feed_menu_label": "Menú de fuentes: ",
|
"preferences_feed_menu_label": "Menú de fuentes: ",
|
||||||
"preferences_show_nick_label": "Mostrar nombre de usuario arriba: ",
|
"preferences_show_nick_label": "Mostrar nombre de usuario encima: ",
|
||||||
"Top enabled: ": "¿Habilitar los destacados? ",
|
"Top enabled: ": "¿Habilitar los destacados? ",
|
||||||
"CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ",
|
"CAPTCHA enabled: ": "¿Habilitar los CAPTCHA? ",
|
||||||
"Login enabled: ": "¿Habilitar el inicio de sesión? ",
|
"Login enabled: ": "¿Habilitar el inicio de sesión? ",
|
||||||
|
@ -144,13 +144,13 @@
|
||||||
"License: ": "Licencia: ",
|
"License: ": "Licencia: ",
|
||||||
"Family friendly? ": "¿Filtrar contenidos? ",
|
"Family friendly? ": "¿Filtrar contenidos? ",
|
||||||
"Wilson score: ": "Puntuación Wilson: ",
|
"Wilson score: ": "Puntuación Wilson: ",
|
||||||
"Engagement: ": "Compromiso: ",
|
"Engagement: ": "Retención: ",
|
||||||
"Whitelisted regions: ": "Regiones permitidas: ",
|
"Whitelisted regions: ": "Regiones permitidas: ",
|
||||||
"Blacklisted regions: ": "Regiones bloqueadas: ",
|
"Blacklisted regions: ": "Regiones bloqueadas: ",
|
||||||
"Shared `x`": "Compartido `x`",
|
"Shared `x`": "Compartido `x`",
|
||||||
"Premieres in `x`": "Se estrena en `x`",
|
"Premieres in `x`": "Se estrena en `x`",
|
||||||
"Premieres `x`": "Estrenos `x`",
|
"Premieres `x`": "Estrenos `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.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, pero tengas en cuenta que pueden tardar un poco más en cargarse.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "¡Hola! Parece que tienes JavaScript desactivado. Haz clic aquí para ver los comentarios, ten en cuenta que pueden tardar un poco más en cargar.",
|
||||||
"View YouTube comments": "Ver los comentarios de YouTube",
|
"View YouTube comments": "Ver los comentarios de YouTube",
|
||||||
"View more comments on Reddit": "Ver más comentarios en Reddit",
|
"View more comments on Reddit": "Ver más comentarios en Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
|
@ -312,7 +312,7 @@
|
||||||
"Download as: ": "Descargar como: ",
|
"Download as: ": "Descargar como: ",
|
||||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
"(edited)": "(editado)",
|
"(edited)": "(editado)",
|
||||||
"YouTube comment permalink": "Enlace permanente de YouTube del comentario",
|
"YouTube comment permalink": "Enlace permanente de comentario de YouTube",
|
||||||
"permalink": "enlace permanente",
|
"permalink": "enlace permanente",
|
||||||
"`x` marked it with a ❤": "`x` lo ha marcado con un ❤",
|
"`x` marked it with a ❤": "`x` lo ha marcado con un ❤",
|
||||||
"Audio mode": "Modo de audio",
|
"Audio mode": "Modo de audio",
|
||||||
|
@ -324,10 +324,10 @@
|
||||||
"search_filters_sort_option_rating": "Valoración",
|
"search_filters_sort_option_rating": "Valoración",
|
||||||
"search_filters_sort_option_date": "Fecha de subida",
|
"search_filters_sort_option_date": "Fecha de subida",
|
||||||
"search_filters_sort_option_views": "Visualizaciones",
|
"search_filters_sort_option_views": "Visualizaciones",
|
||||||
"search_filters_type_label": "tipo de contenido",
|
"search_filters_type_label": "Tipo de contenido",
|
||||||
"search_filters_duration_label": "duración",
|
"search_filters_duration_label": "Duración",
|
||||||
"search_filters_features_label": "funcionalidades",
|
"search_filters_features_label": "Funcionalidades",
|
||||||
"search_filters_sort_label": "ordenar",
|
"search_filters_sort_label": "Ordenar",
|
||||||
"search_filters_date_option_hour": "Última hora",
|
"search_filters_date_option_hour": "Última hora",
|
||||||
"search_filters_date_option_today": "Hoy",
|
"search_filters_date_option_today": "Hoy",
|
||||||
"search_filters_date_option_week": "Esta semana",
|
"search_filters_date_option_week": "Esta semana",
|
||||||
|
@ -390,43 +390,58 @@
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
"search_filters_features_option_three_sixty": "360°",
|
||||||
"videoinfo_watch_on_youTube": "Ver en YouTube",
|
"videoinfo_watch_on_youTube": "Ver en YouTube",
|
||||||
"preferences_save_player_pos_label": "Guardar posición de reproducción: ",
|
"preferences_save_player_pos_label": "Guardar posición de reproducción: ",
|
||||||
"generic_views_count": "{{count}} visualización",
|
"generic_views_count_0": "{{count}} visualización",
|
||||||
"generic_views_count_plural": "{{count}} visualizaciones",
|
"generic_views_count_1": "{{count}} visualizaciones",
|
||||||
"generic_subscribers_count": "{{count}} suscriptor",
|
"generic_views_count_2": "{{count}} visualizaciones",
|
||||||
"generic_subscribers_count_plural": "{{count}} suscriptores",
|
"generic_subscribers_count_0": "{{count}} suscriptor",
|
||||||
"generic_subscriptions_count": "{{count}} suscripción",
|
"generic_subscribers_count_1": "{{count}} suscriptores",
|
||||||
"generic_subscriptions_count_plural": "{{count}} suscripciones",
|
"generic_subscribers_count_2": "{{count}} suscriptores",
|
||||||
"subscriptions_unseen_notifs_count": "{{count}} notificación no vista",
|
"generic_subscriptions_count_0": "{{count}} suscripción",
|
||||||
"subscriptions_unseen_notifs_count_plural": "{{count}} notificaciones no vistas",
|
"generic_subscriptions_count_1": "{{count}} suscripciones",
|
||||||
"generic_count_days": "{{count}} día",
|
"generic_subscriptions_count_2": "{{count}} suscripciones",
|
||||||
"generic_count_days_plural": "{{count}} días",
|
"subscriptions_unseen_notifs_count_0": "{{count}} notificación sin ver",
|
||||||
"comments_view_x_replies": "Ver {{count}} respuesta",
|
"subscriptions_unseen_notifs_count_1": "{{count}} notificaciones sin ver",
|
||||||
"comments_view_x_replies_plural": "Ver {{count}} respuestas",
|
"subscriptions_unseen_notifs_count_2": "{{count}} notificaciones sin ver",
|
||||||
"generic_count_weeks": "{{count}} semana",
|
"generic_count_days_0": "{{count}} día",
|
||||||
"generic_count_weeks_plural": "{{count}} semanas",
|
"generic_count_days_1": "{{count}} días",
|
||||||
"generic_playlists_count": "{{count}} lista de reproducción",
|
"generic_count_days_2": "{{count}} días",
|
||||||
"generic_playlists_count_plural": "{{count}} listas de reproducciones",
|
"comments_view_x_replies_0": "Ver {{count}} respuesta",
|
||||||
"generic_videos_count": "{{count}} video",
|
"comments_view_x_replies_1": "Ver {{count}} respuestas",
|
||||||
"generic_videos_count_plural": "{{count}} video",
|
"comments_view_x_replies_2": "Ver {{count}} respuestas",
|
||||||
"generic_count_months": "{{count}} mes",
|
"generic_count_weeks_0": "{{count}} semana",
|
||||||
"generic_count_months_plural": "{{count}} meses",
|
"generic_count_weeks_1": "{{count}} semanas",
|
||||||
"comments_points_count": "{{count}} punto",
|
"generic_count_weeks_2": "{{count}} semanas",
|
||||||
"comments_points_count_plural": "{{count}} puntos",
|
"generic_playlists_count_0": "{{count}} lista de reproducción",
|
||||||
"generic_count_years": "{{count}} año",
|
"generic_playlists_count_1": "{{count}} listas de reproducciones",
|
||||||
"generic_count_years_plural": "{{count}} años",
|
"generic_playlists_count_2": "{{count}} listas de reproducciones",
|
||||||
"generic_count_hours": "{{count}} hora",
|
"generic_videos_count_0": "{{count}} video",
|
||||||
"generic_count_hours_plural": "{{count}} horas",
|
"generic_videos_count_1": "{{count}} videos",
|
||||||
"generic_count_minutes": "{{count}} minuto",
|
"generic_videos_count_2": "{{count}} videos",
|
||||||
"generic_count_minutes_plural": "{{count}} minutos",
|
"generic_count_months_0": "{{count}} mes",
|
||||||
"generic_count_seconds": "{{count}} segundo",
|
"generic_count_months_1": "{{count}} meses",
|
||||||
"generic_count_seconds_plural": "{{count}} segundos",
|
"generic_count_months_2": "{{count}} meses",
|
||||||
|
"comments_points_count_0": "{{count}} punto",
|
||||||
|
"comments_points_count_1": "{{count}} puntos",
|
||||||
|
"comments_points_count_2": "{{count}} puntos",
|
||||||
|
"generic_count_years_0": "{{count}} año",
|
||||||
|
"generic_count_years_1": "{{count}} años",
|
||||||
|
"generic_count_years_2": "{{count}} años",
|
||||||
|
"generic_count_hours_0": "{{count}} hora",
|
||||||
|
"generic_count_hours_1": "{{count}} horas",
|
||||||
|
"generic_count_hours_2": "{{count}} horas",
|
||||||
|
"generic_count_minutes_0": "{{count}} minuto",
|
||||||
|
"generic_count_minutes_1": "{{count}} minutos",
|
||||||
|
"generic_count_minutes_2": "{{count}} minutos",
|
||||||
|
"generic_count_seconds_0": "{{count}} segundo",
|
||||||
|
"generic_count_seconds_1": "{{count}} segundos",
|
||||||
|
"generic_count_seconds_2": "{{count}} segundos",
|
||||||
"crash_page_before_reporting": "Antes de notificar un error asegúrate de que has:",
|
"crash_page_before_reporting": "Antes de notificar un error asegúrate de que has:",
|
||||||
"crash_page_switch_instance": "probado a <a href=\"`x`\">usar otra instancia</a>",
|
"crash_page_switch_instance": "probado a <a href=\"`x`\">usar otra instancia</a>",
|
||||||
"crash_page_read_the_faq": "leído las <a href=\"`x`\">Preguntas Frecuentes</a>",
|
"crash_page_read_the_faq": "leído las <a href=\"`x`\">Preguntas Frecuentes</a>",
|
||||||
"crash_page_search_issue": "buscado <a href=\"`x`\">problemas existentes en GitHub</a>",
|
"crash_page_search_issue": "buscado <a href=\"`x`\">problemas existentes en GitHub</a>",
|
||||||
"crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!",
|
"crash_page_you_found_a_bug": "¡Parece que has encontrado un error en Invidious!",
|
||||||
"crash_page_refresh": "probado a <a href=\"`x`\">recargar la página</a>",
|
"crash_page_refresh": "probado a <a href=\"`x`\">recargar la página</a>",
|
||||||
"crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye verbatim el siguiente texto en tu mensaje:",
|
"crash_page_report_issue": "Si nada de lo anterior ha sido de ayuda, por favor, <a href=\"`x`\">abre una nueva incidencia en GitHub</a> (preferiblemente en inglés) e incluye el siguiente texto en tu mensaje (NO traduzcas este texto):",
|
||||||
"English (United States)": "Inglés (Estados Unidos)",
|
"English (United States)": "Inglés (Estados Unidos)",
|
||||||
"Cantonese (Hong Kong)": "Cantonés (Hong Kong)",
|
"Cantonese (Hong Kong)": "Cantonés (Hong Kong)",
|
||||||
"Dutch (auto-generated)": "Neerlandés (generados automáticamente)",
|
"Dutch (auto-generated)": "Neerlandés (generados automáticamente)",
|
||||||
|
@ -454,14 +469,15 @@
|
||||||
"search_message_no_results": "No se han encontrado resultados.",
|
"search_message_no_results": "No se han encontrado resultados.",
|
||||||
"search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.",
|
"search_message_change_filters_or_query": "Pruebe ampliar la consulta de búsqueda y/o a cambiar los filtros.",
|
||||||
"search_filters_title": "Filtros",
|
"search_filters_title": "Filtros",
|
||||||
"search_filters_date_label": "fecha de subida",
|
"search_filters_date_label": "Fecha de subida",
|
||||||
"search_filters_date_option_none": "Cualquier fecha",
|
"search_filters_date_option_none": "Cualquier fecha",
|
||||||
"search_filters_type_option_all": "Cualquier tipo",
|
"search_filters_type_option_all": "Cualquier tipo",
|
||||||
"search_filters_duration_option_none": "Cualquier duración",
|
"search_filters_duration_option_none": "Cualquier duración",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_filters_apply_button": "Aplicar filtros",
|
"search_filters_apply_button": "Aplicar filtros",
|
||||||
"tokens_count": "{{count}} token",
|
"tokens_count_0": "{{count}} token",
|
||||||
"tokens_count_plural": "{{count}} tokens",
|
"tokens_count_1": "{{count}} tokens",
|
||||||
|
"tokens_count_2": "{{count}} tokens",
|
||||||
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
|
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
|
||||||
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
"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>",
|
"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>",
|
||||||
|
@ -485,6 +501,9 @@
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
"channel_tab_podcasts_label": "Podcasts",
|
||||||
"channel_tab_releases_label": "Publicaciones",
|
"channel_tab_releases_label": "Publicaciones",
|
||||||
"generic_channels_count": "{{count}} canal",
|
"generic_channels_count_0": "{{count}} canal",
|
||||||
"generic_channels_count_plural": "{{count}} canales"
|
"generic_channels_count_1": "{{count}} canales",
|
||||||
|
"generic_channels_count_2": "{{count}} canales",
|
||||||
|
"Import YouTube watch history (.json)": "Importar el historial de las visualizaciones de YouTube (.json)",
|
||||||
|
"toggle_theme": "Alternar tema"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
{
|
{
|
||||||
"generic_views_count_0": "{{count}} بازدید",
|
"generic_views_count": "{{count}} بازدید",
|
||||||
"generic_videos_count_0": "{{count}} ویدئو",
|
"generic_views_count_plural": "{{count}} بازدید",
|
||||||
"generic_playlists_count_0": "{{count}} فهرست پخش",
|
"generic_videos_count": "{{count}} ویدئو",
|
||||||
"generic_subscribers_count_0": "{{count}} دنبال کننده",
|
"generic_videos_count_plural": "{{count}} ویدئو",
|
||||||
"generic_subscriptions_count_0": "{{count}} اشتراک ها",
|
"generic_playlists_count": "{{count}} فهرست پخش",
|
||||||
|
"generic_playlists_count_plural": "{{count}} فهرست پخش",
|
||||||
|
"generic_subscribers_count": "{{count}} دنبال کننده",
|
||||||
|
"generic_subscribers_count_plural": "{{count}} دنبال کننده",
|
||||||
|
"generic_subscriptions_count": "{{count}} اشتراک",
|
||||||
|
"generic_subscriptions_count_plural": "{{count}} اشتراک",
|
||||||
"LIVE": "زنده",
|
"LIVE": "زنده",
|
||||||
"Shared `x` ago": "`x` پیش به اشتراک گذاشته شده",
|
"Shared `x` ago": "`x` پیش به اشتراک گذاشته شده",
|
||||||
"Unsubscribe": "لغو اشتراک",
|
"Unsubscribe": "لغو اشتراک",
|
||||||
|
@ -117,13 +122,15 @@
|
||||||
"Subscription manager": "مدیریت اشتراک",
|
"Subscription manager": "مدیریت اشتراک",
|
||||||
"Token manager": "مدیر توکن",
|
"Token manager": "مدیر توکن",
|
||||||
"Token": "توکن",
|
"Token": "توکن",
|
||||||
"tokens_count_0": "{{count}} توکن ها",
|
"tokens_count": "{{count}} توکن",
|
||||||
|
"tokens_count_plural": "{{count}} توکن",
|
||||||
"Import/export": "وارد کردن/خارج کردن",
|
"Import/export": "وارد کردن/خارج کردن",
|
||||||
"unsubscribe": "لغو اشتراک",
|
"unsubscribe": "لغو اشتراک",
|
||||||
"revoke": "ابطال",
|
"revoke": "ابطال",
|
||||||
"Subscriptions": "اشتراک ها",
|
"Subscriptions": "اشتراک ها",
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}} اعلان نادیده",
|
"subscriptions_unseen_notifs_count": "{{count}} اعلان نادیده",
|
||||||
"search": "جستجو",
|
"subscriptions_unseen_notifs_count_plural": "{{count}} اعلان نادیده",
|
||||||
|
"search": "جست و جو",
|
||||||
"Log out": "خروج",
|
"Log out": "خروج",
|
||||||
"Released under the AGPLv3 on Github.": "منتشر شده تحت پروانه AGPLv3 روی گیتهاب.",
|
"Released under the AGPLv3 on Github.": "منتشر شده تحت پروانه AGPLv3 روی گیتهاب.",
|
||||||
"Source available here.": "منبع اینجا دردسترس است.",
|
"Source available here.": "منبع اینجا دردسترس است.",
|
||||||
|
@ -183,10 +190,12 @@
|
||||||
"This channel does not exist.": "این کانال وجود ندارد.",
|
"This channel does not exist.": "این کانال وجود ندارد.",
|
||||||
"Could not get channel info.": "نمیتوان اطلاعات کانال را دریافت کرد.",
|
"Could not get channel info.": "نمیتوان اطلاعات کانال را دریافت کرد.",
|
||||||
"Could not fetch comments": "نمیتوان نظرات را دریافت کرد",
|
"Could not fetch comments": "نمیتوان نظرات را دریافت کرد",
|
||||||
"comments_view_x_replies_0": "نمایش {{count}} پاسخ ها",
|
"comments_view_x_replies": "نمایش {{count}} پاسخ",
|
||||||
|
"comments_view_x_replies_plural": "نمایش {{count}} پاسخ",
|
||||||
"`x` ago": "`x` پیش",
|
"`x` ago": "`x` پیش",
|
||||||
"Load more": "بارگذاری بیشتر",
|
"Load more": "بارگذاری بیشتر",
|
||||||
"comments_points_count_0": "{{count}} نقطه ها",
|
"comments_points_count": "{{count}} نقطه",
|
||||||
|
"comments_points_count_plural": "{{count}} نقطه",
|
||||||
"Could not create mix.": "نمیتوان میکس ساخت.",
|
"Could not create mix.": "نمیتوان میکس ساخت.",
|
||||||
"Empty playlist": "سیاههٔ پخش خالی",
|
"Empty playlist": "سیاههٔ پخش خالی",
|
||||||
"Not a playlist.": "یک سیاههٔ پخش نیست.",
|
"Not a playlist.": "یک سیاههٔ پخش نیست.",
|
||||||
|
@ -304,16 +313,23 @@
|
||||||
"Yiddish": "ییدیش",
|
"Yiddish": "ییدیش",
|
||||||
"Yoruba": "یوروبایی",
|
"Yoruba": "یوروبایی",
|
||||||
"Zulu": "زولو",
|
"Zulu": "زولو",
|
||||||
"generic_count_years_0": "{{count}} سال",
|
"generic_count_years": "{{count}} سال",
|
||||||
"generic_count_months_0": "{{count}} ماه",
|
"generic_count_years_plural": "{{count}} سال",
|
||||||
"generic_count_weeks_0": "{{count}} هفته",
|
"generic_count_months": "{{count}} ماه",
|
||||||
"generic_count_days_0": "{{count}} روز",
|
"generic_count_months_plural": "{{count}} ماه",
|
||||||
"generic_count_hours_0": "{{count}} ساعت",
|
"generic_count_weeks": "{{count}} هفته",
|
||||||
"generic_count_minutes_0": "{{count}} دقیقه",
|
"generic_count_weeks_plural": "{{count}} هفته",
|
||||||
"generic_count_seconds_0": "{{count}} ثانیه",
|
"generic_count_days": "{{count}} روز",
|
||||||
|
"generic_count_days_plural": "{{count}} روز",
|
||||||
|
"generic_count_hours": "{{count}} ساعت",
|
||||||
|
"generic_count_hours_plural": "{{count}} ساعت",
|
||||||
|
"generic_count_minutes": "{{count}} دقیقه",
|
||||||
|
"generic_count_minutes_plural": "{{count}} دقیقه",
|
||||||
|
"generic_count_seconds": "{{count}} ثانیه",
|
||||||
|
"generic_count_seconds_plural": "{{count}} ثانیه",
|
||||||
"Fallback comments: ": "نظرات عقب گرد: ",
|
"Fallback comments: ": "نظرات عقب گرد: ",
|
||||||
"Popular": "محبوب",
|
"Popular": "محبوب",
|
||||||
"Search": "جستجو",
|
"Search": "جست و جو",
|
||||||
"Top": "بالا",
|
"Top": "بالا",
|
||||||
"About": "درباره",
|
"About": "درباره",
|
||||||
"Rating: ": "رتبه دهی: ",
|
"Rating: ": "رتبه دهی: ",
|
||||||
|
@ -445,5 +461,28 @@
|
||||||
"Song: ": "آهنگ: ",
|
"Song: ": "آهنگ: ",
|
||||||
"Channel Sponsor": "اسپانسر کانال",
|
"Channel Sponsor": "اسپانسر کانال",
|
||||||
"Standard YouTube license": "پروانه استاندارد YouTube",
|
"Standard YouTube license": "پروانه استاندارد YouTube",
|
||||||
"search_message_use_another_instance": " شما همچنین میتوانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>."
|
"search_message_use_another_instance": " شما همچنین میتوانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>.",
|
||||||
|
"Download is disabled": "دریافت غیرفعال است",
|
||||||
|
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
|
||||||
|
"playlist_button_add_items": "افزودن ویدیو",
|
||||||
|
"user_saved_playlists": "فهرستهای پخش ذخیره شده",
|
||||||
|
"crash_page_refresh": "که صفحه را <a href=\"`x`\">بازنشانی</a> کردهاید",
|
||||||
|
"generic_button_save": "ذخیره",
|
||||||
|
"generic_button_cancel": "لغو",
|
||||||
|
"generic_channels_count": "{{count}} کانال",
|
||||||
|
"generic_channels_count_plural": "{{count}} کانال",
|
||||||
|
"generic_button_edit": "ویرایش",
|
||||||
|
"crash_page_switch_instance": "که تلاش کردهاید <a href=\"`x`\">از یک نمونهٔ دیگر</a> استفاده کنید",
|
||||||
|
"generic_button_rss": "خوراک RSS",
|
||||||
|
"crash_page_read_the_faq": "که <a href=\"`x`\">سوالات بیشتر پرسیده شده (FAQ)</a> را خواندهاید",
|
||||||
|
"generic_button_delete": "حذف",
|
||||||
|
"Import YouTube playlist (.csv)": "واردکردن فهرستپخش YouTube (.csv)",
|
||||||
|
"Import YouTube watch history (.json)": "وارد کردن فهرست پخش YouTube (.json)",
|
||||||
|
"crash_page_you_found_a_bug": "به نظر میرسد که ایرادی در Invidious پیدا کردهاید!",
|
||||||
|
"channel_tab_podcasts_label": "پادکستها",
|
||||||
|
"channel_tab_streams_label": "پخش زندهها",
|
||||||
|
"channel_tab_shorts_label": "Shortها",
|
||||||
|
"channel_tab_playlists_label": "فهرستهای پخش",
|
||||||
|
"channel_tab_channels_label": "کانالها",
|
||||||
|
"error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. <a href=\"`x`\">کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید.</a>"
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
"Clear watch history?": "Tyhjennä katseluhistoria?",
|
"Clear watch history?": "Tyhjennä katseluhistoria?",
|
||||||
"New password": "Uusi salasana",
|
"New password": "Uusi salasana",
|
||||||
"New passwords must match": "Uusien salasanojen täytyy täsmätä",
|
"New passwords must match": "Uusien salasanojen täytyy täsmätä",
|
||||||
"Authorize token?": "Valuutetaanko tunnus?",
|
"Authorize token?": "Valtuutetaanko tunnus?",
|
||||||
"Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?",
|
"Authorize token for `x`?": "Valtuutetaanko tunnus `x`:lle?",
|
||||||
"Yes": "Kyllä",
|
"Yes": "Kyllä",
|
||||||
"No": "Ei",
|
"No": "Ei",
|
||||||
|
|
|
@ -503,5 +503,6 @@
|
||||||
"Download is disabled": "Le téléchargement est désactivé",
|
"Download is disabled": "Le téléchargement est désactivé",
|
||||||
"Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)",
|
"Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)",
|
||||||
"channel_tab_releases_label": "Parutions",
|
"channel_tab_releases_label": "Parutions",
|
||||||
"channel_tab_podcasts_label": "Émissions audio"
|
"channel_tab_podcasts_label": "Émissions audio",
|
||||||
|
"Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -476,7 +476,7 @@
|
||||||
"generic_button_cancel": "रद्द करें",
|
"generic_button_cancel": "रद्द करें",
|
||||||
"generic_button_rss": "आरएसएस",
|
"generic_button_rss": "आरएसएस",
|
||||||
"generic_button_edit": "संपादित करें",
|
"generic_button_edit": "संपादित करें",
|
||||||
"generic_button_delete": "मिटाएं",
|
"generic_button_delete": "हटाएं",
|
||||||
"playlist_button_add_items": "वीडियो जोड़ें",
|
"playlist_button_add_items": "वीडियो जोड़ें",
|
||||||
"Song: ": "गाना: ",
|
"Song: ": "गाना: ",
|
||||||
"channel_tab_podcasts_label": "पाॅडकास्ट",
|
"channel_tab_podcasts_label": "पाॅडकास्ट",
|
||||||
|
@ -484,5 +484,8 @@
|
||||||
"Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें",
|
"Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें",
|
||||||
"Standard YouTube license": "मानक यूट्यूब लाइसेंस",
|
"Standard YouTube license": "मानक यूट्यूब लाइसेंस",
|
||||||
"Channel Sponsor": "चैनल प्रायोजक",
|
"Channel Sponsor": "चैनल प्रायोजक",
|
||||||
"Download is disabled": "डाउनलोड करना अक्षम है"
|
"Download is disabled": "डाउनलोड करना अक्षम है",
|
||||||
|
"generic_channels_count": "{{count}} चैनल",
|
||||||
|
"generic_channels_count_plural": "{{count}} चैनल",
|
||||||
|
"Import YouTube watch history (.json)": "YouTube पर देखने का इतिहास आयात करें (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -503,5 +503,6 @@
|
||||||
"channel_tab_releases_label": "Izdanja",
|
"channel_tab_releases_label": "Izdanja",
|
||||||
"generic_channels_count_0": "{{count}} kanal",
|
"generic_channels_count_0": "{{count}} kanal",
|
||||||
"generic_channels_count_1": "{{count}} kanala",
|
"generic_channels_count_1": "{{count}} kanala",
|
||||||
"generic_channels_count_2": "{{count}} kanala"
|
"generic_channels_count_2": "{{count}} kanala",
|
||||||
|
"Import YouTube watch history (.json)": "Uvezi YouTube povijest gledanja (.json)"
|
||||||
}
|
}
|
||||||
|
|
41
locales/ia.json
Normal file
41
locales/ia.json
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"New password": "Nove contrasigno",
|
||||||
|
"preferences_player_style_label": "Stylo de reproductor: ",
|
||||||
|
"preferences_region_label": "Pais de contento: ",
|
||||||
|
"oldest": "plus ancian",
|
||||||
|
"published": "data de publication",
|
||||||
|
"invidious": "Invidious",
|
||||||
|
"Image CAPTCHA": "Imagine CAPTCHA",
|
||||||
|
"newest": "plus nove",
|
||||||
|
"generic_button_save": "Salvar",
|
||||||
|
"Dark mode: ": "Modo obscur: ",
|
||||||
|
"preferences_dark_mode_label": "Thema: ",
|
||||||
|
"preferences_category_subscription": "Preferentias de subscription",
|
||||||
|
"last": "ultime",
|
||||||
|
"generic_button_cancel": "Cancellar",
|
||||||
|
"popular": "popular",
|
||||||
|
"Time (h:mm:ss):": "Tempore (h:mm:ss):",
|
||||||
|
"preferences_autoplay_label": "Reproduction automatic: ",
|
||||||
|
"Sign In": "Aperir le session",
|
||||||
|
"Log in": "Initiar le session",
|
||||||
|
"preferences_speed_label": "Velocitate per predefinition: ",
|
||||||
|
"preferences_comments_label": "Commentos predefinite: ",
|
||||||
|
"light": "clar",
|
||||||
|
"No": "Non",
|
||||||
|
"youtube": "YouTube",
|
||||||
|
"LIVE": "IN DIRECTE",
|
||||||
|
"reddit": "Reddit",
|
||||||
|
"preferences_category_player": "Preferentias de reproductor",
|
||||||
|
"Preferences": "Preferentias",
|
||||||
|
"preferences_quality_dash_option_auto": "Automatic",
|
||||||
|
"dark": "obscur",
|
||||||
|
"generic_button_rss": "RSS",
|
||||||
|
"Export": "Exportar",
|
||||||
|
"History": "Chronologia",
|
||||||
|
"Password": "Contrasigno",
|
||||||
|
"User ID": "ID de usator",
|
||||||
|
"E-mail": "E-mail",
|
||||||
|
"Delete account?": "Deler conto?",
|
||||||
|
"preferences_volume_label": "Volumine del reproductor: ",
|
||||||
|
"preferences_sort_label": "Ordinar le videos per: "
|
||||||
|
}
|
|
@ -469,5 +469,6 @@
|
||||||
"error_video_not_in_playlist": "Video yang diminta tidak ada dalam daftar putar ini. <a href=\"`x`\">Klik di sini untuk halaman beranda daftar putar.</a>",
|
"error_video_not_in_playlist": "Video yang diminta tidak ada dalam daftar putar ini. <a href=\"`x`\">Klik di sini untuk halaman beranda daftar putar.</a>",
|
||||||
"generic_button_delete": "Hapus",
|
"generic_button_delete": "Hapus",
|
||||||
"Import YouTube playlist (.csv)": "Impor daftar putar YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Impor daftar putar YouTube (.csv)",
|
||||||
"Standard YouTube license": "Lisensi YouTube standar"
|
"Standard YouTube license": "Lisensi YouTube standar",
|
||||||
|
"Import YouTube watch history (.json)": "Impor riwayat tontonan YouTube (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -503,5 +503,6 @@
|
||||||
"channel_tab_podcasts_label": "Podcast",
|
"channel_tab_podcasts_label": "Podcast",
|
||||||
"generic_channels_count_0": "{{count}} canale",
|
"generic_channels_count_0": "{{count}} canale",
|
||||||
"generic_channels_count_1": "{{count}} canali",
|
"generic_channels_count_1": "{{count}} canali",
|
||||||
"generic_channels_count_2": "{{count}} canali"
|
"generic_channels_count_2": "{{count}} canali",
|
||||||
|
"Import YouTube watch history (.json)": "Importa la cronologia delle visualizzazioni di YouTube (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@
|
||||||
"preferences_category_player": "プレイヤーの設定",
|
"preferences_category_player": "プレイヤーの設定",
|
||||||
"preferences_video_loop_label": "常にループ: ",
|
"preferences_video_loop_label": "常にループ: ",
|
||||||
"preferences_autoplay_label": "自動再生: ",
|
"preferences_autoplay_label": "自動再生: ",
|
||||||
"preferences_continue_label": "次の動画を自動再生: ",
|
"preferences_continue_label": "次の動画に移動: ",
|
||||||
"preferences_continue_autoplay_label": "次の動画を自動再生: ",
|
"preferences_continue_autoplay_label": "次の動画を自動再生: ",
|
||||||
"preferences_listen_label": "音声モードを使用: ",
|
"preferences_listen_label": "音声モードを使用: ",
|
||||||
"preferences_local_label": "動画視聴にプロキシを経由: ",
|
"preferences_local_label": "動画視聴にプロキシを経由: ",
|
||||||
|
@ -363,9 +363,9 @@
|
||||||
"search_filters_features_option_location": "場所",
|
"search_filters_features_option_location": "場所",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"Current version: ": "現在のバージョン: ",
|
"Current version: ": "現在のバージョン: ",
|
||||||
"next_steps_error_message": "下記のものを試して下さい: ",
|
"next_steps_error_message": "以下をお試してください: ",
|
||||||
"next_steps_error_message_refresh": "再読込",
|
"next_steps_error_message_refresh": "再読み込み",
|
||||||
"next_steps_error_message_go_to_youtube": "YouTubeへ",
|
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
|
||||||
"search_filters_duration_option_short": "4分未満",
|
"search_filters_duration_option_short": "4分未満",
|
||||||
"footer_documentation": "説明書",
|
"footer_documentation": "説明書",
|
||||||
"footer_source_code": "ソースコード",
|
"footer_source_code": "ソースコード",
|
||||||
|
@ -459,7 +459,7 @@
|
||||||
"Song: ": "曲: ",
|
"Song: ": "曲: ",
|
||||||
"Channel Sponsor": "チャンネルのスポンサー",
|
"Channel Sponsor": "チャンネルのスポンサー",
|
||||||
"Standard YouTube license": "標準 Youtube ライセンス",
|
"Standard YouTube license": "標準 Youtube ライセンス",
|
||||||
"Download is disabled": "ダウンロード: このインスタンスでは未対応",
|
"Download is disabled": "ダウンロード: このインスタンスは未対応",
|
||||||
"Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)",
|
"Import YouTube playlist (.csv)": "YouTube 再生リストをインポート (.csv)",
|
||||||
"generic_button_delete": "削除",
|
"generic_button_delete": "削除",
|
||||||
"generic_button_cancel": "キャンセル",
|
"generic_button_cancel": "キャンセル",
|
||||||
|
@ -469,5 +469,6 @@
|
||||||
"generic_button_save": "保存",
|
"generic_button_save": "保存",
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
"playlist_button_add_items": "動画を追加",
|
"playlist_button_add_items": "動画を追加",
|
||||||
"generic_channels_count_0": "{{count}}個のチャンネル"
|
"generic_channels_count_0": "{{count}}個のチャンネル",
|
||||||
|
"Import YouTube watch history (.json)": "YouTube 視聴履歴をインポート (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
"source": "출처",
|
"source": "출처",
|
||||||
"JavaScript license information": "자바스크립트 라이선스 정보",
|
"JavaScript license information": "자바스크립트 라이선스 정보",
|
||||||
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
||||||
"History": "역사",
|
"History": "시청 기록",
|
||||||
"Delete account?": "계정을 삭제 하시겠습니까?",
|
"Delete account?": "계정을 삭제 하시겠습니까?",
|
||||||
"Export data as JSON": "JSON으로 데이터 내보내기",
|
"Export data as JSON": "JSON으로 데이터 내보내기",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
|
||||||
|
@ -351,7 +351,7 @@
|
||||||
"News": "뉴스",
|
"News": "뉴스",
|
||||||
"Gaming": "게임",
|
"Gaming": "게임",
|
||||||
"Music": "음악",
|
"Music": "음악",
|
||||||
"Default": "디폴트",
|
"Default": "전체",
|
||||||
"Rating: ": "평점: ",
|
"Rating: ": "평점: ",
|
||||||
"About": "정보",
|
"About": "정보",
|
||||||
"Top": "최고",
|
"Top": "최고",
|
||||||
|
@ -469,5 +469,6 @@
|
||||||
"generic_button_cancel": "취소",
|
"generic_button_cancel": "취소",
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
"channel_tab_releases_label": "출시",
|
"channel_tab_releases_label": "출시",
|
||||||
"generic_channels_count_0": "{{count}} 채널"
|
"generic_channels_count_0": "{{count}} 채널",
|
||||||
|
"Import YouTube watch history (.json)": "유튜브 시청 기록 가져오기 (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -486,5 +486,6 @@
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
"playlist_button_add_items": "Legg til videoer",
|
"playlist_button_add_items": "Legg til videoer",
|
||||||
"generic_channels_count": "{{count}} kanal",
|
"generic_channels_count": "{{count}} kanal",
|
||||||
"generic_channels_count_plural": "{{count}} kanaler"
|
"generic_channels_count_plural": "{{count}} kanaler",
|
||||||
|
"Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,10 +107,10 @@
|
||||||
"Report statistics: ": "Statistieken bijhouden? ",
|
"Report statistics: ": "Statistieken bijhouden? ",
|
||||||
"Save preferences": "Instellingen opslaan",
|
"Save preferences": "Instellingen opslaan",
|
||||||
"Subscription manager": "Abonnementen beheren",
|
"Subscription manager": "Abonnementen beheren",
|
||||||
"Token manager": "Toegangssleutels beheren",
|
"Token manager": "Toegangssleutelbeheerder",
|
||||||
"Token": "Toegangssleutel",
|
"Token": "Toegangssleutel",
|
||||||
"Import/export": "Importeren/Exporteren",
|
"Import/export": "Importeren/Exporteren",
|
||||||
"unsubscribe": "Deabonneren",
|
"unsubscribe": "deabonneren",
|
||||||
"revoke": "Intrekken",
|
"revoke": "Intrekken",
|
||||||
"Subscriptions": "Abonnementen",
|
"Subscriptions": "Abonnementen",
|
||||||
"search": "zoeken",
|
"search": "zoeken",
|
||||||
|
@ -357,7 +357,7 @@
|
||||||
"footer_original_source_code": "Originele bron-code",
|
"footer_original_source_code": "Originele bron-code",
|
||||||
"footer_modfied_source_code": "Gewijzigde bron-code",
|
"footer_modfied_source_code": "Gewijzigde bron-code",
|
||||||
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
|
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
|
||||||
"next_steps_error_message": "Waarna u moet proberen om: ",
|
"next_steps_error_message": "Daarna moet u proberen om: ",
|
||||||
"footer_source_code": "Bron-code",
|
"footer_source_code": "Bron-code",
|
||||||
"search_filters_duration_option_long": "Lang (> 20 minuten)",
|
"search_filters_duration_option_long": "Lang (> 20 minuten)",
|
||||||
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
|
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
|
||||||
|
@ -462,5 +462,30 @@
|
||||||
"Spanish (auto-generated)": "Spaans (automatisch gegenereerd)",
|
"Spanish (auto-generated)": "Spaans (automatisch gegenereerd)",
|
||||||
"crash_page_you_found_a_bug": "Je lijkt een bug in Invidious tegengekomen te zijn!",
|
"crash_page_you_found_a_bug": "Je lijkt een bug in Invidious tegengekomen te zijn!",
|
||||||
"search_filters_duration_option_medium": "Gemiddeld (4 - 20 minuten)",
|
"search_filters_duration_option_medium": "Gemiddeld (4 - 20 minuten)",
|
||||||
"crash_page_report_issue": "Indien het bovenstaande niet hielp, gelieve dan <a href=\"`x`\">een nieuw ticket op GitHub</a> te openen (liefst in het Engels) en neem de volgende tekst op in je bericht (gelieve deze NIET te vertalen):"
|
"crash_page_report_issue": "Indien het bovenstaande niet hielp, gelieve dan <a href=\"`x`\">een nieuw ticket op GitHub</a> te openen (liefst in het Engels) en neem de volgende tekst op in je bericht (gelieve deze NIET te vertalen):",
|
||||||
|
"channel_tab_podcasts_label": "Podcasts",
|
||||||
|
"Download is disabled": "Downloaden is uitgeschakeld",
|
||||||
|
"Channel Sponsor": "Kanaalsponsor",
|
||||||
|
"channel_tab_streams_label": "Livestreams",
|
||||||
|
"playlist_button_add_items": "Video's toevoegen",
|
||||||
|
"Artist: ": "Artiest: ",
|
||||||
|
"generic_button_save": "Opslaan",
|
||||||
|
"generic_button_cancel": "Annuleren",
|
||||||
|
"Album: ": "Album: ",
|
||||||
|
"channel_tab_shorts_label": "Shorts",
|
||||||
|
"channel_tab_releases_label": "Uitgaves",
|
||||||
|
"Song: ": "Lied: ",
|
||||||
|
"generic_channels_count": "{{count}} kanaal",
|
||||||
|
"generic_channels_count_plural": "{{count}} kanalen",
|
||||||
|
"Popular enabled: ": "Populair geactiveerd: ",
|
||||||
|
"channel_tab_playlists_label": "Afspeellijsten",
|
||||||
|
"generic_button_edit": "Bewerken",
|
||||||
|
"Music in this video": "Muziek in deze video",
|
||||||
|
"generic_button_rss": "RSS",
|
||||||
|
"channel_tab_channels_label": "Kanalen",
|
||||||
|
"error_video_not_in_playlist": "De gevraagde video bestaat niet in deze afspeellijst. <a href=\"`x`\">Klik hier voor de startpagina van de afspeellijst.</a>",
|
||||||
|
"generic_button_delete": "Verwijderen",
|
||||||
|
"Import YouTube playlist (.csv)": "YouTube-afspeellijst importeren (.csv)",
|
||||||
|
"Standard YouTube license": "Standaard YouTube-licentie",
|
||||||
|
"Import YouTube watch history (.json)": "YouTube-kijkgeschiedenis importeren (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -492,7 +492,7 @@
|
||||||
"Song: ": "Piosenka: ",
|
"Song: ": "Piosenka: ",
|
||||||
"Channel Sponsor": "Sponsor kanału",
|
"Channel Sponsor": "Sponsor kanału",
|
||||||
"Standard YouTube license": "Standardowa licencja YouTube",
|
"Standard YouTube license": "Standardowa licencja YouTube",
|
||||||
"Import YouTube playlist (.csv)": "Importuj playlistę YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Importuj playlistę z YouTube (.csv)",
|
||||||
"generic_button_edit": "Edytuj",
|
"generic_button_edit": "Edytuj",
|
||||||
"generic_button_cancel": "Anuluj",
|
"generic_button_cancel": "Anuluj",
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
|
@ -503,5 +503,7 @@
|
||||||
"playlist_button_add_items": "Dodaj filmy",
|
"playlist_button_add_items": "Dodaj filmy",
|
||||||
"generic_channels_count_0": "{{count}} kanał",
|
"generic_channels_count_0": "{{count}} kanał",
|
||||||
"generic_channels_count_1": "{{count}} kanały",
|
"generic_channels_count_1": "{{count}} kanały",
|
||||||
"generic_channels_count_2": "{{count}} kanałów"
|
"generic_channels_count_2": "{{count}} kanałów",
|
||||||
|
"Import YouTube watch history (.json)": "Importuj historię oglądania z YouTube (.json)",
|
||||||
|
"toggle_theme": "Przełącz motyw"
|
||||||
}
|
}
|
||||||
|
|
|
@ -503,5 +503,7 @@
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
"generic_channels_count_0": "{{count}} canal",
|
"generic_channels_count_0": "{{count}} canal",
|
||||||
"generic_channels_count_1": "{{count}} canais",
|
"generic_channels_count_1": "{{count}} canais",
|
||||||
"generic_channels_count_2": "{{count}} canais"
|
"generic_channels_count_2": "{{count}} canais",
|
||||||
|
"Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)",
|
||||||
|
"toggle_theme": "Alternar Tema"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"search_filters_type_option_show": "Espetáculo",
|
"search_filters_type_option_show": "Série",
|
||||||
"search_filters_sort_option_views": "Visualizações",
|
"search_filters_sort_option_views": "Visualizações",
|
||||||
"search_filters_sort_option_date": "Data de envio",
|
"search_filters_sort_option_date": "Data de carregamento",
|
||||||
"search_filters_sort_option_rating": "Avaliação",
|
"search_filters_sort_option_rating": "Avaliação",
|
||||||
"search_filters_sort_option_relevance": "Relevância",
|
"search_filters_sort_option_relevance": "Relevância",
|
||||||
"Switch Invidious Instance": "Mudar a instância do Invidious",
|
"Switch Invidious Instance": "Mudar a instância do Invidious",
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
"preferences_category_misc": "Preferências diversas",
|
"preferences_category_misc": "Preferências diversas",
|
||||||
"preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ",
|
"preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ",
|
||||||
"preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ",
|
"preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ",
|
||||||
"next_steps_error_message_go_to_youtube": "Ir ao YouTube",
|
"next_steps_error_message_go_to_youtube": "Ir para o YouTube",
|
||||||
"next_steps_error_message": "Pode tentar as seguintes opções: ",
|
"next_steps_error_message": "Pode tentar as seguintes opções: ",
|
||||||
"next_steps_error_message_refresh": "Atualizar",
|
"next_steps_error_message_refresh": "Atualizar",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
|
@ -44,20 +44,27 @@
|
||||||
"Default": "Predefinido",
|
"Default": "Predefinido",
|
||||||
"Top": "Destaques",
|
"Top": "Destaques",
|
||||||
"Search": "Pesquisar",
|
"Search": "Pesquisar",
|
||||||
"generic_count_years": "{{count}} segundo",
|
"generic_count_years_0": "{{count}} ano",
|
||||||
"generic_count_years_plural": "{{count}} segundos",
|
"generic_count_years_1": "{{count}} anos",
|
||||||
"generic_count_months": "{{count}} minuto",
|
"generic_count_years_2": "{{count}} anos",
|
||||||
"generic_count_months_plural": "{{count}} minutos",
|
"generic_count_months_0": "{{count}} mês",
|
||||||
"generic_count_weeks": "{{count}} hora",
|
"generic_count_months_1": "{{count}} meses",
|
||||||
"generic_count_weeks_plural": "{{count}} horas",
|
"generic_count_months_2": "{{count}} meses",
|
||||||
"generic_count_days": "{{count}} dia",
|
"generic_count_weeks_0": "{{count}} semana",
|
||||||
"generic_count_days_plural": "{{count}} dias",
|
"generic_count_weeks_1": "{{count}} semanas",
|
||||||
"generic_count_hours": "{{count}} seman",
|
"generic_count_weeks_2": "{{count}} semanas",
|
||||||
"generic_count_hours_plural": "{{count}} semanas",
|
"generic_count_days_0": "{{count}} dia",
|
||||||
"generic_count_minutes": "{{count}} mês",
|
"generic_count_days_1": "{{count}} dias",
|
||||||
"generic_count_minutes_plural": "{{count}} meses",
|
"generic_count_days_2": "{{count}} dias",
|
||||||
"generic_count_seconds": "{{count}} ano",
|
"generic_count_hours_0": "{{count}} hora",
|
||||||
"generic_count_seconds_plural": "{{count}} anos",
|
"generic_count_hours_1": "{{count}} horas",
|
||||||
|
"generic_count_hours_2": "{{count}} horas",
|
||||||
|
"generic_count_minutes_0": "{{count}} minuto",
|
||||||
|
"generic_count_minutes_1": "{{count}} minutos",
|
||||||
|
"generic_count_minutes_2": "{{count}} minutos",
|
||||||
|
"generic_count_seconds_0": "{{count}} segundo",
|
||||||
|
"generic_count_seconds_1": "{{count}} segundos",
|
||||||
|
"generic_count_seconds_2": "{{count}} segundos",
|
||||||
"Chinese (Traditional)": "Chinês (tradicional)",
|
"Chinese (Traditional)": "Chinês (tradicional)",
|
||||||
"Chinese (Simplified)": "Chinês (simplificado)",
|
"Chinese (Simplified)": "Chinês (simplificado)",
|
||||||
"Could not pull trending pages.": "Não foi possível obter as páginas de tendências.",
|
"Could not pull trending pages.": "Não foi possível obter as páginas de tendências.",
|
||||||
|
@ -75,7 +82,7 @@
|
||||||
"Import/export data": "Importar / exportar dados",
|
"Import/export data": "Importar / exportar dados",
|
||||||
"preferences_annotations_label": "Mostrar anotações sempre: ",
|
"preferences_annotations_label": "Mostrar anotações sempre: ",
|
||||||
"preferences_continue_label": "Reproduzir sempre o próximo: ",
|
"preferences_continue_label": "Reproduzir sempre o próximo: ",
|
||||||
"Sign In": "Iniciar sessão",
|
"Sign In": "Entrar",
|
||||||
"Log in/register": "Iniciar sessão/registar",
|
"Log in/register": "Iniciar sessão/registar",
|
||||||
"Delete account?": "Eliminar conta?",
|
"Delete account?": "Eliminar conta?",
|
||||||
"Import and Export Data": "Importar e exportar dados",
|
"Import and Export Data": "Importar e exportar dados",
|
||||||
|
@ -167,8 +174,9 @@
|
||||||
"Log out": "Terminar sessão",
|
"Log out": "Terminar sessão",
|
||||||
"Subscriptions": "Subscrições",
|
"Subscriptions": "Subscrições",
|
||||||
"revoke": "revogar",
|
"revoke": "revogar",
|
||||||
"tokens_count": "{{count}} token",
|
"tokens_count_0": "{{count}} Token",
|
||||||
"tokens_count_plural": "{{count}} tokens",
|
"tokens_count_1": "{{count}} Tokens",
|
||||||
|
"tokens_count_2": "{{count}} Tokens",
|
||||||
"Token": "Token",
|
"Token": "Token",
|
||||||
"Token manager": "Gerir tokens",
|
"Token manager": "Gerir tokens",
|
||||||
"Subscription manager": "Gerir subscrições",
|
"Subscription manager": "Gerir subscrições",
|
||||||
|
@ -402,31 +410,39 @@
|
||||||
"videoinfo_youTube_embed_link": "Incorporar",
|
"videoinfo_youTube_embed_link": "Incorporar",
|
||||||
"preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ",
|
"preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ",
|
||||||
"download_subtitles": "Legendas - `x` (.vtt)",
|
"download_subtitles": "Legendas - `x` (.vtt)",
|
||||||
"generic_views_count": "{{count}} visualização",
|
"generic_views_count_0": "{{count}} visualização",
|
||||||
"generic_views_count_plural": "{{count}} visualizações",
|
"generic_views_count_1": "{{count}} visualizações",
|
||||||
|
"generic_views_count_2": "{{count}} visualizações",
|
||||||
"videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`",
|
"videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`",
|
||||||
"user_saved_playlists": "`x` listas de reprodução guardadas",
|
"user_saved_playlists": "`x` listas de reprodução guardadas",
|
||||||
"generic_videos_count": "{{count}} vídeo",
|
"generic_videos_count_0": "{{count}} vídeo",
|
||||||
"generic_videos_count_plural": "{{count}} vídeos",
|
"generic_videos_count_1": "{{count}} vídeos",
|
||||||
"generic_playlists_count": "{{count}} lista de reprodução",
|
"generic_videos_count_2": "{{count}} vídeos",
|
||||||
"generic_playlists_count_plural": "{{count}} listas de reprodução",
|
"generic_playlists_count_0": "{{count}} lista de reprodução",
|
||||||
"subscriptions_unseen_notifs_count": "{{count}} notificação não vista",
|
"generic_playlists_count_1": "{{count}} listas de reprodução",
|
||||||
"subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas",
|
"generic_playlists_count_2": "{{count}} listas de reprodução",
|
||||||
"comments_view_x_replies": "Ver {{count}} resposta",
|
"subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista",
|
||||||
"comments_view_x_replies_plural": "Ver {{count}} respostas",
|
"subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas",
|
||||||
"generic_subscribers_count": "{{count}} inscrito",
|
"subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas",
|
||||||
"generic_subscribers_count_plural": "{{count}} inscritos",
|
"comments_view_x_replies_0": "Ver {{count}} resposta",
|
||||||
"generic_subscriptions_count": "{{count}} inscrição",
|
"comments_view_x_replies_1": "Ver {{count}} respostas",
|
||||||
"generic_subscriptions_count_plural": "{{count}} inscrições",
|
"comments_view_x_replies_2": "Ver {{count}} respostas",
|
||||||
"comments_points_count": "{{count}} ponto",
|
"generic_subscribers_count_0": "{{count}} inscrito",
|
||||||
"comments_points_count_plural": "{{count}} pontos",
|
"generic_subscribers_count_1": "{{count}} inscritos",
|
||||||
|
"generic_subscribers_count_2": "{{count}} inscritos",
|
||||||
|
"generic_subscriptions_count_0": "{{count}} inscrição",
|
||||||
|
"generic_subscriptions_count_1": "{{count}} inscrições",
|
||||||
|
"generic_subscriptions_count_2": "{{count}} inscrições",
|
||||||
|
"comments_points_count_0": "{{count}} ponto",
|
||||||
|
"comments_points_count_1": "{{count}} pontos",
|
||||||
|
"comments_points_count_2": "{{count}} pontos",
|
||||||
"crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!",
|
"crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!",
|
||||||
"crash_page_before_reporting": "Antes de reportar um erro, verifique se:",
|
"crash_page_before_reporting": "Antes de reportar um erro, verifique se:",
|
||||||
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
|
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
|
||||||
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
|
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
|
||||||
"crash_page_read_the_faq": "leia as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
|
"crash_page_read_the_faq": "leia as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
|
||||||
"crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
|
"crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
|
||||||
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):",
|
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto (NÃO o traduza):",
|
||||||
"user_created_playlists": "`x` listas de reprodução criadas",
|
"user_created_playlists": "`x` listas de reprodução criadas",
|
||||||
"search_filters_title": "Filtro",
|
"search_filters_title": "Filtro",
|
||||||
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
||||||
|
@ -464,7 +480,7 @@
|
||||||
"search_filters_type_option_all": "Qualquer tipo",
|
"search_filters_type_option_all": "Qualquer tipo",
|
||||||
"search_filters_duration_option_none": "Qualquer duração",
|
"search_filters_duration_option_none": "Qualquer duração",
|
||||||
"Popular enabled: ": "Página \"popular\" ativada: ",
|
"Popular enabled: ": "Página \"popular\" ativada: ",
|
||||||
"error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>",
|
"error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para voltar à página inicial da lista de reprodução.</a>",
|
||||||
"channel_tab_playlists_label": "Listas de reprodução",
|
"channel_tab_playlists_label": "Listas de reprodução",
|
||||||
"channel_tab_channels_label": "Canais",
|
"channel_tab_channels_label": "Canais",
|
||||||
"channel_tab_shorts_label": "Curtos",
|
"channel_tab_shorts_label": "Curtos",
|
||||||
|
@ -484,5 +500,10 @@
|
||||||
"channel_tab_releases_label": "Lançamentos",
|
"channel_tab_releases_label": "Lançamentos",
|
||||||
"generic_button_save": "Salvar",
|
"generic_button_save": "Salvar",
|
||||||
"generic_button_cancel": "Cancelar",
|
"generic_button_cancel": "Cancelar",
|
||||||
"playlist_button_add_items": "Adicionar vídeos"
|
"playlist_button_add_items": "Adicionar vídeos",
|
||||||
|
"generic_channels_count_0": "{{count}} canal",
|
||||||
|
"generic_channels_count_1": "{{count}} canais",
|
||||||
|
"generic_channels_count_2": "{{count}} canais",
|
||||||
|
"Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)",
|
||||||
|
"toggle_theme": "Trocar tema"
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@
|
||||||
"newest": "сначала новые",
|
"newest": "сначала новые",
|
||||||
"oldest": "сначала старые",
|
"oldest": "сначала старые",
|
||||||
"popular": "популярные",
|
"popular": "популярные",
|
||||||
"last": "недавние",
|
"last": "последние",
|
||||||
"Next page": "Следующая страница",
|
"Next page": "Следующая страница",
|
||||||
"Previous page": "Предыдущая страница",
|
"Previous page": "Предыдущая страница",
|
||||||
"Clear watch history?": "Очистить историю просмотров?",
|
"Clear watch history?": "Очистить историю просмотров?",
|
||||||
"New password": "Новый пароль",
|
"New password": "Новый пароль",
|
||||||
"New passwords must match": "Новые пароли не совпадают",
|
"New passwords must match": "Новые пароли не совпадают",
|
||||||
"Authorize token?": "Авторизовать токен?",
|
"Authorize token?": "Авторизовать токен?",
|
||||||
"Authorize token for `x`?": "Авторизовать токен для `x`?",
|
"Authorize token for `x`?": "Токен авторизации для `x`?",
|
||||||
"Yes": "Да",
|
"Yes": "Да",
|
||||||
"No": "Нет",
|
"No": "Нет",
|
||||||
"Import and Export Data": "Импорт и экспорт данных",
|
"Import and Export Data": "Импорт и экспорт данных",
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
"Export subscriptions as OPML": "Экспортировать подписки в формате OPML",
|
"Export subscriptions as OPML": "Экспортировать подписки в формате OPML",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Экспортировать подписки в формате OPML (для NewPipe и FreeTube)",
|
||||||
"Export data as JSON": "Экспортировать данные Invidious в формате JSON",
|
"Export data as JSON": "Экспортировать данные Invidious в формате JSON",
|
||||||
"Delete account?": "Удалить учётку?",
|
"Delete account?": "Удалить учётную запись?",
|
||||||
"History": "История",
|
"History": "История",
|
||||||
"An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube",
|
"An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube",
|
||||||
"JavaScript license information": "Информация о лицензиях JavaScript",
|
"JavaScript license information": "Информация о лицензиях JavaScript",
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
"Text CAPTCHA": "Текстовая капча (англ.)",
|
"Text CAPTCHA": "Текстовая капча (англ.)",
|
||||||
"Image CAPTCHA": "Капча-картинка",
|
"Image CAPTCHA": "Капча-картинка",
|
||||||
"Sign In": "Войти",
|
"Sign In": "Войти",
|
||||||
"Register": "Зарегистрироваться",
|
"Register": "Регистрация",
|
||||||
"E-mail": "Эл. почта",
|
"E-mail": "Эл. почта",
|
||||||
"Preferences": "Настройки",
|
"Preferences": "Настройки",
|
||||||
"preferences_category_player": "Настройки проигрывателя",
|
"preferences_category_player": "Настройки проигрывателя",
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
"preferences_captions_label": "Основной язык субтитров: ",
|
"preferences_captions_label": "Основной язык субтитров: ",
|
||||||
"Fallback captions: ": "Дополнительный язык субтитров: ",
|
"Fallback captions: ": "Дополнительный язык субтитров: ",
|
||||||
"preferences_related_videos_label": "Показывать похожие видео? ",
|
"preferences_related_videos_label": "Показывать похожие видео? ",
|
||||||
"preferences_annotations_label": "Всегда показывать аннотации? ",
|
"preferences_annotations_label": "Показывать аннотации по умолчанию: ",
|
||||||
"preferences_extend_desc_label": "Автоматически раскрывать описание видео: ",
|
"preferences_extend_desc_label": "Автоматически раскрывать описание видео: ",
|
||||||
"preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ",
|
"preferences_vr_mode_label": "Интерактивные 360-градусные видео (необходим WebGL): ",
|
||||||
"preferences_category_visual": "Настройки сайта",
|
"preferences_category_visual": "Настройки сайта",
|
||||||
|
@ -77,13 +77,13 @@
|
||||||
"preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ",
|
"preferences_annotations_subscribed_label": "Всегда показывать аннотации на каналах из ваших подписок? ",
|
||||||
"Redirect homepage to feed: ": "Показывать подписки на главной странице: ",
|
"Redirect homepage to feed: ": "Показывать подписки на главной странице: ",
|
||||||
"preferences_max_results_label": "Число видео в ленте: ",
|
"preferences_max_results_label": "Число видео в ленте: ",
|
||||||
"preferences_sort_label": "Сортировать видео: ",
|
"preferences_sort_label": "Сортировать видео по: ",
|
||||||
"published": "по дате публикации",
|
"published": "дате публикации",
|
||||||
"published - reverse": "по дате публикации в обратном порядке",
|
"published - reverse": "дате публикации в обратном порядке",
|
||||||
"alphabetically": "по алфавиту",
|
"alphabetically": "алфавиту",
|
||||||
"alphabetically - reverse": "по алфавиту в обратном порядке",
|
"alphabetically - reverse": "алфавиту в обратном порядке",
|
||||||
"channel name": "по названию канала",
|
"channel name": "названию канала",
|
||||||
"channel name - reverse": "по названию канала в обратном порядке",
|
"channel name - reverse": "названию канала в обратном порядке",
|
||||||
"Only show latest video from channel: ": "Показывать только последние видео с каналов: ",
|
"Only show latest video from channel: ": "Показывать только последние видео с каналов: ",
|
||||||
"Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ",
|
"Only show latest unwatched video from channel: ": "Показывать только последние непросмотренные видео с канала: ",
|
||||||
"preferences_unseen_only_label": "Показывать только непросмотренные видео: ",
|
"preferences_unseen_only_label": "Показывать только непросмотренные видео: ",
|
||||||
|
@ -134,8 +134,8 @@
|
||||||
"Title": "Заголовок",
|
"Title": "Заголовок",
|
||||||
"Playlist privacy": "Видимость плейлиста",
|
"Playlist privacy": "Видимость плейлиста",
|
||||||
"Editing playlist `x`": "Редактирование плейлиста `x`",
|
"Editing playlist `x`": "Редактирование плейлиста `x`",
|
||||||
"Show more": "Развернуть",
|
"Show more": "Показать больше",
|
||||||
"Show less": "Свернуть",
|
"Show less": "Показать меньше",
|
||||||
"Watch on YouTube": "Смотреть на YouTube",
|
"Watch on YouTube": "Смотреть на YouTube",
|
||||||
"Switch Invidious Instance": "Сменить зеркало Invidious",
|
"Switch Invidious Instance": "Сменить зеркало Invidious",
|
||||||
"Hide annotations": "Скрыть аннотации",
|
"Hide annotations": "Скрыть аннотации",
|
||||||
|
@ -414,7 +414,7 @@
|
||||||
"generic_count_days_0": "{{count}} день",
|
"generic_count_days_0": "{{count}} день",
|
||||||
"generic_count_days_1": "{{count}} дня",
|
"generic_count_days_1": "{{count}} дня",
|
||||||
"generic_count_days_2": "{{count}} дней",
|
"generic_count_days_2": "{{count}} дней",
|
||||||
"preferences_quality_dash_option_auto": "Автоматическое",
|
"preferences_quality_dash_option_auto": "Авто",
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
"preferences_quality_dash_option_1080p": "1080p",
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
"preferences_quality_dash_option_720p": "720p",
|
||||||
"generic_subscriptions_count_0": "{{count}} подписка",
|
"generic_subscriptions_count_0": "{{count}} подписка",
|
||||||
|
@ -466,7 +466,7 @@
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
"search_filters_features_option_three_sixty": "360°",
|
||||||
"Video unavailable": "Видео недоступно",
|
"Video unavailable": "Видео недоступно",
|
||||||
"preferences_save_player_pos_label": "Запоминать позицию: ",
|
"preferences_save_player_pos_label": "Запоминать позицию: ",
|
||||||
"preferences_region_label": "Страна: ",
|
"preferences_region_label": "Страна источник ",
|
||||||
"preferences_watch_history_label": "Включить историю просмотров: ",
|
"preferences_watch_history_label": "Включить историю просмотров: ",
|
||||||
"search_filters_title": "Фильтр",
|
"search_filters_title": "Фильтр",
|
||||||
"search_filters_duration_option_none": "Любой длины",
|
"search_filters_duration_option_none": "Любой длины",
|
||||||
|
@ -476,7 +476,7 @@
|
||||||
"search_message_no_results": "Ничего не найдено.",
|
"search_message_no_results": "Ничего не найдено.",
|
||||||
"search_message_use_another_instance": " Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.",
|
"search_message_use_another_instance": " Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос или изменить фильтры.",
|
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос и/или изменить фильтры.",
|
||||||
"search_filters_duration_option_medium": "Средние (4 - 20 минут)",
|
"search_filters_duration_option_medium": "Средние (4 - 20 минут)",
|
||||||
"search_filters_apply_button": "Применить фильтры",
|
"search_filters_apply_button": "Применить фильтры",
|
||||||
"Popular enabled: ": "Популярное включено: ",
|
"Popular enabled: ": "Популярное включено: ",
|
||||||
|
@ -503,5 +503,6 @@
|
||||||
"channel_tab_podcasts_label": "Подкасты",
|
"channel_tab_podcasts_label": "Подкасты",
|
||||||
"generic_channels_count_0": "{{count}} канал",
|
"generic_channels_count_0": "{{count}} канал",
|
||||||
"generic_channels_count_1": "{{count}} канала",
|
"generic_channels_count_1": "{{count}} канала",
|
||||||
"generic_channels_count_2": "{{count}} каналов"
|
"generic_channels_count_2": "{{count}} каналов",
|
||||||
|
"Import YouTube watch history (.json)": "Импортировать историю просмотра из YouTube (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -520,5 +520,6 @@
|
||||||
"generic_channels_count_0": "{{count}} kanal",
|
"generic_channels_count_0": "{{count}} kanal",
|
||||||
"generic_channels_count_1": "{{count}} kanala",
|
"generic_channels_count_1": "{{count}} kanala",
|
||||||
"generic_channels_count_2": "{{count}} kanali",
|
"generic_channels_count_2": "{{count}} kanali",
|
||||||
"generic_channels_count_3": "{{count}} kanalov"
|
"generic_channels_count_3": "{{count}} kanalov",
|
||||||
|
"Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@
|
||||||
"invidious": "Invidious",
|
"invidious": "Invidious",
|
||||||
"preferences_captions_label": "Titra parazgjedhje: ",
|
"preferences_captions_label": "Titra parazgjedhje: ",
|
||||||
"preferences_extend_desc_label": "Zgjero automatikisht përshkrimin e videos: ",
|
"preferences_extend_desc_label": "Zgjero automatikisht përshkrimin e videos: ",
|
||||||
"preferences_player_style_label": "Silt lojtësi: ",
|
"preferences_player_style_label": "Stil lojtësi: ",
|
||||||
"Dark mode: ": "Mënyra e errët: ",
|
"Dark mode: ": "Mënyra e errët: ",
|
||||||
"preferences_dark_mode_label": "Temë: ",
|
"preferences_dark_mode_label": "Temë: ",
|
||||||
"dark": "e errët",
|
"dark": "e errët",
|
||||||
|
@ -477,5 +477,12 @@
|
||||||
"channel_tab_releases_label": "Hedhje në qarkullim",
|
"channel_tab_releases_label": "Hedhje në qarkullim",
|
||||||
"Song: ": "Pjesë: ",
|
"Song: ": "Pjesë: ",
|
||||||
"Import YouTube playlist (.csv)": "Importoni luajlistë YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Importoni luajlistë YouTube (.csv)",
|
||||||
"Standard YouTube license": "Licencë YouTube standarde"
|
"Standard YouTube license": "Licencë YouTube standarde",
|
||||||
|
"published - reverse": "publikuar më - së prapthi",
|
||||||
|
"channel_tab_podcasts_label": "Podcast-e",
|
||||||
|
"channel name - reverse": "emër kanali - së prapthi",
|
||||||
|
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
|
||||||
|
"preferences_local_label": "Video përmes ndërmjetësi: ",
|
||||||
|
"Fallback captions: ": "Titra nga halli: ",
|
||||||
|
"Erroneous challenge": "Zgjidhje e gabuar"
|
||||||
}
|
}
|
||||||
|
|
|
@ -503,5 +503,6 @@
|
||||||
"crash_page_you_found_a_bug": "Izgleda da ste pronašli grešku u Invidious-u!",
|
"crash_page_you_found_a_bug": "Izgleda da ste pronašli grešku u Invidious-u!",
|
||||||
"generic_views_count_0": "{{count}} pregled",
|
"generic_views_count_0": "{{count}} pregled",
|
||||||
"generic_views_count_1": "{{count}} pregleda",
|
"generic_views_count_1": "{{count}} pregleda",
|
||||||
"generic_views_count_2": "{{count}} pregleda"
|
"generic_views_count_2": "{{count}} pregleda",
|
||||||
|
"Import YouTube watch history (.json)": "Uvezi YouTube istoriju gledanja (.json)"
|
||||||
}
|
}
|
||||||
|
|
|
@ -503,5 +503,7 @@
|
||||||
"crash_page_you_found_a_bug": "Изгледа да сте пронашли грешку у Invidious-у!",
|
"crash_page_you_found_a_bug": "Изгледа да сте пронашли грешку у Invidious-у!",
|
||||||
"generic_views_count_0": "{{count}} преглед",
|
"generic_views_count_0": "{{count}} преглед",
|
||||||
"generic_views_count_1": "{{count}} прегледа",
|
"generic_views_count_1": "{{count}} прегледа",
|
||||||
"generic_views_count_2": "{{count}} прегледа"
|
"generic_views_count_2": "{{count}} прегледа",
|
||||||
|
"Import YouTube watch history (.json)": "Увези YouTube историју гледањa (.json)",
|
||||||
|
"toggle_theme": "Укључи тему"
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,15 +20,15 @@
|
||||||
"No": "Nej",
|
"No": "Nej",
|
||||||
"Import and Export Data": "Importera och exportera data",
|
"Import and Export Data": "Importera och exportera data",
|
||||||
"Import": "Importera",
|
"Import": "Importera",
|
||||||
"Import Invidious data": "Importera Invidious-data",
|
"Import Invidious data": "Importera Invidious JSON data",
|
||||||
"Import YouTube subscriptions": "Importera YouTube-prenumerationer",
|
"Import YouTube subscriptions": "Importera YouTube/OPML prenumerationer",
|
||||||
"Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)",
|
"Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)",
|
"Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)",
|
||||||
"Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)",
|
"Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)",
|
||||||
"Export": "Exportera",
|
"Export": "Exportera",
|
||||||
"Export subscriptions as OPML": "Exportera prenumerationer som OPML",
|
"Export subscriptions as OPML": "Exportera prenumerationer som OPML",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportera prenumerationer som OPML (för NewPipe och FreeTube)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exportera prenumerationer som OPML (för NewPipe och FreeTube)",
|
||||||
"Export data as JSON": "Exportera data som JSON",
|
"Export data as JSON": "Exportera Invidious data som JSON",
|
||||||
"Delete account?": "Radera konto?",
|
"Delete account?": "Radera konto?",
|
||||||
"History": "Historik",
|
"History": "Historik",
|
||||||
"An alternative front-end to YouTube": "Ett alternativt gränssnitt till YouTube",
|
"An alternative front-end to YouTube": "Ett alternativt gränssnitt till YouTube",
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
"preferences_related_videos_label": "Visa relaterade videor? ",
|
"preferences_related_videos_label": "Visa relaterade videor? ",
|
||||||
"preferences_annotations_label": "Visa länkar-i-videon som förval? ",
|
"preferences_annotations_label": "Visa länkar-i-videon som förval? ",
|
||||||
"preferences_extend_desc_label": "Förläng videobeskrivning automatiskt: ",
|
"preferences_extend_desc_label": "Förläng videobeskrivning automatiskt: ",
|
||||||
"preferences_vr_mode_label": "Interaktiva 360-gradervideos: ",
|
"preferences_vr_mode_label": "Interaktiva 360-gradervideos (kräver WebGL): ",
|
||||||
"preferences_category_visual": "Visuella inställningar",
|
"preferences_category_visual": "Visuella inställningar",
|
||||||
"preferences_player_style_label": "Spelarstil: ",
|
"preferences_player_style_label": "Spelarstil: ",
|
||||||
"Dark mode: ": "Mörkt läge: ",
|
"Dark mode: ": "Mörkt läge: ",
|
||||||
|
@ -152,7 +152,7 @@
|
||||||
"View YouTube comments": "Visa YouTube-kommentarer",
|
"View YouTube comments": "Visa YouTube-kommentarer",
|
||||||
"View more comments on Reddit": "Visa flera kommentarer på Reddit",
|
"View more comments on Reddit": "Visa flera kommentarer på Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentarer",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Visa `x` kommentar",
|
||||||
"": "Visa `x` kommentarer"
|
"": "Visa `x` kommentarer"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Visa Reddit-kommentarer",
|
"View Reddit comments": "Visa Reddit-kommentarer",
|
||||||
|
@ -167,7 +167,7 @@
|
||||||
"Wrong username or password": "Ogiltigt användarnamn eller lösenord",
|
"Wrong username or password": "Ogiltigt användarnamn eller lösenord",
|
||||||
"Password cannot be empty": "Lösenordet kan inte vara tomt",
|
"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",
|
"Password cannot be longer than 55 characters": "Lösenordet kan inte vara längre än 55 tecken",
|
||||||
"Please log in": "Logga in",
|
"Please log in": "Snälla logga in",
|
||||||
"Invidious Private Feed for `x`": "Ogiltig privat flöde för `x`",
|
"Invidious Private Feed for `x`": "Ogiltig privat flöde för `x`",
|
||||||
"channel:`x`": "kanal `x`",
|
"channel:`x`": "kanal `x`",
|
||||||
"Deleted or invalid channel": "Raderad eller ogiltig kanal",
|
"Deleted or invalid channel": "Raderad eller ogiltig kanal",
|
||||||
|
@ -311,8 +311,8 @@
|
||||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
"(edited)": "(redigerad)",
|
"(edited)": "(redigerad)",
|
||||||
"YouTube comment permalink": "Permanent YouTube-länk till innehållet",
|
"YouTube comment permalink": "Permanent YouTube-länk till innehållet",
|
||||||
"permalink": "permalänk",
|
"permalink": "permanent länk",
|
||||||
"`x` marked it with a ❤": "`x` lämnade ett ❤",
|
"`x` marked it with a ❤": "`x` markerade det med ett ❤",
|
||||||
"Audio mode": "Ljudläge",
|
"Audio mode": "Ljudläge",
|
||||||
"Video mode": "Videoläge",
|
"Video mode": "Videoläge",
|
||||||
"channel_tab_videos_label": "Videor",
|
"channel_tab_videos_label": "Videor",
|
||||||
|
@ -320,30 +320,30 @@
|
||||||
"channel_tab_community_label": "Gemenskap",
|
"channel_tab_community_label": "Gemenskap",
|
||||||
"search_filters_sort_option_relevance": "Relevans",
|
"search_filters_sort_option_relevance": "Relevans",
|
||||||
"search_filters_sort_option_rating": "Rankning",
|
"search_filters_sort_option_rating": "Rankning",
|
||||||
"search_filters_sort_option_date": "Datum",
|
"search_filters_sort_option_date": "Uppladdnings Datum",
|
||||||
"search_filters_sort_option_views": "Visningar",
|
"search_filters_sort_option_views": "Visningar",
|
||||||
"search_filters_type_label": "Typ",
|
"search_filters_type_label": "Typ",
|
||||||
"search_filters_duration_label": "Varaktighet",
|
"search_filters_duration_label": "Varaktighet",
|
||||||
"search_filters_features_label": "Funktioner",
|
"search_filters_features_label": "Funktioner",
|
||||||
"search_filters_sort_label": "Sortera efter",
|
"search_filters_sort_label": "Sortera efter",
|
||||||
"search_filters_date_option_hour": "timme",
|
"search_filters_date_option_hour": "Senaste Timmen",
|
||||||
"search_filters_date_option_today": "idag",
|
"search_filters_date_option_today": "Idag",
|
||||||
"search_filters_date_option_week": "vecka",
|
"search_filters_date_option_week": "Denna vecka",
|
||||||
"search_filters_date_option_month": "månad",
|
"search_filters_date_option_month": "Denna månad",
|
||||||
"search_filters_date_option_year": "år",
|
"search_filters_date_option_year": "Detta år",
|
||||||
"search_filters_type_option_video": "video",
|
"search_filters_type_option_video": "Video",
|
||||||
"search_filters_type_option_channel": "kanal",
|
"search_filters_type_option_channel": "Kanal",
|
||||||
"search_filters_type_option_playlist": "spellista",
|
"search_filters_type_option_playlist": "Spellista",
|
||||||
"search_filters_type_option_movie": "film",
|
"search_filters_type_option_movie": "Film",
|
||||||
"search_filters_type_option_show": "tv-serie",
|
"search_filters_type_option_show": "Serie",
|
||||||
"search_filters_features_option_hd": "hd",
|
"search_filters_features_option_hd": "HD",
|
||||||
"search_filters_features_option_subtitles": "undertexter",
|
"search_filters_features_option_subtitles": "Undertexter/CC",
|
||||||
"search_filters_features_option_c_commons": "creative_commons",
|
"search_filters_features_option_c_commons": "Creative Commons",
|
||||||
"search_filters_features_option_three_d": "3d",
|
"search_filters_features_option_three_d": "3D",
|
||||||
"search_filters_features_option_live": "live",
|
"search_filters_features_option_live": "Live",
|
||||||
"search_filters_features_option_four_k": "4k",
|
"search_filters_features_option_four_k": "4K",
|
||||||
"search_filters_features_option_location": "plats",
|
"search_filters_features_option_location": "Plats",
|
||||||
"search_filters_features_option_hdr": "hdr",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"Current version: ": "Nuvarande version: ",
|
"Current version: ": "Nuvarande version: ",
|
||||||
"next_steps_error_message_refresh": "Uppdatera",
|
"next_steps_error_message_refresh": "Uppdatera",
|
||||||
"next_steps_error_message_go_to_youtube": "Gå till Youtube",
|
"next_steps_error_message_go_to_youtube": "Gå till Youtube",
|
||||||
|
@ -352,5 +352,141 @@
|
||||||
"search_filters_duration_option_long": "Lång (> 20 minuter)",
|
"search_filters_duration_option_long": "Lång (> 20 minuter)",
|
||||||
"footer_documentation": "Dokumentation",
|
"footer_documentation": "Dokumentation",
|
||||||
"search_filters_duration_option_short": "Kort (< 4 minuter)",
|
"search_filters_duration_option_short": "Kort (< 4 minuter)",
|
||||||
"search_filters_title": "Filter"
|
"search_filters_title": "Filter",
|
||||||
|
"Korean (auto-generated)": "Koreanska (auto-genererad)",
|
||||||
|
"search_filters_features_option_three_sixty": "360°",
|
||||||
|
"preferences_quality_dash_option_worst": "Sämst",
|
||||||
|
"channel_tab_podcasts_label": "Podcaster",
|
||||||
|
"preferences_save_player_pos_label": "Spara uppspelningsposition: ",
|
||||||
|
"Spanish (Mexico)": "Spanska (Mexiko)",
|
||||||
|
"preferences_region_label": "Innehållsland: ",
|
||||||
|
"generic_subscriptions_count": "{{count}} prenumeration",
|
||||||
|
"generic_subscriptions_count_plural": "{{count}} prenumerationer",
|
||||||
|
"search_filters_apply_button": "Använd valda filter",
|
||||||
|
"Download is disabled": "Nedladdning är inaktiverad",
|
||||||
|
"comments_points_count": "{{count}} poäng",
|
||||||
|
"comments_points_count_plural": "{{count}} poäng",
|
||||||
|
"preferences_quality_dash_option_2160p": "2160p",
|
||||||
|
"German (auto-generated)": "Tyska (auto-genererad)",
|
||||||
|
"Japanese (auto-generated)": "Japanska (auto-genererad)",
|
||||||
|
"preferences_quality_option_medium": "Medium",
|
||||||
|
"footer_donate_page": "Donera",
|
||||||
|
"search_message_change_filters_or_query": "Prova att bredda din sökfråga och/eller ändra filtren.",
|
||||||
|
"crash_page_before_reporting": "Innan du rapporterar en bugg, se till att du har:",
|
||||||
|
"preferences_quality_dash_option_best": "Bäst",
|
||||||
|
"Channel Sponsor": "Kanal Sponsor",
|
||||||
|
"generic_videos_count": "{{count}} video",
|
||||||
|
"generic_videos_count_plural": "{{count}} videor",
|
||||||
|
"videoinfo_started_streaming_x_ago": "Började sända `x` sedan",
|
||||||
|
"videoinfo_youTube_embed_link": "Bädda in",
|
||||||
|
"channel_tab_streams_label": "Livesändningar",
|
||||||
|
"playlist_button_add_items": "Lägg till videor",
|
||||||
|
"generic_count_minutes": "{{count}}minut",
|
||||||
|
"generic_count_minutes_plural": "{{count}}minuter",
|
||||||
|
"preferences_quality_dash_option_720p": "720p",
|
||||||
|
"preferences_watch_history_label": "Aktivera visningshistorik: ",
|
||||||
|
"user_saved_playlists": "`x` sparade spellistor",
|
||||||
|
"Spanish (Spain)": "Spanska (Spanien)",
|
||||||
|
"invidious": "Invidious",
|
||||||
|
"crash_page_refresh": "försökte <a href=\"`x`\">uppdatera sidan</a>",
|
||||||
|
"Chinese (Hong Kong)": "Kinesiska (Hong Kong)",
|
||||||
|
"Artist: ": "Artist: ",
|
||||||
|
"generic_count_months": "{{count}}månad",
|
||||||
|
"generic_count_months_plural": "{{count}}månader",
|
||||||
|
"search_message_use_another_instance": " Du kan också <a href=\"`x`\">söka på en annan instans</a>.",
|
||||||
|
"generic_subscribers_count": "{{count}} prenumerant",
|
||||||
|
"generic_subscribers_count_plural": "{{count}} prenumeranter",
|
||||||
|
"download_subtitles": "Undertexter - `x` (.vtt)",
|
||||||
|
"generic_button_save": "Spara",
|
||||||
|
"crash_page_search_issue": "sökte efter <a href=\"`x`\">befintliga problem på GitHub</a>",
|
||||||
|
"generic_button_cancel": "Avbryt",
|
||||||
|
"none": "ingen",
|
||||||
|
"English (United States)": "English (Förenta staterna)",
|
||||||
|
"subscriptions_unseen_notifs_count": "{{count}}osedd notifikation",
|
||||||
|
"subscriptions_unseen_notifs_count_plural": "{{count}}osedda notifikationer",
|
||||||
|
"Album: ": "Album: ",
|
||||||
|
"preferences_quality_option_dash": "DASH (adaptiv kvalitet)",
|
||||||
|
"preferences_quality_dash_option_1080p": "1080p",
|
||||||
|
"Video unavailable": "Video inte tillgänglig",
|
||||||
|
"tokens_count": "{{count}}nyckel",
|
||||||
|
"tokens_count_plural": "{{count}}nycklar",
|
||||||
|
"Chinese (China)": "Kinesiska (Kina)",
|
||||||
|
"Italian (auto-generated)": "Italienska (auto-genererad)",
|
||||||
|
"channel_tab_shorts_label": "Shorts",
|
||||||
|
"preferences_quality_dash_option_1440p": "1440p",
|
||||||
|
"preferences_quality_dash_option_360p": "360p",
|
||||||
|
"search_message_no_results": "Inga resultat hittades.",
|
||||||
|
"channel_tab_releases_label": "Releaser",
|
||||||
|
"preferences_quality_dash_option_144p": "144p",
|
||||||
|
"Interlingue": "Interlingue (auto-genererad)",
|
||||||
|
"Song: ": "Låt: ",
|
||||||
|
"generic_channels_count": "{{count}} kanal",
|
||||||
|
"generic_channels_count_plural": "{{count}} kanaler",
|
||||||
|
"Chinese (Taiwan)": "Kinesiska (Taiwan)",
|
||||||
|
"preferences_quality_dash_label": "Önskad DASH-videokvalitet: ",
|
||||||
|
"adminprefs_modified_source_code_url_label": "URL till modifierad källkodslager",
|
||||||
|
"Turkish (auto-generated)": "Turkiska (auto-genererad)",
|
||||||
|
"Indonesian (auto-generated)": "Indonesiska (auto-genererad)",
|
||||||
|
"Portuguese (auto-generated)": "Portugisiska (auto-genererad)",
|
||||||
|
"generic_count_years": "{{count}}år",
|
||||||
|
"generic_count_years_plural": "{{count}}år",
|
||||||
|
"videoinfo_invidious_embed_link": "Bädda in länk",
|
||||||
|
"Popular enabled: ": "Populär aktiverad: ",
|
||||||
|
"Spanish (auto-generated)": "Spanska (auto-genererad)",
|
||||||
|
"preferences_quality_option_small": "Liten",
|
||||||
|
"English (United Kingdom)": "Engelska (Storbritannien)",
|
||||||
|
"channel_tab_playlists_label": "Spellistor",
|
||||||
|
"generic_button_edit": "Redigera",
|
||||||
|
"generic_playlists_count": "{{count}} spellista",
|
||||||
|
"generic_playlists_count_plural": "{{count}} spellistor",
|
||||||
|
"preferences_quality_option_hd720": "HD720p",
|
||||||
|
"search_filters_features_option_purchased": "Köpt",
|
||||||
|
"search_filters_date_option_none": "Vilket datum som helst",
|
||||||
|
"preferences_quality_dash_option_auto": "Auto",
|
||||||
|
"Cantonese (Hong Kong)": "Katonesiska (Hong Kong)",
|
||||||
|
"crash_page_report_issue": "Om inget av ovanstående hjälpte, vänligen <a href=\"`x`\">öppna ett nytt nummer på GitHub</a> (helst på engelska) och inkludera följande text i ditt meddelande (översätt INTE den texten):",
|
||||||
|
"crash_page_switch_instance": "försökte <a href=\"`x`\">använda en annan instans</a>",
|
||||||
|
"generic_count_weeks": "{{count}}vecka",
|
||||||
|
"generic_count_weeks_plural": "{{count}}veckor",
|
||||||
|
"videoinfo_watch_on_youTube": "Titta på YouTube",
|
||||||
|
"Music in this video": "Musik i denna video",
|
||||||
|
"footer_modfied_source_code": "Modifierad källkod",
|
||||||
|
"generic_button_rss": "RSS",
|
||||||
|
"preferences_quality_dash_option_4320p": "4320p",
|
||||||
|
"generic_count_hours": "{{count}}timme",
|
||||||
|
"generic_count_hours_plural": "{{count}}timmar",
|
||||||
|
"French (auto-generated)": "Franska (auto-genererad)",
|
||||||
|
"crash_page_read_the_faq": "läs <a href=\"`x`\">Vanliga frågor (FAQ)</a>",
|
||||||
|
"user_created_playlists": "`x` skapade spellistor",
|
||||||
|
"channel_tab_channels_label": "Kanaler",
|
||||||
|
"search_filters_type_option_all": "Vilken typ som helst",
|
||||||
|
"Russian (auto-generated)": "Ryska (auto-genererad)",
|
||||||
|
"preferences_quality_dash_option_480p": "480p",
|
||||||
|
"comments_view_x_replies": "Se {{count}} svar",
|
||||||
|
"comments_view_x_replies_plural": "Se {{count}} svar",
|
||||||
|
"footer_original_source_code": "Ursprunglig källkod",
|
||||||
|
"Portuguese (Brazil)": "Portugisiska (Brasilien)",
|
||||||
|
"search_filters_features_option_vr180": "VR180",
|
||||||
|
"error_video_not_in_playlist": "Den begärda videon finns inte i den här spellistan. <a href=\"`x`\">Klicka här för startsidan för spellistan.</a>",
|
||||||
|
"Dutch (auto-generated)": "Nederländska (auto-genererad)",
|
||||||
|
"generic_count_days": "{{count}}dag",
|
||||||
|
"generic_count_days_plural": "{{count}}dagar",
|
||||||
|
"Vietnamese (auto-generated)": "Vietnamesiska (auto-genererad)",
|
||||||
|
"search_filters_duration_option_none": "Vilken varaktighet som helst",
|
||||||
|
"preferences_quality_dash_option_240p": "240p",
|
||||||
|
"Chinese": "Kinesiska",
|
||||||
|
"preferences_automatic_instance_redirect_label": "Automatisk instansomdirigering (återgång till redirect.invidious.io): ",
|
||||||
|
"generic_button_delete": "Radera",
|
||||||
|
"Import YouTube playlist (.csv)": "Importera YouTube spellista (.csv)",
|
||||||
|
"next_steps_error_message": "Därefter bör du försöka: ",
|
||||||
|
"Standard YouTube license": "Standard YouTube licens",
|
||||||
|
"Import YouTube watch history (.json)": "Importera YouTube visningshistorik (.json)",
|
||||||
|
"search_filters_duration_option_medium": "Medium (4 - 20 minuter)",
|
||||||
|
"generic_count_seconds": "{{count}}sekund",
|
||||||
|
"generic_count_seconds_plural": "{{count}}sekunder",
|
||||||
|
"search_filters_date_label": "Uppladdningsdatum",
|
||||||
|
"crash_page_you_found_a_bug": "Det verkar som att du har hittat en bugg i Invidious!",
|
||||||
|
"generic_views_count": "{{count}} visning",
|
||||||
|
"generic_views_count_plural": "{{count}} visningar",
|
||||||
|
"toggle_theme": "Växla tema"
|
||||||
}
|
}
|
||||||
|
|
|
@ -486,5 +486,7 @@
|
||||||
"playlist_button_add_items": "Video ekle",
|
"playlist_button_add_items": "Video ekle",
|
||||||
"channel_tab_podcasts_label": "Podcast'ler",
|
"channel_tab_podcasts_label": "Podcast'ler",
|
||||||
"generic_channels_count": "{{count}} kanal",
|
"generic_channels_count": "{{count}} kanal",
|
||||||
"generic_channels_count_plural": "{{count}} kanal"
|
"generic_channels_count_plural": "{{count}} kanal",
|
||||||
|
"Import YouTube watch history (.json)": "YouTube İzleme Geçmişini İçe Aktar (.json)",
|
||||||
|
"toggle_theme": "Temayı Değiştir"
|
||||||
}
|
}
|
||||||
|
|
|
@ -503,5 +503,7 @@
|
||||||
"generic_button_save": "Зберегти",
|
"generic_button_save": "Зберегти",
|
||||||
"generic_channels_count_0": "{{count}} канал",
|
"generic_channels_count_0": "{{count}} канал",
|
||||||
"generic_channels_count_1": "{{count}} канали",
|
"generic_channels_count_1": "{{count}} канали",
|
||||||
"generic_channels_count_2": "{{count}} каналів"
|
"generic_channels_count_2": "{{count}} каналів",
|
||||||
|
"Import YouTube watch history (.json)": "Імпортувати історію переглядів YouTube (.json)",
|
||||||
|
"toggle_theme": "Перемкнути тему"
|
||||||
}
|
}
|
||||||
|
|
301
locales/vi.json
301
locales/vi.json
|
@ -1,16 +1,16 @@
|
||||||
{
|
{
|
||||||
"generic_videos_count_0": "{{count}} video",
|
"generic_videos_count_0": "{{count}} video",
|
||||||
"generic_subscribers_count_0": "{{count}} người theo dõi",
|
"generic_subscribers_count_0": "{{count}} người đăng ký",
|
||||||
"LIVE": "TRỰC TIẾP",
|
"LIVE": "TRỰC TIẾP",
|
||||||
"Shared `x` ago": "Đã chia sẻ `x` trước",
|
"Shared `x` ago": "Đã chia sẻ `x` trước",
|
||||||
"Unsubscribe": "Hủy theo dõi",
|
"Unsubscribe": "Hủy đăng ký",
|
||||||
"Subscribe": "Theo dõi",
|
"Subscribe": "Đăng ký",
|
||||||
"View channel on YouTube": "Xem kênh trên YouTube",
|
"View channel on YouTube": "Xem kênh trên YouTube",
|
||||||
"View playlist on YouTube": "Xem danh sách phát trên YouTube",
|
"View playlist on YouTube": "Xem danh sách phát trên YouTube",
|
||||||
"newest": "mới nhất",
|
"newest": "Mới nhất",
|
||||||
"oldest": "lâu đời nhất",
|
"oldest": "Cũ nhất",
|
||||||
"popular": "phổ biến",
|
"popular": "Phổ biến",
|
||||||
"last": "Cuối cùng",
|
"last": "cuối cùng",
|
||||||
"Next page": "Trang tiếp theo",
|
"Next page": "Trang tiếp theo",
|
||||||
"Previous page": "Trang trước",
|
"Previous page": "Trang trước",
|
||||||
"Clear watch history?": "Xóa lịch sử xem?",
|
"Clear watch history?": "Xóa lịch sử xem?",
|
||||||
|
@ -18,45 +18,45 @@
|
||||||
"New passwords must match": "Mật khẩu mới phải khớp",
|
"New passwords must match": "Mật khẩu mới phải khớp",
|
||||||
"Authorize token?": "Cấp phép mã thông báo?",
|
"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`?",
|
"Authorize token for `x`?": "Cấp phép mã thông báo cho `x`?",
|
||||||
"Yes": "Đúng",
|
"Yes": "Có",
|
||||||
"No": "Không",
|
"No": "Không",
|
||||||
"Import and Export Data": "Nhập và xuất dữ liệu",
|
"Import and Export Data": "Nhập và xuất dữ liệu",
|
||||||
"Import": "Nhập",
|
"Import": "Nhập",
|
||||||
"Import Invidious data": "Nhập dữ liệu Invidious JSON",
|
"Import Invidious data": "Nhập dữ liệu Invidious dưới dạng JSON",
|
||||||
"Import YouTube subscriptions": "Nhập dữ liệu thuê bao YouTube/OPML",
|
"Import YouTube subscriptions": "Nhập các kênh đã đăng ký từ YouTube/OPML",
|
||||||
"Import FreeTube subscriptions (.db)": "Nhập đăng ký FreeTube (.db)",
|
"Import FreeTube subscriptions (.db)": "Nhập các kênh đã đăng ký từ FreeTube (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Nhập đăng ký NewPipe (.json)",
|
"Import NewPipe subscriptions (.json)": "Nhập các kênh đã đăng ký từ NewPipe (.json)",
|
||||||
"Import NewPipe data (.zip)": "Nhập dữ liệu NewPipe (.zip)",
|
"Import NewPipe data (.zip)": "Nhập dữ liệu từ NewPipe (.zip)",
|
||||||
"Export": "Xuất",
|
"Export": "Xuất",
|
||||||
"Export subscriptions as OPML": "Xuất đăng ký dưới dạng OPML",
|
"Export subscriptions as OPML": "Xuất các kênh đã đă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 subscriptions as OPML (for NewPipe & FreeTube)": "Xuất các kênh đã đăng ký dưới dạng OPML (cho NewPipe & FreeTube)",
|
||||||
"Export data as JSON": "Xuất dữ liệu Invidious 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?",
|
"Delete account?": "Xóa tài khoản?",
|
||||||
"History": "Lịch sử",
|
"History": "Lịch sử",
|
||||||
"An alternative front-end to YouTube": "Giao diện người dùng thay thế cho YouTube",
|
"An alternative front-end to YouTube": "Một front-end thay thế cho YouTube",
|
||||||
"JavaScript license information": "Thông tin giấy phép JavaScript",
|
"JavaScript license information": "Thông tin giấy phép JavaScript",
|
||||||
"source": "nguồn",
|
"source": "nguồn",
|
||||||
"Log in": "Đăng nhập",
|
"Log in": "Đăng nhập",
|
||||||
"Log in/register": "Đăng nhập / đăng ký",
|
"Log in/register": "Đăng nhập / đăng ký",
|
||||||
"User ID": "Tên người dùng",
|
"User ID": "ID người dùng",
|
||||||
"Password": "Mật khẩu",
|
"Password": "Mật khẩu",
|
||||||
"Time (h:mm:ss):": "Thời gian (h:mm:ss):",
|
"Time (h:mm:ss):": "Thời gian (h:mm:ss):",
|
||||||
"Text CAPTCHA": "Nhắn tin tới CAPTCHA",
|
"Text CAPTCHA": "CAPTCHA dạng chữ",
|
||||||
"Image CAPTCHA": "Hình ảnh CAPTCHA",
|
"Image CAPTCHA": "CAPTCHA dạng ảnh",
|
||||||
"Sign In": "Đăng nhập",
|
"Sign In": "Đăng nhập",
|
||||||
"Register": "Đăng ký",
|
"Register": "Đăng ký",
|
||||||
"E-mail": "E-mail",
|
"E-mail": "E-mail",
|
||||||
"Preferences": "Sở thích",
|
"Preferences": "Sở thích",
|
||||||
"preferences_category_player": "Tùy chọn trình phát video",
|
"preferences_category_player": "Tùy chọn trình phát video",
|
||||||
"preferences_video_loop_label": "Luôn lặp lại: ",
|
"preferences_video_loop_label": "Luôn lặp lại: ",
|
||||||
"preferences_autoplay_label": "Tự chạy: ",
|
"preferences_autoplay_label": "Tự động phát: ",
|
||||||
"preferences_continue_label": "Phát kế tiếp 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_continue_autoplay_label": "Tự động phát video tiếp theo: ",
|
||||||
"preferences_listen_label": "Nghe theo mặc định: ",
|
"preferences_listen_label": "Nghe theo mặc định: ",
|
||||||
"preferences_local_label": "Video proxy: ",
|
"preferences_local_label": "Video proxy: ",
|
||||||
"preferences_speed_label": "Tốc độ mặc định: ",
|
"preferences_speed_label": "Tốc độ mặc định: ",
|
||||||
"preferences_quality_label": "Chất lượng video ưa thích: ",
|
"preferences_quality_label": "Chất lượng video ưa thích: ",
|
||||||
"preferences_volume_label": "Âm lượng trình phát video: ",
|
"preferences_volume_label": "Âm lượng video: ",
|
||||||
"preferences_comments_label": "Nhận xét mặc định: ",
|
"preferences_comments_label": "Nhận xét mặc định: ",
|
||||||
"youtube": "YouTube",
|
"youtube": "YouTube",
|
||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
"Fallback captions: ": "Phụ đề dự phòng: ",
|
"Fallback captions: ": "Phụ đề dự phòng: ",
|
||||||
"preferences_related_videos_label": "Hiển thị các video có liên quan: ",
|
"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_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_extend_desc_label": "Tự động mở rộng phần mô tả của video: ",
|
||||||
"preferences_vr_mode_label": "Video 360 độ tương tác (yêu cầu WebGL): ",
|
"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_category_visual": "Tùy chọn hình ảnh",
|
||||||
"preferences_player_style_label": "Phong cách trình phát: ",
|
"preferences_player_style_label": "Phong cách trình phát: ",
|
||||||
|
@ -82,13 +82,13 @@
|
||||||
"preferences_sort_label": "Sắp xếp video theo: ",
|
"preferences_sort_label": "Sắp xếp video theo: ",
|
||||||
"published": "được phát hành",
|
"published": "được phát hành",
|
||||||
"published - reverse": "đã xuất bản - đảo ngược",
|
"published - reverse": "đã xuất bản - đảo ngược",
|
||||||
"alphabetically": "theo thứ tự bảng chữ cái",
|
"alphabetically": "Thứ tự (A - Z)",
|
||||||
"alphabetically - reverse": "theo thứ tự bảng chữ cái - đảo ngược",
|
"alphabetically - reverse": "Thứ tự (Z - A)",
|
||||||
"channel name": "Tên kênh",
|
"channel name": "Tên kênh (A - Z)",
|
||||||
"channel name - reverse": "tên kênh - đảo ngược",
|
"channel name - reverse": "Tên kênh (Z - A)",
|
||||||
"Only show latest video from channel: ": "Chỉ hiển thị video mới nhất từ kênh: ",
|
"Only show latest video from channel: ": "Chỉ hiển thị video mới nhất từ kênh: ",
|
||||||
"Only show latest unwatched video from channel: ": "Chỉ hiển thị video chưa xem mới nhất từ kênh: ",
|
"Only show latest unwatched video from channel: ": "Chỉ hiển thị video chưa xem mới nhất từ kênh: ",
|
||||||
"preferences_unseen_only_label": "Chỉ hiển thị chưa xem: ",
|
"preferences_unseen_only_label": "Chỉ hiển thị các video chưa từng xem: ",
|
||||||
"preferences_notifications_only_label": "Chỉ hiển thị thông báo (nếu có): ",
|
"preferences_notifications_only_label": "Chỉ hiển thị thông báo (nếu có): ",
|
||||||
"Enable web notifications": "Bật thông báo web",
|
"Enable web notifications": "Bật thông báo web",
|
||||||
"`x` uploaded a video": "`x` đã tải lên một video",
|
"`x` uploaded a video": "`x` đã tải lên một video",
|
||||||
|
@ -99,7 +99,7 @@
|
||||||
"Change password": "Đổi mật khẩu",
|
"Change password": "Đổi mật khẩu",
|
||||||
"Manage subscriptions": "Quản lý các mục đăng kí",
|
"Manage subscriptions": "Quản lý các mục đăng kí",
|
||||||
"Manage tokens": "Quản lý mã thông báo",
|
"Manage tokens": "Quản lý mã thông báo",
|
||||||
"Watch history": "Lịch sử xem",
|
"Watch history": "Xem lịch sử",
|
||||||
"Delete account": "Xóa tài khoản",
|
"Delete account": "Xóa tài khoản",
|
||||||
"preferences_category_admin": "Tùy chọn quản trị viên",
|
"preferences_category_admin": "Tùy chọn quản trị viên",
|
||||||
"preferences_default_home_label": "Trang chủ mặc định: ",
|
"preferences_default_home_label": "Trang chủ mặc định: ",
|
||||||
|
@ -121,7 +121,7 @@
|
||||||
"View privacy policy.": "Xem chính sách bảo mật.",
|
"View privacy policy.": "Xem chính sách bảo mật.",
|
||||||
"Trending": "Xu hướng",
|
"Trending": "Xu hướng",
|
||||||
"Public": "Công khai",
|
"Public": "Công khai",
|
||||||
"Unlisted": "Không hiển thị",
|
"Unlisted": "Không công khai",
|
||||||
"Private": "Riêng tư",
|
"Private": "Riêng tư",
|
||||||
"View all playlists": "Xem tất cả danh sách phát",
|
"View all playlists": "Xem tất cả danh sách phát",
|
||||||
"Updated `x` ago": "Đã cập nhật` x` trước",
|
"Updated `x` ago": "Đã cập nhật` x` trước",
|
||||||
|
@ -131,24 +131,24 @@
|
||||||
"Title": "Tiêu đề",
|
"Title": "Tiêu đề",
|
||||||
"Playlist privacy": "Bảo mật danh sách phát",
|
"Playlist privacy": "Bảo mật danh sách phát",
|
||||||
"Editing playlist `x`": "Chỉnh sửa danh sách phát` x`",
|
"Editing playlist `x`": "Chỉnh sửa danh sách phát` x`",
|
||||||
"Show more": "Cho xem nhiều hơn",
|
"Show more": "Hiển thị thêm",
|
||||||
"Show less": "Hiện ít hơn",
|
"Show less": "Hiển thị ít hơn",
|
||||||
"Watch on YouTube": "Xem trên YouTube",
|
"Watch on YouTube": "Xem trên YouTube",
|
||||||
"Switch Invidious Instance": "Chuyển phiên bản Invidious",
|
"Switch Invidious Instance": "Chuyển phiên bản Invidious",
|
||||||
"Hide annotations": "Ẩn chú thích",
|
"Hide annotations": "Ẩn chú thích",
|
||||||
"Show annotations": "Hiển thị chú thích",
|
"Show annotations": "Hiển thị chú thích",
|
||||||
"Genre: ": "Thể loại: ",
|
"Genre: ": "Thể loại: ",
|
||||||
"License: ": "Giấy phép: ",
|
"License: ": "Giấy phép: ",
|
||||||
"Family friendly? ": "Gia đình thân thiện? ",
|
"Family friendly? ": "Thân thiện với gia đình? ",
|
||||||
"Wilson score: ": "Điểm số Wilson: ",
|
"Wilson score: ": "Điểm số Wilson: ",
|
||||||
"Engagement: ": "Hôn ước: ",
|
"Engagement: ": "Hôn ước: ",
|
||||||
"Whitelisted regions: ": "Các vùng nằm trong danh sách trắng: ",
|
"Whitelisted regions: ": "Các vùng nằm trong danh sách trắng: ",
|
||||||
"Blacklisted regions: ": "Khu vực nằm trong danh sách đen: ",
|
"Blacklisted regions: ": "Các vùng nằm trong danh sách đen: ",
|
||||||
"Shared `x`": "Chia sẻ` x`",
|
"Shared `x`": "Chia sẻ` x`",
|
||||||
"View Reddit comments": "Xem nhận xét trên Reddit",
|
"View Reddit comments": "Xem bình luận trên Reddit",
|
||||||
"Hide replies": "Ẩn câu trả lời",
|
"Hide replies": "Ẩn phản hồi",
|
||||||
"Show replies": "Hiển thị câu trả lời",
|
"Show replies": "Hiển thị phản hồi",
|
||||||
"Incorrect password": "Mật khẩu không đúng",
|
"Incorrect password": "Mật khẩu không chính xác",
|
||||||
"Wrong answer": "Câu trả lời sai",
|
"Wrong answer": "Câu trả lời sai",
|
||||||
"Erroneous CAPTCHA": "CAPTCHA bị lỗi",
|
"Erroneous CAPTCHA": "CAPTCHA bị lỗi",
|
||||||
"CAPTCHA is a required field": "CAPTCHA là trường bắt buộc",
|
"CAPTCHA is a required field": "CAPTCHA là trường bắt buộc",
|
||||||
|
@ -190,35 +190,35 @@
|
||||||
"Bulgarian": "Tiếng Bungari",
|
"Bulgarian": "Tiếng Bungari",
|
||||||
"Burmese": "Tiếng Miến Điện",
|
"Burmese": "Tiếng Miến Điện",
|
||||||
"Catalan": "Tiếng Catalan",
|
"Catalan": "Tiếng Catalan",
|
||||||
"Cebuano": "Cebuano",
|
"Cebuano": "Tiếng Cebu",
|
||||||
"Chinese (Simplified)": "Tiếng Trung (Giản thể)",
|
"Chinese (Simplified)": "Tiếng Trung (Giản thể)",
|
||||||
"Chinese (Traditional)": "Tiếng Trung (Phồn thể)",
|
"Chinese (Traditional)": "Tiếng Trung (Phồn thể)",
|
||||||
"Corsican": "Corsican",
|
"Corsican": "Tiếng Corse",
|
||||||
"Croatian": "Tiếng Croatia",
|
"Croatian": "Tiếng Croatia",
|
||||||
"Czech": "Tiếng Séc",
|
"Czech": "Tiếng Séc",
|
||||||
"Danish": "Người Đan Mạch",
|
"Danish": "Tiếng Đan Mạch",
|
||||||
"Dutch": "Tiếng Hà Lan",
|
"Dutch": "Tiếng Hà Lan",
|
||||||
"Esperanto": "Quốc tế ngữ",
|
"Esperanto": "Quốc tế ngữ",
|
||||||
"Estonian": "Tiếng Estonia",
|
"Estonian": "Tiếng Estonia",
|
||||||
"Filipino": "Filipino",
|
"Filipino": "Tiếng Philippines",
|
||||||
"Finnish": "Tiếng Phần Lan",
|
"Finnish": "Tiếng Phần Lan",
|
||||||
"French": "Người Pháp",
|
"French": "Tiếng Pháp",
|
||||||
"Galician": "Tiếng Galicia",
|
"Galician": "Tiếng Galicia",
|
||||||
"Georgian": "Tiếng Georgia",
|
"Georgian": "Tiếng Georgia",
|
||||||
"German": "Tiếng Đức",
|
"German": "Tiếng Đức",
|
||||||
"Greek": "Người Hy Lạp",
|
"Greek": "Tiếng Hy Lạp",
|
||||||
"Gujarati": "Gujarati",
|
"Gujarati": "Tiếng Gujarat",
|
||||||
"Haitian Creole": "Tiếng Creole của Haiti",
|
"Haitian Creole": "Tiếng Creole (Haiti)",
|
||||||
"Hausa": "Hausa",
|
"Hausa": "Tiếng Hausa",
|
||||||
"Hawaiian": "Tiếng Hawaii",
|
"Hawaiian": "Tiếng Hawaii",
|
||||||
"Hebrew": "Tiếng Do Thái",
|
"Hebrew": "Tiếng Do Thái",
|
||||||
"Hindi": "Tiếng Hindi",
|
"Hindi": "Tiếng Hindi",
|
||||||
"Hmong": "Hmong",
|
"Hmong": "Tiếng Hmong",
|
||||||
"Hungarian": "Người Hungary",
|
"Hungarian": "Tiếng Hungary",
|
||||||
"Icelandic": "Tiếng Iceland",
|
"Icelandic": "Tiếng Iceland",
|
||||||
"Igbo": "Igbo",
|
"Igbo": "Tiếng Igbo",
|
||||||
"Indonesian": "Tiếng Indonesia",
|
"Indonesian": "Tiếng Indonesia",
|
||||||
"Irish": "Tiếng Ailen",
|
"Irish": "Tiếng Ireland",
|
||||||
"Italian": "Tiếng Ý",
|
"Italian": "Tiếng Ý",
|
||||||
"Japanese": "Tiếng Nhật",
|
"Japanese": "Tiếng Nhật",
|
||||||
"Javanese": "Tiếng Java",
|
"Javanese": "Tiếng Java",
|
||||||
|
@ -237,37 +237,37 @@
|
||||||
"Malagasy": "Tiếng Malagasy",
|
"Malagasy": "Tiếng Malagasy",
|
||||||
"Malay": "Tiếng Mã Lai",
|
"Malay": "Tiếng Mã Lai",
|
||||||
"Malayalam": "Tiếng Malayalam",
|
"Malayalam": "Tiếng Malayalam",
|
||||||
"Maltese": "Cây nho",
|
"Maltese": "Tiếng Malta",
|
||||||
"Maori": "Tiếng Maori",
|
"Maori": "Tiếng Maori",
|
||||||
"Marathi": "Marathi",
|
"Marathi": "Tiếng Marathi",
|
||||||
"Mongolian": "Tiếng Mông Cổ",
|
"Mongolian": "Tiếng Mông Cổ",
|
||||||
"Nepali": "Tiếng Nepal",
|
"Nepali": "Tiếng Nepal",
|
||||||
"Norwegian Bokmål": "Tiếng Na Uy Bokmål",
|
"Norwegian Bokmål": "Tiếng Na Uy (Bokmål)",
|
||||||
"Nyanja": "Nyanja",
|
"Nyanja": "Tiếng Chewa / Nyanja",
|
||||||
"Pashto": "Pashto",
|
"Pashto": "Tiếng Pashtun",
|
||||||
"Persian": "Tiếng Ba Tư",
|
"Persian": "Tiếng Ba Tư",
|
||||||
"Polish": "Đánh bóng",
|
"Polish": "Tiếng Ba Lan",
|
||||||
"Portuguese": "Tiếng Bồ Đào Nha",
|
"Portuguese": "Tiếng Bồ Đào Nha",
|
||||||
"Punjabi": "Punjabi",
|
"Punjabi": "Tiếng Punjab",
|
||||||
"Romanian": "Tiếng Rumani",
|
"Romanian": "Tiếng Rumani",
|
||||||
"Russian": "Tiếng Nga",
|
"Russian": "Tiếng Nga",
|
||||||
"Samoan": "Samoan",
|
"Samoan": "Tiếng Samoa",
|
||||||
"Scottish Gaelic": "Tiếng Gaelic Scotland",
|
"Scottish Gaelic": "Tiếng Gaelic (Scotland)",
|
||||||
"Serbian": "Tiếng Serbia",
|
"Serbian": "Tiếng Serbia",
|
||||||
"Shona": "Shona",
|
"Shona": "Tiếng Shona",
|
||||||
"Sindhi": "Sindhi",
|
"Sindhi": "Tiếng Sindh",
|
||||||
"Sinhala": "Sinhala",
|
"Sinhala": "Tiếng Sinhala",
|
||||||
"Slovak": "Tiếng Slovak",
|
"Slovak": "Tiếng Slovak",
|
||||||
"Slovenian": "Tiếng Slovenia",
|
"Slovenian": "Tiếng Slovenia",
|
||||||
"Somali": "Tiếng Somali",
|
"Somali": "Tiếng Somali",
|
||||||
"Southern Sotho": "Southern Sotho",
|
"Southern Sotho": "Southern Sotho",
|
||||||
"Spanish": "Người Tây Ban Nha",
|
"Spanish": "Tiếng Tây Ban Nha",
|
||||||
"Spanish (Latin America)": "Tiếng Tây Ban Nha (Mỹ Latinh)",
|
"Spanish (Latin America)": "Tiếng Tây Ban Nha (Mỹ Latinh)",
|
||||||
"Sundanese": "Tiếng Sundan",
|
"Sundanese": "Tiếng Sundan",
|
||||||
"Swahili": "Tiếng Swahili",
|
"Swahili": "Tiếng Swahili",
|
||||||
"Swedish": "Tiếng Thụy Điển",
|
"Swedish": "Tiếng Thụy Điển",
|
||||||
"Tajik": "Tajik",
|
"Tajik": "Tiếng Tajik",
|
||||||
"Tamil": "Tamil",
|
"Tamil": "Tiếng Tamil",
|
||||||
"Telugu": "Tiếng Telugu",
|
"Telugu": "Tiếng Telugu",
|
||||||
"Thai": "Tiếng Thái",
|
"Thai": "Tiếng Thái",
|
||||||
"Turkish": "Tiếng Thổ Nhĩ Kỳ",
|
"Turkish": "Tiếng Thổ Nhĩ Kỳ",
|
||||||
|
@ -275,17 +275,17 @@
|
||||||
"Urdu": "Tiếng Urdu",
|
"Urdu": "Tiếng Urdu",
|
||||||
"Uzbek": "Tiếng Uzbek",
|
"Uzbek": "Tiếng Uzbek",
|
||||||
"Vietnamese": "Tiếng Việt",
|
"Vietnamese": "Tiếng Việt",
|
||||||
"Welsh": "Người xứ Wales",
|
"Welsh": "Tiếng Wales",
|
||||||
"Western Frisian": "Western Frisian",
|
"Western Frisian": "Tiếng Tây Frisia",
|
||||||
"Xhosa": "Xhosa",
|
"Xhosa": "Tiếng Nam Phi",
|
||||||
"Yiddish": "Yiddish",
|
"Yiddish": "Tiếng Yiddish",
|
||||||
"Yoruba": "Yoruba",
|
"Yoruba": "Tiếng Yoruba",
|
||||||
"Zulu": "Tiếng Zulu",
|
"Zulu": "Tiếng Zulu",
|
||||||
"Fallback comments: ": "Nhận xét dự phòng: ",
|
"Fallback comments: ": "Nhận xét dự phòng: ",
|
||||||
"Popular": "Phổ biến",
|
"Popular": "Phổ biến",
|
||||||
"Search": "Tìm kiếm",
|
"Search": "Tìm kiếm",
|
||||||
"Top": "Hàng đầu",
|
"Top": "Hàng đầu",
|
||||||
"About": "Trong khoảng",
|
"About": "Giới thiệu",
|
||||||
"Rating: ": "Xếp hạng: ",
|
"Rating: ": "Xếp hạng: ",
|
||||||
"preferences_locale_label": "Ngôn ngữ: ",
|
"preferences_locale_label": "Ngôn ngữ: ",
|
||||||
"View as playlist": "Xem dưới dạng danh sách phát",
|
"View as playlist": "Xem dưới dạng danh sách phát",
|
||||||
|
@ -295,45 +295,45 @@
|
||||||
"News": "Tin tức",
|
"News": "Tin tức",
|
||||||
"Movies": "Phim",
|
"Movies": "Phim",
|
||||||
"Download": "Tải xuống",
|
"Download": "Tải xuống",
|
||||||
"Download as: ": "Tải tệp dưới dạng: ",
|
"Download as: ": "Tải xuống dưới dạng: ",
|
||||||
"%A %B %-d, %Y": "% A% B% -d,% Y",
|
"%A %B %-d, %Y": "% A% B% -d,% Y",
|
||||||
"(edited)": "(đã chỉnh sửa)",
|
"(edited)": "(đã chỉnh sửa)",
|
||||||
"YouTube comment permalink": "Liên kết cố định nhận xét trên YouTube",
|
"YouTube comment permalink": "Liên kết cố định nhận xét trên YouTube",
|
||||||
"permalink": "liên kết cố định",
|
"permalink": "liên kết cố định",
|
||||||
"`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤",
|
"`x` marked it with a ❤": "` x` đã đánh dấu nó bằng một ❤",
|
||||||
"Audio mode": "Chế độ âm thanh",
|
"Audio mode": "Chế độ audio",
|
||||||
"Video mode": "Chế độ quay",
|
"Video mode": "Chế độ video",
|
||||||
"channel_tab_videos_label": "Video",
|
"channel_tab_videos_label": "Video",
|
||||||
"Playlists": "Danh sách phát",
|
"Playlists": "Danh sách phát",
|
||||||
"channel_tab_community_label": "Cộng đồng",
|
"channel_tab_community_label": "Cộng đồng",
|
||||||
"search_filters_sort_option_relevance": "liên quan",
|
"search_filters_sort_option_relevance": "Liên quan",
|
||||||
"search_filters_sort_option_rating": "Xếp hạng",
|
"search_filters_sort_option_rating": "Xếp hạng",
|
||||||
"search_filters_sort_option_date": "ngày",
|
"search_filters_sort_option_date": "Ngày tải lên",
|
||||||
"search_filters_sort_option_views": "lượt xem",
|
"search_filters_sort_option_views": "Lượt xem",
|
||||||
"search_filters_type_label": "content_type",
|
"search_filters_type_label": "Thể loại",
|
||||||
"search_filters_duration_label": "thời lượng",
|
"search_filters_duration_label": "Thời lượng",
|
||||||
"search_filters_features_label": "đặc trưng",
|
"search_filters_features_label": "Đặc điểm",
|
||||||
"search_filters_sort_label": "sắp xếp",
|
"search_filters_sort_label": "Sắp xếp theo",
|
||||||
"search_filters_date_option_hour": "giờ",
|
"search_filters_date_option_hour": "Một giờ qua",
|
||||||
"search_filters_date_option_today": "hôm nay",
|
"search_filters_date_option_today": "Hôm nay",
|
||||||
"search_filters_date_option_week": "tuần",
|
"search_filters_date_option_week": "Tuần này",
|
||||||
"search_filters_date_option_month": "tháng",
|
"search_filters_date_option_month": "Tháng này",
|
||||||
"search_filters_date_option_year": "năm",
|
"search_filters_date_option_year": "Năm này",
|
||||||
"search_filters_type_option_video": "video",
|
"search_filters_type_option_video": "video",
|
||||||
"search_filters_type_option_channel": "kênh",
|
"search_filters_type_option_channel": "Kênh",
|
||||||
"search_filters_type_option_playlist": "danh sách phát",
|
"search_filters_type_option_playlist": "Danh sách phát",
|
||||||
"search_filters_type_option_movie": "bộ phim",
|
"search_filters_type_option_movie": "Phim",
|
||||||
"search_filters_type_option_show": "chỉ",
|
"search_filters_type_option_show": "Hiện",
|
||||||
"search_filters_features_option_hd": "hd",
|
"search_filters_features_option_hd": "HD",
|
||||||
"search_filters_features_option_subtitles": "phụ đề",
|
"search_filters_features_option_subtitles": "Phụ đề",
|
||||||
"search_filters_features_option_c_commons": "Commons sáng tạo",
|
"search_filters_features_option_c_commons": "Giấy phép Creative Commons",
|
||||||
"search_filters_features_option_three_d": "3d",
|
"search_filters_features_option_three_d": "3D",
|
||||||
"search_filters_features_option_live": "trực tiếp",
|
"search_filters_features_option_live": "Trực tiếp",
|
||||||
"search_filters_features_option_four_k": "4k",
|
"search_filters_features_option_four_k": "4K",
|
||||||
"search_filters_features_option_location": "vị trí",
|
"search_filters_features_option_location": "Vị trí",
|
||||||
"search_filters_features_option_hdr": "hdr",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"Current version: ": "Phiên bản hiện tại: ",
|
"Current version: ": "Phiên bản hiện tại: ",
|
||||||
"search_filters_title": "bộ lọc",
|
"search_filters_title": "Bộ lọc",
|
||||||
"generic_playlists_count": "{{count}} danh sách phát",
|
"generic_playlists_count": "{{count}} danh sách phát",
|
||||||
"generic_views_count": "{{count}} lượt xem",
|
"generic_views_count": "{{count}} lượt xem",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
|
@ -350,31 +350,31 @@
|
||||||
"preferences_quality_dash_label": "Chất lượng video DASH ưa thích ",
|
"preferences_quality_dash_label": "Chất lượng video DASH ưa thích ",
|
||||||
"preferences_quality_dash_option_auto": "Tự động",
|
"preferences_quality_dash_option_auto": "Tự động",
|
||||||
"Subscriptions": "Thuê bao",
|
"Subscriptions": "Thuê bao",
|
||||||
"View YouTube comments": "Hiển thị bình luận trên YouTube",
|
"View YouTube comments": "Hiển thị bình luận từ YouTube",
|
||||||
"View more comments on Reddit": "Hiển thị thêm bình luận từ Reddit",
|
"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",
|
"Music in this video": "Nhạc trong video này",
|
||||||
"Artist: ": "Nghệ sĩ: ",
|
"Artist: ": "Nghệ sĩ: ",
|
||||||
"Premieres `x`": "Phát lần đầu `x`",
|
"Premieres `x`": "Phát lần đầu `x`",
|
||||||
"preferences_region_label": "Nội dung theo quốc gia ",
|
"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.",
|
"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_option_small": "Thấp",
|
||||||
"preferences_quality_dash_option_144p": "144p",
|
"preferences_quality_dash_option_144p": "144p",
|
||||||
"invidious": "Invidious",
|
"invidious": "Invidious",
|
||||||
"preferences_quality_dash_option_240p": "240p",
|
"preferences_quality_dash_option_240p": "240p",
|
||||||
"Import/export": "Xuất/nhập dữ liệu",
|
"Import/export": "Nhập/Xuất",
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
"preferences_quality_dash_option_4320p": "4320p (8K)",
|
||||||
"preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)",
|
"preferences_quality_option_dash": "DASH (tự tối ưu chất lượng)",
|
||||||
"generic_subscriptions_count_0": "{{count}} người đăng kí",
|
"generic_subscriptions_count_0": "{{count}} người đăng kí",
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
"preferences_quality_dash_option_1440p": "1440p (2K)",
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
"preferences_quality_dash_option_480p": "480p",
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
"preferences_quality_dash_option_2160p": "2160p (4K)",
|
||||||
"search_message_no_results": "Tìm kiếm không có kết quả.",
|
"search_message_no_results": "Tìm kiếm không có kết quả.",
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
"preferences_quality_dash_option_1080p": "1080p",
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
"preferences_quality_dash_option_720p": "720p",
|
||||||
"preferences_quality_option_medium": "Trung bình",
|
"preferences_quality_option_medium": "Trung bình",
|
||||||
"Load more": "Hiển thị thêm",
|
"Load more": "Tải thêm",
|
||||||
"comments_points_count_0": "{{count}} điểm",
|
"comments_points_count_0": "{{count}} điểm",
|
||||||
"Import YouTube playlist (.csv)": "Nhập danh sách phát YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Nhập các danh sách phát từ YouTube (.csv)",
|
||||||
"preferences_quality_dash_option_best": "Tốt nhất",
|
"preferences_quality_dash_option_best": "Tốt nhất",
|
||||||
"preferences_quality_dash_option_360p": "360p",
|
"preferences_quality_dash_option_360p": "360p",
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}} thông báo chưa đọc",
|
"subscriptions_unseen_notifs_count_0": "{{count}} thông báo chưa đọc",
|
||||||
|
@ -382,10 +382,93 @@
|
||||||
"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>.",
|
"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",
|
"Standard YouTube license": "Giấy phép YouTube thông thường",
|
||||||
"Album: ": "Album: ",
|
"Album: ": "Album: ",
|
||||||
"preferences_save_player_pos_label": "Lưu vị trí xem cuối cùng ",
|
"preferences_save_player_pos_label": "Lưu vị trí xem: ",
|
||||||
"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.",
|
"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.",
|
||||||
"Chinese (China)": "Tiếng Trung (Trung Quốc)",
|
"Chinese (China)": "Tiếng Trung (Trung Quốc)",
|
||||||
"generic_button_cancel": "Hủy",
|
"generic_button_cancel": "Hủy",
|
||||||
"Chinese": "Tiếng Trung",
|
"Chinese": "Tiếng Trung",
|
||||||
"generic_button_delete": "Xóa"
|
"generic_button_delete": "Xóa",
|
||||||
|
"Korean (auto-generated)": "Tiếng Hàn (được tạo tự động)",
|
||||||
|
"search_filters_features_option_three_sixty": "360°",
|
||||||
|
"channel_tab_podcasts_label": "Podcast",
|
||||||
|
"Spanish (Mexico)": "Tiếng Tây Ban Nha (Mexico)",
|
||||||
|
"search_filters_apply_button": "Áp dụng các mục đã chọn",
|
||||||
|
"Download is disabled": "Tải xuống đã bị vô hiệu hóa.",
|
||||||
|
"next_steps_error_message_go_to_youtube": "Đi đến YouTube",
|
||||||
|
"German (auto-generated)": "Tiếng Đức (được tạo tự động)",
|
||||||
|
"Japanese (auto-generated)": "Tiếng Nhật (được tạo tự động)",
|
||||||
|
"footer_donate_page": "Ủng hộ",
|
||||||
|
"crash_page_before_reporting": "Trước khi báo cáo lỗi, hãy chắc chắn rằng bạn đã:",
|
||||||
|
"Channel Sponsor": "Nhà tài trợ của kênh",
|
||||||
|
"videoinfo_started_streaming_x_ago": "Đã bắt đầu phát sóng `x` trước",
|
||||||
|
"videoinfo_youTube_embed_link": "Nhúng",
|
||||||
|
"channel_tab_streams_label": "Phát trực tiếp",
|
||||||
|
"playlist_button_add_items": "Thêm video",
|
||||||
|
"generic_count_minutes_0": "{{count}} phút",
|
||||||
|
"user_saved_playlists": "`x` danh sách phát đã lưu",
|
||||||
|
"Spanish (Spain)": "Tiếng Tây Ban Nha (Tây Ban Nha)",
|
||||||
|
"crash_page_refresh": "Đã thử <a href=\"`x`\">tải lại trang</a>",
|
||||||
|
"Chinese (Hong Kong)": "Tiếng Trung (Hồng Kông)",
|
||||||
|
"generic_count_months_0": "{{count}} tháng",
|
||||||
|
"download_subtitles": "Phụ đề - `x` (.vtt)",
|
||||||
|
"generic_button_save": "Lưu",
|
||||||
|
"crash_page_search_issue": "Tìm <a href=\"`x`\">lỗi có sẵn trên GitHub</a>",
|
||||||
|
"none": "không",
|
||||||
|
"English (United States)": "Tiếng Anh (Mỹ)",
|
||||||
|
"next_steps_error_message_refresh": "Tải lại",
|
||||||
|
"Video unavailable": "Video không có sẵn",
|
||||||
|
"footer_source_code": "Mã nguồn",
|
||||||
|
"search_filters_duration_option_short": "Ngắn (< 4 phút)",
|
||||||
|
"search_filters_duration_option_long": "Dài (> 20 phút)",
|
||||||
|
"tokens_count_0": "{{count}} mã thông báo",
|
||||||
|
"Italian (auto-generated)": "Tiếng Ý (được tạo tự động)",
|
||||||
|
"channel_tab_shorts_label": "Shorts",
|
||||||
|
"channel_tab_releases_label": "Mới tải lên",
|
||||||
|
"`x` ago": "`x` trước",
|
||||||
|
"Interlingue": "Tiếng Khoa học Quốc tế",
|
||||||
|
"generic_channels_count_0": "{{count}} kênh",
|
||||||
|
"Chinese (Taiwan)": "Tiếng Trung (Đài Loan)",
|
||||||
|
"adminprefs_modified_source_code_url_label": "URL tới kho lưu trữ mã nguồn đã sửa đổi",
|
||||||
|
"Turkish (auto-generated)": "Tiếng Thổ Nhĩ Kỳ (được tạo tự động)",
|
||||||
|
"Indonesian (auto-generated)": "Tiếng Indonesia (được tạo tự động)",
|
||||||
|
"Portuguese (auto-generated)": "Tiếng Bồ Đào Nha (được tạo tự động)",
|
||||||
|
"generic_count_years_0": "{{count}} năm",
|
||||||
|
"videoinfo_invidious_embed_link": "Liên kết nhúng",
|
||||||
|
"Popular enabled: ": "Đã bật phổ biến: ",
|
||||||
|
"Spanish (auto-generated)": "Tiếng Tây Ban Nha (được tạo tự động)",
|
||||||
|
"English (United Kingdom)": "Tiếng Anh Anh",
|
||||||
|
"channel_tab_playlists_label": "Danh sách phát",
|
||||||
|
"generic_button_edit": "Sửa",
|
||||||
|
"search_filters_features_option_purchased": "Đã mua",
|
||||||
|
"search_filters_date_option_none": "Mọi thời điểm",
|
||||||
|
"Cantonese (Hong Kong)": "Tiếng Quảng Châu (Hồng Kông)",
|
||||||
|
"crash_page_report_issue": "Nếu các điều trên không giúp được, xin hãy <a href=\"`x`\">tạo vấn đề mới trên GitHub</a> (ưu tiên tiếng Anh) và đính kèm đoạn chữ sau trong nội dung (giữ nguyên KHÔNG dịch):",
|
||||||
|
"crash_page_switch_instance": "Đã thử <a href=\"`x`\">dùng một phiên bản khác</a>",
|
||||||
|
"generic_count_weeks_0": "{{count}} tuần",
|
||||||
|
"videoinfo_watch_on_youTube": "Xem trên YouTube",
|
||||||
|
"footer_modfied_source_code": "Mã nguồn đã chỉnh sửa",
|
||||||
|
"generic_button_rss": "RSS",
|
||||||
|
"generic_count_hours_0": "{{count}} giờ",
|
||||||
|
"French (auto-generated)": "Tiếng Pháp (được tạo tự động)",
|
||||||
|
"crash_page_read_the_faq": "Đọc <a href=\"`x`\">Hỏi đáp thường gặp (FAQ)</a>",
|
||||||
|
"user_created_playlists": "`x` danh sách phát đã tạo",
|
||||||
|
"channel_tab_channels_label": "Kênh",
|
||||||
|
"search_filters_type_option_all": "Mọi thể loại",
|
||||||
|
"Russian (auto-generated)": "Tiếng Nga (được tạo tự động)",
|
||||||
|
"comments_view_x_replies_0": "Xem {{count}} lượt trả lời",
|
||||||
|
"footer_original_source_code": "Mã nguồn gốc",
|
||||||
|
"Portuguese (Brazil)": "Tiếng Bồ Đào Nha (Brazil)",
|
||||||
|
"search_filters_features_option_vr180": "VR180",
|
||||||
|
"error_video_not_in_playlist": "Video không tồn tại trong danh sách phát. <a href=\"`x`\">Bấm để trở về trang chủ của danh sách phát.</a>",
|
||||||
|
"Dutch (auto-generated)": "Tiếng Hà Lan (được tạo tự động)",
|
||||||
|
"generic_count_days_0": "{{count}} ngày",
|
||||||
|
"Vietnamese (auto-generated)": "Tiếng Việt (được tạo tự động)",
|
||||||
|
"search_filters_duration_option_none": "Mọi thời lượng",
|
||||||
|
"footer_documentation": "Tài liệu",
|
||||||
|
"next_steps_error_message": "Bạn có thể thử: ",
|
||||||
|
"Import YouTube watch history (.json)": "Nhập lịch sử xem từ YouTube (.json)",
|
||||||
|
"search_filters_duration_option_medium": "Trung bình (4 - 20 phút)",
|
||||||
|
"generic_count_seconds_0": "{{count}} giây",
|
||||||
|
"search_filters_date_label": "Ngày tải lên",
|
||||||
|
"crash_page_you_found_a_bug": "Có vẻ như bạn đã tìm ra lỗi trong Indivious!"
|
||||||
}
|
}
|
||||||
|
|
|
@ -470,5 +470,6 @@
|
||||||
"generic_button_save": "保存",
|
"generic_button_save": "保存",
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
"channel_tab_releases_label": "公告",
|
"channel_tab_releases_label": "公告",
|
||||||
"generic_channels_count_0": "{{count}} 个频道"
|
"generic_channels_count_0": "{{count}} 个频道",
|
||||||
|
"toggle_theme": "切换主题"
|
||||||
}
|
}
|
||||||
|
|
|
@ -470,5 +470,6 @@
|
||||||
"playlist_button_add_items": "新增影片",
|
"playlist_button_add_items": "新增影片",
|
||||||
"channel_tab_podcasts_label": "Podcast",
|
"channel_tab_podcasts_label": "Podcast",
|
||||||
"channel_tab_releases_label": "發布",
|
"channel_tab_releases_label": "發布",
|
||||||
"generic_channels_count_0": "{{count}} 個頻道"
|
"generic_channels_count_0": "{{count}} 個頻道",
|
||||||
|
"toggle_theme": "切換佈景主題"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,27 @@
|
||||||
require "../../spec_helper.cr"
|
require "../../spec_helper.cr"
|
||||||
|
|
||||||
MockLines = [
|
MockLines = ["Line 1", "Line 2"]
|
||||||
{
|
MockLinesWithEscapableCharacter = ["<Line 1>", "&Line 2>", '\u200E' + "Line\u200F 3", "\u00A0Line 4"]
|
||||||
"start_time": Time::Span.new(seconds: 1),
|
|
||||||
"end_time": Time::Span.new(seconds: 2),
|
|
||||||
"text": "Line 1",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"start_time": Time::Span.new(seconds: 2),
|
|
||||||
"end_time": Time::Span.new(seconds: 3),
|
|
||||||
"text": "Line 2",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
Spectator.describe "WebVTT::Builder" do
|
Spectator.describe "WebVTT::Builder" do
|
||||||
it "correctly builds a vtt file" do
|
it "correctly builds a vtt file" do
|
||||||
result = WebVTT.build do |vtt|
|
result = WebVTT.build do |vtt|
|
||||||
MockLines.each do |line|
|
2.times do |i|
|
||||||
vtt.cue(line["start_time"], line["end_time"], line["text"])
|
vtt.cue(
|
||||||
|
Time::Span.new(seconds: i),
|
||||||
|
Time::Span.new(seconds: i + 1),
|
||||||
|
MockLines[i]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
expect(result).to eq([
|
expect(result).to eq([
|
||||||
"WEBVTT",
|
"WEBVTT",
|
||||||
"",
|
"",
|
||||||
"00:00:01.000 --> 00:00:02.000",
|
"00:00:00.000 --> 00:00:01.000",
|
||||||
"Line 1",
|
"Line 1",
|
||||||
"",
|
"",
|
||||||
"00:00:02.000 --> 00:00:03.000",
|
"00:00:01.000 --> 00:00:02.000",
|
||||||
"Line 2",
|
"Line 2",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
@ -42,8 +35,12 @@ Spectator.describe "WebVTT::Builder" do
|
||||||
}
|
}
|
||||||
|
|
||||||
result = WebVTT.build(setting_fields) do |vtt|
|
result = WebVTT.build(setting_fields) do |vtt|
|
||||||
MockLines.each do |line|
|
2.times do |i|
|
||||||
vtt.cue(line["start_time"], line["end_time"], line["text"])
|
vtt.cue(
|
||||||
|
Time::Span.new(seconds: i),
|
||||||
|
Time::Span.new(seconds: i + 1),
|
||||||
|
MockLines[i]
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -52,13 +49,39 @@ Spectator.describe "WebVTT::Builder" do
|
||||||
"Kind: captions",
|
"Kind: captions",
|
||||||
"Language: en",
|
"Language: en",
|
||||||
"",
|
"",
|
||||||
"00:00:01.000 --> 00:00:02.000",
|
"00:00:00.000 --> 00:00:01.000",
|
||||||
"Line 1",
|
"Line 1",
|
||||||
"",
|
"",
|
||||||
"00:00:02.000 --> 00:00:03.000",
|
"00:00:01.000 --> 00:00:02.000",
|
||||||
"Line 2",
|
"Line 2",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
].join('\n'))
|
].join('\n'))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "properly escapes characters" do
|
||||||
|
result = WebVTT.build do |vtt|
|
||||||
|
4.times do |i|
|
||||||
|
vtt.cue(Time::Span.new(seconds: i), Time::Span.new(seconds: i + 1), MockLinesWithEscapableCharacter[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(result).to eq([
|
||||||
|
"WEBVTT",
|
||||||
|
"",
|
||||||
|
"00:00:00.000 --> 00:00:01.000",
|
||||||
|
"<Line 1>",
|
||||||
|
"",
|
||||||
|
"00:00:01.000 --> 00:00:02.000",
|
||||||
|
"&Line 2>",
|
||||||
|
"",
|
||||||
|
"00:00:02.000 --> 00:00:03.000",
|
||||||
|
"‎Line‏ 3",
|
||||||
|
"",
|
||||||
|
"00:00:03.000 --> 00:00:04.000",
|
||||||
|
" Line 4",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
].join('\n'))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ FORM_TESTS = {
|
||||||
"cy" => I18next::Plurals::PluralForms::Special_Welsh,
|
"cy" => I18next::Plurals::PluralForms::Special_Welsh,
|
||||||
"fr" => I18next::Plurals::PluralForms::Special_French_Portuguese,
|
"fr" => I18next::Plurals::PluralForms::Special_French_Portuguese,
|
||||||
"en" => I18next::Plurals::PluralForms::Single_not_one,
|
"en" => I18next::Plurals::PluralForms::Single_not_one,
|
||||||
"es" => I18next::Plurals::PluralForms::Single_not_one,
|
"es" => I18next::Plurals::PluralForms::Special_Spanish_Italian,
|
||||||
"ga" => I18next::Plurals::PluralForms::Special_Irish,
|
"ga" => I18next::Plurals::PluralForms::Special_Irish,
|
||||||
"gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic,
|
"gd" => I18next::Plurals::PluralForms::Special_Scottish_Gaelic,
|
||||||
"he" => I18next::Plurals::PluralForms::Special_Hebrew,
|
"he" => I18next::Plurals::PluralForms::Special_Hebrew,
|
||||||
|
@ -33,7 +33,8 @@ FORM_TESTS = {
|
||||||
"mt" => I18next::Plurals::PluralForms::Special_Maltese,
|
"mt" => I18next::Plurals::PluralForms::Special_Maltese,
|
||||||
"or" => I18next::Plurals::PluralForms::Special_Odia,
|
"or" => I18next::Plurals::PluralForms::Special_Odia,
|
||||||
"pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian,
|
"pl" => I18next::Plurals::PluralForms::Special_Polish_Kashubian,
|
||||||
"pt" => I18next::Plurals::PluralForms::Single_gt_one,
|
"pt" => I18next::Plurals::PluralForms::Special_French_Portuguese,
|
||||||
|
"pt-PT" => I18next::Plurals::PluralForms::Single_gt_one,
|
||||||
"pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese,
|
"pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese,
|
||||||
"ro" => I18next::Plurals::PluralForms::Special_Romanian,
|
"ro" => I18next::Plurals::PluralForms::Special_Romanian,
|
||||||
"sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak,
|
"sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak,
|
||||||
|
@ -77,10 +78,10 @@ SUFFIX_TESTS = {
|
||||||
{num: 10, suffix: "_plural"},
|
{num: 10, suffix: "_plural"},
|
||||||
],
|
],
|
||||||
"es" => [
|
"es" => [
|
||||||
{num: 0, suffix: "_plural"},
|
{num: 0, suffix: "_2"},
|
||||||
{num: 1, suffix: ""},
|
{num: 1, suffix: "_0"},
|
||||||
{num: 10, suffix: "_plural"},
|
{num: 10, suffix: "_2"},
|
||||||
{num: 6_000_000, suffix: "_plural"},
|
{num: 6_000_000, suffix: "_1"},
|
||||||
],
|
],
|
||||||
"fr" => [
|
"fr" => [
|
||||||
{num: 0, suffix: "_0"},
|
{num: 0, suffix: "_0"},
|
||||||
|
|
|
@ -3,18 +3,6 @@ require "../spec_helper"
|
||||||
CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
|
CONFIG = Config.from_yaml(File.open("config/config.example.yml"))
|
||||||
|
|
||||||
Spectator.describe "Helper" do
|
Spectator.describe "Helper" do
|
||||||
describe "#produce_channel_videos_url" do
|
|
||||||
it "correctly produces url for requesting page `x` of a channel's videos" do
|
|
||||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw")).to eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en")
|
|
||||||
#
|
|
||||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFPQ%3D%3D&gl=US&hl=en")
|
|
||||||
|
|
||||||
# expect(produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20)).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUE9PQ%3D%3D&gl=US&hl=en")
|
|
||||||
|
|
||||||
# expect(produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, sort_by: "popular")).to eq("/browse_ajax?continuation=4qmFsgJAEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaJEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUJnQg%3D%3D&gl=US&hl=en")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "#produce_channel_search_continuation" do
|
describe "#produce_channel_search_continuation" do
|
||||||
it "correctly produces token for searching a specific channel" do
|
it "correctly produces token for searching a specific channel" do
|
||||||
expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
|
expect(produce_channel_search_continuation("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100)).to eq("4qmFsgJqEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FUZ0JZQUY2QkVkS2IxaTRBUUE9WgCaAilicm93c2UtZmVlZFVDWHVxU0JsSEFFNlh3LXllSkEwVHVud3NlYXJjaA%3D%3D")
|
||||||
|
|
|
@ -12,45 +12,45 @@ end
|
||||||
# page of Youtube with any browser devtools HTML inspector.
|
# page of Youtube with any browser devtools HTML inspector.
|
||||||
|
|
||||||
DATE_FILTERS = {
|
DATE_FILTERS = {
|
||||||
Invidious::Search::Filters::Date::Hour => "EgIIAQ%3D%3D",
|
Invidious::Search::Filters::Date::Hour => "EgIIAfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Date::Today => "EgIIAg%3D%3D",
|
Invidious::Search::Filters::Date::Today => "EgIIAvABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Date::Week => "EgIIAw%3D%3D",
|
Invidious::Search::Filters::Date::Week => "EgIIA_ABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Date::Month => "EgIIBA%3D%3D",
|
Invidious::Search::Filters::Date::Month => "EgIIBPABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Date::Year => "EgIIBQ%3D%3D",
|
Invidious::Search::Filters::Date::Year => "EgIIBfABAQ%3D%3D",
|
||||||
}
|
}
|
||||||
|
|
||||||
TYPE_FILTERS = {
|
TYPE_FILTERS = {
|
||||||
Invidious::Search::Filters::Type::Video => "EgIQAQ%3D%3D",
|
Invidious::Search::Filters::Type::Video => "EgIQAfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Type::Channel => "EgIQAg%3D%3D",
|
Invidious::Search::Filters::Type::Channel => "EgIQAvABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Type::Playlist => "EgIQAw%3D%3D",
|
Invidious::Search::Filters::Type::Playlist => "EgIQA_ABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Type::Movie => "EgIQBA%3D%3D",
|
Invidious::Search::Filters::Type::Movie => "EgIQBPABAQ%3D%3D",
|
||||||
}
|
}
|
||||||
|
|
||||||
DURATION_FILTERS = {
|
DURATION_FILTERS = {
|
||||||
Invidious::Search::Filters::Duration::Short => "EgIYAQ%3D%3D",
|
Invidious::Search::Filters::Duration::Short => "EgIYAfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Duration::Medium => "EgIYAw%3D%3D",
|
Invidious::Search::Filters::Duration::Medium => "EgIYA_ABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Duration::Long => "EgIYAg%3D%3D",
|
Invidious::Search::Filters::Duration::Long => "EgIYAvABAQ%3D%3D",
|
||||||
}
|
}
|
||||||
|
|
||||||
FEATURE_FILTERS = {
|
FEATURE_FILTERS = {
|
||||||
Invidious::Search::Filters::Features::Live => "EgJAAQ%3D%3D",
|
Invidious::Search::Filters::Features::Live => "EgJAAfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Features::FourK => "EgJwAQ%3D%3D",
|
Invidious::Search::Filters::Features::FourK => "EgJwAfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Features::HD => "EgIgAQ%3D%3D",
|
Invidious::Search::Filters::Features::HD => "EgIgAfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Features::Subtitles => "EgIoAQ%3D%3D",
|
Invidious::Search::Filters::Features::Subtitles => "EgIoAfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Features::CCommons => "EgIwAQ%3D%3D",
|
Invidious::Search::Filters::Features::CCommons => "EgIwAfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AQ%3D%3D",
|
Invidious::Search::Filters::Features::ThreeSixty => "EgJ4AfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Features::VR180 => "EgPQAQE%3D",
|
Invidious::Search::Filters::Features::VR180 => "EgPQAQHwAQE%3D",
|
||||||
Invidious::Search::Filters::Features::ThreeD => "EgI4AQ%3D%3D",
|
Invidious::Search::Filters::Features::ThreeD => "EgI4AfABAQ%3D%3D",
|
||||||
Invidious::Search::Filters::Features::HDR => "EgPIAQE%3D",
|
Invidious::Search::Filters::Features::HDR => "EgPIAQHwAQE%3D",
|
||||||
Invidious::Search::Filters::Features::Location => "EgO4AQE%3D",
|
Invidious::Search::Filters::Features::Location => "EgO4AQHwAQE%3D",
|
||||||
Invidious::Search::Filters::Features::Purchased => "EgJIAQ%3D%3D",
|
Invidious::Search::Filters::Features::Purchased => "EgJIAfABAQ%3D%3D",
|
||||||
}
|
}
|
||||||
|
|
||||||
SORT_FILTERS = {
|
SORT_FILTERS = {
|
||||||
Invidious::Search::Filters::Sort::Relevance => "",
|
Invidious::Search::Filters::Sort::Relevance => "8AEB",
|
||||||
Invidious::Search::Filters::Sort::Date => "CAI%3D",
|
Invidious::Search::Filters::Sort::Date => "CALwAQE%3D",
|
||||||
Invidious::Search::Filters::Sort::Views => "CAM%3D",
|
Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D",
|
||||||
Invidious::Search::Filters::Sort::Rating => "CAE%3D",
|
Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D",
|
||||||
}
|
}
|
||||||
|
|
||||||
Spectator.describe Invidious::Search::Filters do
|
Spectator.describe Invidious::Search::Filters do
|
||||||
|
|
|
@ -18,8 +18,8 @@ record AboutChannel,
|
||||||
|
|
||||||
def get_about_info(ucid, locale) : AboutChannel
|
def get_about_info(ucid, locale) : AboutChannel
|
||||||
begin
|
begin
|
||||||
# "EgVhYm91dA==" is the base64-encoded protobuf object {"2:string":"about"}
|
# Fetch channel information from channel home page
|
||||||
initdata = YoutubeAPI.browse(browse_id: ucid, params: "EgVhYm91dA==")
|
initdata = YoutubeAPI.browse(browse_id: ucid, params: "")
|
||||||
rescue
|
rescue
|
||||||
raise InfoException.new("Could not get channel info.")
|
raise InfoException.new("Could not get channel info.")
|
||||||
end
|
end
|
||||||
|
|
|
@ -93,7 +93,7 @@ struct ChannelVideo
|
||||||
def to_tuple
|
def to_tuple
|
||||||
{% begin %}
|
{% begin %}
|
||||||
{
|
{
|
||||||
{{*@type.instance_vars.map(&.name)}}
|
{{@type.instance_vars.map(&.name).splat}}
|
||||||
}
|
}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
|
|
@ -62,12 +62,6 @@ def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, so
|
||||||
return continuation
|
return continuation
|
||||||
end
|
end
|
||||||
|
|
||||||
# Used in bypass_captcha_job.cr
|
|
||||||
def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
|
|
||||||
continuation = produce_channel_videos_continuation(ucid, page, auto_generated, sort_by, v2)
|
|
||||||
return "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
|
|
||||||
end
|
|
||||||
|
|
||||||
module Invidious::Channel::Tabs
|
module Invidious::Channel::Tabs
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
|
|
|
@ -48,7 +48,7 @@ struct ConfigPreferences
|
||||||
def to_tuple
|
def to_tuple
|
||||||
{% begin %}
|
{% begin %}
|
||||||
{
|
{
|
||||||
{{*@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }}}
|
{{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}}
|
||||||
}
|
}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
@ -133,10 +133,6 @@ class Config
|
||||||
# Saved cookies in "name1=value1; name2=value2..." format
|
# Saved cookies in "name1=value1; name2=value2..." format
|
||||||
@[YAML::Field(converter: Preferences::StringToCookies)]
|
@[YAML::Field(converter: Preferences::StringToCookies)]
|
||||||
property cookies : HTTP::Cookies = HTTP::Cookies.new
|
property cookies : HTTP::Cookies = HTTP::Cookies.new
|
||||||
# Key for Anti-Captcha
|
|
||||||
property captcha_key : String? = nil
|
|
||||||
# API URL for Anti-Captcha
|
|
||||||
property captcha_api_url : String = "https://api.anti-captcha.com"
|
|
||||||
|
|
||||||
# Playlist length limit
|
# Playlist length limit
|
||||||
property playlist_length_limit : Int32 = 500
|
property playlist_length_limit : Int32 = 500
|
||||||
|
|
|
@ -15,7 +15,7 @@ module Invidious::Database::Statistics
|
||||||
PG_DB.query_one(request, as: Int64)
|
PG_DB.query_one(request, as: Int64)
|
||||||
end
|
end
|
||||||
|
|
||||||
def count_users_active_1m : Int64
|
def count_users_active_6m : Int64
|
||||||
request = <<-SQL
|
request = <<-SQL
|
||||||
SELECT count(*) FROM users
|
SELECT count(*) FROM users
|
||||||
WHERE CURRENT_TIMESTAMP - updated < '6 months'
|
WHERE CURRENT_TIMESTAMP - updated < '6 months'
|
||||||
|
@ -24,7 +24,7 @@ module Invidious::Database::Statistics
|
||||||
PG_DB.query_one(request, as: Int64)
|
PG_DB.query_one(request, as: Int64)
|
||||||
end
|
end
|
||||||
|
|
||||||
def count_users_active_6m : Int64
|
def count_users_active_1m : Int64
|
||||||
request = <<-SQL
|
request = <<-SQL
|
||||||
SELECT count(*) FROM users
|
SELECT count(*) FROM users
|
||||||
WHERE CURRENT_TIMESTAMP - updated < '1 month'
|
WHERE CURRENT_TIMESTAMP - updated < '1 month'
|
||||||
|
|
|
@ -33,7 +33,7 @@ module Invidious::Frontend::Comments
|
||||||
<a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
|
<a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
|
||||||
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
|
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
|
||||||
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
|
#{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>
|
<span title="#{child.created_utc.to_s("%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>
|
<a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a>
|
||||||
</p>
|
</p>
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -107,6 +107,36 @@ module Invidious::Frontend::Comments
|
||||||
</div>
|
</div>
|
||||||
END_HTML
|
END_HTML
|
||||||
end
|
end
|
||||||
|
when "multiImage"
|
||||||
|
html << <<-END_HTML
|
||||||
|
<section class="carousel">
|
||||||
|
<a class="skip-link" href="#skip-#{child["commentId"]}">#{translate(locale, "carousel_skip")}</a>
|
||||||
|
<div class="slides">
|
||||||
|
END_HTML
|
||||||
|
image_array = attachment["images"].as_a
|
||||||
|
|
||||||
|
image_array.each_index do |i|
|
||||||
|
html << <<-END_HTML
|
||||||
|
<div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
|
||||||
|
<img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" />
|
||||||
|
</div>
|
||||||
|
END_HTML
|
||||||
|
end
|
||||||
|
|
||||||
|
html << <<-END_HTML
|
||||||
|
</div>
|
||||||
|
<div class="carousel__nav">
|
||||||
|
END_HTML
|
||||||
|
attachment["images"].as_a.each_index do |i|
|
||||||
|
html << <<-END_HTML
|
||||||
|
<a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a>
|
||||||
|
END_HTML
|
||||||
|
end
|
||||||
|
html << <<-END_HTML
|
||||||
|
</div>
|
||||||
|
<div id="skip-#{child["commentId"]}"></div>
|
||||||
|
</section>
|
||||||
|
END_HTML
|
||||||
else nil # Ignore
|
else nil # Ignore
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
macro error_template(*args)
|
macro error_template(*args)
|
||||||
error_template_helper(env, {{*args}})
|
error_template_helper(env, {{args.splat}})
|
||||||
end
|
end
|
||||||
|
|
||||||
def github_details(summary : String, content : String)
|
def github_details(summary : String, content : String)
|
||||||
|
@ -95,7 +95,7 @@ end
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
macro error_atom(*args)
|
macro error_atom(*args)
|
||||||
error_atom_helper(env, {{*args}})
|
error_atom_helper(env, {{args.splat}})
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
|
def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
|
||||||
|
@ -121,7 +121,7 @@ end
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
macro error_json(*args)
|
macro error_json(*args)
|
||||||
error_json_helper(env, {{*args}})
|
error_json_helper(env, {{args.splat}})
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_json_helper(
|
def error_json_helper(
|
||||||
|
|
|
@ -142,63 +142,8 @@ class APIHandler < Kemal::Handler
|
||||||
exclude ["/api/v1/auth/notifications"], "POST"
|
exclude ["/api/v1/auth/notifications"], "POST"
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
return call_next env unless only_match? env
|
env.response.headers["Access-Control-Allow-Origin"] = "*" if only_match?(env)
|
||||||
|
|
||||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
|
||||||
|
|
||||||
# Since /api/v1/notifications is an event-stream, we don't want
|
|
||||||
# to wrap the response
|
|
||||||
return call_next env if exclude_match? env
|
|
||||||
|
|
||||||
# Here we swap out the socket IO so we can modify the response as needed
|
|
||||||
output = env.response.output
|
|
||||||
env.response.output = IO::Memory.new
|
|
||||||
|
|
||||||
begin
|
|
||||||
call_next env
|
call_next env
|
||||||
|
|
||||||
env.response.output.rewind
|
|
||||||
|
|
||||||
if env.response.output.as(IO::Memory).size != 0 &&
|
|
||||||
env.response.headers.includes_word?("Content-Type", "application/json")
|
|
||||||
response = JSON.parse(env.response.output)
|
|
||||||
|
|
||||||
if fields_text = env.params.query["fields"]?
|
|
||||||
begin
|
|
||||||
JSONFilter.filter(response, fields_text)
|
|
||||||
rescue ex
|
|
||||||
env.response.status_code = 400
|
|
||||||
response = {"error" => ex.message}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if env.params.query["pretty"]?.try &.== "1"
|
|
||||||
response = response.to_pretty_json
|
|
||||||
else
|
|
||||||
response = response.to_json
|
|
||||||
end
|
|
||||||
else
|
|
||||||
response = env.response.output.gets_to_end
|
|
||||||
end
|
|
||||||
rescue ex
|
|
||||||
env.response.content_type = "application/json" if env.response.headers.includes_word?("Content-Type", "text/html")
|
|
||||||
env.response.status_code = 500
|
|
||||||
|
|
||||||
if env.response.headers.includes_word?("Content-Type", "application/json")
|
|
||||||
response = {"error" => ex.message || "Unspecified error"}
|
|
||||||
|
|
||||||
if env.params.query["pretty"]?.try &.== "1"
|
|
||||||
response = response.to_pretty_json
|
|
||||||
else
|
|
||||||
response = response.to_json
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
env.response.output = output
|
|
||||||
env.response.print response
|
|
||||||
|
|
||||||
env.response.flush
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -78,15 +78,6 @@ def create_notification_stream(env, topics, connection_channel)
|
||||||
video.published = published
|
video.published = published
|
||||||
response = JSON.parse(video.to_json(locale, nil))
|
response = JSON.parse(video.to_json(locale, nil))
|
||||||
|
|
||||||
if fields_text = env.params.query["fields"]?
|
|
||||||
begin
|
|
||||||
JSONFilter.filter(response, fields_text)
|
|
||||||
rescue ex
|
|
||||||
env.response.status_code = 400
|
|
||||||
response = {"error" => ex.message}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
env.response.puts "id: #{id}"
|
env.response.puts "id: #{id}"
|
||||||
env.response.puts "data: #{response.to_json}"
|
env.response.puts "data: #{response.to_json}"
|
||||||
env.response.puts
|
env.response.puts
|
||||||
|
@ -113,15 +104,6 @@ def create_notification_stream(env, topics, connection_channel)
|
||||||
Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video|
|
Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video|
|
||||||
response = JSON.parse(video.to_json(locale))
|
response = JSON.parse(video.to_json(locale))
|
||||||
|
|
||||||
if fields_text = env.params.query["fields"]?
|
|
||||||
begin
|
|
||||||
JSONFilter.filter(response, fields_text)
|
|
||||||
rescue ex
|
|
||||||
env.response.status_code = 400
|
|
||||||
response = {"error" => ex.message}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
env.response.puts "id: #{id}"
|
env.response.puts "id: #{id}"
|
||||||
env.response.puts "data: #{response.to_json}"
|
env.response.puts "data: #{response.to_json}"
|
||||||
env.response.puts
|
env.response.puts
|
||||||
|
@ -155,15 +137,6 @@ def create_notification_stream(env, topics, connection_channel)
|
||||||
video.published = Time.unix(published)
|
video.published = Time.unix(published)
|
||||||
response = JSON.parse(video.to_json(locale, nil))
|
response = JSON.parse(video.to_json(locale, nil))
|
||||||
|
|
||||||
if fields_text = env.params.query["fields"]?
|
|
||||||
begin
|
|
||||||
JSONFilter.filter(response, fields_text)
|
|
||||||
rescue ex
|
|
||||||
env.response.status_code = 400
|
|
||||||
response = {"error" => ex.message}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
env.response.puts "id: #{id}"
|
env.response.puts "id: #{id}"
|
||||||
env.response.puts "data: #{response.to_json}"
|
env.response.puts "data: #{response.to_json}"
|
||||||
env.response.puts
|
env.response.puts
|
||||||
|
@ -208,3 +181,20 @@ def proxy_file(response, env)
|
||||||
IO.copy response.body_io, env.response
|
IO.copy response.body_io, env.response
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fetch the playback requests tracker from the statistics endpoint.
|
||||||
|
#
|
||||||
|
# Creates a new tracker when unavailable.
|
||||||
|
def get_playback_statistic
|
||||||
|
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]) && tracker.as(Hash).empty?
|
||||||
|
tracker = {
|
||||||
|
"totalRequests" => 0_i64,
|
||||||
|
"successfulRequests" => 0_i64,
|
||||||
|
"ratio" => 0_f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker
|
||||||
|
end
|
||||||
|
|
||||||
|
return tracker.as(Hash(String, Int64 | Float64))
|
||||||
|
end
|
||||||
|
|
|
@ -78,7 +78,7 @@ def load_all_locales
|
||||||
return locales
|
return locales
|
||||||
end
|
end
|
||||||
|
|
||||||
def translate(locale : String?, key : String, text : String | Nil = nil) : String
|
def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
|
||||||
# Log a warning if "key" doesn't exist in en-US locale and return
|
# Log a warning if "key" doesn't exist in en-US locale and return
|
||||||
# that key as the text, so this is more or less transparent to the user.
|
# that key as the text, so this is more or less transparent to the user.
|
||||||
if !LOCALES["en-US"].has_key?(key)
|
if !LOCALES["en-US"].has_key?(key)
|
||||||
|
@ -101,6 +101,7 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin
|
||||||
match_length = 0
|
match_length = 0
|
||||||
|
|
||||||
raw_data.as_h.each do |hash_key, value|
|
raw_data.as_h.each do |hash_key, value|
|
||||||
|
if text.is_a?(String)
|
||||||
if md = text.try &.match(/#{hash_key}/)
|
if md = text.try &.match(/#{hash_key}/)
|
||||||
if md[0].size >= match_length
|
if md[0].size >= match_length
|
||||||
translation = value.as_s
|
translation = value.as_s
|
||||||
|
@ -108,14 +109,20 @@ def translate(locale : String?, key : String, text : String | Nil = nil) : Strin
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
when .as_s?
|
when .as_s?
|
||||||
translation = raw_data.as_s
|
translation = raw_data.as_s
|
||||||
else
|
else
|
||||||
raise "Invalid translation \"#{raw_data}\""
|
raise "Invalid translation \"#{raw_data}\""
|
||||||
end
|
end
|
||||||
|
|
||||||
if text
|
if text.is_a?(String)
|
||||||
translation = translation.gsub("`x`", text)
|
translation = translation.gsub("`x`", text)
|
||||||
|
elsif text.is_a?(Hash(String, String))
|
||||||
|
# adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic
|
||||||
|
text.each_key do |hash_key|
|
||||||
|
translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return translation
|
return translation
|
||||||
|
|
|
@ -47,19 +47,19 @@ module I18next::Plurals
|
||||||
|
|
||||||
private PLURAL_SETS = {
|
private PLURAL_SETS = {
|
||||||
PluralForms::Single_gt_one => [
|
PluralForms::Single_gt_one => [
|
||||||
"ach", "ak", "am", "arn", "br", "fil", "gun", "ln", "mfe", "mg",
|
"ach", "ak", "am", "arn", "br", "fa", "fil", "gun", "ln", "mfe", "mg",
|
||||||
"mi", "oc", "pt", "tg", "tl", "ti", "tr", "uz", "wa",
|
"mi", "oc", "pt-PT", "tg", "tl", "ti", "tr", "uz", "wa",
|
||||||
],
|
],
|
||||||
PluralForms::Single_not_one => [
|
PluralForms::Single_not_one => [
|
||||||
"af", "an", "ast", "az", "bg", "bn", "ca", "da", "de", "dev", "el", "en",
|
"af", "an", "ast", "az", "bg", "bn", "ca", "da", "de", "dev", "el", "en",
|
||||||
"eo", "es", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi",
|
"eo", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi",
|
||||||
"hu", "hy", "ia", "kk", "kn", "ku", "lb", "mai", "ml", "mn", "mr",
|
"hu", "hy", "ia", "kk", "kn", "ku", "lb", "mai", "ml", "mn", "mr",
|
||||||
"nah", "nap", "nb", "ne", "nl", "nn", "no", "nso", "pa", "pap", "pms",
|
"nah", "nap", "nb", "ne", "nl", "nn", "no", "nso", "pa", "pap", "pms",
|
||||||
"ps", "rm", "sco", "se", "si", "so", "son", "sq", "sv", "sw",
|
"ps", "rm", "sco", "se", "si", "so", "son", "sq", "sv", "sw",
|
||||||
"ta", "te", "tk", "ur", "yo",
|
"ta", "te", "tk", "ur", "yo",
|
||||||
],
|
],
|
||||||
PluralForms::None => [
|
PluralForms::None => [
|
||||||
"ay", "bo", "cgg", "fa", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky",
|
"ay", "bo", "cgg", "ht", "id", "ja", "jbo", "ka", "km", "ko", "ky",
|
||||||
"lo", "ms", "sah", "su", "th", "tt", "ug", "vi", "wo", "zh",
|
"lo", "ms", "sah", "su", "th", "tt", "ug", "vi", "wo", "zh",
|
||||||
],
|
],
|
||||||
PluralForms::Dual_Slavic => [
|
PluralForms::Dual_Slavic => [
|
||||||
|
@ -90,10 +90,12 @@ module I18next::Plurals
|
||||||
"sk" => PluralForms::Special_Czech_Slovak,
|
"sk" => PluralForms::Special_Czech_Slovak,
|
||||||
"sl" => PluralForms::Special_Slovenian,
|
"sl" => PluralForms::Special_Slovenian,
|
||||||
# Mixed v3/v4 rules
|
# Mixed v3/v4 rules
|
||||||
|
"es" => PluralForms::Special_Spanish_Italian,
|
||||||
"fr" => PluralForms::Special_French_Portuguese,
|
"fr" => PluralForms::Special_French_Portuguese,
|
||||||
"hr" => PluralForms::Special_Hungarian_Serbian,
|
"hr" => PluralForms::Special_Hungarian_Serbian,
|
||||||
"it" => PluralForms::Special_Spanish_Italian,
|
"it" => PluralForms::Special_Spanish_Italian,
|
||||||
"pt-BR" => PluralForms::Special_French_Portuguese,
|
"pt" => PluralForms::Special_French_Portuguese,
|
||||||
|
"pt" => PluralForms::Special_French_Portuguese,
|
||||||
"sr" => PluralForms::Special_Hungarian_Serbian,
|
"sr" => PluralForms::Special_Hungarian_Serbian,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +167,7 @@ module I18next::Plurals
|
||||||
|
|
||||||
def get_plural_form(locale : String) : PluralForms
|
def get_plural_form(locale : String) : PluralForms
|
||||||
# Extract the ISO 639-1 or 639-2 code from an RFC 5646 language code
|
# Extract the ISO 639-1 or 639-2 code from an RFC 5646 language code
|
||||||
if !locale.matches?(/^pt-BR$/)
|
if !locale.matches?(/^pt-PT$/)
|
||||||
locale = locale.split('-')[0]
|
locale = locale.split('-')[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,248 +0,0 @@
|
||||||
module JSONFilter
|
|
||||||
alias BracketIndex = Hash(Int64, Int64)
|
|
||||||
|
|
||||||
alias GroupedFieldsValue = String | Array(GroupedFieldsValue)
|
|
||||||
alias GroupedFieldsList = Array(GroupedFieldsValue)
|
|
||||||
|
|
||||||
class FieldsParser
|
|
||||||
class ParseError < Exception
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the `Regex` pattern used to match nest groups
|
|
||||||
def self.nest_group_pattern : Regex
|
|
||||||
# uses a '.' character to match json keys as they are allowed
|
|
||||||
# to contain any unicode codepoint
|
|
||||||
/(?:|,)(?<groupname>[^,\n]*?)\(/
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the `Regex` pattern used to check if there are any empty nest groups
|
|
||||||
def self.unnamed_nest_group_pattern : Regex
|
|
||||||
/^\(|\(\(|\/\(/
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.parse_fields(fields_text : String, &) : Nil
|
|
||||||
if fields_text.empty?
|
|
||||||
raise FieldsParser::ParseError.new "Fields is empty"
|
|
||||||
end
|
|
||||||
|
|
||||||
opening_bracket_count = fields_text.count('(')
|
|
||||||
closing_bracket_count = fields_text.count(')')
|
|
||||||
|
|
||||||
if opening_bracket_count != closing_bracket_count
|
|
||||||
bracket_type = opening_bracket_count > closing_bracket_count ? "opening" : "closing"
|
|
||||||
raise FieldsParser::ParseError.new "There are too many #{bracket_type} brackets (#{opening_bracket_count}:#{closing_bracket_count})"
|
|
||||||
elsif match_result = unnamed_nest_group_pattern.match(fields_text)
|
|
||||||
raise FieldsParser::ParseError.new "Unnamed nest group at position #{match_result.begin}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# first, handle top-level single nested properties: items/id, playlistItems/snippet, etc
|
|
||||||
parse_single_nests(fields_text) { |nest_list| yield nest_list }
|
|
||||||
|
|
||||||
# next, handle nest groups: items(id, etag, etc)
|
|
||||||
parse_nest_groups(fields_text) { |nest_list| yield nest_list }
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.parse_single_nests(fields_text : String, &) : Nil
|
|
||||||
single_nests = remove_nest_groups(fields_text)
|
|
||||||
|
|
||||||
if !single_nests.empty?
|
|
||||||
property_nests = single_nests.split(',')
|
|
||||||
|
|
||||||
property_nests.each do |nest|
|
|
||||||
nest_list = nest.split('/')
|
|
||||||
if nest_list.includes? ""
|
|
||||||
raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list}"
|
|
||||||
end
|
|
||||||
yield nest_list
|
|
||||||
end
|
|
||||||
# else
|
|
||||||
# raise FieldsParser::ParseError.new "Empty key in nest list 22: #{fields_text} | #{single_nests}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.parse_nest_groups(fields_text : String, &) : Nil
|
|
||||||
nest_stack = [] of NamedTuple(group_name: String, closing_bracket_index: Int64)
|
|
||||||
bracket_pairs = get_bracket_pairs(fields_text, true)
|
|
||||||
|
|
||||||
text_index = 0
|
|
||||||
regex_index = 0
|
|
||||||
|
|
||||||
while regex_result = self.nest_group_pattern.match(fields_text, regex_index)
|
|
||||||
raw_match = regex_result[0]
|
|
||||||
group_name = regex_result["groupname"]
|
|
||||||
|
|
||||||
text_index = regex_result.begin
|
|
||||||
regex_index = regex_result.end
|
|
||||||
|
|
||||||
if text_index.nil? || regex_index.nil?
|
|
||||||
raise FieldsParser::ParseError.new "Received invalid index while parsing nest groups: text_index: #{text_index} | regex_index: #{regex_index}"
|
|
||||||
end
|
|
||||||
|
|
||||||
offset = raw_match.starts_with?(',') ? 1 : 0
|
|
||||||
|
|
||||||
opening_bracket_index = (text_index + group_name.size) + offset
|
|
||||||
closing_bracket_index = bracket_pairs[opening_bracket_index]
|
|
||||||
content_start = opening_bracket_index + 1
|
|
||||||
|
|
||||||
content = fields_text[content_start...closing_bracket_index]
|
|
||||||
|
|
||||||
if content.empty?
|
|
||||||
raise FieldsParser::ParseError.new "Empty nest group at position #{content_start}"
|
|
||||||
else
|
|
||||||
content = remove_nest_groups(content)
|
|
||||||
end
|
|
||||||
|
|
||||||
while nest_stack.size > 0 && closing_bracket_index > nest_stack[nest_stack.size - 1][:closing_bracket_index]
|
|
||||||
if nest_stack.size
|
|
||||||
nest_stack.pop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
group_name.split('/').each do |name|
|
|
||||||
nest_stack.push({
|
|
||||||
group_name: name,
|
|
||||||
closing_bracket_index: closing_bracket_index,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
if !content.empty?
|
|
||||||
properties = content.split(',')
|
|
||||||
|
|
||||||
properties.each do |prop|
|
|
||||||
nest_list = nest_stack.map { |nest_prop| nest_prop[:group_name] }
|
|
||||||
|
|
||||||
if !prop.empty?
|
|
||||||
if prop.includes?('/')
|
|
||||||
parse_single_nests(prop) { |list| nest_list += list }
|
|
||||||
else
|
|
||||||
nest_list.push prop
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raise FieldsParser::ParseError.new "Empty key in nest list: #{nest_list << prop}"
|
|
||||||
end
|
|
||||||
|
|
||||||
yield nest_list
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.remove_nest_groups(text : String) : String
|
|
||||||
content_bracket_pairs = get_bracket_pairs(text, false)
|
|
||||||
|
|
||||||
content_bracket_pairs.each_key.to_a.reverse.each do |opening_bracket|
|
|
||||||
closing_bracket = content_bracket_pairs[opening_bracket]
|
|
||||||
last_comma = text.rindex(',', opening_bracket) || 0
|
|
||||||
|
|
||||||
text = text[0...last_comma] + text[closing_bracket + 1...text.size]
|
|
||||||
end
|
|
||||||
|
|
||||||
return text.starts_with?(',') ? text[1...text.size] : text
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.get_bracket_pairs(text : String, recursive = true) : BracketIndex
|
|
||||||
istart = [] of Int64
|
|
||||||
bracket_index = BracketIndex.new
|
|
||||||
|
|
||||||
text.each_char_with_index do |char, index|
|
|
||||||
if char == '('
|
|
||||||
istart.push(index.to_i64)
|
|
||||||
end
|
|
||||||
|
|
||||||
if char == ')'
|
|
||||||
begin
|
|
||||||
opening = istart.pop
|
|
||||||
if recursive || (!recursive && istart.size == 0)
|
|
||||||
bracket_index[opening] = index.to_i64
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
raise FieldsParser::ParseError.new "No matching opening parenthesis at: #{index}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if istart.size != 0
|
|
||||||
idx = istart.pop
|
|
||||||
raise FieldsParser::ParseError.new "No matching closing parenthesis at: #{idx}"
|
|
||||||
end
|
|
||||||
|
|
||||||
return bracket_index
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class FieldsGrouper
|
|
||||||
alias SkeletonValue = Hash(String, SkeletonValue)
|
|
||||||
|
|
||||||
def self.create_json_skeleton(fields_text : String) : SkeletonValue
|
|
||||||
root_hash = {} of String => SkeletonValue
|
|
||||||
|
|
||||||
FieldsParser.parse_fields(fields_text) do |nest_list|
|
|
||||||
current_item = root_hash
|
|
||||||
nest_list.each do |key|
|
|
||||||
if current_item[key]?
|
|
||||||
current_item = current_item[key]
|
|
||||||
else
|
|
||||||
current_item[key] = {} of String => SkeletonValue
|
|
||||||
current_item = current_item[key]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
root_hash
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.create_grouped_fields_list(json_skeleton : SkeletonValue) : GroupedFieldsList
|
|
||||||
grouped_fields_list = GroupedFieldsList.new
|
|
||||||
json_skeleton.each do |key, value|
|
|
||||||
grouped_fields_list.push key
|
|
||||||
|
|
||||||
nested_keys = create_grouped_fields_list(value)
|
|
||||||
grouped_fields_list.push nested_keys unless nested_keys.empty?
|
|
||||||
end
|
|
||||||
return grouped_fields_list
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class FilterError < Exception
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.filter(item : JSON::Any, fields_text : String, in_place : Bool = true)
|
|
||||||
skeleton = FieldsGrouper.create_json_skeleton(fields_text)
|
|
||||||
grouped_fields_list = FieldsGrouper.create_grouped_fields_list(skeleton)
|
|
||||||
filter(item, grouped_fields_list, in_place)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.filter(item : JSON::Any, grouped_fields_list : GroupedFieldsList, in_place : Bool = true) : JSON::Any
|
|
||||||
item = item.clone unless in_place
|
|
||||||
|
|
||||||
if !item.as_h? && !item.as_a?
|
|
||||||
raise FilterError.new "Can't filter '#{item}' by #{grouped_fields_list}"
|
|
||||||
end
|
|
||||||
|
|
||||||
top_level_keys = Array(String).new
|
|
||||||
grouped_fields_list.each do |value|
|
|
||||||
if value.is_a? String
|
|
||||||
top_level_keys.push value
|
|
||||||
elsif value.is_a? Array
|
|
||||||
if !top_level_keys.empty?
|
|
||||||
key_to_filter = top_level_keys.last
|
|
||||||
|
|
||||||
if item.as_h?
|
|
||||||
filter(item[key_to_filter], value, in_place: true)
|
|
||||||
elsif item.as_a?
|
|
||||||
item.as_a.each { |arr_item| filter(arr_item[key_to_filter], value, in_place: true) }
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raise FilterError.new "Tried to filter while top level keys list is empty"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if item.as_h?
|
|
||||||
item.as_h.select! top_level_keys
|
|
||||||
elsif item.as_a?
|
|
||||||
item.as_a.map { |value| filter(value, top_level_keys, in_place: true) }
|
|
||||||
end
|
|
||||||
|
|
||||||
item
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -262,7 +262,7 @@ def get_referer(env, fallback = "/", unroll = true)
|
||||||
end
|
end
|
||||||
|
|
||||||
referer = referer.request_target
|
referer = referer.request_target
|
||||||
referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,0-9a-zA-Z]/, "").lstrip("/\\")
|
referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z]/, "").lstrip("/\\")
|
||||||
|
|
||||||
if referer == env.request.path
|
if referer == env.request.path
|
||||||
referer = fallback
|
referer = fallback
|
||||||
|
|
|
@ -4,13 +4,23 @@
|
||||||
module WebVTT
|
module WebVTT
|
||||||
# A WebVTT builder generates WebVTT files
|
# A WebVTT builder generates WebVTT files
|
||||||
private class Builder
|
private class Builder
|
||||||
|
# See https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API#cue_payload
|
||||||
|
private ESCAPE_SUBSTITUTIONS = {
|
||||||
|
'&' => "&",
|
||||||
|
'<' => "<",
|
||||||
|
'>' => ">",
|
||||||
|
'\u200E' => "‎",
|
||||||
|
'\u200F' => "‏",
|
||||||
|
'\u00A0' => " ",
|
||||||
|
}
|
||||||
|
|
||||||
def initialize(@io : IO)
|
def initialize(@io : IO)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Writes an vtt cue with the specified time stamp and contents
|
# Writes an vtt cue with the specified time stamp and contents
|
||||||
def cue(start_time : Time::Span, end_time : Time::Span, text : String)
|
def cue(start_time : Time::Span, end_time : Time::Span, text : String)
|
||||||
timestamp(start_time, end_time)
|
timestamp(start_time, end_time)
|
||||||
@io << text
|
@io << self.escape(text)
|
||||||
@io << "\n\n"
|
@io << "\n\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -29,6 +39,10 @@ module WebVTT
|
||||||
@io << '.' << timestamp.milliseconds.to_s.rjust(3, '0')
|
@io << '.' << timestamp.milliseconds.to_s.rjust(3, '0')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private def escape(text : String) : String
|
||||||
|
return text.gsub(ESCAPE_SUBSTITUTIONS)
|
||||||
|
end
|
||||||
|
|
||||||
def document(setting_fields : Hash(String, String)? = nil, &)
|
def document(setting_fields : Hash(String, String)? = nil, &)
|
||||||
@io << "WEBVTT\n"
|
@io << "WEBVTT\n"
|
||||||
|
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob
|
|
||||||
def begin
|
|
||||||
loop do
|
|
||||||
begin
|
|
||||||
random_video = PG_DB.query_one?("select id, ucid from (select id, ucid from channel_videos limit 1000) as s ORDER BY RANDOM() LIMIT 1", as: {id: String, ucid: String})
|
|
||||||
if !random_video
|
|
||||||
random_video = {id: "zj82_v2R6ts", ucid: "UCK87Lox575O_HCHBWaBSyGA"}
|
|
||||||
end
|
|
||||||
{"/watch?v=#{random_video["id"]}&gl=US&hl=en&has_verified=1&bpctr=9999999999", produce_channel_videos_url(ucid: random_video["ucid"])}.each do |path|
|
|
||||||
response = YT_POOL.client &.get(path)
|
|
||||||
if response.body.includes?("To continue with your YouTube experience, please fill out the form below.")
|
|
||||||
html = XML.parse_html(response.body)
|
|
||||||
form = html.xpath_node(%(//form[@action="/das_captcha"])).not_nil!
|
|
||||||
site_key = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-sitekey"]
|
|
||||||
s_value = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-s"]
|
|
||||||
|
|
||||||
inputs = {} of String => String
|
|
||||||
form.xpath_nodes(%(.//input[@name])).map do |node|
|
|
||||||
inputs[node["name"]] = node["value"]
|
|
||||||
end
|
|
||||||
|
|
||||||
headers = response.cookies.add_request_headers(HTTP::Headers.new)
|
|
||||||
|
|
||||||
response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/createTask",
|
|
||||||
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
|
|
||||||
"clientKey" => CONFIG.captcha_key,
|
|
||||||
"task" => {
|
|
||||||
"type" => "NoCaptchaTaskProxyless",
|
|
||||||
"websiteURL" => "https://www.youtube.com#{path}",
|
|
||||||
"websiteKey" => site_key,
|
|
||||||
"recaptchaDataSValue" => s_value,
|
|
||||||
},
|
|
||||||
}.to_json).body)
|
|
||||||
|
|
||||||
raise response["error"].as_s if response["error"]?
|
|
||||||
task_id = response["taskId"].as_i
|
|
||||||
|
|
||||||
loop do
|
|
||||||
sleep 10.seconds
|
|
||||||
|
|
||||||
response = JSON.parse(HTTP::Client.post(CONFIG.captcha_api_url + "/getTaskResult",
|
|
||||||
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
|
|
||||||
"clientKey" => CONFIG.captcha_key,
|
|
||||||
"taskId" => task_id,
|
|
||||||
}.to_json).body)
|
|
||||||
|
|
||||||
if response["status"]?.try &.== "ready"
|
|
||||||
break
|
|
||||||
elsif response["errorId"]?.try &.as_i != 0
|
|
||||||
raise response["errorDescription"].as_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s
|
|
||||||
headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
|
|
||||||
response = YT_POOL.client &.post("/das_captcha", headers, form: inputs)
|
|
||||||
|
|
||||||
response.cookies
|
|
||||||
.select { |cookie| cookie.name != "PREF" }
|
|
||||||
.each { |cookie| CONFIG.cookies << cookie }
|
|
||||||
|
|
||||||
# Persist cookies between runs
|
|
||||||
File.write("config/config.yml", CONFIG.to_yaml)
|
|
||||||
elsif response.headers["Location"]?.try &.includes?("/sorry/index")
|
|
||||||
location = response.headers["Location"].try { |u| URI.parse(u) }
|
|
||||||
headers = HTTP::Headers{":authority" => location.host.not_nil!}
|
|
||||||
response = YT_POOL.client &.get(location.request_target, headers)
|
|
||||||
|
|
||||||
html = XML.parse_html(response.body)
|
|
||||||
form = html.xpath_node(%(//form[@action="index"])).not_nil!
|
|
||||||
site_key = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-sitekey"]
|
|
||||||
s_value = form.xpath_node(%(.//div[@id="recaptcha"])).try &.["data-s"]
|
|
||||||
|
|
||||||
inputs = {} of String => String
|
|
||||||
form.xpath_nodes(%(.//input[@name])).map do |node|
|
|
||||||
inputs[node["name"]] = node["value"]
|
|
||||||
end
|
|
||||||
|
|
||||||
captcha_client = HTTPClient.new(URI.parse(CONFIG.captcha_api_url))
|
|
||||||
captcha_client.family = CONFIG.force_resolve || Socket::Family::INET
|
|
||||||
response = JSON.parse(captcha_client.post("/createTask",
|
|
||||||
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
|
|
||||||
"clientKey" => CONFIG.captcha_key,
|
|
||||||
"task" => {
|
|
||||||
"type" => "NoCaptchaTaskProxyless",
|
|
||||||
"websiteURL" => location.to_s,
|
|
||||||
"websiteKey" => site_key,
|
|
||||||
"recaptchaDataSValue" => s_value,
|
|
||||||
},
|
|
||||||
}.to_json).body)
|
|
||||||
|
|
||||||
captcha_client.close
|
|
||||||
|
|
||||||
raise response["error"].as_s if response["error"]?
|
|
||||||
task_id = response["taskId"].as_i
|
|
||||||
|
|
||||||
loop do
|
|
||||||
sleep 10.seconds
|
|
||||||
|
|
||||||
response = JSON.parse(captcha_client.post("/getTaskResult",
|
|
||||||
headers: HTTP::Headers{"Content-Type" => "application/json"}, body: {
|
|
||||||
"clientKey" => CONFIG.captcha_key,
|
|
||||||
"taskId" => task_id,
|
|
||||||
}.to_json).body)
|
|
||||||
|
|
||||||
if response["status"]?.try &.== "ready"
|
|
||||||
break
|
|
||||||
elsif response["errorId"]?.try &.as_i != 0
|
|
||||||
raise response["errorDescription"].as_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
inputs["g-recaptcha-response"] = response["solution"]["gRecaptchaResponse"].as_s
|
|
||||||
headers["Cookies"] = response["solution"]["cookies"].as_h?.try &.map { |k, v| "#{k}=#{v}" }.join("; ") || ""
|
|
||||||
response = YT_POOL.client &.post("/sorry/index", headers: headers, form: inputs)
|
|
||||||
headers = HTTP::Headers{
|
|
||||||
"Cookie" => URI.parse(response.headers["location"]).query_params["google_abuse"].split(";")[0],
|
|
||||||
}
|
|
||||||
cookies = HTTP::Cookies.from_client_headers(headers)
|
|
||||||
|
|
||||||
cookies.each { |cookie| CONFIG.cookies << cookie }
|
|
||||||
|
|
||||||
# Persist cookies between runs
|
|
||||||
File.write("config/config.yml", CONFIG.to_yaml)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue ex
|
|
||||||
LOGGER.error("BypassCaptchaJob: #{ex.message}")
|
|
||||||
ensure
|
|
||||||
sleep 1.minute
|
|
||||||
Fiber.yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -18,6 +18,13 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
|
||||||
"updatedAt" => Time.utc.to_unix,
|
"updatedAt" => Time.utc.to_unix,
|
||||||
"lastChannelRefreshedAt" => 0_i64,
|
"lastChannelRefreshedAt" => 0_i64,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#
|
||||||
|
# "totalRequests" => 0_i64,
|
||||||
|
# "successfulRequests" => 0_i64
|
||||||
|
# "ratio" => 0_i64
|
||||||
|
#
|
||||||
|
"playback" => {} of String => Int64 | Float64,
|
||||||
}
|
}
|
||||||
|
|
||||||
private getter db : DB::Database
|
private getter db : DB::Database
|
||||||
|
@ -30,7 +37,7 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
refresh_stats
|
refresh_stats
|
||||||
sleep 1.minute
|
sleep 10.minute
|
||||||
Fiber.yield
|
Fiber.yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -49,12 +56,15 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
|
||||||
users = STATISTICS.dig("usage", "users").as(Hash(String, Int64))
|
users = STATISTICS.dig("usage", "users").as(Hash(String, Int64))
|
||||||
|
|
||||||
users["total"] = Invidious::Database::Statistics.count_users_total
|
users["total"] = Invidious::Database::Statistics.count_users_total
|
||||||
users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_1m
|
users["activeHalfyear"] = Invidious::Database::Statistics.count_users_active_6m
|
||||||
users["activeMonth"] = Invidious::Database::Statistics.count_users_active_6m
|
users["activeMonth"] = Invidious::Database::Statistics.count_users_active_1m
|
||||||
|
|
||||||
STATISTICS["metadata"] = {
|
STATISTICS["metadata"] = {
|
||||||
"updatedAt" => Time.utc.to_unix,
|
"updatedAt" => Time.utc.to_unix,
|
||||||
"lastChannelRefreshedAt" => Invidious::Database::Statistics.channel_last_update.try &.to_unix || 0_i64,
|
"lastChannelRefreshedAt" => Invidious::Database::Statistics.channel_last_update.try &.to_unix || 0_i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Reset playback requests tracker
|
||||||
|
STATISTICS["playback"] = {} of String => Int64 | Float64
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6,6 +6,22 @@ module Invidious::Routes::API::V1::Misc
|
||||||
if !CONFIG.statistics_enabled
|
if !CONFIG.statistics_enabled
|
||||||
return {"software" => SOFTWARE}.to_json
|
return {"software" => SOFTWARE}.to_json
|
||||||
else
|
else
|
||||||
|
# Calculate playback success rate
|
||||||
|
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]?)
|
||||||
|
tracker = tracker.as(Hash(String, Int64 | Float64))
|
||||||
|
|
||||||
|
if !tracker.empty?
|
||||||
|
total_requests = tracker["totalRequests"]
|
||||||
|
success_count = tracker["successfulRequests"]
|
||||||
|
|
||||||
|
if total_requests.zero?
|
||||||
|
tracker["ratio"] = 1_i64
|
||||||
|
else
|
||||||
|
tracker["ratio"] = (success_count / (total_requests)).round(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json
|
return Invidious::Jobs::StatisticsRefreshJob::STATISTICS.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -175,6 +191,8 @@ module Invidious::Routes::API::V1::Misc
|
||||||
json.object do
|
json.object do
|
||||||
json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]?
|
json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]?
|
||||||
json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]?
|
json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]?
|
||||||
|
json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]?
|
||||||
|
json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]?
|
||||||
json.field "params", params.try &.as_s
|
json.field "params", params.try &.as_s
|
||||||
json.field "pageType", pageType
|
json.field "pageType", pageType
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,11 +32,14 @@ module Invidious::Routes::API::V1::Search
|
||||||
|
|
||||||
begin
|
begin
|
||||||
client = HTTP::Client.new("suggestqueries-clients6.youtube.com")
|
client = HTTP::Client.new("suggestqueries-clients6.youtube.com")
|
||||||
url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&xssi=t&gs_ri=youtube&ds=yt"
|
client.before_request { |r| add_yt_headers(r) }
|
||||||
|
|
||||||
|
url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt"
|
||||||
|
|
||||||
response = client.get(url).body
|
response = client.get(url).body
|
||||||
|
client.close
|
||||||
|
|
||||||
body = JSON.parse(response[5..-1]).as_a
|
body = JSON.parse(response[19..-2]).as_a
|
||||||
suggestions = body[1].as_a[0..-2]
|
suggestions = body[1].as_a[0..-2]
|
||||||
|
|
||||||
JSON.build do |json|
|
JSON.build do |json|
|
||||||
|
|
|
@ -363,4 +363,47 @@ module Invidious::Routes::API::V1::Videos
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.clips(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
clip_id = env.params.url["id"]
|
||||||
|
region = env.params.query["region"]?
|
||||||
|
proxy = {"1", "true"}.any? &.== env.params.query["local"]?
|
||||||
|
|
||||||
|
response = YoutubeAPI.resolve_url("https://www.youtube.com/clip/#{clip_id}")
|
||||||
|
return error_json(400, "Invalid clip ID") if response["error"]?
|
||||||
|
|
||||||
|
video_id = response.dig?("endpoint", "watchEndpoint", "videoId").try &.as_s
|
||||||
|
return error_json(400, "Invalid clip ID") if video_id.nil?
|
||||||
|
|
||||||
|
start_time = nil
|
||||||
|
end_time = nil
|
||||||
|
clip_title = nil
|
||||||
|
|
||||||
|
if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s
|
||||||
|
start_time, end_time, clip_title = parse_clip_parameters(params)
|
||||||
|
end
|
||||||
|
|
||||||
|
begin
|
||||||
|
video = get_video(video_id, region: region)
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
return JSON.build do |json|
|
||||||
|
json.object do
|
||||||
|
json.field "startTime", start_time
|
||||||
|
json.field "endTime", end_time
|
||||||
|
json.field "clipTitle", clip_title
|
||||||
|
json.field "video" do
|
||||||
|
Invidious::JSONify::APIv1.video(video, json, locale: locale, proxy: proxy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -407,14 +407,23 @@ module Invidious::Routes::Feeds
|
||||||
end
|
end
|
||||||
|
|
||||||
spawn do
|
spawn do
|
||||||
rss = XML.parse_html(body)
|
# TODO: unify this with the other almost identical looking parts in this and channels.cr somehow?
|
||||||
rss.xpath_nodes("//feed/entry").each do |entry|
|
namespaces = {
|
||||||
id = entry.xpath_node("videoid").not_nil!.content
|
"yt" => "http://www.youtube.com/xml/schemas/2015",
|
||||||
author = entry.xpath_node("author/name").not_nil!.content
|
"default" => "http://www.w3.org/2005/Atom",
|
||||||
published = Time.parse_rfc3339(entry.xpath_node("published").not_nil!.content)
|
}
|
||||||
updated = Time.parse_rfc3339(entry.xpath_node("updated").not_nil!.content)
|
rss = XML.parse(body)
|
||||||
|
rss.xpath_nodes("//default:feed/default:entry", namespaces).each do |entry|
|
||||||
|
id = entry.xpath_node("yt:videoId", namespaces).not_nil!.content
|
||||||
|
author = entry.xpath_node("default:author/default:name", 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)
|
||||||
|
|
||||||
|
begin
|
||||||
video = get_video(id, force_refresh: true)
|
video = get_video(id, force_refresh: true)
|
||||||
|
rescue
|
||||||
|
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
|
||||||
|
end
|
||||||
|
|
||||||
if CONFIG.enable_user_notifications
|
if CONFIG.enable_user_notifications
|
||||||
# Deliver notifications to `/api/v1/auth/notifications`
|
# Deliver notifications to `/api/v1/auth/notifications`
|
||||||
|
|
|
@ -42,7 +42,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
headers["Range"] = "bytes=#{range_for_head}"
|
headers["Range"] = "bytes=#{range_for_head}"
|
||||||
end
|
end
|
||||||
|
|
||||||
client = make_client(URI.parse(host), region)
|
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||||
response = HTTP::Client::Response.new(500)
|
response = HTTP::Client::Response.new(500)
|
||||||
error = ""
|
error = ""
|
||||||
5.times do
|
5.times do
|
||||||
|
@ -57,7 +57,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
if new_host != host
|
if new_host != host
|
||||||
host = new_host
|
host = new_host
|
||||||
client.close
|
client.close
|
||||||
client = make_client(URI.parse(new_host), region)
|
client = make_client(URI.parse(new_host), region, force_resolve = true)
|
||||||
end
|
end
|
||||||
|
|
||||||
url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
||||||
|
@ -71,7 +71,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
fvip = "3"
|
fvip = "3"
|
||||||
|
|
||||||
host = "https://r#{fvip}---#{mn}.googlevideo.com"
|
host = "https://r#{fvip}---#{mn}.googlevideo.com"
|
||||||
client = make_client(URI.parse(host), region)
|
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||||
rescue ex
|
rescue ex
|
||||||
error = ex.message
|
error = ex.message
|
||||||
end
|
end
|
||||||
|
@ -80,9 +80,14 @@ module Invidious::Routes::VideoPlayback
|
||||||
# Remove the Range header added previously.
|
# Remove the Range header added previously.
|
||||||
headers.delete("Range") if range_header.nil?
|
headers.delete("Range") if range_header.nil?
|
||||||
|
|
||||||
|
playback_statistics = get_playback_statistic()
|
||||||
|
playback_statistics["totalRequests"] += 1
|
||||||
|
|
||||||
if response.status_code >= 400
|
if response.status_code >= 400
|
||||||
env.response.content_type = "text/plain"
|
env.response.content_type = "text/plain"
|
||||||
haltf env, response.status_code
|
haltf env, response.status_code
|
||||||
|
else
|
||||||
|
playback_statistics["successfulRequests"] += 1
|
||||||
end
|
end
|
||||||
|
|
||||||
if url.includes? "&file=seg.ts"
|
if url.includes? "&file=seg.ts"
|
||||||
|
@ -191,7 +196,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
client.close
|
client.close
|
||||||
client = make_client(URI.parse(host), region)
|
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -275,6 +275,12 @@ module Invidious::Routes::Watch
|
||||||
return error_template(400, "Invalid clip ID") if response["error"]?
|
return error_template(400, "Invalid clip ID") if response["error"]?
|
||||||
|
|
||||||
if video_id = response.dig?("endpoint", "watchEndpoint", "videoId")
|
if video_id = response.dig?("endpoint", "watchEndpoint", "videoId")
|
||||||
|
if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s
|
||||||
|
start_time, end_time, _ = parse_clip_parameters(params)
|
||||||
|
env.params.query["start"] = start_time.to_s if start_time != nil
|
||||||
|
env.params.query["end"] = end_time.to_s if end_time != nil
|
||||||
|
end
|
||||||
|
|
||||||
return env.redirect "/watch?v=#{video_id}&#{env.params.query}"
|
return env.redirect "/watch?v=#{video_id}&#{env.params.query}"
|
||||||
else
|
else
|
||||||
return error_template(404, "The requested clip doesn't exist")
|
return error_template(404, "The requested clip doesn't exist")
|
||||||
|
|
|
@ -235,6 +235,7 @@ module Invidious::Routing
|
||||||
get "/api/v1/captions/:id", {{namespace}}::Videos, :captions
|
get "/api/v1/captions/:id", {{namespace}}::Videos, :captions
|
||||||
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
|
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
|
||||||
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments
|
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments
|
||||||
|
get "/api/v1/clips/:id", {{namespace}}::Videos, :clips
|
||||||
|
|
||||||
# Feeds
|
# Feeds
|
||||||
get "/api/v1/trending", {{namespace}}::Feeds, :trending
|
get "/api/v1/trending", {{namespace}}::Feeds, :trending
|
||||||
|
|
|
@ -300,9 +300,9 @@ module Invidious::Search
|
||||||
object["9:varint"] = ((page - 1) * 20).to_i64
|
object["9:varint"] = ((page - 1) * 20).to_i64
|
||||||
end
|
end
|
||||||
|
|
||||||
# If the object is empty, return an empty string,
|
# Prevent censoring of self harm topics
|
||||||
# otherwise encode to protobuf then to base64
|
# See https://github.com/iv-org/invidious/issues/4398
|
||||||
return "" if object.empty?
|
object["30:varint"] = 1.to_i64
|
||||||
|
|
||||||
return object
|
return object
|
||||||
.try { |i| Protodec::Any.cast_json(i) }
|
.try { |i| Protodec::Any.cast_json(i) }
|
||||||
|
|
|
@ -227,8 +227,22 @@ struct Video
|
||||||
info.dig?("streamingData", "hlsManifestUrl").try &.as_s
|
info.dig?("streamingData", "hlsManifestUrl").try &.as_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def dash_manifest_url
|
def dash_manifest_url : String?
|
||||||
info.dig?("streamingData", "dashManifestUrl").try &.as_s
|
raw_dash_url = info.dig?("streamingData", "dashManifestUrl").try &.as_s
|
||||||
|
return nil if raw_dash_url.nil?
|
||||||
|
|
||||||
|
# Use manifest v5 parameter to reduce file size
|
||||||
|
# See https://github.com/iv-org/invidious/issues/4186
|
||||||
|
dash_url = URI.parse(raw_dash_url)
|
||||||
|
dash_query = dash_url.query || ""
|
||||||
|
|
||||||
|
if dash_query.empty?
|
||||||
|
dash_url.path = "#{dash_url.path}/mpd_version/5"
|
||||||
|
else
|
||||||
|
dash_url.query = "#{dash_query}&mpd_version=5"
|
||||||
|
end
|
||||||
|
|
||||||
|
return dash_url.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def genre_url : String?
|
def genre_url : String?
|
||||||
|
|
22
src/invidious/videos/clip.cr
Normal file
22
src/invidious/videos/clip.cr
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
# returns start_time, end_time and clip_title
|
||||||
|
def parse_clip_parameters(params) : {Float64?, Float64?, String?}
|
||||||
|
decoded_protobuf = params.try { |i| URI.decode_www_form(i) }
|
||||||
|
.try { |i| Base64.decode(i) }
|
||||||
|
.try { |i| IO::Memory.new(i) }
|
||||||
|
.try { |i| Protodec::Any.parse(i) }
|
||||||
|
|
||||||
|
start_time = decoded_protobuf
|
||||||
|
.try(&.["50:0:embedded"]["2:1:varint"].as_i64)
|
||||||
|
.try { |i| i/1000 }
|
||||||
|
|
||||||
|
end_time = decoded_protobuf
|
||||||
|
.try(&.["50:0:embedded"]["3:2:varint"].as_i64)
|
||||||
|
.try { |i| i/1000 }
|
||||||
|
|
||||||
|
clip_title = decoded_protobuf
|
||||||
|
.try(&.["50:0:embedded"]["4:3:string"].as_s)
|
||||||
|
|
||||||
|
return start_time, end_time, clip_title
|
||||||
|
end
|
|
@ -78,6 +78,11 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
|
||||||
# YouTube may return a different video player response than expected.
|
# YouTube may return a different video player response than expected.
|
||||||
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
|
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
|
||||||
# Line to be reverted if one day we solve the video not available issue.
|
# Line to be reverted if one day we solve the video not available issue.
|
||||||
|
|
||||||
|
# Although technically not a call to /videoplayback the fact that YouTube is returning the
|
||||||
|
# wrong video means that we should count it as a failure.
|
||||||
|
get_playback_statistic()["totalRequests"] += 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
||||||
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
|
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
|
||||||
|
|
|
@ -82,11 +82,19 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="video-card-row flexible">
|
<div class="video-card-row flexible">
|
||||||
<div class="flex-left"><a href="/channel/<%= item.ucid %>">
|
<div class="flex-left">
|
||||||
|
<% if !item.ucid.to_s.empty? %>
|
||||||
|
<a href="/channel/<%= item.ucid %>">
|
||||||
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
|
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
|
||||||
<%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%>
|
<%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%>
|
||||||
</p>
|
</p>
|
||||||
</a></div>
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
|
||||||
|
<%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% when Category %>
|
<% when Category %>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
@ -160,11 +168,19 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="video-card-row flexible">
|
<div class="video-card-row flexible">
|
||||||
<div class="flex-left"><a href="/channel/<%= item.ucid %>">
|
<div class="flex-left">
|
||||||
|
<% if !item.ucid.to_s.empty? %>
|
||||||
|
<a href="/channel/<%= item.ucid %>">
|
||||||
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
|
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
|
||||||
<%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%>
|
<%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%>
|
||||||
</p>
|
</p>
|
||||||
</a></div>
|
</a>
|
||||||
|
<% else %>
|
||||||
|
<p class="channel-name" dir="auto"><%= HTML.escape(item.author) %>
|
||||||
|
<%- if author_verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end -%>
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
<%= rendered "components/video-context-buttons" %>
|
<%= rendered "components/video-context-buttons" %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
|
<%
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
||||||
|
%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="<%= env.get("preferences").as(Preferences).locale %>">
|
<html lang="<%= locale %>">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
@ -17,19 +21,14 @@
|
||||||
<link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>">
|
<link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>">
|
||||||
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
|
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
|
||||||
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
|
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
|
||||||
|
<link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>">
|
||||||
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
|
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<%
|
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
|
||||||
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
|
||||||
%>
|
|
||||||
|
|
||||||
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
|
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
|
||||||
<span style="display:none" id="dark_mode_pref"><%= env.get("preferences").as(Preferences).dark_mode %></span>
|
<span style="display:none" id="dark_mode_pref"><%= dark_mode %></span>
|
||||||
<div class="pure-g">
|
<div class="pure-g">
|
||||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
<div class="pure-u-1 pure-u-xl-20-24" id="contents">
|
||||||
<div class="pure-u-1 pure-u-md-20-24" id="contents">
|
|
||||||
<div class="pure-g navbar h-box">
|
<div class="pure-g navbar h-box">
|
||||||
<% if navbar_search %>
|
<% if navbar_search %>
|
||||||
<div class="pure-u-1 pure-u-md-4-24">
|
<div class="pure-u-1 pure-u-md-4-24">
|
||||||
|
@ -43,8 +42,8 @@
|
||||||
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
||||||
<% if env.get? "user" %>
|
<% if env.get? "user" %>
|
||||||
<div class="pure-u-1-4">
|
<div class="pure-u-1-4">
|
||||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
|
||||||
<% if env.get("preferences").as(Preferences).dark_mode == "dark" %>
|
<% if dark_mode == "dark" %>
|
||||||
<i class="icon ion-ios-sunny"></i>
|
<i class="icon ion-ios-sunny"></i>
|
||||||
<% else %>
|
<% else %>
|
||||||
<i class="icon ion-ios-moon"></i>
|
<i class="icon ion-ios-moon"></i>
|
||||||
|
@ -81,8 +80,8 @@
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="pure-u-1-3">
|
<div class="pure-u-1-3">
|
||||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
|
||||||
<% if env.get("preferences").as(Preferences).dark_mode == "dark" %>
|
<% if dark_mode == "dark" %>
|
||||||
<i class="icon ion-ios-sunny"></i>
|
<i class="icon ion-ios-sunny"></i>
|
||||||
<% else %>
|
<% else %>
|
||||||
<i class="icon ion-ios-moon"></i>
|
<i class="icon ion-ios-moon"></i>
|
||||||
|
@ -156,7 +155,6 @@
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script>
|
<script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script>
|
||||||
<script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script>
|
<script src="/js/themes.js?v=<%= ASSET_COMMIT %>"></script>
|
||||||
|
|
|
@ -118,7 +118,7 @@ we're going to need to do it here in order to allow for translations.
|
||||||
link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}")
|
link_yt_embed = URI.new(scheme: "https", host: "www.youtube.com", path: "/embed/#{video.id}")
|
||||||
|
|
||||||
if !plid.nil? && !continuation.nil?
|
if !plid.nil? && !continuation.nil?
|
||||||
link_yt_param = URI::Params{"plid" => [plid], "index" => [continuation.to_s]}
|
link_yt_param = URI::Params{"list" => [plid], "index" => [continuation.to_s]}
|
||||||
link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param)
|
link_yt_watch = IV::HttpServer::Utils.add_params_to_url(link_yt_watch, link_yt_param)
|
||||||
link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param)
|
link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param)
|
||||||
end
|
end
|
||||||
|
@ -346,7 +346,7 @@ we're going to need to do it here in order to allow for translations.
|
||||||
|
|
||||||
<h5 class="pure-g">
|
<h5 class="pure-g">
|
||||||
<div class="pure-u-14-24">
|
<div class="pure-u-14-24">
|
||||||
<% if rv["ucid"]? %>
|
<% if !rv["ucid"].empty? %>
|
||||||
<b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
|
<b style="width:100%"><a href="/channel/<%= rv["ucid"] %>"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></a></b>
|
||||||
<% else %>
|
<% else %>
|
||||||
<b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></b>
|
<b style="width:100%"><%= rv["author"]? %><% if rv["author_verified"]? == "true" %> <i class="icon ion ion-md-checkmark-circle"></i><% end %></b>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
def add_yt_headers(request)
|
def add_yt_headers(request)
|
||||||
if request.headers["User-Agent"] == "Crystal"
|
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
|
||||||
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
|
||||||
end
|
|
||||||
|
|
||||||
request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
|
request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
|
||||||
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||||
|
@ -27,7 +26,7 @@ struct YoutubeConnectionPool
|
||||||
|
|
||||||
def client(region = nil, &block)
|
def client(region = nil, &block)
|
||||||
if region
|
if region
|
||||||
conn = make_client(url, region)
|
conn = make_client(url, region, force_resolve = true)
|
||||||
response = yield conn
|
response = yield conn
|
||||||
else
|
else
|
||||||
conn = pool.checkout
|
conn = pool.checkout
|
||||||
|
@ -37,7 +36,7 @@ struct YoutubeConnectionPool
|
||||||
conn.close
|
conn.close
|
||||||
conn = HTTP::Client.new(url)
|
conn = HTTP::Client.new(url)
|
||||||
|
|
||||||
conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
|
conn.family = CONFIG.force_resolve
|
||||||
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
||||||
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||||
response = yield conn
|
response = yield conn
|
||||||
|
@ -52,7 +51,7 @@ struct YoutubeConnectionPool
|
||||||
private def build_pool
|
private def build_pool
|
||||||
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
|
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
|
||||||
conn = HTTP::Client.new(url)
|
conn = HTTP::Client.new(url)
|
||||||
conn.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::INET
|
conn.family = CONFIG.force_resolve
|
||||||
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
||||||
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||||
conn
|
conn
|
||||||
|
@ -60,9 +59,14 @@ struct YoutubeConnectionPool
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_client(url : URI, region = nil)
|
def make_client(url : URI, region = nil, force_resolve : Bool = false)
|
||||||
client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure)
|
client = HTTPClient.new(url, OpenSSL::SSL::Context::Client.insecure)
|
||||||
client.family = (url.host == "www.youtube.com") ? CONFIG.force_resolve : Socket::Family::UNSPEC
|
|
||||||
|
# Some services do not support IPv6.
|
||||||
|
if force_resolve
|
||||||
|
client.family = CONFIG.force_resolve
|
||||||
|
end
|
||||||
|
|
||||||
client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||||
client.read_timeout = 10.seconds
|
client.read_timeout = 10.seconds
|
||||||
client.connect_timeout = 10.seconds
|
client.connect_timeout = 10.seconds
|
||||||
|
@ -81,8 +85,8 @@ def make_client(url : URI, region = nil)
|
||||||
return client
|
return client
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_client(url : URI, region = nil, &block)
|
def make_client(url : URI, region = nil, force_resolve : Bool = false, &block)
|
||||||
client = make_client(url, region)
|
client = make_client(url, region, force_resolve)
|
||||||
begin
|
begin
|
||||||
yield client
|
yield client
|
||||||
ensure
|
ensure
|
||||||
|
|
|
@ -822,9 +822,9 @@ module HelperExtractors
|
||||||
end
|
end
|
||||||
|
|
||||||
# Retrieves the ID required for querying the InnerTube browse endpoint.
|
# Retrieves the ID required for querying the InnerTube browse endpoint.
|
||||||
# Raises when it's unable to do so
|
# Returns an empty string when it's unable to do so
|
||||||
def self.get_browse_id(container)
|
def self.get_browse_id(container)
|
||||||
return container.dig("navigationEndpoint", "browseEndpoint", "browseId").as_s
|
return container.dig?("navigationEndpoint", "browseEndpoint", "browseId").try &.as_s || ""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,17 +7,18 @@ module YoutubeAPI
|
||||||
|
|
||||||
private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
private DEFAULT_API_KEY = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8"
|
||||||
|
|
||||||
private ANDROID_APP_VERSION = "18.20.38"
|
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
|
||||||
# github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1308
|
private ANDROID_APP_VERSION = "19.09.36"
|
||||||
private ANDROID_USER_AGENT = "com.google.android.youtube/18.20.38 (Linux; U; Android 12; US) gzip"
|
private ANDROID_USER_AGENT = "com.google.android.youtube/19.09.36 (Linux; U; Android 12; US) gzip"
|
||||||
private ANDROID_SDK_VERSION = 31_i64
|
private ANDROID_SDK_VERSION = 31_i64
|
||||||
private ANDROID_VERSION = "12"
|
private ANDROID_VERSION = "12"
|
||||||
|
|
||||||
private IOS_APP_VERSION = "18.21.3"
|
# For Apple device names, see https://gist.github.com/adamawolf/3048717
|
||||||
# github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1330
|
# For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases,
|
||||||
private IOS_USER_AGENT = "com.google.ios.youtube/18.21.3 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)"
|
# then go to the dedicated article of the major version you want.
|
||||||
# github.com/TeamNewPipe/NewPipeExtractor/blob/943b7c033bb9d07ead63ddab4441c287653e4384/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/YoutubeParsingHelper.java#L1224
|
private IOS_APP_VERSION = "19.09.3"
|
||||||
private IOS_VERSION = "15.6.0.19G71"
|
private IOS_USER_AGENT = "com.google.ios.youtube/19.09.3 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)"
|
||||||
|
private IOS_VERSION = "17.4.0.21E219" # Major.Minor.Patch.Build
|
||||||
|
|
||||||
private WINDOWS_VERSION = "10.0"
|
private WINDOWS_VERSION = "10.0"
|
||||||
|
|
||||||
|
@ -45,7 +46,7 @@ module YoutubeAPI
|
||||||
ClientType::Web => {
|
ClientType::Web => {
|
||||||
name: "WEB",
|
name: "WEB",
|
||||||
name_proto: "1",
|
name_proto: "1",
|
||||||
version: "2.20230602.01.00",
|
version: "2.20240304.00.00",
|
||||||
api_key: DEFAULT_API_KEY,
|
api_key: DEFAULT_API_KEY,
|
||||||
screen: "WATCH_FULL_SCREEN",
|
screen: "WATCH_FULL_SCREEN",
|
||||||
os_name: "Windows",
|
os_name: "Windows",
|
||||||
|
@ -55,7 +56,7 @@ module YoutubeAPI
|
||||||
ClientType::WebEmbeddedPlayer => {
|
ClientType::WebEmbeddedPlayer => {
|
||||||
name: "WEB_EMBEDDED_PLAYER",
|
name: "WEB_EMBEDDED_PLAYER",
|
||||||
name_proto: "56",
|
name_proto: "56",
|
||||||
version: "1.20220803.01.00",
|
version: "1.20240303.00.00",
|
||||||
api_key: DEFAULT_API_KEY,
|
api_key: DEFAULT_API_KEY,
|
||||||
screen: "EMBED",
|
screen: "EMBED",
|
||||||
os_name: "Windows",
|
os_name: "Windows",
|
||||||
|
@ -65,7 +66,7 @@ module YoutubeAPI
|
||||||
ClientType::WebMobile => {
|
ClientType::WebMobile => {
|
||||||
name: "MWEB",
|
name: "MWEB",
|
||||||
name_proto: "2",
|
name_proto: "2",
|
||||||
version: "2.20230531.05.00",
|
version: "2.20240304.08.00",
|
||||||
api_key: DEFAULT_API_KEY,
|
api_key: DEFAULT_API_KEY,
|
||||||
os_name: "Android",
|
os_name: "Android",
|
||||||
os_version: ANDROID_VERSION,
|
os_version: ANDROID_VERSION,
|
||||||
|
@ -74,7 +75,7 @@ module YoutubeAPI
|
||||||
ClientType::WebScreenEmbed => {
|
ClientType::WebScreenEmbed => {
|
||||||
name: "WEB",
|
name: "WEB",
|
||||||
name_proto: "1",
|
name_proto: "1",
|
||||||
version: "2.20220804.00.00",
|
version: "2.20240304.00.00",
|
||||||
api_key: DEFAULT_API_KEY,
|
api_key: DEFAULT_API_KEY,
|
||||||
screen: "EMBED",
|
screen: "EMBED",
|
||||||
os_name: "Windows",
|
os_name: "Windows",
|
||||||
|
@ -99,7 +100,7 @@ module YoutubeAPI
|
||||||
name: "ANDROID_EMBEDDED_PLAYER",
|
name: "ANDROID_EMBEDDED_PLAYER",
|
||||||
name_proto: "55",
|
name_proto: "55",
|
||||||
version: ANDROID_APP_VERSION,
|
version: ANDROID_APP_VERSION,
|
||||||
api_key: DEFAULT_API_KEY,
|
api_key: "AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw",
|
||||||
},
|
},
|
||||||
ClientType::AndroidScreenEmbed => {
|
ClientType::AndroidScreenEmbed => {
|
||||||
name: "ANDROID",
|
name: "ANDROID",
|
||||||
|
@ -143,9 +144,9 @@ module YoutubeAPI
|
||||||
ClientType::IOSMusic => {
|
ClientType::IOSMusic => {
|
||||||
name: "IOS_MUSIC",
|
name: "IOS_MUSIC",
|
||||||
name_proto: "26",
|
name_proto: "26",
|
||||||
version: "5.21",
|
version: "6.42",
|
||||||
api_key: "AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s",
|
api_key: "AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s",
|
||||||
user_agent: "com.google.ios.youtubemusic/5.21 (iPhone14,5; U; CPU iOS 15_6 like Mac OS X;)",
|
user_agent: "com.google.ios.youtubemusic/6.42 (iPhone14,5; U; CPU iOS 17_4 like Mac OS X;)",
|
||||||
device_make: "Apple",
|
device_make: "Apple",
|
||||||
device_model: "iPhone14,5",
|
device_model: "iPhone14,5",
|
||||||
os_name: "iPhone",
|
os_name: "iPhone",
|
||||||
|
@ -158,7 +159,7 @@ module YoutubeAPI
|
||||||
ClientType::TvHtml5 => {
|
ClientType::TvHtml5 => {
|
||||||
name: "TVHTML5",
|
name: "TVHTML5",
|
||||||
name_proto: "7",
|
name_proto: "7",
|
||||||
version: "7.20220325",
|
version: "7.20240304.10.00",
|
||||||
api_key: DEFAULT_API_KEY,
|
api_key: DEFAULT_API_KEY,
|
||||||
},
|
},
|
||||||
ClientType::TvHtml5ScreenEmbed => {
|
ClientType::TvHtml5ScreenEmbed => {
|
||||||
|
|
Loading…
Reference in a new issue