forked from midou/invidious
Compare commits
1 Commits
master
...
limit-feed
Author | SHA1 | Date | |
---|---|---|---|
|
1e6a035109 |
21
.github/workflows/ci.yml
vendored
21
.github/workflows/ci.yml
vendored
@ -38,21 +38,22 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
stable: [true]
|
stable: [true]
|
||||||
crystal:
|
crystal:
|
||||||
|
- 1.4.1
|
||||||
|
- 1.5.1
|
||||||
|
- 1.6.2
|
||||||
- 1.7.3
|
- 1.7.3
|
||||||
- 1.8.2
|
- 1.8.1
|
||||||
- 1.9.2
|
|
||||||
- 1.10.1
|
|
||||||
include:
|
include:
|
||||||
- crystal: nightly
|
- crystal: nightly
|
||||||
stable: false
|
stable: false
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.8.0
|
uses: crystal-lang/install-crystal@v1.7.0
|
||||||
with:
|
with:
|
||||||
crystal: ${{ matrix.crystal }}
|
crystal: ${{ matrix.crystal }}
|
||||||
|
|
||||||
@ -87,7 +88,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build Docker
|
- name: Build Docker
|
||||||
run: docker-compose build --build-arg release=0
|
run: docker-compose build --build-arg release=0
|
||||||
@ -103,18 +104,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Build Docker ARM64 image
|
- name: Build Docker ARM64 image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
|
81
.github/workflows/container-release.yml
vendored
81
.github/workflows/container-release.yml
vendored
@ -11,6 +11,7 @@ on:
|
|||||||
- invidious.service
|
- invidious.service
|
||||||
- .git*
|
- .git*
|
||||||
- .editorconfig
|
- .editorconfig
|
||||||
|
|
||||||
- screenshots/*
|
- screenshots/*
|
||||||
- .github/ISSUE_TEMPLATE/*
|
- .github/ISSUE_TEMPLATE/*
|
||||||
- kubernetes/**
|
- kubernetes/**
|
||||||
@ -21,12 +22,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.8.0
|
uses: crystal-lang/install-crystal@v1.6.0
|
||||||
with:
|
with:
|
||||||
crystal: 1.9.2
|
crystal: 1.5.0
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: |
|
run: |
|
||||||
@ -37,64 +38,68 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v2
|
||||||
with:
|
with:
|
||||||
platforms: arm64
|
platforms: arm64
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Login to registry
|
- name: Login to registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
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
|
- name: Build and push Docker AMD64 image without QUIC for Push Event
|
||||||
id: meta
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/build-push-action@v3
|
||||||
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
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: quay.expires-after=12w
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: quay.io/invidious/invidious:${{ github.sha }},quay.io/invidious/invidious:latest
|
||||||
build-args: |
|
build-args: |
|
||||||
"release=1"
|
"release=1"
|
||||||
|
"disable_quic=1"
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Build and push Docker ARM64 image without QUIC for Push Event
|
||||||
id: meta-arm64
|
if: github.ref == 'refs/heads/master'
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/build-push-action@v3
|
||||||
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
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
platforms: linux/arm64/v8
|
platforms: linux/arm64/v8
|
||||||
labels: ${{ steps.meta-arm64.outputs.labels }}
|
labels: quay.expires-after=12w
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta-arm64.outputs.tags }}
|
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64,quay.io/invidious/invidious:latest-arm64
|
||||||
build-args: |
|
build-args: |
|
||||||
"release=1"
|
"release=1"
|
||||||
|
"disable_quic=1"
|
||||||
|
|
||||||
|
- name: Build and push Docker AMD64 image with QUIC for Push Event
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
labels: quay.expires-after=12w
|
||||||
|
push: true
|
||||||
|
tags: quay.io/invidious/invidious:${{ github.sha }}-quic,quay.io/invidious/invidious:latest-quic
|
||||||
|
build-args: release=1
|
||||||
|
|
||||||
|
- name: Build and push Docker ARM64 image with QUIC for Push Event
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: docker/Dockerfile.arm64
|
||||||
|
platforms: linux/arm64/v8
|
||||||
|
labels: quay.expires-after=12w
|
||||||
|
push: true
|
||||||
|
tags: quay.io/invidious/invidious:${{ github.sha }}-arm64-quic,quay.io/invidious/invidious:latest-arm64-quic
|
||||||
|
build-args: release=1
|
||||||
|
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@ -10,11 +10,11 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v5
|
||||||
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: 45 # PRs should be active. Anything that hasn't had activity in more than 45 days should be considered abandoned.
|
||||||
days-before-close: 30
|
days-before-close: 30
|
||||||
exempt-pr-labels: blocked
|
exempt-pr-labels: blocked
|
||||||
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.'
|
||||||
|
6
Makefile
6
Makefile
@ -5,6 +5,7 @@
|
|||||||
RELEASE := 1
|
RELEASE := 1
|
||||||
STATIC := 0
|
STATIC := 0
|
||||||
|
|
||||||
|
DISABLE_QUIC := 1
|
||||||
NO_DBG_SYMBOLS := 0
|
NO_DBG_SYMBOLS := 0
|
||||||
|
|
||||||
|
|
||||||
@ -26,6 +27,10 @@ else
|
|||||||
FLAGS += --debug
|
FLAGS += --debug
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ifeq ($(DISABLE_QUIC), 1)
|
||||||
|
FLAGS += -Ddisable_quic
|
||||||
|
endif
|
||||||
|
|
||||||
ifeq ($(API_ONLY), 1)
|
ifeq ($(API_ONLY), 1)
|
||||||
FLAGS += -Dapi_only
|
FLAGS += -Dapi_only
|
||||||
endif
|
endif
|
||||||
@ -110,6 +115,7 @@ help:
|
|||||||
@echo " STATIC Link libraries statically (Default: 0)"
|
@echo " STATIC Link libraries statically (Default: 0)"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo " API_ONLY Build invidious without a GUI (Default: 0)"
|
@echo " API_ONLY Build invidious without a GUI (Default: 0)"
|
||||||
|
@echo " DISABLE_QUIC Disable support for QUIC (Default: 0)"
|
||||||
@echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)"
|
@echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)"
|
||||||
|
|
||||||
|
|
||||||
|
15
README.md
15
README.md
@ -82,7 +82,7 @@
|
|||||||
|
|
||||||
**Data import/export**
|
**Data import/export**
|
||||||
- Import subscriptions from YouTube, NewPipe and Freetube
|
- Import subscriptions from YouTube, NewPipe and Freetube
|
||||||
- Import watch history from YouTube and NewPipe
|
- Import watch history from NewPipe
|
||||||
- Export subscriptions to NewPipe and Freetube
|
- Export subscriptions to NewPipe and Freetube
|
||||||
- Import/Export Invidious user data
|
- Import/Export Invidious user data
|
||||||
|
|
||||||
@ -145,7 +145,18 @@ Weblate also allows you to log-in with major SSO providers like Github, Gitlab,
|
|||||||
|
|
||||||
## Projects using Invidious
|
## Projects using Invidious
|
||||||
|
|
||||||
A list of projects and extensions for or utilizing Invidious can be found in the documentation: https://docs.invidious.io/applications/
|
- [FreeTube](https://github.com/FreeTubeApp/FreeTube): A libre software YouTube app for privacy.
|
||||||
|
- [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-wa): 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
|
||||||
|
|
||||||
|
@ -392,19 +392,11 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
|
|||||||
* Comments & community posts
|
* Comments & community posts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.comments {
|
#comments {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* We don't want the top and bottom margin on the post page.
|
|
||||||
*/
|
|
||||||
.comments.post-comments {
|
|
||||||
margin-bottom: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-iframe-wrapper {
|
.video-iframe-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 0;
|
height: 0;
|
||||||
@ -441,26 +433,16 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
|
color: #919191;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding: 1.5em 0;
|
padding: 1.5em 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-height: 30vh;
|
max-height: 30vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.light-theme footer {
|
footer a {
|
||||||
color: #7c7c7c;
|
color: #919191 !important;
|
||||||
}
|
text-decoration: underline;
|
||||||
|
|
||||||
.dark-theme footer {
|
|
||||||
color: #adadad;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-theme footer a {
|
|
||||||
color: #7c7c7c !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark-theme footer a {
|
|
||||||
color: #adadad !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer span {
|
footer span {
|
||||||
@ -566,14 +548,6 @@ span > select {
|
|||||||
color: #303030;
|
color: #303030;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-theme footer {
|
|
||||||
color: #7c7c7c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-theme footer a {
|
|
||||||
color: #7c7c7c !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-theme .pure-menu-heading {
|
.light-theme .pure-menu-heading {
|
||||||
color: #565d64;
|
color: #565d64;
|
||||||
}
|
}
|
||||||
@ -607,7 +581,7 @@ span > select {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark-theme a {
|
.dark-theme a {
|
||||||
color: #adadad;
|
color: #a0a0a0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -661,7 +635,7 @@ body.dark-theme {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.no-theme a {
|
.no-theme a {
|
||||||
color: #adadad;
|
color: #a0a0a0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,14 +666,6 @@ body.dark-theme {
|
|||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-theme footer {
|
|
||||||
color: #adadad;
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-theme footer a {
|
|
||||||
color: #adadad !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -793,7 +759,3 @@ h1, h2, h3, h4, h5, p,
|
|||||||
.channel-emoji {
|
.channel-emoji {
|
||||||
margin: 0 2px;
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#download_widget {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg width="128" height="128" viewBox="0 0 128 128" version="1.1" id="svg5" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<g>
|
|
||||||
<rect fill="#c84fff" width="128" height="128" x="0" y="0" />
|
|
||||||
<g aria-label="#" transform="matrix(1.1326954,0,0,1.1326954,-20.255282,-23.528147)">
|
|
||||||
<path d="m 87.780593,70.524217 -2.624999,13.666661 h 11.666662 v 5.708331 H 84.030595 L 80.61393,107.73253 H 74.488932 L 77.988931,89.899209 H 65.863936 L 62.447271,107.73253 H 56.447273 L 59.697272,89.899209 H 48.947276 V 84.190878 H 60.822271 L 63.530603,70.524217 H 52.113942 V 64.815886 H 64.57227 l 3.416665,-17.999993 h 6.124997 l -3.416665,17.999993 h 12.208328 l 3.499999,-17.999993 h 5.999997 l -3.499998,17.999993 h 10.916662 v 5.708331 z M 66.947269,84.190878 H 79.072264 L 81.738929,70.524217 H 69.613934 Z" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 918 B |
@ -1,174 +0,0 @@
|
|||||||
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
|
||||||
|
|
||||||
var spinnerHTML = '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
|
||||||
var spinnerHTMLwithHR = spinnerHTML + '<hr>';
|
|
||||||
|
|
||||||
String.prototype.supplant = function (o) {
|
|
||||||
return this.replace(/{([^{}]*)}/g, function (a, b) {
|
|
||||||
var r = o[b];
|
|
||||||
return typeof r === 'string' || typeof r === 'number' ? r : a;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function toggle_comments(event) {
|
|
||||||
var target = event.target;
|
|
||||||
var body = target.parentNode.parentNode.parentNode.children[1];
|
|
||||||
if (body.style.display === 'none') {
|
|
||||||
target.textContent = '[ − ]';
|
|
||||||
body.style.display = '';
|
|
||||||
} else {
|
|
||||||
target.textContent = '[ + ]';
|
|
||||||
body.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide_youtube_replies(event) {
|
|
||||||
var target = event.target;
|
|
||||||
|
|
||||||
var sub_text = target.getAttribute('data-inner-text');
|
|
||||||
var inner_text = target.getAttribute('data-sub-text');
|
|
||||||
|
|
||||||
var body = target.parentNode.parentNode.children[1];
|
|
||||||
body.style.display = 'none';
|
|
||||||
|
|
||||||
target.textContent = sub_text;
|
|
||||||
target.onclick = show_youtube_replies;
|
|
||||||
target.setAttribute('data-inner-text', inner_text);
|
|
||||||
target.setAttribute('data-sub-text', sub_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_youtube_replies(event) {
|
|
||||||
var target = event.target;
|
|
||||||
|
|
||||||
var sub_text = target.getAttribute('data-inner-text');
|
|
||||||
var inner_text = target.getAttribute('data-sub-text');
|
|
||||||
|
|
||||||
var body = target.parentNode.parentNode.children[1];
|
|
||||||
body.style.display = '';
|
|
||||||
|
|
||||||
target.textContent = sub_text;
|
|
||||||
target.onclick = hide_youtube_replies;
|
|
||||||
target.setAttribute('data-inner-text', inner_text);
|
|
||||||
target.setAttribute('data-sub-text', sub_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_youtube_comments() {
|
|
||||||
var comments = document.getElementById('comments');
|
|
||||||
|
|
||||||
var fallback = comments.innerHTML;
|
|
||||||
comments.innerHTML = spinnerHTML;
|
|
||||||
|
|
||||||
var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id
|
|
||||||
var url = baseUrl +
|
|
||||||
'?format=html' +
|
|
||||||
'&hl=' + video_data.preferences.locale +
|
|
||||||
'&thin_mode=' + video_data.preferences.thin_mode;
|
|
||||||
|
|
||||||
if (video_data.ucid) {
|
|
||||||
url += '&ucid=' + video_data.ucid
|
|
||||||
}
|
|
||||||
|
|
||||||
var onNon200 = function (xhr) { comments.innerHTML = fallback; };
|
|
||||||
if (video_data.params.comments[1] === 'youtube')
|
|
||||||
onNon200 = function (xhr) {};
|
|
||||||
|
|
||||||
helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, {
|
|
||||||
on200: function (response) {
|
|
||||||
var commentInnerHtml = ' \
|
|
||||||
<div> \
|
|
||||||
<h3> \
|
|
||||||
<a href="javascript:void(0)">[ − ]</a> \
|
|
||||||
{commentsText} \
|
|
||||||
</h3> \
|
|
||||||
<b> \
|
|
||||||
'
|
|
||||||
if (video_data.support_reddit) {
|
|
||||||
commentInnerHtml += ' <a href="javascript:void(0)" data-comments="reddit"> \
|
|
||||||
{redditComments} \
|
|
||||||
</a> \
|
|
||||||
'
|
|
||||||
}
|
|
||||||
commentInnerHtml += ' </b> \
|
|
||||||
</div> \
|
|
||||||
<div>{contentHtml}</div> \
|
|
||||||
<hr>'
|
|
||||||
commentInnerHtml = commentInnerHtml.supplant({
|
|
||||||
contentHtml: response.contentHtml,
|
|
||||||
redditComments: video_data.reddit_comments_text,
|
|
||||||
commentsText: video_data.comments_text.supplant({
|
|
||||||
// toLocaleString correctly splits number with local thousands separator. e.g.:
|
|
||||||
// '1,234,567.89' for user with English locale
|
|
||||||
// '1 234 567,89' for user with Russian locale
|
|
||||||
// '1.234.567,89' for user with Portuguese locale
|
|
||||||
commentCount: response.commentCount.toLocaleString()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
comments.innerHTML = commentInnerHtml;
|
|
||||||
comments.children[0].children[0].children[0].onclick = toggle_comments;
|
|
||||||
if (video_data.support_reddit) {
|
|
||||||
comments.children[0].children[1].children[0].onclick = swap_comments;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onNon200: onNon200, // declared above
|
|
||||||
onError: function (xhr) {
|
|
||||||
comments.innerHTML = spinnerHTML;
|
|
||||||
},
|
|
||||||
onTimeout: function (xhr) {
|
|
||||||
comments.innerHTML = spinnerHTML;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function get_youtube_replies(target, load_more, load_replies) {
|
|
||||||
var continuation = target.getAttribute('data-continuation');
|
|
||||||
|
|
||||||
var body = target.parentNode.parentNode;
|
|
||||||
var fallback = body.innerHTML;
|
|
||||||
body.innerHTML = spinnerHTML;
|
|
||||||
var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id
|
|
||||||
var url = baseUrl +
|
|
||||||
'?format=html' +
|
|
||||||
'&hl=' + video_data.preferences.locale +
|
|
||||||
'&thin_mode=' + video_data.preferences.thin_mode +
|
|
||||||
'&continuation=' + continuation;
|
|
||||||
|
|
||||||
if (video_data.ucid) {
|
|
||||||
url += '&ucid=' + video_data.ucid
|
|
||||||
}
|
|
||||||
if (load_replies) url += '&action=action_get_comment_replies';
|
|
||||||
|
|
||||||
helpers.xhr('GET', url, {}, {
|
|
||||||
on200: function (response) {
|
|
||||||
if (load_more) {
|
|
||||||
body = body.parentNode.parentNode;
|
|
||||||
body.removeChild(body.lastElementChild);
|
|
||||||
body.insertAdjacentHTML('beforeend', response.contentHtml);
|
|
||||||
} else {
|
|
||||||
body.removeChild(body.lastElementChild);
|
|
||||||
|
|
||||||
var p = document.createElement('p');
|
|
||||||
var a = document.createElement('a');
|
|
||||||
p.appendChild(a);
|
|
||||||
|
|
||||||
a.href = 'javascript:void(0)';
|
|
||||||
a.onclick = hide_youtube_replies;
|
|
||||||
a.setAttribute('data-sub-text', video_data.hide_replies_text);
|
|
||||||
a.setAttribute('data-inner-text', video_data.show_replies_text);
|
|
||||||
a.textContent = video_data.hide_replies_text;
|
|
||||||
|
|
||||||
var div = document.createElement('div');
|
|
||||||
div.innerHTML = response.contentHtml;
|
|
||||||
|
|
||||||
body.appendChild(p);
|
|
||||||
body.appendChild(div);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onNon200: function (xhr) {
|
|
||||||
body.innerHTML = fallback;
|
|
||||||
},
|
|
||||||
onTimeout: function (xhr) {
|
|
||||||
console.warn('Pulling comments failed');
|
|
||||||
body.innerHTML = fallback;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -98,13 +98,11 @@ if (video_data.params.quality === 'dash') {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Function for add time argument to url
|
* Function for add time argument to url
|
||||||
*
|
|
||||||
* @param {String} url
|
* @param {String} url
|
||||||
* @param {String} [base]
|
|
||||||
* @returns {URL} urlWithTimeArg
|
* @returns {URL} urlWithTimeArg
|
||||||
*/
|
*/
|
||||||
function addCurrentTimeToURL(url, base) {
|
function addCurrentTimeToURL(url) {
|
||||||
var urlUsed = new URL(url, base);
|
var urlUsed = new URL(url);
|
||||||
urlUsed.searchParams.delete('start');
|
urlUsed.searchParams.delete('start');
|
||||||
var currentTime = Math.ceil(player.currentTime());
|
var currentTime = Math.ceil(player.currentTime());
|
||||||
if (currentTime > 0)
|
if (currentTime > 0)
|
||||||
@ -114,50 +112,6 @@ function addCurrentTimeToURL(url, base) {
|
|||||||
return urlUsed;
|
return urlUsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Global variable to save the last timestamp (in full seconds) at which the external
|
|
||||||
* links were updated by the 'timeupdate' callback below.
|
|
||||||
*
|
|
||||||
* It is initialized to 5s so that the video will always restart from the beginning
|
|
||||||
* if the user hasn't really started watching before switching to the other website.
|
|
||||||
*/
|
|
||||||
var timeupdate_last_ts = 5;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback that updates the timestamp on all external links
|
|
||||||
*/
|
|
||||||
player.on('timeupdate', function () {
|
|
||||||
// Only update once every second
|
|
||||||
let current_ts = Math.floor(player.currentTime());
|
|
||||||
if (current_ts > timeupdate_last_ts) timeupdate_last_ts = current_ts;
|
|
||||||
else return;
|
|
||||||
|
|
||||||
// YouTube links
|
|
||||||
|
|
||||||
let elem_yt_watch = document.getElementById('link-yt-watch');
|
|
||||||
let elem_yt_embed = document.getElementById('link-yt-embed');
|
|
||||||
|
|
||||||
let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url');
|
|
||||||
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
|
|
||||||
|
|
||||||
elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch);
|
|
||||||
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
|
|
||||||
|
|
||||||
// Invidious links
|
|
||||||
|
|
||||||
let domain = window.location.origin;
|
|
||||||
|
|
||||||
let elem_iv_embed = document.getElementById('link-iv-embed');
|
|
||||||
let elem_iv_other = document.getElementById('link-iv-other');
|
|
||||||
|
|
||||||
let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url');
|
|
||||||
let base_url_iv_other = elem_iv_other.getAttribute('data-base-url');
|
|
||||||
|
|
||||||
elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain);
|
|
||||||
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
var shareOptions = {
|
var shareOptions = {
|
||||||
socials: ['fbFeed', 'tw', 'reddit', 'email'],
|
socials: ['fbFeed', 'tw', 'reddit', 'email'],
|
||||||
|
|
||||||
@ -747,17 +701,6 @@ 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');
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
addEventListener('load', function (e) {
|
|
||||||
get_youtube_comments();
|
|
||||||
});
|
|
@ -1,4 +1,14 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
||||||
|
var spinnerHTML = '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
|
||||||
|
var spinnerHTMLwithHR = spinnerHTML + '<hr>';
|
||||||
|
|
||||||
|
String.prototype.supplant = function (o) {
|
||||||
|
return this.replace(/{([^{}]*)}/g, function (a, b) {
|
||||||
|
var r = o[b];
|
||||||
|
return typeof r === 'string' || typeof r === 'number' ? r : a;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function toggle_parent(target) {
|
function toggle_parent(target) {
|
||||||
var body = target.parentNode.parentNode.children[1];
|
var body = target.parentNode.parentNode.children[1];
|
||||||
@ -11,6 +21,18 @@ function toggle_parent(target) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggle_comments(event) {
|
||||||
|
var target = event.target;
|
||||||
|
var body = target.parentNode.parentNode.parentNode.children[1];
|
||||||
|
if (body.style.display === 'none') {
|
||||||
|
target.textContent = '[ − ]';
|
||||||
|
body.style.display = '';
|
||||||
|
} else {
|
||||||
|
target.textContent = '[ + ]';
|
||||||
|
body.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function swap_comments(event) {
|
function swap_comments(event) {
|
||||||
var source = event.target.getAttribute('data-comments');
|
var source = event.target.getAttribute('data-comments');
|
||||||
|
|
||||||
@ -21,6 +43,36 @@ function swap_comments(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hide_youtube_replies(event) {
|
||||||
|
var target = event.target;
|
||||||
|
|
||||||
|
var sub_text = target.getAttribute('data-inner-text');
|
||||||
|
var inner_text = target.getAttribute('data-sub-text');
|
||||||
|
|
||||||
|
var body = target.parentNode.parentNode.children[1];
|
||||||
|
body.style.display = 'none';
|
||||||
|
|
||||||
|
target.textContent = sub_text;
|
||||||
|
target.onclick = show_youtube_replies;
|
||||||
|
target.setAttribute('data-inner-text', inner_text);
|
||||||
|
target.setAttribute('data-sub-text', sub_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show_youtube_replies(event) {
|
||||||
|
var target = event.target;
|
||||||
|
|
||||||
|
var sub_text = target.getAttribute('data-inner-text');
|
||||||
|
var inner_text = target.getAttribute('data-sub-text');
|
||||||
|
|
||||||
|
var body = target.parentNode.parentNode.children[1];
|
||||||
|
body.style.display = '';
|
||||||
|
|
||||||
|
target.textContent = sub_text;
|
||||||
|
target.onclick = hide_youtube_replies;
|
||||||
|
target.setAttribute('data-inner-text', inner_text);
|
||||||
|
target.setAttribute('data-sub-text', sub_text);
|
||||||
|
}
|
||||||
|
|
||||||
var continue_button = document.getElementById('continue');
|
var continue_button = document.getElementById('continue');
|
||||||
if (continue_button) {
|
if (continue_button) {
|
||||||
continue_button.onclick = continue_autoplay;
|
continue_button.onclick = continue_autoplay;
|
||||||
@ -156,6 +208,111 @@ function get_reddit_comments() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function get_youtube_comments() {
|
||||||
|
var comments = document.getElementById('comments');
|
||||||
|
|
||||||
|
var fallback = comments.innerHTML;
|
||||||
|
comments.innerHTML = spinnerHTML;
|
||||||
|
|
||||||
|
var url = '/api/v1/comments/' + video_data.id +
|
||||||
|
'?format=html' +
|
||||||
|
'&hl=' + video_data.preferences.locale +
|
||||||
|
'&thin_mode=' + video_data.preferences.thin_mode;
|
||||||
|
|
||||||
|
var onNon200 = function (xhr) { comments.innerHTML = fallback; };
|
||||||
|
if (video_data.params.comments[1] === 'youtube')
|
||||||
|
onNon200 = function (xhr) {};
|
||||||
|
|
||||||
|
helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, {
|
||||||
|
on200: function (response) {
|
||||||
|
comments.innerHTML = ' \
|
||||||
|
<div> \
|
||||||
|
<h3> \
|
||||||
|
<a href="javascript:void(0)">[ − ]</a> \
|
||||||
|
{commentsText} \
|
||||||
|
</h3> \
|
||||||
|
<b> \
|
||||||
|
<a href="javascript:void(0)" data-comments="reddit"> \
|
||||||
|
{redditComments} \
|
||||||
|
</a> \
|
||||||
|
</b> \
|
||||||
|
</div> \
|
||||||
|
<div>{contentHtml}</div> \
|
||||||
|
<hr>'.supplant({
|
||||||
|
contentHtml: response.contentHtml,
|
||||||
|
redditComments: video_data.reddit_comments_text,
|
||||||
|
commentsText: video_data.comments_text.supplant({
|
||||||
|
// toLocaleString correctly splits number with local thousands separator. e.g.:
|
||||||
|
// '1,234,567.89' for user with English locale
|
||||||
|
// '1 234 567,89' for user with Russian locale
|
||||||
|
// '1.234.567,89' for user with Portuguese locale
|
||||||
|
commentCount: response.commentCount.toLocaleString()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
comments.children[0].children[0].children[0].onclick = toggle_comments;
|
||||||
|
comments.children[0].children[1].children[0].onclick = swap_comments;
|
||||||
|
},
|
||||||
|
onNon200: onNon200, // declared above
|
||||||
|
onError: function (xhr) {
|
||||||
|
comments.innerHTML = spinnerHTML;
|
||||||
|
},
|
||||||
|
onTimeout: function (xhr) {
|
||||||
|
comments.innerHTML = spinnerHTML;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_youtube_replies(target, load_more, load_replies) {
|
||||||
|
var continuation = target.getAttribute('data-continuation');
|
||||||
|
|
||||||
|
var body = target.parentNode.parentNode;
|
||||||
|
var fallback = body.innerHTML;
|
||||||
|
body.innerHTML = spinnerHTML;
|
||||||
|
|
||||||
|
var url = '/api/v1/comments/' + video_data.id +
|
||||||
|
'?format=html' +
|
||||||
|
'&hl=' + video_data.preferences.locale +
|
||||||
|
'&thin_mode=' + video_data.preferences.thin_mode +
|
||||||
|
'&continuation=' + continuation;
|
||||||
|
if (load_replies) url += '&action=action_get_comment_replies';
|
||||||
|
|
||||||
|
helpers.xhr('GET', url, {}, {
|
||||||
|
on200: function (response) {
|
||||||
|
if (load_more) {
|
||||||
|
body = body.parentNode.parentNode;
|
||||||
|
body.removeChild(body.lastElementChild);
|
||||||
|
body.insertAdjacentHTML('beforeend', response.contentHtml);
|
||||||
|
} else {
|
||||||
|
body.removeChild(body.lastElementChild);
|
||||||
|
|
||||||
|
var p = document.createElement('p');
|
||||||
|
var a = document.createElement('a');
|
||||||
|
p.appendChild(a);
|
||||||
|
|
||||||
|
a.href = 'javascript:void(0)';
|
||||||
|
a.onclick = hide_youtube_replies;
|
||||||
|
a.setAttribute('data-sub-text', video_data.hide_replies_text);
|
||||||
|
a.setAttribute('data-inner-text', video_data.show_replies_text);
|
||||||
|
a.textContent = video_data.hide_replies_text;
|
||||||
|
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.innerHTML = response.contentHtml;
|
||||||
|
|
||||||
|
body.appendChild(p);
|
||||||
|
body.appendChild(div);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onNon200: function (xhr) {
|
||||||
|
body.innerHTML = fallback;
|
||||||
|
},
|
||||||
|
onTimeout: function (xhr) {
|
||||||
|
console.warn('Pulling comments failed');
|
||||||
|
body.innerHTML = fallback;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (video_data.play_next) {
|
if (video_data.play_next) {
|
||||||
player.on('ended', function () {
|
player.on('ended', function () {
|
||||||
var url = new URL('https://example.com/watch?v=' + video_data.next_video);
|
var url = new URL('https://example.com/watch?v=' + video_data.next_video);
|
||||||
|
@ -15,7 +15,5 @@
|
|||||||
],
|
],
|
||||||
"theme_color": "#575757",
|
"theme_color": "#575757",
|
||||||
"background_color": "#575757",
|
"background_color": "#575757",
|
||||||
"display": "standalone",
|
"display": "standalone"
|
||||||
"description": "An alternative front-end to YouTube",
|
|
||||||
"start_url": "/"
|
|
||||||
}
|
}
|
||||||
|
@ -140,6 +140,27 @@ https_only: false
|
|||||||
##
|
##
|
||||||
#pool_size: 100
|
#pool_size: 100
|
||||||
|
|
||||||
|
##
|
||||||
|
## Enable/Disable the use of QUIC (HTTP/3) when connecting
|
||||||
|
## to the youtube API and websites ('youtube.com', 'ytimg.com').
|
||||||
|
## QUIC's main advantages are its lower latency and lower bandwidth
|
||||||
|
## use, compared to its predecessors. However, the current version
|
||||||
|
## of QUIC used in invidious is still based on the IETF draft 31,
|
||||||
|
## meaning that the underlying library may still not be fully
|
||||||
|
## optimized. You can read more about QUIC at the link below:
|
||||||
|
## https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-31
|
||||||
|
##
|
||||||
|
## Note: you should try both options and see what is the best for your
|
||||||
|
## instance. In general QUIC is recommended for public instances. Your
|
||||||
|
## mileage may vary.
|
||||||
|
##
|
||||||
|
## Note 2: Using QUIC prevents some captcha challenges from appearing.
|
||||||
|
## See: https://github.com/iv-org/invidious/issues/957#issuecomment-576424042
|
||||||
|
##
|
||||||
|
## Accepted values: true, false
|
||||||
|
## Default: false
|
||||||
|
##
|
||||||
|
#use_quic: false
|
||||||
|
|
||||||
##
|
##
|
||||||
## Additional cookies to be sent when requesting the youtube API.
|
## Additional cookies to be sent when requesting the youtube API.
|
||||||
@ -161,19 +182,6 @@ https_only: false
|
|||||||
#force_resolve:
|
#force_resolve:
|
||||||
|
|
||||||
|
|
||||||
##
|
|
||||||
## Use Innertube's transcripts API instead of timedtext for closed captions
|
|
||||||
##
|
|
||||||
## Useful for larger instances as InnerTube is **not ratelimited**. See https://github.com/iv-org/invidious/issues/2567
|
|
||||||
##
|
|
||||||
## Subtitle experience may differ slightly on Invidious.
|
|
||||||
##
|
|
||||||
## Accepted values: true, false
|
|
||||||
## Default: false
|
|
||||||
##
|
|
||||||
# use_innertube_for_captions: false
|
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Logging
|
# Logging
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
@ -392,6 +400,27 @@ 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
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
6
config/migrate-scripts/migrate-db-8bc91ce.sh
Normal file
6
config/migrate-scripts/migrate-db-8bc91ce.sh
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE INDEX channel_videos_ucid_published_idx
|
||||||
|
ON public.channel_videos
|
||||||
|
USING btree
|
||||||
|
(ucid COLLATE pg_catalog."default", published);
|
||||||
|
|
||||||
|
DROP INDEX channel_videos_ucid_idx;
|
@ -19,12 +19,12 @@ CREATE TABLE IF NOT EXISTS public.channel_videos
|
|||||||
|
|
||||||
GRANT ALL ON TABLE public.channel_videos TO current_user;
|
GRANT ALL ON TABLE public.channel_videos TO current_user;
|
||||||
|
|
||||||
-- Index: public.channel_videos_ucid_idx
|
-- Index: public.channel_videos_ucid_published_idx
|
||||||
|
|
||||||
-- DROP INDEX public.channel_videos_ucid_idx;
|
-- DROP INDEX public.channel_videos_ucid_published_idx;
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx
|
CREATE INDEX IF NOT EXISTS channel_videos_ucid_published_idx
|
||||||
ON public.channel_videos
|
ON public.channel_videos
|
||||||
USING btree
|
USING btree
|
||||||
(ucid COLLATE pg_catalog."default");
|
(ucid COLLATE pg_catalog."default", published);
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ services:
|
|||||||
- invidious-db
|
- invidious-db
|
||||||
|
|
||||||
invidious-db:
|
invidious-db:
|
||||||
image: docker.io/library/postgres:14
|
image: docker.io/library/postgres:13
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- postgresdata:/var/lib/postgresql/data
|
- postgresdata:/var/lib/postgresql/data
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
FROM crystallang/crystal:1.8.2-alpine AS builder
|
FROM crystallang/crystal:1.4.1-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-static yaml-static
|
RUN apk add --no-cache sqlite-static yaml-static
|
||||||
|
|
||||||
ARG release
|
ARG release
|
||||||
|
ARG disable_quic
|
||||||
|
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
COPY ./shard.yml ./shard.yml
|
COPY ./shard.yml ./shard.yml
|
||||||
COPY ./shard.lock ./shard.lock
|
COPY ./shard.lock ./shard.lock
|
||||||
RUN shards install --production
|
RUN shards install --production
|
||||||
|
|
||||||
|
COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a
|
||||||
|
|
||||||
COPY ./src/ ./src/
|
COPY ./src/ ./src/
|
||||||
# TODO: .git folder is required for building – this is destructive.
|
# TODO: .git folder is required for building – this is destructive.
|
||||||
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
||||||
@ -21,7 +23,14 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
|||||||
|
|
||||||
RUN crystal spec --warnings all \
|
RUN crystal spec --warnings all \
|
||||||
--link-flags "-lxml2 -llzma"
|
--link-flags "-lxml2 -llzma"
|
||||||
RUN if [[ "${release}" == 1 ]] ; then \
|
|
||||||
|
RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \
|
||||||
|
crystal build ./src/invidious.cr \
|
||||||
|
--release \
|
||||||
|
-Ddisable_quic \
|
||||||
|
--static --warnings all \
|
||||||
|
--link-flags "-lxml2 -llzma"; \
|
||||||
|
elif [[ "${release}" == 1 ]] ; then \
|
||||||
crystal build ./src/invidious.cr \
|
crystal build ./src/invidious.cr \
|
||||||
--release \
|
--release \
|
||||||
--static --warnings all \
|
--static --warnings all \
|
||||||
@ -32,8 +41,9 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
|||||||
--link-flags "-lxml2 -llzma"; \
|
--link-flags "-lxml2 -llzma"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.18
|
|
||||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
FROM alpine:3.16
|
||||||
|
RUN apk add --no-cache librsvg 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
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
FROM alpine:3.18 AS builder
|
FROM alpine:3.16 AS builder
|
||||||
RUN apk add --no-cache 'crystal=1.8.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static zlib-static openssl-libs-static openssl-dev musl-dev xz-static
|
RUN apk add --no-cache 'crystal=1.4.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-dev zlib-static openssl-libs-static openssl-dev musl-dev
|
||||||
|
|
||||||
ARG release
|
ARG release
|
||||||
|
ARG disable_quic
|
||||||
|
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
COPY ./shard.yml ./shard.yml
|
COPY ./shard.yml ./shard.yml
|
||||||
COPY ./shard.lock ./shard.lock
|
COPY ./shard.lock ./shard.lock
|
||||||
RUN shards install --production
|
RUN shards install --production
|
||||||
|
|
||||||
|
COPY --from=quay.io/invidious/lsquic-compiled /root/liblsquic.a ./lib/lsquic/src/lsquic/ext/liblsquic.a
|
||||||
|
|
||||||
COPY ./src/ ./src/
|
COPY ./src/ ./src/
|
||||||
# TODO: .git folder is required for building – this is destructive.
|
# TODO: .git folder is required for building – this is destructive.
|
||||||
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
||||||
@ -21,7 +24,13 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
|||||||
RUN crystal spec --warnings all \
|
RUN crystal spec --warnings all \
|
||||||
--link-flags "-lxml2 -llzma"
|
--link-flags "-lxml2 -llzma"
|
||||||
|
|
||||||
RUN if [[ "${release}" == 1 ]] ; then \
|
RUN if [[ "${release}" == 1 && "${disable_quic}" == 1 ]] ; then \
|
||||||
|
crystal build ./src/invidious.cr \
|
||||||
|
--release \
|
||||||
|
-Ddisable_quic \
|
||||||
|
--static --warnings all \
|
||||||
|
--link-flags "-lxml2 -llzma"; \
|
||||||
|
elif [[ "${release}" == 1 ]] ; then \
|
||||||
crystal build ./src/invidious.cr \
|
crystal build ./src/invidious.cr \
|
||||||
--release \
|
--release \
|
||||||
--static --warnings all \
|
--static --warnings all \
|
||||||
@ -32,8 +41,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
|||||||
--link-flags "-lxml2 -llzma"; \
|
--link-flags "-lxml2 -llzma"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.18
|
FROM alpine:3.16
|
||||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
RUN apk add --no-cache librsvg 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
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
repository: https://charts.bitnami.com/bitnami/
|
repository: https://charts.bitnami.com/bitnami/
|
||||||
version: 12.11.1
|
version: 12.1.9
|
||||||
digest: sha256:3c10008175c4f5c1cec38782f5a7316154b89074c77ebbd9bcc4be4f5ff21122
|
digest: sha256:71ff342a6c0a98bece3d7fe199983afb2113f8db65a3e3819de875af2c45add7
|
||||||
generated: "2023-09-14T22:40:43.171275362Z"
|
generated: "2023-01-20T20:42:32.757707004Z"
|
||||||
|
@ -17,6 +17,6 @@ maintainers:
|
|||||||
email: mail@leonklingele.de
|
email: mail@leonklingele.de
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
version: ~12.11.0
|
version: ~12.1.6
|
||||||
repository: "https://charts.bitnami.com/bitnami/"
|
repository: "https://charts.bitnami.com/bitnami/"
|
||||||
engine: gotpl
|
engine: gotpl
|
||||||
|
@ -49,7 +49,6 @@ postgresql:
|
|||||||
# Adapted from ../config/config.yml
|
# Adapted from ../config/config.yml
|
||||||
config:
|
config:
|
||||||
channel_threads: 1
|
channel_threads: 1
|
||||||
feed_threads: 1
|
|
||||||
db:
|
db:
|
||||||
user: kemal
|
user: kemal
|
||||||
password: kemal
|
password: kemal
|
||||||
|
@ -540,19 +540,5 @@
|
|||||||
"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_save": "حفظ",
|
|
||||||
"generic_button_delete": "حذف",
|
|
||||||
"generic_button_edit": "تحرير",
|
|
||||||
"generic_button_cancel": "الغاء",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_releases_label": "الإصدارات",
|
|
||||||
"playlist_button_add_items": "إضافة مقاطع فيديو",
|
|
||||||
"channel_tab_podcasts_label": "البودكاست",
|
|
||||||
"generic_channels_count_0": "{{count}} قناة",
|
|
||||||
"generic_channels_count_1": "{{count}} قناة",
|
|
||||||
"generic_channels_count_2": "{{count}} قناتان",
|
|
||||||
"generic_channels_count_3": "{{count}} قنوات",
|
|
||||||
"generic_channels_count_4": "{{count}} قنوات",
|
|
||||||
"generic_channels_count_5": "{{count}} قناة"
|
|
||||||
}
|
}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
{}
|
|
@ -1 +0,0 @@
|
|||||||
{}
|
|
490
locales/bg.json
490
locales/bg.json
@ -1,490 +0,0 @@
|
|||||||
{
|
|
||||||
"Korean (auto-generated)": "Корейски (автоматично генерирано)",
|
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
|
||||||
"published - reverse": "публикувани - в обратен ред",
|
|
||||||
"preferences_quality_dash_option_worst": "Най-ниско качество",
|
|
||||||
"Password is a required field": "Парола е задължитело поле",
|
|
||||||
"channel_tab_podcasts_label": "Подкасти",
|
|
||||||
"Token is expired, please try again": "Токенът е изтекъл, моля опитайте отново",
|
|
||||||
"Turkish": "Турски",
|
|
||||||
"preferences_save_player_pos_label": "Запази позицията на плейъра: ",
|
|
||||||
"View Reddit comments": "Виж Reddit коментари",
|
|
||||||
"Export data as JSON": "Експортиране на Invidious информацията като JSON",
|
|
||||||
"About": "За сайта",
|
|
||||||
"Save preferences": "Запази промените",
|
|
||||||
"Load more": "Зареди още",
|
|
||||||
"Import/export": "Импортиране/експортиране",
|
|
||||||
"Albanian": "Албански",
|
|
||||||
"New password": "Нова парола",
|
|
||||||
"Southern Sotho": "Южен Сото",
|
|
||||||
"channel_tab_videos_label": "Видеа",
|
|
||||||
"Spanish (Mexico)": "Испански (Мексико)",
|
|
||||||
"preferences_player_style_label": "Стил на плейъра: ",
|
|
||||||
"preferences_region_label": "Държавата на съдържанието: ",
|
|
||||||
"Premieres in `x`": "Премиера в `x`",
|
|
||||||
"Watch history": "История на гледане",
|
|
||||||
"generic_subscriptions_count": "{{count}} абонамент",
|
|
||||||
"generic_subscriptions_count_plural": "{{count}} абонамента",
|
|
||||||
"preferences_continue_label": "Пускай следващото видео автоматично: ",
|
|
||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Здравей! Изглежда си изключил JavaScript. Натисни тук за да видиш коментарите, но обърни внимание, че може да отнеме повече време да заредят.",
|
|
||||||
"Polish": "Полски",
|
|
||||||
"Icelandic": "Исландски",
|
|
||||||
"preferences_local_label": "Пускане на видеа през прокси: ",
|
|
||||||
"Hebrew": "Иврит",
|
|
||||||
"Fallback captions: ": "Резервни надписи: ",
|
|
||||||
"search_filters_title": "Филтри",
|
|
||||||
"search_filters_apply_button": "Приложете избрани филтри",
|
|
||||||
"Download is disabled": "Изтеглянето е деактивирано",
|
|
||||||
"User ID is a required field": "Потребителско име е задължително поле",
|
|
||||||
"comments_points_count": "{{count}} точка",
|
|
||||||
"comments_points_count_plural": "{{count}} точки",
|
|
||||||
"next_steps_error_message_go_to_youtube": "Отидеш в YouTube",
|
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
|
||||||
"search_filters_type_option_video": "Видео",
|
|
||||||
"Spanish (Latin America)": "Испански (Латинска Америка)",
|
|
||||||
"Download as: ": "Изтегли като: ",
|
|
||||||
"Default": "По подразбиране",
|
|
||||||
"search_filters_sort_option_views": "Гледания",
|
|
||||||
"search_filters_features_option_four_k": "4K",
|
|
||||||
"Igbo": "Игбо",
|
|
||||||
"Subscriptions": "Абонаменти",
|
|
||||||
"German (auto-generated)": "Немски (автоматично генерирано)",
|
|
||||||
"`x` is live": "`x` е на живо",
|
|
||||||
"Azerbaijani": "Азербайджански",
|
|
||||||
"Premieres `x`": "Премиера `x`",
|
|
||||||
"Japanese (auto-generated)": "Японски (автоматично генерирано)",
|
|
||||||
"preferences_quality_option_medium": "Средно",
|
|
||||||
"footer_donate_page": "Даряване",
|
|
||||||
"Show replies": "Покажи отговорите",
|
|
||||||
"Esperanto": "Есперанто",
|
|
||||||
"search_message_change_filters_or_query": "Опитай да разшириш търсенето си и/или да смениш филтрите.",
|
|
||||||
"CAPTCHA enabled: ": "Активиране на CAPTCHA: ",
|
|
||||||
"View playlist on YouTube": "Виж плейлиста в YouTube",
|
|
||||||
"crash_page_before_reporting": "Преди докладването на бъг, бъди сигурен, че си:",
|
|
||||||
"Top enabled: ": "Активиране на страница с топ видеа: ",
|
|
||||||
"preferences_quality_dash_option_best": "Най-високо",
|
|
||||||
"search_filters_duration_label": "Продължителност",
|
|
||||||
"Slovak": "Словашки",
|
|
||||||
"Channel Sponsor": "Канален спонсор",
|
|
||||||
"generic_videos_count": "{{count}} видео",
|
|
||||||
"generic_videos_count_plural": "{{count}} видеа",
|
|
||||||
"videoinfo_started_streaming_x_ago": "Започна да излъчва преди `x`",
|
|
||||||
"videoinfo_youTube_embed_link": "Вграждане",
|
|
||||||
"channel_tab_streams_label": "Стриймове",
|
|
||||||
"oldest": "най-стари",
|
|
||||||
"playlist_button_add_items": "Добавяне на видеа",
|
|
||||||
"Import NewPipe data (.zip)": "Импортиране на NewPipe информация (.zip)",
|
|
||||||
"Clear watch history": "Изчистване на историята на гледане",
|
|
||||||
"generic_count_minutes": "{{count}} минута",
|
|
||||||
"generic_count_minutes_plural": "{{count}} минути",
|
|
||||||
"published": "публикувани",
|
|
||||||
"Show annotations": "Покажи анотации",
|
|
||||||
"Login enabled: ": "Активиране на впизване: ",
|
|
||||||
"Somali": "Сомалийски",
|
|
||||||
"YouTube comment permalink": "Постоянна връзка на коментарите на YouTube",
|
|
||||||
"Kurdish": "Кюрдски",
|
|
||||||
"search_filters_date_option_hour": "Последния час",
|
|
||||||
"Lao": "Лаоски",
|
|
||||||
"Maltese": "Малтийски",
|
|
||||||
"Register": "Регистрация",
|
|
||||||
"View channel on YouTube": "Виж канала в YouTube",
|
|
||||||
"Playlist privacy": "Поверителен плейлист",
|
|
||||||
"preferences_unseen_only_label": "Показвай само негледаните: ",
|
|
||||||
"Gujarati": "Гуджарати",
|
|
||||||
"Please log in": "Моля влезте",
|
|
||||||
"search_filters_sort_option_rating": "Рейтинг",
|
|
||||||
"Manage subscriptions": "Управление на абонаментите",
|
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
|
||||||
"preferences_watch_history_label": "Активирай историята на гледане: ",
|
|
||||||
"user_saved_playlists": "`x` запази плейлисти",
|
|
||||||
"preferences_extend_desc_label": "Автоматично разшири описанието на видеото ",
|
|
||||||
"preferences_max_results_label": "Брой видеа показани на началната страница: ",
|
|
||||||
"Spanish (Spain)": "Испански (Испания)",
|
|
||||||
"invidious": "Invidious",
|
|
||||||
"crash_page_refresh": "пробвал да <a href=\"`x`\">опресниш страницата</a>",
|
|
||||||
"Image CAPTCHA": "CAPTCHA с Изображение",
|
|
||||||
"search_filters_features_option_hd": "HD",
|
|
||||||
"Chinese (Hong Kong)": "Китайски (Хонг Конг)",
|
|
||||||
"Import Invidious data": "Импортиране на Invidious JSON информацията",
|
|
||||||
"Blacklisted regions: ": "Неразрешени региони: ",
|
|
||||||
"Only show latest video from channel: ": "Показвай само най-новите видеа в канала: ",
|
|
||||||
"Hmong": "Хмонг",
|
|
||||||
"French": "Френски",
|
|
||||||
"search_filters_type_option_channel": "Канал",
|
|
||||||
"Artist: ": "Артист: ",
|
|
||||||
"generic_count_months": "{{count}} месец",
|
|
||||||
"generic_count_months_plural": "{{count}} месеца",
|
|
||||||
"preferences_annotations_subscribed_label": "Показвай анотаций по подразбиране за абонирани канали? ",
|
|
||||||
"search_message_use_another_instance": " Можеш също да <a href=\"`x`\">търсиш на друга инстанция</a>.",
|
|
||||||
"Danish": "Датски",
|
|
||||||
"generic_subscribers_count": "{{count}} абонат",
|
|
||||||
"generic_subscribers_count_plural": "{{count}} абоната",
|
|
||||||
"Galician": "Галисий",
|
|
||||||
"newest": "най-нови",
|
|
||||||
"Empty playlist": "Плейлиста е празен",
|
|
||||||
"download_subtitles": "Субритри - `x` (.vtt)",
|
|
||||||
"preferences_category_misc": "Различни предпочитания",
|
|
||||||
"Uzbek": "Узбекски",
|
|
||||||
"View JavaScript license information.": "Виж Javascript лиценза.",
|
|
||||||
"Filipino": "Филипински",
|
|
||||||
"Malagasy": "Мадагаскарски",
|
|
||||||
"generic_button_save": "Запиши",
|
|
||||||
"Dark mode: ": "Тъмен режим: ",
|
|
||||||
"Public": "Публичен",
|
|
||||||
"Basque": "Баскски",
|
|
||||||
"channel:`x`": "Канал:`x`",
|
|
||||||
"Armenian": "Арменски",
|
|
||||||
"This channel does not exist.": "Този канал не съществува.",
|
|
||||||
"Luxembourgish": "Люксембургски",
|
|
||||||
"preferences_related_videos_label": "Покажи подобни видеа: ",
|
|
||||||
"English": "Английски",
|
|
||||||
"Delete account": "Изтриване на акаунт",
|
|
||||||
"Gaming": "Игри",
|
|
||||||
"Video mode": "Видео режим",
|
|
||||||
"preferences_dark_mode_label": "Тема: ",
|
|
||||||
"crash_page_search_issue": "потърсил за <a href=\"`x`\">съществуващи проблеми в GitHub</a>",
|
|
||||||
"preferences_category_subscription": "Предпочитания за абонаменти",
|
|
||||||
"last": "най-скорощни",
|
|
||||||
"Chinese (Simplified)": "Китайски (Опростен)",
|
|
||||||
"Could not create mix.": "Създаването на микс е неуспешно.",
|
|
||||||
"generic_button_cancel": "Отказ",
|
|
||||||
"search_filters_type_option_movie": "Филм",
|
|
||||||
"search_filters_date_option_year": "Тази година",
|
|
||||||
"Swedish": "Шведски",
|
|
||||||
"Previous page": "Предишна страница",
|
|
||||||
"none": "нищо",
|
|
||||||
"popular": "най-популярни",
|
|
||||||
"Unsubscribe": "Отписване",
|
|
||||||
"Slovenian": "Словенски",
|
|
||||||
"Nepali": "Непалски",
|
|
||||||
"Time (h:mm:ss):": "Време (h:mm:ss):",
|
|
||||||
"English (auto-generated)": "Английски (автоматично генерирано)",
|
|
||||||
"search_filters_sort_label": "Сортирай по",
|
|
||||||
"View more comments on Reddit": "Виж повече коментари в Reddit",
|
|
||||||
"Sinhala": "Синхалски",
|
|
||||||
"preferences_feed_menu_label": "Меню с препоръки: ",
|
|
||||||
"preferences_autoplay_label": "Автоматично пускане: ",
|
|
||||||
"Pashto": "Пущунски",
|
|
||||||
"English (United States)": "Английски (САЩ)",
|
|
||||||
"Sign In": "Вход",
|
|
||||||
"subscriptions_unseen_notifs_count": "{{count}} невидяно известие",
|
|
||||||
"subscriptions_unseen_notifs_count_plural": "{{count}} невидяни известия",
|
|
||||||
"Log in": "Вход",
|
|
||||||
"Engagement: ": "Участие: ",
|
|
||||||
"Album: ": "Албум: ",
|
|
||||||
"preferences_speed_label": "Скорост по подразбиране: ",
|
|
||||||
"Import FreeTube subscriptions (.db)": "Импортиране на FreeTube абонаменти (.db)",
|
|
||||||
"preferences_quality_option_dash": "DASH (адаптивно качество)",
|
|
||||||
"preferences_show_nick_label": "Показвай потребителското име отгоре: ",
|
|
||||||
"Private": "Частен",
|
|
||||||
"Samoan": "Самоански",
|
|
||||||
"preferences_notifications_only_label": "Показвай само известията (ако има такива): ",
|
|
||||||
"Create playlist": "Създаване на плейлист",
|
|
||||||
"next_steps_error_message_refresh": "Опресниш",
|
|
||||||
"Top": "Топ",
|
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
|
||||||
"Malayalam": "Малаялам",
|
|
||||||
"Token": "Токен",
|
|
||||||
"preferences_comments_label": "Коментари по подразбиране: ",
|
|
||||||
"Movies": "Филми",
|
|
||||||
"light": "светла",
|
|
||||||
"Unlisted": "Скрит",
|
|
||||||
"preferences_category_admin": "Администраторни предпочитания",
|
|
||||||
"Erroneous token": "Невалиден токен",
|
|
||||||
"No": "Не",
|
|
||||||
"CAPTCHA is a required field": "CAPTCHA е задължително поле",
|
|
||||||
"Video unavailable": "Неналично видео",
|
|
||||||
"footer_source_code": "Изходен код",
|
|
||||||
"New passwords must match": "Новите пароли трябва да съвпадат",
|
|
||||||
"Playlist does not exist.": "Плейлиста не съществува.",
|
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Експортиране на абонаментите като OPML (за NewPipe и FreeTube)",
|
|
||||||
"search_filters_duration_option_short": "Кратко (< 4 минути)",
|
|
||||||
"search_filters_duration_option_long": "Дълго (> 20 минути)",
|
|
||||||
"tokens_count": "{{count}} токен",
|
|
||||||
"tokens_count_plural": "{{count}} токена",
|
|
||||||
"Yes": "Да",
|
|
||||||
"Dutch": "Холандски",
|
|
||||||
"Arabic": "Арабски",
|
|
||||||
"An alternative front-end to YouTube": "Алтернативен преден план на YouTube",
|
|
||||||
"View `x` comments": {
|
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Виж `x` коментар",
|
|
||||||
"": "Виж `x` коментари"
|
|
||||||
},
|
|
||||||
"Chinese (China)": "Китайски (Китай)",
|
|
||||||
"Italian (auto-generated)": "Италиански (автоматично генерирано)",
|
|
||||||
"alphabetically - reverse": "обратно на азбучния ред",
|
|
||||||
"channel_tab_shorts_label": "Shorts",
|
|
||||||
"`x` marked it with a ❤": "`x` го маркира със ❤",
|
|
||||||
"Current version: ": "Текуща версия: ",
|
|
||||||
"channel_tab_community_label": "Общност",
|
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
|
||||||
"preferences_quality_dash_option_360p": "360p",
|
|
||||||
"`x` uploaded a video": "`x` качи видео",
|
|
||||||
"Welsh": "Уелски",
|
|
||||||
"search_message_no_results": "Няма намерени резултати.",
|
|
||||||
"channel_tab_releases_label": "Версии",
|
|
||||||
"Bangla": "Бенгалски",
|
|
||||||
"preferences_quality_dash_option_144p": "144p",
|
|
||||||
"Indonesian": "Индонезийски",
|
|
||||||
"`x` ago": "преди `x`",
|
|
||||||
"Invidious Private Feed for `x`": "Invidious персонални видеа за `x`",
|
|
||||||
"Finnish": "Финландски",
|
|
||||||
"Amharic": "Амхарски",
|
|
||||||
"Malay": "Малайски",
|
|
||||||
"Interlingue": "Интерлинг",
|
|
||||||
"search_filters_date_option_month": "Този месец",
|
|
||||||
"Georgian": "Грузински",
|
|
||||||
"Xhosa": "Кхоса",
|
|
||||||
"Marathi": "Маратхи",
|
|
||||||
"Yoruba": "Йоруба",
|
|
||||||
"Song: ": "Музика: ",
|
|
||||||
"Scottish Gaelic": "Шотландски гелски",
|
|
||||||
"search_filters_features_label": "Функции",
|
|
||||||
"preferences_quality_label": "Предпочитано качество на видеото: ",
|
|
||||||
"generic_channels_count": "{{count}} канал",
|
|
||||||
"generic_channels_count_plural": "{{count}} канала",
|
|
||||||
"Croatian": "Хърватски",
|
|
||||||
"Thai": "Тайски",
|
|
||||||
"Chinese (Taiwan)": "Китайски (Тайван)",
|
|
||||||
"youtube": "YouTube",
|
|
||||||
"Source available here.": "Източник наличен тук.",
|
|
||||||
"LIVE": "На живо",
|
|
||||||
"Ukrainian": "Украински",
|
|
||||||
"Russian": "Руски",
|
|
||||||
"Tajik": "Таджикски",
|
|
||||||
"Token manager": "Управляване на токени",
|
|
||||||
"preferences_quality_dash_label": "Предпочитано DASH качество на видеото: ",
|
|
||||||
"adminprefs_modified_source_code_url_label": "URL до хранилището на променения изходен код",
|
|
||||||
"Japanese": "Японски",
|
|
||||||
"Title": "Заглавие",
|
|
||||||
"Authorize token for `x`?": "Разреши токена за `x`?",
|
|
||||||
"reddit": "Reddit",
|
|
||||||
"permalink": "постоянна връзка",
|
|
||||||
"Trending": "На върха",
|
|
||||||
"Turkish (auto-generated)": "Турски (автоматично генерирано)",
|
|
||||||
"Bulgarian": "Български",
|
|
||||||
"Indonesian (auto-generated)": "Индонезийски (автоматично генерирано)",
|
|
||||||
"Enable web notifications": "Активирай уеб известия",
|
|
||||||
"Western Frisian": "Западен фризски",
|
|
||||||
"search_filters_date_option_week": "Тази седмица",
|
|
||||||
"Yiddish": "Идиш",
|
|
||||||
"preferences_category_player": "Предпочитания за плейъра",
|
|
||||||
"Shared `x` ago": "Споделено преди `x`",
|
|
||||||
"Swahili": "Суахили",
|
|
||||||
"Portuguese (auto-generated)": "Португалски (автоматично генерирано)",
|
|
||||||
"generic_count_years": "{{count}} година",
|
|
||||||
"generic_count_years_plural": "{{count}} години",
|
|
||||||
"Wilson score: ": "Wilson оценка: ",
|
|
||||||
"Genre: ": "Жанр: ",
|
|
||||||
"videoinfo_invidious_embed_link": "Вграждане на линк",
|
|
||||||
"Popular enabled: ": "Активиране на популярната страница: ",
|
|
||||||
"Wrong username or password": "Грешно потребителско име или парола",
|
|
||||||
"Vietnamese": "Виетнамски",
|
|
||||||
"alphabetically": "по азбучен ред",
|
|
||||||
"Afrikaans": "Африкаанс",
|
|
||||||
"Zulu": "Зулуски",
|
|
||||||
"(edited)": "(редактирано)",
|
|
||||||
"Whitelisted regions: ": "Разрешени региони: ",
|
|
||||||
"Spanish (auto-generated)": "Испански (автоматично генерирано)",
|
|
||||||
"Could not fetch comments": "Получаването на коментарите е неуспешно",
|
|
||||||
"Sindhi": "Синдхи",
|
|
||||||
"News": "Новини",
|
|
||||||
"preferences_video_loop_label": "Винаги повтаряй: ",
|
|
||||||
"%A %B %-d, %Y": "%-d %B %Y, %A",
|
|
||||||
"preferences_quality_option_small": "Ниско",
|
|
||||||
"English (United Kingdom)": "Английски (Великобритания)",
|
|
||||||
"Rating: ": "Рейтинг: ",
|
|
||||||
"channel_tab_playlists_label": "Плейлисти",
|
|
||||||
"generic_button_edit": "Редактирай",
|
|
||||||
"Report statistics: ": "Активиране на статистики за репортиране: ",
|
|
||||||
"Cebuano": "Себуано",
|
|
||||||
"Chinese (Traditional)": "Китайски (Традиционен)",
|
|
||||||
"generic_playlists_count": "{{count}} плейлист",
|
|
||||||
"generic_playlists_count_plural": "{{count}} плейлиста",
|
|
||||||
"Import NewPipe subscriptions (.json)": "Импортиране на NewPipe абонаменти (.json)",
|
|
||||||
"Preferences": "Предпочитания",
|
|
||||||
"Subscribe": "Абониране",
|
|
||||||
"Import and Export Data": "Импортиране и експортиране на информация",
|
|
||||||
"preferences_quality_option_hd720": "HD720",
|
|
||||||
"search_filters_type_option_playlist": "Плейлист",
|
|
||||||
"Serbian": "Сръбски",
|
|
||||||
"Kazakh": "Казахски",
|
|
||||||
"Telugu": "Телугу",
|
|
||||||
"search_filters_features_option_purchased": "Купено",
|
|
||||||
"revoke": "отмяна",
|
|
||||||
"search_filters_sort_option_date": "Дата на качване",
|
|
||||||
"preferences_category_data": "Предпочитания за информацията",
|
|
||||||
"search_filters_date_option_none": "Всякаква дата",
|
|
||||||
"Log out": "Излизане",
|
|
||||||
"Search": "Търсене",
|
|
||||||
"preferences_quality_dash_option_auto": "Автоматично",
|
|
||||||
"dark": "тъмна",
|
|
||||||
"Cantonese (Hong Kong)": "Кантонски (Хонг Конг)",
|
|
||||||
"crash_page_report_issue": "Ако никои от горепосочените не помогнаха, моля <a href=\"`x`\">отворете нов проблем в GitHub</a> (предпочитано на Английски) и добавете следния текст в съобщението (НЕ превеждайте този текст):",
|
|
||||||
"Czech": "Чешки",
|
|
||||||
"crash_page_switch_instance": "пробвал да <a href=\"`x`\">ползваш друга инстанция</a>",
|
|
||||||
"generic_count_weeks": "{{count}} седмица",
|
|
||||||
"generic_count_weeks_plural": "{{count}} седмици",
|
|
||||||
"search_filters_features_option_subtitles": "Субтитри",
|
|
||||||
"videoinfo_watch_on_youTube": "Виж в YouTube",
|
|
||||||
"Portuguese": "Португалски",
|
|
||||||
"Music in this video": "Музика в това видео",
|
|
||||||
"Hide replies": "Скрий отговорите",
|
|
||||||
"Password cannot be longer than 55 characters": "Паролата не може да бъде по-дълга от 55 символа",
|
|
||||||
"footer_modfied_source_code": "Променен изходен код",
|
|
||||||
"Bosnian": "Босненски",
|
|
||||||
"Deleted or invalid channel": "Изтрит или невалиден канал",
|
|
||||||
"Popular": "Популярно",
|
|
||||||
"search_filters_type_label": "Тип",
|
|
||||||
"preferences_locale_label": "Език: ",
|
|
||||||
"Playlists": "Плейлисти",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"Export": "Експортиране",
|
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
|
||||||
"Erroneous challenge": "Невалиден тест",
|
|
||||||
"History": "История",
|
|
||||||
"generic_count_hours": "{{count}} час",
|
|
||||||
"generic_count_hours_plural": "{{count}} часа",
|
|
||||||
"Registration enabled: ": "Активиране на регистрация: ",
|
|
||||||
"Music": "Музика",
|
|
||||||
"Incorrect password": "Грешна парола",
|
|
||||||
"Persian": "Перскийски",
|
|
||||||
"Import": "Импортиране",
|
|
||||||
"Import/export data": "Импортиране/Експортиране на информация",
|
|
||||||
"Shared `x`": "Споделено `x`",
|
|
||||||
"Javanese": "Явански",
|
|
||||||
"French (auto-generated)": "Френски (автоматично генерирано)",
|
|
||||||
"Norwegian Bokmål": "Норвежки",
|
|
||||||
"Catalan": "Каталунски",
|
|
||||||
"Hindi": "Хинди",
|
|
||||||
"Tamil": "Тамилски",
|
|
||||||
"search_filters_features_option_live": "На живо",
|
|
||||||
"crash_page_read_the_faq": "прочел <a href=\"`x`\">Често задавани въпроси (FAQ)</a>",
|
|
||||||
"preferences_default_home_label": "Начална страница по подразбиране: ",
|
|
||||||
"Download": "Изтегляне",
|
|
||||||
"Show less": "Покажи по-малко",
|
|
||||||
"Password": "Парола",
|
|
||||||
"User ID": "Потребителско име",
|
|
||||||
"Subscription manager": "Управляване на абонаменти",
|
|
||||||
"search": "търсене",
|
|
||||||
"No such user": "Няма такъв потребител",
|
|
||||||
"View privacy policy.": "Виж политиката за поверителност.",
|
|
||||||
"Only show latest unwatched video from channel: ": "Показвай само най-новите негледани видеа в канала: ",
|
|
||||||
"user_created_playlists": "`x` създаде плейлисти",
|
|
||||||
"Editing playlist `x`": "Редактиране на плейлист `x`",
|
|
||||||
"preferences_thin_mode_label": "Тънък режим: ",
|
|
||||||
"E-mail": "Имейл",
|
|
||||||
"Haitian Creole": "Хаитянски креол",
|
|
||||||
"Irish": "Ирландски",
|
|
||||||
"channel_tab_channels_label": "Канали",
|
|
||||||
"Delete account?": "Изтрий акаунта?",
|
|
||||||
"Redirect homepage to feed: ": "Препращане на началната страница до препоръки ",
|
|
||||||
"Urdu": "Урду",
|
|
||||||
"preferences_vr_mode_label": "Интерактивни 360 градусови видеа (изисква WebGL): ",
|
|
||||||
"Password cannot be empty": "Паролата не може да бъде празна",
|
|
||||||
"Mongolian": "Монголски",
|
|
||||||
"Authorize token?": "Разреши токена?",
|
|
||||||
"search_filters_type_option_all": "Всякакъв тип",
|
|
||||||
"Romanian": "Румънски",
|
|
||||||
"Belarusian": "Беларуски",
|
|
||||||
"channel name - reverse": "име на канал - в обратен ред",
|
|
||||||
"Erroneous CAPTCHA": "Невалидна CAPTCHA",
|
|
||||||
"Watch on YouTube": "Гледай в YouTube",
|
|
||||||
"search_filters_features_option_location": "Местоположение",
|
|
||||||
"Could not pull trending pages.": "Получаването на трендинг страниците е неуспешно.",
|
|
||||||
"German": "Немски",
|
|
||||||
"search_filters_features_option_c_commons": "Creative Commons",
|
|
||||||
"Family friendly? ": "За всяка възраст? ",
|
|
||||||
"Hidden field \"token\" is a required field": "Скритото поле \"токен\" е задължително поле",
|
|
||||||
"Russian (auto-generated)": "Руски (автоматично генерирано)",
|
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
|
||||||
"Corsican": "Корсикански",
|
|
||||||
"Macedonian": "Македонски",
|
|
||||||
"comments_view_x_replies": "Виж {{count}} отговор",
|
|
||||||
"comments_view_x_replies_plural": "Виж {{count}} отговора",
|
|
||||||
"footer_original_source_code": "Оригинален изходен код",
|
|
||||||
"Import YouTube subscriptions": "Импортиране на YouTube/OPML абонаменти",
|
|
||||||
"Lithuanian": "Литовски",
|
|
||||||
"Nyanja": "Нянджа",
|
|
||||||
"Updated `x` ago": "Актуализирано преди `x`",
|
|
||||||
"JavaScript license information": "Информация за Javascript лиценза",
|
|
||||||
"Spanish": "Испански",
|
|
||||||
"Latin": "Латински",
|
|
||||||
"Shona": "Шона",
|
|
||||||
"Portuguese (Brazil)": "Португалски (Бразилия)",
|
|
||||||
"Show more": "Покажи още",
|
|
||||||
"Clear watch history?": "Изчисти историята на търсене?",
|
|
||||||
"Manage tokens": "Управление на токени",
|
|
||||||
"Hausa": "Хауса",
|
|
||||||
"search_filters_features_option_vr180": "VR180",
|
|
||||||
"preferences_category_visual": "Визуални предпочитания",
|
|
||||||
"Italian": "Италиански",
|
|
||||||
"preferences_volume_label": "Сила на звука на плейъра: ",
|
|
||||||
"error_video_not_in_playlist": "Заявеното видео не съществува в този плейлист. <a href=\"`x`\">Натиснете тук за началната страница на плейлиста.</a>",
|
|
||||||
"preferences_listen_label": "Само звук по подразбиране: ",
|
|
||||||
"Dutch (auto-generated)": "Холандски (автоматично генерирано)",
|
|
||||||
"preferences_captions_label": "Надписи по подразбиране: ",
|
|
||||||
"generic_count_days": "{{count}} ден",
|
|
||||||
"generic_count_days_plural": "{{count}} дни",
|
|
||||||
"Hawaiian": "Хавайски",
|
|
||||||
"Could not get channel info.": "Получаването на информация за канала е неуспешно.",
|
|
||||||
"View as playlist": "Виж като плейлист",
|
|
||||||
"Vietnamese (auto-generated)": "Виетнамски (автоматично генерирано)",
|
|
||||||
"search_filters_duration_option_none": "Всякаква продължителност",
|
|
||||||
"preferences_quality_dash_option_240p": "240p",
|
|
||||||
"Latvian": "Латвийски",
|
|
||||||
"search_filters_features_option_hdr": "HDR",
|
|
||||||
"preferences_sort_label": "Сортирай видеата по: ",
|
|
||||||
"Estonian": "Естонски",
|
|
||||||
"Hidden field \"challenge\" is a required field": "Скритото поле \"тест\" е задължително поле",
|
|
||||||
"footer_documentation": "Документация",
|
|
||||||
"Kyrgyz": "Киргизски",
|
|
||||||
"preferences_continue_autoplay_label": "Пускай следващотото видео автоматично: ",
|
|
||||||
"Chinese": "Китайски",
|
|
||||||
"search_filters_sort_option_relevance": "Уместност",
|
|
||||||
"source": "източник",
|
|
||||||
"Fallback comments: ": "Резервни коментари: ",
|
|
||||||
"preferences_automatic_instance_redirect_label": "Автоматично препращане на инстанция (чрез redirect.invidious.io): ",
|
|
||||||
"Maori": "Маори",
|
|
||||||
"generic_button_delete": "Изтрий",
|
|
||||||
"Import YouTube playlist (.csv)": "Импортиране на YouTube плейлист (.csv)",
|
|
||||||
"Switch Invidious Instance": "Смени Invidious инстанция",
|
|
||||||
"channel name": "име на канал",
|
|
||||||
"Audio mode": "Аудио режим",
|
|
||||||
"search_filters_type_option_show": "Сериал",
|
|
||||||
"search_filters_date_option_today": "Днес",
|
|
||||||
"search_filters_features_option_three_d": "3D",
|
|
||||||
"next_steps_error_message": "След което можеш да пробваш да: ",
|
|
||||||
"Hide annotations": "Скрий анотации",
|
|
||||||
"Standard YouTube license": "Стандартен YouTube лиценз",
|
|
||||||
"Text CAPTCHA": "Текст CAPTCHA",
|
|
||||||
"Log in/register": "Вход/регистрация",
|
|
||||||
"Punjabi": "Пенджаби",
|
|
||||||
"Change password": "Смяна на паролата",
|
|
||||||
"License: ": "Лиценз: ",
|
|
||||||
"search_filters_duration_option_medium": "Средно (4 - 20 минути)",
|
|
||||||
"Delete playlist": "Изтриване на плейлист",
|
|
||||||
"Delete playlist `x`?": "Изтрий плейлиста `x`?",
|
|
||||||
"Korean": "Корейски",
|
|
||||||
"Export subscriptions as OPML": "Експортиране на абонаментите като OPML",
|
|
||||||
"unsubscribe": "отписване",
|
|
||||||
"View YouTube comments": "Виж YouTube коментарите",
|
|
||||||
"Kannada": "Каннада",
|
|
||||||
"Not a playlist.": "Невалиден плейлист.",
|
|
||||||
"Wrong answer": "Грешен отговор",
|
|
||||||
"Released under the AGPLv3 on Github.": "Публикувано под AGPLv3 в GitHub.",
|
|
||||||
"Burmese": "Бирмански",
|
|
||||||
"Sundanese": "Сундански",
|
|
||||||
"Hungarian": "Унгарски",
|
|
||||||
"generic_count_seconds": "{{count}} секунда",
|
|
||||||
"generic_count_seconds_plural": "{{count}} секунди",
|
|
||||||
"search_filters_date_label": "Дата на качване",
|
|
||||||
"Greek": "Гръцки",
|
|
||||||
"crash_page_you_found_a_bug": "Изглежда намери бъг в Invidious!",
|
|
||||||
"View all playlists": "Виж всички плейлисти",
|
|
||||||
"Khmer": "Кхмерски",
|
|
||||||
"preferences_annotations_label": "Покажи анотаций по подразбиране: ",
|
|
||||||
"generic_views_count": "{{count}} гледане",
|
|
||||||
"generic_views_count_plural": "{{count}} гледания",
|
|
||||||
"Next page": "Следваща страница"
|
|
||||||
}
|
|
@ -476,15 +476,5 @@
|
|||||||
"Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: ",
|
"Redirect homepage to feed: ": "Redirigeix la pàgina d'inici al feed: ",
|
||||||
"Standard YouTube license": "Llicència estàndard de YouTube",
|
"Standard YouTube license": "Llicència estàndard de YouTube",
|
||||||
"Download is disabled": "Les baixades s'han inhabilitat",
|
"Download is disabled": "Les baixades s'han inhabilitat",
|
||||||
"Import YouTube playlist (.csv)": "Importar llista de reproducció de YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Importar llista de reproducció de YouTube (.csv)"
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
|
||||||
"playlist_button_add_items": "Afegeix vídeos",
|
|
||||||
"generic_button_save": "Desa",
|
|
||||||
"generic_button_cancel": "Cancel·la",
|
|
||||||
"channel_tab_releases_label": "Publicacions",
|
|
||||||
"generic_channels_count": "{{count}} canal",
|
|
||||||
"generic_channels_count_plural": "{{count}} canals",
|
|
||||||
"generic_button_edit": "Edita",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"generic_button_delete": "Suprimeix"
|
|
||||||
}
|
}
|
||||||
|
@ -492,16 +492,5 @@
|
|||||||
"Song: ": "Skladba: ",
|
"Song: ": "Skladba: ",
|
||||||
"Standard YouTube license": "Standardní licence YouTube",
|
"Standard YouTube license": "Standardní licence YouTube",
|
||||||
"Download is disabled": "Stahování je zakázáno",
|
"Download is disabled": "Stahování je zakázáno",
|
||||||
"Import YouTube playlist (.csv)": "Importovat YouTube playlist (.csv)",
|
"Import YouTube playlist (.csv)": "Importovat YouTube playlist (.csv)"
|
||||||
"generic_button_save": "Uložit",
|
|
||||||
"generic_button_delete": "Odstranit",
|
|
||||||
"generic_button_cancel": "Zrušit",
|
|
||||||
"channel_tab_podcasts_label": "Podcasty",
|
|
||||||
"channel_tab_releases_label": "Vydání",
|
|
||||||
"generic_button_edit": "Upravit",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"playlist_button_add_items": "Přidat videa",
|
|
||||||
"generic_channels_count_0": "{{count}} kanál",
|
|
||||||
"generic_channels_count_1": "{{count}} kanály",
|
|
||||||
"generic_channels_count_2": "{{count}} kanálů"
|
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
"Change password": "Passwort ändern",
|
"Change password": "Passwort ändern",
|
||||||
"Manage subscriptions": "Abonnements verwalten",
|
"Manage subscriptions": "Abonnements verwalten",
|
||||||
"Manage tokens": "Tokens verwalten",
|
"Manage tokens": "Tokens verwalten",
|
||||||
"Watch history": "Wiedergabeverlauf",
|
"Watch history": "Verlauf",
|
||||||
"Delete account": "Account löschen",
|
"Delete account": "Account löschen",
|
||||||
"preferences_category_admin": "Administrator-Einstellungen",
|
"preferences_category_admin": "Administrator-Einstellungen",
|
||||||
"preferences_default_home_label": "Standard-Startseite: ",
|
"preferences_default_home_label": "Standard-Startseite: ",
|
||||||
@ -476,15 +476,5 @@
|
|||||||
"Standard YouTube license": "Standard YouTube-Lizenz",
|
"Standard YouTube license": "Standard YouTube-Lizenz",
|
||||||
"Song: ": "Musik: ",
|
"Song: ": "Musik: ",
|
||||||
"Download is disabled": "Herunterladen ist deaktiviert",
|
"Download is disabled": "Herunterladen ist deaktiviert",
|
||||||
"Import YouTube playlist (.csv)": "YouTube Wiedergabeliste importieren (.csv)",
|
"Import YouTube playlist (.csv)": "YouTube Playlist Importieren (.csv)"
|
||||||
"generic_button_delete": "Löschen",
|
|
||||||
"generic_button_edit": "Bearbeiten",
|
|
||||||
"generic_button_save": "Speichern",
|
|
||||||
"generic_button_cancel": "Abbrechen",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"playlist_button_add_items": "Videos hinzufügen",
|
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
|
||||||
"channel_tab_releases_label": "Veröffentlichungen",
|
|
||||||
"generic_channels_count": "{{count}} Kanal",
|
|
||||||
"generic_channels_count_plural": "{{count}} Kanäle"
|
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
"Time (h:mm:ss):": "Ώρα (ω:λλ:δδ):",
|
"Time (h:mm:ss):": "Ώρα (ω:λλ:δδ):",
|
||||||
"Text CAPTCHA": "Κείμενο CAPTCHA",
|
"Text CAPTCHA": "Κείμενο CAPTCHA",
|
||||||
"Image CAPTCHA": "Εικόνα CAPTCHA",
|
"Image CAPTCHA": "Εικόνα CAPTCHA",
|
||||||
"Sign In": "Εγγραφή",
|
"Sign In": "Σύνδεση",
|
||||||
"Register": "Εγγραφή",
|
"Register": "Εγγραφή",
|
||||||
"E-mail": "Ηλεκτρονικό ταχυδρομείο",
|
"E-mail": "Ηλεκτρονικό ταχυδρομείο",
|
||||||
"Preferences": "Προτιμήσεις",
|
"Preferences": "Προτιμήσεις",
|
||||||
@ -145,7 +145,7 @@
|
|||||||
"View YouTube comments": "Προβολή σχολίων από το YouTube",
|
"View YouTube comments": "Προβολή σχολίων από το YouTube",
|
||||||
"View more comments on Reddit": "Προβολή περισσότερων σχολίων στο Reddit",
|
"View more comments on Reddit": "Προβολή περισσότερων σχολίων στο Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Προβολή `x` σχολίου",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Προβολή `x` σχολίων",
|
||||||
"": "Προβολή `x` σχολίων"
|
"": "Προβολή `x` σχολίων"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Προβολή σχολίων από το Reddit",
|
"View Reddit comments": "Προβολή σχολίων από το Reddit",
|
||||||
@ -349,7 +349,7 @@
|
|||||||
"crash_page_you_found_a_bug": "Φαίνεται ότι βρήκατε ένα σφάλμα στο Invidious!",
|
"crash_page_you_found_a_bug": "Φαίνεται ότι βρήκατε ένα σφάλμα στο Invidious!",
|
||||||
"crash_page_before_reporting": "Πριν αναφέρετε ένα σφάλμα, βεβαιωθείτε ότι έχετε:",
|
"crash_page_before_reporting": "Πριν αναφέρετε ένα σφάλμα, βεβαιωθείτε ότι έχετε:",
|
||||||
"crash_page_refresh": "προσπαθήσει να <a href=\"`x`\">ανανεώσετε τη σελίδα</a>",
|
"crash_page_refresh": "προσπαθήσει να <a href=\"`x`\">ανανεώσετε τη σελίδα</a>",
|
||||||
"crash_page_read_the_faq": "διαβάστε τις <a href=\"`x`\">Συχνές Ερωτήσεις (ΣΕ)</a>",
|
"crash_page_read_the_faq": "διαβάσει τις <a href=\"`x`\">Συχνές Ερωτήσεις (ΣΕ)</a>",
|
||||||
"crash_page_search_issue": "αναζητήσει για <a href=\"`x`\">υπάρχοντα θέματα στο GitHub</a>",
|
"crash_page_search_issue": "αναζητήσει για <a href=\"`x`\">υπάρχοντα θέματα στο GitHub</a>",
|
||||||
"generic_views_count": "{{count}} προβολή",
|
"generic_views_count": "{{count}} προβολή",
|
||||||
"generic_views_count_plural": "{{count}} προβολές",
|
"generic_views_count_plural": "{{count}} προβολές",
|
||||||
@ -442,49 +442,5 @@
|
|||||||
"search_filters_type_option_show": "Μπάρα προόδου διαβάσματος",
|
"search_filters_type_option_show": "Μπάρα προόδου διαβάσματος",
|
||||||
"preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: ",
|
"preferences_watch_history_label": "Ενεργοποίηση ιστορικού παρακολούθησης: ",
|
||||||
"search_filters_title": "Φίλτρο",
|
"search_filters_title": "Φίλτρο",
|
||||||
"search_message_no_results": "Δε βρέθηκαν αποτελέσματα.",
|
"search_message_no_results": "Δε βρέθηκαν αποτελέσματα."
|
||||||
"channel_tab_podcasts_label": "Podcast",
|
|
||||||
"preferences_save_player_pos_label": "Αποθήκευση σημείου αναπαραγωγής: ",
|
|
||||||
"search_filters_apply_button": "Εφαρμογή επιλεγμένων φίλτρων",
|
|
||||||
"Download is disabled": "Είναι απενεργοποιημένη η λήψη",
|
|
||||||
"comments_points_count": "{{count}} βαθμός",
|
|
||||||
"comments_points_count_plural": "{{count}} βαθμοί",
|
|
||||||
"search_filters_sort_option_views": "Προβολές",
|
|
||||||
"search_message_change_filters_or_query": "Προσπαθήστε να διευρύνετε το ερώτημα αναζήτησης ή/και να αλλάξετε τα φίλτρα.",
|
|
||||||
"Channel Sponsor": "Χορηγός Καναλιού",
|
|
||||||
"channel_tab_streams_label": "Ζωντανή μετάδοση",
|
|
||||||
"playlist_button_add_items": "Προσθήκη βίντεο",
|
|
||||||
"Artist: ": "Καλλιτέχνης: ",
|
|
||||||
"search_message_use_another_instance": " Μπορείτε επίσης <a href=\"`x`\">να αναζητήσετε σε άλλο instance</a>.",
|
|
||||||
"generic_button_save": "Αποθήκευση",
|
|
||||||
"generic_button_cancel": "Ακύρωση",
|
|
||||||
"subscriptions_unseen_notifs_count": "{{count}} μη αναγνωσμένη ειδοποίηση",
|
|
||||||
"subscriptions_unseen_notifs_count_plural": "{{count}} μη αναγνωσμένες ειδοποιήσεις",
|
|
||||||
"Album: ": "Δίσκος: ",
|
|
||||||
"tokens_count": "{{count}} σύμβολο",
|
|
||||||
"tokens_count_plural": "{{count}} σύμβολα",
|
|
||||||
"channel_tab_shorts_label": "Short",
|
|
||||||
"channel_tab_releases_label": "Κυκλοφορίες",
|
|
||||||
"Song: ": "Τραγούδι: ",
|
|
||||||
"generic_channels_count": "{{count}} κανάλι",
|
|
||||||
"generic_channels_count_plural": "{{count}} κανάλια",
|
|
||||||
"Popular enabled: ": "Ενεργοποιημένα Δημοφιλή: ",
|
|
||||||
"channel_tab_playlists_label": "Λίστες αναπαραγωγής",
|
|
||||||
"generic_button_edit": "Επεξεργασία",
|
|
||||||
"search_filters_date_option_none": "Οποιαδήποτε ημερομηνία",
|
|
||||||
"crash_page_switch_instance": "προσπάθεια <a href=\"`x`\">χρήσης άλλου instance</a>",
|
|
||||||
"Music in this video": "Μουσική σε αυτό το βίντεο",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_channels_label": "Κανάλια",
|
|
||||||
"search_filters_type_option_all": "Οποιοσδήποτε τύπος",
|
|
||||||
"search_filters_features_option_vr180": "VR180",
|
|
||||||
"error_video_not_in_playlist": "Το αιτούμενο βίντεο δεν υπάρχει στη δεδομένη λίστα αναπαραγωγής. <a href=\"`x`\">Πατήστε εδώ για επιστροφή στη κεντρική σελίδα λιστών αναπαραγωγής.</a>",
|
|
||||||
"search_filters_duration_option_none": "Οποιαδήποτε διάρκεια",
|
|
||||||
"preferences_automatic_instance_redirect_label": "Αυτόματη ανακατεύθυνση instance (εναλλακτική σε redirect.invidious.io): ",
|
|
||||||
"generic_button_delete": "Διαγραφή",
|
|
||||||
"Import YouTube playlist (.csv)": "Εισαγωγή λίστας αναπαραγωγής YouTube (.csv)",
|
|
||||||
"Switch Invidious Instance": "Αλλαγή Instance Invidious",
|
|
||||||
"Standard YouTube license": "Τυπική άδεια YouTube",
|
|
||||||
"search_filters_duration_option_medium": "Μεσαία (4 - 20 λεπτά)",
|
|
||||||
"search_filters_date_label": "Ημερομηνία αναφόρτωσης"
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
{
|
{
|
||||||
"generic_channels_count": "{{count}} channel",
|
|
||||||
"generic_channels_count_plural": "{{count}} channels",
|
|
||||||
"generic_views_count": "{{count}} view",
|
"generic_views_count": "{{count}} view",
|
||||||
"generic_views_count_plural": "{{count}} views",
|
"generic_views_count_plural": "{{count}} views",
|
||||||
"generic_videos_count": "{{count}} video",
|
"generic_videos_count": "{{count}} video",
|
||||||
@ -40,7 +38,6 @@
|
|||||||
"Import Invidious data": "Import Invidious JSON data",
|
"Import Invidious data": "Import Invidious JSON data",
|
||||||
"Import YouTube subscriptions": "Import YouTube/OPML subscriptions",
|
"Import YouTube subscriptions": "Import YouTube/OPML subscriptions",
|
||||||
"Import YouTube playlist (.csv)": "Import YouTube playlist (.csv)",
|
"Import YouTube playlist (.csv)": "Import YouTube playlist (.csv)",
|
||||||
"Import YouTube watch history (.json)": "Import YouTube watch history (.json)",
|
|
||||||
"Import FreeTube subscriptions (.db)": "Import FreeTube subscriptions (.db)",
|
"Import FreeTube subscriptions (.db)": "Import FreeTube subscriptions (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Import NewPipe subscriptions (.json)",
|
"Import NewPipe subscriptions (.json)": "Import NewPipe subscriptions (.json)",
|
||||||
"Import NewPipe data (.zip)": "Import NewPipe data (.zip)",
|
"Import NewPipe data (.zip)": "Import NewPipe data (.zip)",
|
||||||
|
@ -154,7 +154,7 @@
|
|||||||
"View YouTube comments": "Vidi komentojn de JuTubo",
|
"View YouTube comments": "Vidi komentojn de JuTubo",
|
||||||
"View more comments on Reddit": "Vidi pli komentoj en Reddit",
|
"View more comments on Reddit": "Vidi pli komentoj en Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Vidi `x` komenton",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Vidi `x` komentojn",
|
||||||
"": "Vidi `x` komentojn"
|
"": "Vidi `x` komentojn"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Vidi komentojn de Reddit",
|
"View Reddit comments": "Vidi komentojn de Reddit",
|
||||||
@ -447,8 +447,8 @@
|
|||||||
"French (auto-generated)": "Franca (aŭtomate generita)",
|
"French (auto-generated)": "Franca (aŭtomate generita)",
|
||||||
"Spanish (Mexico)": "Hispana (Meksiko)",
|
"Spanish (Mexico)": "Hispana (Meksiko)",
|
||||||
"Spanish (auto-generated)": "Hispana (aŭtomate generita)",
|
"Spanish (auto-generated)": "Hispana (aŭtomate generita)",
|
||||||
"generic_count_days": "{{count}} tago",
|
"generic_count_days": "{{count}} jaro",
|
||||||
"generic_count_days_plural": "{{count}} tagoj",
|
"generic_count_days_plural": "{{count}} jaroj",
|
||||||
"search_filters_type_option_all": "Ajna speco",
|
"search_filters_type_option_all": "Ajna speco",
|
||||||
"search_filters_duration_option_none": "Ajna daŭro",
|
"search_filters_duration_option_none": "Ajna daŭro",
|
||||||
"search_filters_apply_button": "Uzi elektitajn filtrilojn",
|
"search_filters_apply_button": "Uzi elektitajn filtrilojn",
|
||||||
@ -476,15 +476,5 @@
|
|||||||
"Song: ": "Muzikaĵo: ",
|
"Song: ": "Muzikaĵo: ",
|
||||||
"Standard YouTube license": "Implicita YouTube-licenco",
|
"Standard YouTube license": "Implicita YouTube-licenco",
|
||||||
"Download is disabled": "Elŝuto estas malebligita",
|
"Download is disabled": "Elŝuto estas malebligita",
|
||||||
"Import YouTube playlist (.csv)": "Importi YouTube-ludliston (.csv)",
|
"Import YouTube playlist (.csv)": "Importi YouTube-ludliston (.csv)"
|
||||||
"generic_button_edit": "Redakti",
|
|
||||||
"playlist_button_add_items": "Aldoni videojn",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"generic_button_delete": "Forigi",
|
|
||||||
"channel_tab_podcasts_label": "Podkastoj",
|
|
||||||
"generic_button_cancel": "Nuligi",
|
|
||||||
"channel_tab_releases_label": "Eldonoj",
|
|
||||||
"generic_button_save": "Konservi",
|
|
||||||
"generic_channels_count": "{{count}} kanalo",
|
|
||||||
"generic_channels_count_plural": "{{count}} kanaloj"
|
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,7 @@
|
|||||||
"Token manager": "Gestor de tokens",
|
"Token manager": "Gestor de tokens",
|
||||||
"Token": "Ficha",
|
"Token": "Ficha",
|
||||||
"Import/export": "Importar/Exportar",
|
"Import/export": "Importar/Exportar",
|
||||||
"unsubscribe": "desuscribirse",
|
"unsubscribe": "Desuscribirse",
|
||||||
"revoke": "revocar",
|
"revoke": "revocar",
|
||||||
"Subscriptions": "Suscripciones",
|
"Subscriptions": "Suscripciones",
|
||||||
"search": "buscar",
|
"search": "buscar",
|
||||||
@ -154,7 +154,7 @@
|
|||||||
"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": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentario",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentarios",
|
||||||
"": "Ver `x` comentarios"
|
"": "Ver `x` comentarios"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Ver los comentarios de Reddit",
|
"View Reddit comments": "Ver los comentarios de Reddit",
|
||||||
@ -476,15 +476,5 @@
|
|||||||
"Channel Sponsor": "Patrocinador del canal",
|
"Channel Sponsor": "Patrocinador del canal",
|
||||||
"Standard YouTube license": "Licencia de YouTube estándar",
|
"Standard YouTube license": "Licencia de YouTube estándar",
|
||||||
"Download is disabled": "La descarga está deshabilitada",
|
"Download is disabled": "La descarga está deshabilitada",
|
||||||
"Import YouTube playlist (.csv)": "Importar lista de reproducción de YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Importar lista de reproducción de YouTube (.csv)"
|
||||||
"playlist_button_add_items": "Añadir vídeos",
|
|
||||||
"generic_button_edit": "Editar",
|
|
||||||
"generic_button_save": "Guardar",
|
|
||||||
"generic_button_delete": "Borrar",
|
|
||||||
"generic_button_cancel": "Cancelar",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
|
||||||
"channel_tab_releases_label": "Publicaciones",
|
|
||||||
"generic_channels_count": "{{count}} canal",
|
|
||||||
"generic_channels_count_plural": "{{count}} canales"
|
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
{
|
{
|
||||||
"generic_channels_count_0": "{{count}} chaîne",
|
"generic_views_count": "{{count}} vue",
|
||||||
"generic_channels_count_1": "{{count}} de chaînes",
|
"generic_views_count_plural": "{{count}} vues",
|
||||||
"generic_channels_count_2": "{{count}} chaînes",
|
"generic_videos_count": "{{count}} vidéo",
|
||||||
"generic_views_count_0": "{{count}} vue",
|
"generic_videos_count_plural": "{{count}} vidéos",
|
||||||
"generic_views_count_1": "{{count}} de vues",
|
"generic_playlists_count": "{{count}} liste de lecture",
|
||||||
"generic_views_count_2": "{{count}} vues",
|
"generic_playlists_count_plural": "{{count}} listes de lecture",
|
||||||
"generic_videos_count_0": "{{count}} vidéo",
|
"generic_subscribers_count": "{{count}} abonné",
|
||||||
"generic_videos_count_1": "{{count}} de vidéos",
|
"generic_subscribers_count_plural": "{{count}} abonnés",
|
||||||
"generic_videos_count_2": "{{count}} vidéos",
|
"generic_subscriptions_count": "{{count}} abonnement",
|
||||||
"generic_playlists_count_0": "{{count}} liste de lecture",
|
"generic_subscriptions_count_plural": "{{count}} abonnements",
|
||||||
"generic_playlists_count_1": "{{count}} listes de lecture",
|
|
||||||
"generic_playlists_count_2": "{{count}} listes de lecture",
|
|
||||||
"generic_subscribers_count_0": "{{count}} abonné",
|
|
||||||
"generic_subscribers_count_1": "{{count}} d'abonnés",
|
|
||||||
"generic_subscribers_count_2": "{{count}} abonnés",
|
|
||||||
"generic_subscriptions_count_0": "{{count}} abonnement",
|
|
||||||
"generic_subscriptions_count_1": "{{count}} d'abonnements",
|
|
||||||
"generic_subscriptions_count_2": "{{count}} abonnements",
|
|
||||||
"generic_button_delete": "Supprimer",
|
"generic_button_delete": "Supprimer",
|
||||||
"generic_button_edit": "Editer",
|
"generic_button_edit": "Editer",
|
||||||
"generic_button_save": "Enregistrer",
|
"generic_button_save": "Enregistrer",
|
||||||
@ -63,10 +55,10 @@
|
|||||||
"Password": "Mot de passe",
|
"Password": "Mot de passe",
|
||||||
"Time (h:mm:ss):": "Heure (h:mm:ss) :",
|
"Time (h:mm:ss):": "Heure (h:mm:ss) :",
|
||||||
"Text CAPTCHA": "CAPTCHA textuel",
|
"Text CAPTCHA": "CAPTCHA textuel",
|
||||||
"Image CAPTCHA": "CAPTCHA pictural",
|
"Image CAPTCHA": "CAPTCHA graphique",
|
||||||
"Sign In": "S'identifier",
|
"Sign In": "Se connecter",
|
||||||
"Register": "S'inscrire",
|
"Register": "S'inscrire",
|
||||||
"E-mail": "Courriel",
|
"E-mail": "E-mail",
|
||||||
"Preferences": "Préférences",
|
"Preferences": "Préférences",
|
||||||
"preferences_category_player": "Préférences du lecteur",
|
"preferences_category_player": "Préférences du lecteur",
|
||||||
"preferences_video_loop_label": "Lire en boucle : ",
|
"preferences_video_loop_label": "Lire en boucle : ",
|
||||||
@ -136,16 +128,14 @@
|
|||||||
"Subscription manager": "Gestionnaire d'abonnement",
|
"Subscription manager": "Gestionnaire d'abonnement",
|
||||||
"Token manager": "Gestionnaire de token",
|
"Token manager": "Gestionnaire de token",
|
||||||
"Token": "Token",
|
"Token": "Token",
|
||||||
"tokens_count_0": "{{count}} jeton",
|
"tokens_count": "{{count}} token",
|
||||||
"tokens_count_1": "{{count}} de jetons",
|
"tokens_count_plural": "{{count}} tokens",
|
||||||
"tokens_count_2": "{{count}} jetons",
|
|
||||||
"Import/export": "Importer/Exporter",
|
"Import/export": "Importer/Exporter",
|
||||||
"unsubscribe": "se désabonner",
|
"unsubscribe": "se désabonner",
|
||||||
"revoke": "révoquer",
|
"revoke": "révoquer",
|
||||||
"Subscriptions": "Abonnements",
|
"Subscriptions": "Abonnements",
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}} notification non vue",
|
"subscriptions_unseen_notifs_count": "{{count}} notification non vue",
|
||||||
"subscriptions_unseen_notifs_count_1": "{{count}} de notifications non vues",
|
"subscriptions_unseen_notifs_count_plural": "{{count}} notifications non vues",
|
||||||
"subscriptions_unseen_notifs_count_2": "{{count}} notifications non vues",
|
|
||||||
"search": "rechercher",
|
"search": "rechercher",
|
||||||
"Log out": "Se déconnecter",
|
"Log out": "Se déconnecter",
|
||||||
"Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur GitHub.",
|
"Released under the AGPLv3 on Github.": "Publié sous licence AGPLv3 sur GitHub.",
|
||||||
@ -207,14 +197,12 @@
|
|||||||
"This channel does not exist.": "Cette chaine n'existe pas.",
|
"This channel does not exist.": "Cette chaine n'existe pas.",
|
||||||
"Could not get channel info.": "Impossible de charger les informations de cette chaîne.",
|
"Could not get channel info.": "Impossible de charger les informations de cette chaîne.",
|
||||||
"Could not fetch comments": "Impossible de charger les commentaires",
|
"Could not fetch comments": "Impossible de charger les commentaires",
|
||||||
"comments_view_x_replies_0": "Voir {{count}} réponse",
|
"comments_view_x_replies": "Voir {{count}} réponse",
|
||||||
"comments_view_x_replies_1": "Voir {{count}} de réponses",
|
"comments_view_x_replies_plural": "Voir {{count}} réponses",
|
||||||
"comments_view_x_replies_2": "Voir {{count}} réponses",
|
|
||||||
"`x` ago": "il y a `x`",
|
"`x` ago": "il y a `x`",
|
||||||
"Load more": "Voir plus",
|
"Load more": "Voir plus",
|
||||||
"comments_points_count_0": "{{count}} point",
|
"comments_points_count": "{{count}} point",
|
||||||
"comments_points_count_1": "{{count}} de points",
|
"comments_points_count_plural": "{{count}} points",
|
||||||
"comments_points_count_2": "{{count}} points",
|
|
||||||
"Could not create mix.": "Impossible de charger cette liste de lecture.",
|
"Could not create mix.": "Impossible de charger cette liste de lecture.",
|
||||||
"Empty playlist": "La liste de lecture est vide",
|
"Empty playlist": "La liste de lecture est vide",
|
||||||
"Not a playlist.": "La liste de lecture est invalide.",
|
"Not a playlist.": "La liste de lecture est invalide.",
|
||||||
@ -332,27 +320,20 @@
|
|||||||
"Yiddish": "Yiddish",
|
"Yiddish": "Yiddish",
|
||||||
"Yoruba": "Yoruba",
|
"Yoruba": "Yoruba",
|
||||||
"Zulu": "Zoulou",
|
"Zulu": "Zoulou",
|
||||||
"generic_count_years_0": "{{count}} an",
|
"generic_count_years": "{{count}} an",
|
||||||
"generic_count_years_1": "{{count}} ans",
|
"generic_count_years_plural": "{{count}} ans",
|
||||||
"generic_count_years_2": "{{count}} ans",
|
"generic_count_months": "{{count}} mois",
|
||||||
"generic_count_months_0": "{{count}} mois",
|
"generic_count_months_plural": "{{count}} mois",
|
||||||
"generic_count_months_1": "{{count}} mois",
|
"generic_count_weeks": "{{count}} semaine",
|
||||||
"generic_count_months_2": "{{count}} mois",
|
"generic_count_weeks_plural": "{{count}} semaines",
|
||||||
"generic_count_weeks_0": "{{count}} semaine",
|
"generic_count_days": "{{count}} jour",
|
||||||
"generic_count_weeks_1": "{{count}} semaines",
|
"generic_count_days_plural": "{{count}} jours",
|
||||||
"generic_count_weeks_2": "{{count}} semaines",
|
"generic_count_hours": "{{count}} heure",
|
||||||
"generic_count_days_0": "{{count}} jour",
|
"generic_count_hours_plural": "{{count}} heures",
|
||||||
"generic_count_days_1": "{{count}} jours",
|
"generic_count_minutes": "{{count}} minute",
|
||||||
"generic_count_days_2": "{{count}} jours",
|
"generic_count_minutes_plural": "{{count}} minutes",
|
||||||
"generic_count_hours_0": "{{count}} heure",
|
"generic_count_seconds": "{{count}} seconde",
|
||||||
"generic_count_hours_1": "{{count}} heures",
|
"generic_count_seconds_plural": "{{count}} secondes",
|
||||||
"generic_count_hours_2": "{{count}} heures",
|
|
||||||
"generic_count_minutes_0": "{{count}} minute",
|
|
||||||
"generic_count_minutes_1": "{{count}} minutes",
|
|
||||||
"generic_count_minutes_2": "{{count}} minutes",
|
|
||||||
"generic_count_seconds_0": "{{count}} seconde",
|
|
||||||
"generic_count_seconds_1": "{{count}} secondes",
|
|
||||||
"generic_count_seconds_2": "{{count}} secondes",
|
|
||||||
"Fallback comments: ": "Commentaires alternatifs : ",
|
"Fallback comments: ": "Commentaires alternatifs : ",
|
||||||
"Popular": "Populaire",
|
"Popular": "Populaire",
|
||||||
"Search": "Rechercher",
|
"Search": "Rechercher",
|
||||||
@ -501,7 +482,5 @@
|
|||||||
"Music in this video": "Musique dans cette vidéo",
|
"Music in this video": "Musique dans cette vidéo",
|
||||||
"Channel Sponsor": "Soutien de la chaîne",
|
"Channel Sponsor": "Soutien de la chaîne",
|
||||||
"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_podcasts_label": "Émissions audio"
|
|
||||||
}
|
}
|
||||||
|
@ -471,18 +471,5 @@
|
|||||||
"channel_tab_shorts_label": "शॉर्ट्स",
|
"channel_tab_shorts_label": "शॉर्ट्स",
|
||||||
"channel_tab_streams_label": "लाइवस्ट्रीम्स",
|
"channel_tab_streams_label": "लाइवस्ट्रीम्स",
|
||||||
"channel_tab_playlists_label": "प्लेलिस्ट्स",
|
"channel_tab_playlists_label": "प्लेलिस्ट्स",
|
||||||
"channel_tab_channels_label": "चैनल्स",
|
"channel_tab_channels_label": "चैनल्स"
|
||||||
"generic_button_save": "सहेजें",
|
|
||||||
"generic_button_cancel": "रद्द करें",
|
|
||||||
"generic_button_rss": "आरएसएस",
|
|
||||||
"generic_button_edit": "संपादित करें",
|
|
||||||
"generic_button_delete": "मिटाएं",
|
|
||||||
"playlist_button_add_items": "वीडियो जोड़ें",
|
|
||||||
"Song: ": "गाना: ",
|
|
||||||
"channel_tab_podcasts_label": "पाॅडकास्ट",
|
|
||||||
"channel_tab_releases_label": "रिलीज़ेस्",
|
|
||||||
"Import YouTube playlist (.csv)": "YouTube प्लेलिस्ट (.csv) आयात करें",
|
|
||||||
"Standard YouTube license": "मानक यूट्यूब लाइसेंस",
|
|
||||||
"Channel Sponsor": "चैनल प्रायोजक",
|
|
||||||
"Download is disabled": "डाउनलोड करना अक्षम है"
|
|
||||||
}
|
}
|
||||||
|
@ -492,16 +492,5 @@
|
|||||||
"Song: ": "Pjesma: ",
|
"Song: ": "Pjesma: ",
|
||||||
"Standard YouTube license": "Standardna YouTube licenca",
|
"Standard YouTube license": "Standardna YouTube licenca",
|
||||||
"Download is disabled": "Preuzimanje je deaktivirano",
|
"Download is disabled": "Preuzimanje je deaktivirano",
|
||||||
"Import YouTube playlist (.csv)": "Uvezi YouTube zbirku (.csv)",
|
"Import YouTube playlist (.csv)": "Uvezi YouTube zbirku (.csv)"
|
||||||
"generic_button_delete": "Izbriši",
|
|
||||||
"playlist_button_add_items": "Dodaj videa",
|
|
||||||
"channel_tab_podcasts_label": "Podcasti",
|
|
||||||
"generic_button_edit": "Uredi",
|
|
||||||
"generic_button_save": "Spremi",
|
|
||||||
"generic_button_cancel": "Odustani",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_releases_label": "Izdanja",
|
|
||||||
"generic_channels_count_0": "{{count}} kanal",
|
|
||||||
"generic_channels_count_1": "{{count}} kanala",
|
|
||||||
"generic_channels_count_2": "{{count}} kanala"
|
|
||||||
}
|
}
|
||||||
|
@ -446,28 +446,5 @@
|
|||||||
"crash_page_read_the_faq": "baca <a href=\"`x`\">Soal Sering Ditanya (SSD/FAQ)</a>",
|
"crash_page_read_the_faq": "baca <a href=\"`x`\">Soal Sering Ditanya (SSD/FAQ)</a>",
|
||||||
"crash_page_search_issue": "mencari <a href=\"`x`\">isu yang ada di GitHub</a>",
|
"crash_page_search_issue": "mencari <a href=\"`x`\">isu yang ada di GitHub</a>",
|
||||||
"crash_page_report_issue": "Jika yang di atas tidak membantu, <a href=\"`x`\">buka isu baru di GitHub</a> (sebaiknya dalam bahasa Inggris) dan sertakan teks berikut dalam pesan Anda (JANGAN terjemahkan teks tersebut):",
|
"crash_page_report_issue": "Jika yang di atas tidak membantu, <a href=\"`x`\">buka isu baru di GitHub</a> (sebaiknya dalam bahasa Inggris) dan sertakan teks berikut dalam pesan Anda (JANGAN terjemahkan teks tersebut):",
|
||||||
"Popular enabled: ": "Populer diaktifkan: ",
|
"Popular enabled: ": "Populer diaktifkan: "
|
||||||
"channel_tab_podcasts_label": "Podcast",
|
|
||||||
"Download is disabled": "Download dinonaktifkan",
|
|
||||||
"Channel Sponsor": "Saluran Sponsor",
|
|
||||||
"channel_tab_streams_label": "Streaming langsung",
|
|
||||||
"playlist_button_add_items": "Tambahkan video",
|
|
||||||
"Artist: ": "Artis: ",
|
|
||||||
"generic_button_save": "Simpan",
|
|
||||||
"generic_button_cancel": "Batal",
|
|
||||||
"Album: ": "Album: ",
|
|
||||||
"channel_tab_shorts_label": "Shorts",
|
|
||||||
"channel_tab_releases_label": "Terbit",
|
|
||||||
"Interlingue": "Interlingue",
|
|
||||||
"Song: ": "Lagu: ",
|
|
||||||
"generic_channels_count_0": "Saluran {{count}}",
|
|
||||||
"channel_tab_playlists_label": "Daftar putar",
|
|
||||||
"generic_button_edit": "Ubah",
|
|
||||||
"Music in this video": "Musik dalam video ini",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_channels_label": "Saluran",
|
|
||||||
"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",
|
|
||||||
"Import YouTube playlist (.csv)": "Impor daftar putar YouTube (.csv)",
|
|
||||||
"Standard YouTube license": "Lisensi YouTube standar"
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
{
|
{
|
||||||
"generic_subscribers_count_0": "{{count}} iscritto",
|
"generic_subscribers_count": "{{count}} iscritto",
|
||||||
"generic_subscribers_count_1": "{{count}} iscritti",
|
"generic_subscribers_count_plural": "{{count}} iscritti",
|
||||||
"generic_subscribers_count_2": "{{count}} iscritti",
|
"generic_videos_count": "{{count}} video",
|
||||||
"generic_videos_count_0": "{{count}} video",
|
"generic_videos_count_plural": "{{count}} video",
|
||||||
"generic_videos_count_1": "{{count}} video",
|
"generic_playlists_count": "{{count}} playlist",
|
||||||
"generic_videos_count_2": "{{count}} video",
|
"generic_playlists_count_plural": "{{count}} playlist",
|
||||||
"generic_playlists_count_0": "{{count}} playlist",
|
|
||||||
"generic_playlists_count_1": "{{count}} playlist",
|
|
||||||
"generic_playlists_count_2": "{{count}} playlist",
|
|
||||||
"LIVE": "IN DIRETTA",
|
"LIVE": "IN DIRETTA",
|
||||||
"Shared `x` ago": "Condiviso `x` fa",
|
"Shared `x` ago": "Condiviso `x` fa",
|
||||||
"Unsubscribe": "Disiscriviti",
|
"Unsubscribe": "Disiscriviti",
|
||||||
@ -16,7 +13,7 @@
|
|||||||
"View playlist on YouTube": "Vedi playlist su YouTube",
|
"View playlist on YouTube": "Vedi playlist su YouTube",
|
||||||
"newest": "più recente",
|
"newest": "più recente",
|
||||||
"oldest": "più vecchio",
|
"oldest": "più vecchio",
|
||||||
"popular": "popolare",
|
"popular": "Tendenze",
|
||||||
"last": "ultimo",
|
"last": "ultimo",
|
||||||
"Next page": "Pagina successiva",
|
"Next page": "Pagina successiva",
|
||||||
"Previous page": "Pagina precedente",
|
"Previous page": "Pagina precedente",
|
||||||
@ -116,19 +113,16 @@
|
|||||||
"Subscription manager": "Gestione delle iscrizioni",
|
"Subscription manager": "Gestione delle iscrizioni",
|
||||||
"Token manager": "Gestione dei gettoni",
|
"Token manager": "Gestione dei gettoni",
|
||||||
"Token": "Gettone",
|
"Token": "Gettone",
|
||||||
"generic_subscriptions_count_0": "{{count}} iscrizione",
|
"generic_subscriptions_count": "{{count}} iscrizione",
|
||||||
"generic_subscriptions_count_1": "{{count}} iscrizioni",
|
"generic_subscriptions_count_plural": "{{count}} iscrizioni",
|
||||||
"generic_subscriptions_count_2": "{{count}} iscrizioni",
|
"tokens_count": "{{count}} gettone",
|
||||||
"tokens_count_0": "{{count}} gettone",
|
"tokens_count_plural": "{{count}} gettoni",
|
||||||
"tokens_count_1": "{{count}} gettoni",
|
|
||||||
"tokens_count_2": "{{count}} gettoni",
|
|
||||||
"Import/export": "Importa/esporta",
|
"Import/export": "Importa/esporta",
|
||||||
"unsubscribe": "disiscriviti",
|
"unsubscribe": "disiscriviti",
|
||||||
"revoke": "revoca",
|
"revoke": "revoca",
|
||||||
"Subscriptions": "Iscrizioni",
|
"Subscriptions": "Iscrizioni",
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}} notifica non visualizzata",
|
"subscriptions_unseen_notifs_count": "{{count}} notifica non visualizzata",
|
||||||
"subscriptions_unseen_notifs_count_1": "{{count}} notifiche non visualizzate",
|
"subscriptions_unseen_notifs_count_plural": "{{count}} notifiche non visualizzate",
|
||||||
"subscriptions_unseen_notifs_count_2": "{{count}} notifiche non visualizzate",
|
|
||||||
"search": "Cerca",
|
"search": "Cerca",
|
||||||
"Log out": "Esci",
|
"Log out": "Esci",
|
||||||
"Source available here.": "Codice sorgente.",
|
"Source available here.": "Codice sorgente.",
|
||||||
@ -157,9 +151,8 @@
|
|||||||
"Whitelisted regions: ": "Regioni in lista bianca: ",
|
"Whitelisted regions: ": "Regioni in lista bianca: ",
|
||||||
"Blacklisted regions: ": "Regioni in lista nera: ",
|
"Blacklisted regions: ": "Regioni in lista nera: ",
|
||||||
"Shared `x`": "Condiviso `x`",
|
"Shared `x`": "Condiviso `x`",
|
||||||
"generic_views_count_0": "{{count}} visualizzazione",
|
"generic_views_count": "{{count}} visualizzazione",
|
||||||
"generic_views_count_1": "{{count}} visualizzazioni",
|
"generic_views_count_plural": "{{count}} visualizzazioni",
|
||||||
"generic_views_count_2": "{{count}} visualizzazioni",
|
|
||||||
"Premieres in `x`": "In anteprima in `x`",
|
"Premieres in `x`": "In anteprima in `x`",
|
||||||
"Premieres `x`": "In anteprima `x`",
|
"Premieres `x`": "In anteprima `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.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Ciao, Sembra che tu abbia disattivato JavaScript. Clicca qui per visualizzare i commenti, ma considera che il caricamento potrebbe richiedere più tempo.",
|
||||||
@ -307,27 +300,20 @@
|
|||||||
"Yiddish": "Yiddish",
|
"Yiddish": "Yiddish",
|
||||||
"Yoruba": "Yoruba",
|
"Yoruba": "Yoruba",
|
||||||
"Zulu": "Zulu",
|
"Zulu": "Zulu",
|
||||||
"generic_count_years_0": "{{count}} anno",
|
"generic_count_years": "{{count}} anno",
|
||||||
"generic_count_years_1": "{{count}} anni",
|
"generic_count_years_plural": "{{count}} anni",
|
||||||
"generic_count_years_2": "{{count}} anni",
|
"generic_count_months": "{{count}} mese",
|
||||||
"generic_count_months_0": "{{count}} mese",
|
"generic_count_months_plural": "{{count}} mesi",
|
||||||
"generic_count_months_1": "{{count}} mesi",
|
"generic_count_weeks": "{{count}} settimana",
|
||||||
"generic_count_months_2": "{{count}} mesi",
|
"generic_count_weeks_plural": "{{count}} settimane",
|
||||||
"generic_count_weeks_0": "{{count}} settimana",
|
"generic_count_days": "{{count}} giorno",
|
||||||
"generic_count_weeks_1": "{{count}} settimane",
|
"generic_count_days_plural": "{{count}} giorni",
|
||||||
"generic_count_weeks_2": "{{count}} settimane",
|
"generic_count_hours": "{{count}} ora",
|
||||||
"generic_count_days_0": "{{count}} giorno",
|
"generic_count_hours_plural": "{{count}} ore",
|
||||||
"generic_count_days_1": "{{count}} giorni",
|
"generic_count_minutes": "{{count}} minuto",
|
||||||
"generic_count_days_2": "{{count}} giorni",
|
"generic_count_minutes_plural": "{{count}} minuti",
|
||||||
"generic_count_hours_0": "{{count}} ora",
|
"generic_count_seconds": "{{count}} secondo",
|
||||||
"generic_count_hours_1": "{{count}} ore",
|
"generic_count_seconds_plural": "{{count}} secondi",
|
||||||
"generic_count_hours_2": "{{count}} ore",
|
|
||||||
"generic_count_minutes_0": "{{count}} minuto",
|
|
||||||
"generic_count_minutes_1": "{{count}} minuti",
|
|
||||||
"generic_count_minutes_2": "{{count}} minuti",
|
|
||||||
"generic_count_seconds_0": "{{count}} secondo",
|
|
||||||
"generic_count_seconds_1": "{{count}} secondi",
|
|
||||||
"generic_count_seconds_2": "{{count}} secondi",
|
|
||||||
"Fallback comments: ": "Commenti alternativi: ",
|
"Fallback comments: ": "Commenti alternativi: ",
|
||||||
"Popular": "Popolare",
|
"Popular": "Popolare",
|
||||||
"Search": "Cerca",
|
"Search": "Cerca",
|
||||||
@ -431,12 +417,10 @@
|
|||||||
"search_filters_duration_option_short": "Corto (< 4 minuti)",
|
"search_filters_duration_option_short": "Corto (< 4 minuti)",
|
||||||
"search_filters_duration_option_long": "Lungo (> 20 minuti)",
|
"search_filters_duration_option_long": "Lungo (> 20 minuti)",
|
||||||
"search_filters_features_option_purchased": "Acquistato",
|
"search_filters_features_option_purchased": "Acquistato",
|
||||||
"comments_view_x_replies_0": "Vedi {{count}} risposta",
|
"comments_view_x_replies": "Vedi {{count}} risposta",
|
||||||
"comments_view_x_replies_1": "Vedi {{count}} risposte",
|
"comments_view_x_replies_plural": "Vedi {{count}} risposte",
|
||||||
"comments_view_x_replies_2": "Vedi {{count}} risposte",
|
"comments_points_count": "{{count}} punto",
|
||||||
"comments_points_count_0": "{{count}} punto",
|
"comments_points_count_plural": "{{count}} punti",
|
||||||
"comments_points_count_1": "{{count}} punti",
|
|
||||||
"comments_points_count_2": "{{count}} punti",
|
|
||||||
"Portuguese (auto-generated)": "Portoghese (generati automaticamente)",
|
"Portuguese (auto-generated)": "Portoghese (generati automaticamente)",
|
||||||
"crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!",
|
"crash_page_you_found_a_bug": "Sembra che tu abbia trovato un bug in Invidious!",
|
||||||
"crash_page_switch_instance": "provato a <a href=\"`x`\">usare un'altra istanza</a>",
|
"crash_page_switch_instance": "provato a <a href=\"`x`\">usare un'altra istanza</a>",
|
||||||
@ -483,7 +467,7 @@
|
|||||||
"channel_tab_shorts_label": "Short",
|
"channel_tab_shorts_label": "Short",
|
||||||
"channel_tab_playlists_label": "Playlist",
|
"channel_tab_playlists_label": "Playlist",
|
||||||
"channel_tab_channels_label": "Canali",
|
"channel_tab_channels_label": "Canali",
|
||||||
"channel_tab_streams_label": "Trasmissioni in diretta",
|
"channel_tab_streams_label": "Livestream",
|
||||||
"channel_tab_community_label": "Comunità",
|
"channel_tab_community_label": "Comunità",
|
||||||
"Music in this video": "Musica in questo video",
|
"Music in this video": "Musica in questo video",
|
||||||
"Artist: ": "Artista: ",
|
"Artist: ": "Artista: ",
|
||||||
@ -492,16 +476,5 @@
|
|||||||
"Song: ": "Canzone: ",
|
"Song: ": "Canzone: ",
|
||||||
"Standard YouTube license": "Licenza standard di YouTube",
|
"Standard YouTube license": "Licenza standard di YouTube",
|
||||||
"Channel Sponsor": "Sponsor del canale",
|
"Channel Sponsor": "Sponsor del canale",
|
||||||
"Import YouTube playlist (.csv)": "Importa playlist di YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Importa playlist di YouTube (.csv)"
|
||||||
"generic_button_edit": "Modifica",
|
|
||||||
"generic_button_cancel": "Annulla",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_releases_label": "Pubblicazioni",
|
|
||||||
"generic_button_delete": "Elimina",
|
|
||||||
"generic_button_save": "Salva",
|
|
||||||
"playlist_button_add_items": "Aggiungi video",
|
|
||||||
"channel_tab_podcasts_label": "Podcast",
|
|
||||||
"generic_channels_count_0": "{{count}} canale",
|
|
||||||
"generic_channels_count_1": "{{count}} canali",
|
|
||||||
"generic_channels_count_2": "{{count}} canali"
|
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,7 @@
|
|||||||
"preferences_category_subscription": "登録チャンネル設定",
|
"preferences_category_subscription": "登録チャンネル設定",
|
||||||
"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": "投稿日 - 逆順",
|
||||||
@ -366,13 +366,13 @@
|
|||||||
"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": "ソースコード",
|
||||||
"footer_original_source_code": "元のソースコード",
|
"footer_original_source_code": "元のソースコード",
|
||||||
"footer_modfied_source_code": "改変して使用",
|
"footer_modfied_source_code": "改変して使用",
|
||||||
"adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリのURL",
|
"adminprefs_modified_source_code_url_label": "改変されたソースコードのレポジトリのURL",
|
||||||
"search_filters_duration_option_long": "20分以上",
|
"search_filters_duration_option_long": "20 分以上",
|
||||||
"preferences_region_label": "地域: ",
|
"preferences_region_label": "地域: ",
|
||||||
"footer_donate_page": "寄付する",
|
"footer_donate_page": "寄付する",
|
||||||
"preferences_quality_dash_label": "優先するDASH画質: ",
|
"preferences_quality_dash_label": "優先するDASH画質: ",
|
||||||
@ -443,7 +443,7 @@
|
|||||||
"search_filters_date_option_none": "すべて",
|
"search_filters_date_option_none": "すべて",
|
||||||
"search_filters_type_option_all": "すべての種類",
|
"search_filters_type_option_all": "すべての種類",
|
||||||
"search_filters_duration_option_none": "すべての長さ",
|
"search_filters_duration_option_none": "すべての長さ",
|
||||||
"search_filters_duration_option_medium": "4 ~ 20分",
|
"search_filters_duration_option_medium": "4 ~ 20 分",
|
||||||
"preferences_save_player_pos_label": "再生位置を保存: ",
|
"preferences_save_player_pos_label": "再生位置を保存: ",
|
||||||
"crash_page_before_reporting": "バグを報告する前に、次のことを確認してください。",
|
"crash_page_before_reporting": "バグを報告する前に、次のことを確認してください。",
|
||||||
"crash_page_report_issue": "上記が助けにならないなら、<a href=\"`x`\">GitHub</a> に新しい issue を作成し(英語が好ましい)、メッセージに次のテキストを含めてください(テキストは翻訳しない)。",
|
"crash_page_report_issue": "上記が助けにならないなら、<a href=\"`x`\">GitHub</a> に新しい issue を作成し(英語が好ましい)、メッセージに次のテキストを含めてください(テキストは翻訳しない)。",
|
||||||
@ -460,14 +460,5 @@
|
|||||||
"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_cancel": "キャンセル",
|
|
||||||
"channel_tab_podcasts_label": "ポッドキャスト",
|
|
||||||
"channel_tab_releases_label": "リリース",
|
|
||||||
"generic_button_edit": "編集",
|
|
||||||
"generic_button_save": "保存",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"playlist_button_add_items": "動画を追加",
|
|
||||||
"generic_channels_count_0": "{{count}}個のチャンネル"
|
|
||||||
}
|
}
|
||||||
|
@ -460,14 +460,5 @@
|
|||||||
"Music in this video": "동영상 속 음악",
|
"Music in this video": "동영상 속 음악",
|
||||||
"Artist: ": "아티스트: ",
|
"Artist: ": "아티스트: ",
|
||||||
"Download is disabled": "다운로드가 비활성화 되어있음",
|
"Download is disabled": "다운로드가 비활성화 되어있음",
|
||||||
"Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)",
|
"Import YouTube playlist (.csv)": "유튜브 플레이리스트 가져오기 (.csv)"
|
||||||
"playlist_button_add_items": "동영상 추가",
|
|
||||||
"channel_tab_podcasts_label": "팟캐스트",
|
|
||||||
"generic_button_delete": "삭제",
|
|
||||||
"generic_button_edit": "편집",
|
|
||||||
"generic_button_save": "저장",
|
|
||||||
"generic_button_cancel": "취소",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_releases_label": "출시",
|
|
||||||
"generic_channels_count_0": "{{count}} 채널"
|
|
||||||
}
|
}
|
||||||
|
@ -154,7 +154,7 @@
|
|||||||
"View YouTube comments": "Vis YouTube-kommentarer",
|
"View YouTube comments": "Vis YouTube-kommentarer",
|
||||||
"View more comments on Reddit": "Vis flere kommenterer på Reddit",
|
"View more comments on Reddit": "Vis flere kommenterer på Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentar",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Vis `x` kommentarer",
|
||||||
"": "Vis `x` kommentarer"
|
"": "Vis `x` kommentarer"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Vis Reddit-kommentarer",
|
"View Reddit comments": "Vis Reddit-kommentarer",
|
||||||
@ -476,15 +476,5 @@
|
|||||||
"Album: ": "Album: ",
|
"Album: ": "Album: ",
|
||||||
"Download is disabled": "Nedlasting er avskrudd",
|
"Download is disabled": "Nedlasting er avskrudd",
|
||||||
"Channel Sponsor": "Kanalsponsor",
|
"Channel Sponsor": "Kanalsponsor",
|
||||||
"Import YouTube playlist (.csv)": "Importer YouTube-spilleliste (.csv)",
|
"Import YouTube playlist (.csv)": "Importer YouTube-spilleliste (.csv)"
|
||||||
"channel_tab_podcasts_label": "Podkaster",
|
|
||||||
"channel_tab_releases_label": "Utgaver",
|
|
||||||
"generic_button_delete": "Slett",
|
|
||||||
"generic_button_edit": "Endre",
|
|
||||||
"generic_button_save": "Lagre",
|
|
||||||
"generic_button_cancel": "Avbryt",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"playlist_button_add_items": "Legg til videoer",
|
|
||||||
"generic_channels_count": "{{count}} kanal",
|
|
||||||
"generic_channels_count_plural": "{{count}} kanaler"
|
|
||||||
}
|
}
|
||||||
|
@ -1,29 +1 @@
|
|||||||
{
|
{}
|
||||||
"preferences_quality_dash_option_720p": "୭୨୦ପି",
|
|
||||||
"preferences_quality_dash_option_4320p": "୪୩୨୦ପି",
|
|
||||||
"preferences_quality_dash_option_240p": "୨୪୦ପି",
|
|
||||||
"preferences_quality_dash_option_2160p": "୨୧୬୦ପି",
|
|
||||||
"preferences_quality_dash_option_144p": "୧୪୪ପି",
|
|
||||||
"reddit": "Reddit",
|
|
||||||
"preferences_quality_dash_option_480p": "୪୮୦ପି",
|
|
||||||
"preferences_dark_mode_label": "ଥିମ୍: ",
|
|
||||||
"dark": "ଗାଢ଼",
|
|
||||||
"published": "ପ୍ରକାଶିତ",
|
|
||||||
"generic_videos_count": "{{count}}ଟିଏ ଵିଡ଼ିଓ",
|
|
||||||
"generic_videos_count_plural": "{{count}}ଟି ଵିଡ଼ିଓ",
|
|
||||||
"generic_button_edit": "ସମ୍ପାଦନା",
|
|
||||||
"light": "ହାଲୁକା",
|
|
||||||
"last": "ଗତ",
|
|
||||||
"New password": "ନୂଆ ପାସ୍ୱର୍ଡ଼",
|
|
||||||
"preferences_quality_dash_option_1440p": "୧୪୪୦ପି",
|
|
||||||
"preferences_quality_dash_option_360p": "୩୬୦ପି",
|
|
||||||
"preferences_quality_option_medium": "ମଧ୍ୟମ",
|
|
||||||
"preferences_quality_dash_option_1080p": "୧୦୮୦ପି",
|
|
||||||
"youtube": "YouTube",
|
|
||||||
"preferences_quality_option_hd720": "HD୭୨୦",
|
|
||||||
"invidious": "Invidious",
|
|
||||||
"generic_playlists_count": "{{count}}ଟିଏ ଚାଳନାତାଲିକା",
|
|
||||||
"generic_playlists_count_plural": "{{count}}ଟି ଚାଳନାତାଲିକା",
|
|
||||||
"Yes": "ହଁ",
|
|
||||||
"No": "ନାହିଁ"
|
|
||||||
}
|
|
||||||
|
@ -148,12 +148,12 @@
|
|||||||
"Blacklisted regions: ": "Niedostępny na obszarach: ",
|
"Blacklisted regions: ": "Niedostępny na obszarach: ",
|
||||||
"Shared `x`": "Udostępniono `x`",
|
"Shared `x`": "Udostępniono `x`",
|
||||||
"Premieres in `x`": "Publikacja za `x`",
|
"Premieres in `x`": "Publikacja za `x`",
|
||||||
"Premieres `x`": "Publikacja `x`",
|
"Premieres `x`": "Publikacja za `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.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Cześć! Wygląda na to, że masz wyłączoną obsługę JavaScriptu. Kliknij tutaj, żeby zobaczyć komentarze. Pamiętaj, że wczytywanie może potrwać dłużej.",
|
||||||
"View YouTube comments": "Wyświetl komentarze z YouTube",
|
"View YouTube comments": "Wyświetl komentarze z YouTube",
|
||||||
"View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie",
|
"View more comments on Reddit": "Wyświetl więcej komentarzy na Reddicie",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Wyświetl `x` komentarz",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Wyświetl `x` komentarzy",
|
||||||
"": "Wyświetl `x` komentarzy"
|
"": "Wyświetl `x` komentarzy"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Wyświetl komentarze z Redditta",
|
"View Reddit comments": "Wyświetl komentarze z Redditta",
|
||||||
@ -492,16 +492,5 @@
|
|||||||
"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ę YouTube (.csv)"
|
||||||
"generic_button_edit": "Edytuj",
|
|
||||||
"generic_button_cancel": "Anuluj",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_podcasts_label": "Podkasty",
|
|
||||||
"channel_tab_releases_label": "Wydania",
|
|
||||||
"generic_button_delete": "Usuń",
|
|
||||||
"generic_button_save": "Zapisz",
|
|
||||||
"playlist_button_add_items": "Dodaj filmy",
|
|
||||||
"generic_channels_count_0": "{{count}} kanał",
|
|
||||||
"generic_channels_count_1": "{{count}} kanały",
|
|
||||||
"generic_channels_count_2": "{{count}} kanałów"
|
|
||||||
}
|
}
|
||||||
|
@ -112,9 +112,8 @@
|
|||||||
"Subscription manager": "Gerenciador de inscrições",
|
"Subscription manager": "Gerenciador de inscrições",
|
||||||
"Token manager": "Gerenciador de tokens",
|
"Token manager": "Gerenciador de tokens",
|
||||||
"Token": "Token",
|
"Token": "Token",
|
||||||
"tokens_count_0": "{{count}} token",
|
"tokens_count": "{{count}} token",
|
||||||
"tokens_count_1": "{{count}} tokens",
|
"tokens_count_plural": "{{count}} tokens",
|
||||||
"tokens_count_2": "{{count}} tokens",
|
|
||||||
"Import/export": "Importar/Exportar",
|
"Import/export": "Importar/Exportar",
|
||||||
"unsubscribe": "cancelar inscrição",
|
"unsubscribe": "cancelar inscrição",
|
||||||
"revoke": "revogar",
|
"revoke": "revogar",
|
||||||
@ -298,27 +297,20 @@
|
|||||||
"Yiddish": "Iídiche",
|
"Yiddish": "Iídiche",
|
||||||
"Yoruba": "Iorubá",
|
"Yoruba": "Iorubá",
|
||||||
"Zulu": "Zulu",
|
"Zulu": "Zulu",
|
||||||
"generic_count_years_0": "{{count}} ano",
|
"generic_count_years": "{{count}} ano",
|
||||||
"generic_count_years_1": "{{count}} anos",
|
"generic_count_years_plural": "{{count}} anos",
|
||||||
"generic_count_years_2": "{{count}} anos",
|
"generic_count_months": "{{count}} mês",
|
||||||
"generic_count_months_0": "{{count}} mês",
|
"generic_count_months_plural": "{{count}} meses",
|
||||||
"generic_count_months_1": "{{count}} meses",
|
"generic_count_weeks": "{{count}} semana",
|
||||||
"generic_count_months_2": "{{count}} meses",
|
"generic_count_weeks_plural": "{{count}} semanas",
|
||||||
"generic_count_weeks_0": "{{count}} semana",
|
"generic_count_days": "{{count}} dia",
|
||||||
"generic_count_weeks_1": "{{count}} semanas",
|
"generic_count_days_plural": "{{count}} dias",
|
||||||
"generic_count_weeks_2": "{{count}} semanas",
|
"generic_count_hours": "{{count}} hora",
|
||||||
"generic_count_days_0": "{{count}} dia",
|
"generic_count_hours_plural": "{{count}} horas",
|
||||||
"generic_count_days_1": "{{count}} dias",
|
"generic_count_minutes": "{{count}} minuto",
|
||||||
"generic_count_days_2": "{{count}} dias",
|
"generic_count_minutes_plural": "{{count}} minutos",
|
||||||
"generic_count_hours_0": "{{count}} hora",
|
"generic_count_seconds": "{{count}} segundo",
|
||||||
"generic_count_hours_1": "{{count}} horas",
|
"generic_count_seconds_plural": "{{count}} segundos",
|
||||||
"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",
|
|
||||||
"Fallback comments: ": "Comentários alternativos: ",
|
"Fallback comments: ": "Comentários alternativos: ",
|
||||||
"Popular": "Populares",
|
"Popular": "Populares",
|
||||||
"Search": "Procurar",
|
"Search": "Procurar",
|
||||||
@ -385,27 +377,20 @@
|
|||||||
"preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ",
|
"preferences_quality_dash_label": "Qualidade de vídeo do painel preferida: ",
|
||||||
"preferences_region_label": "País do conteúdo: ",
|
"preferences_region_label": "País do conteúdo: ",
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
"preferences_quality_dash_option_4320p": "4320p",
|
||||||
"generic_videos_count_0": "{{count}} vídeo",
|
"generic_videos_count": "{{count}} vídeo",
|
||||||
"generic_videos_count_1": "{{count}} vídeos",
|
"generic_videos_count_plural": "{{count}} vídeos",
|
||||||
"generic_videos_count_2": "{{count}} vídeos",
|
"generic_playlists_count": "{{count}} lista de reprodução",
|
||||||
"generic_playlists_count_0": "{{count}} lista de reprodução",
|
"generic_playlists_count_plural": "{{count}} listas de reprodução",
|
||||||
"generic_playlists_count_1": "{{count}} listas de reprodução",
|
"generic_subscribers_count": "{{count}} inscrito",
|
||||||
"generic_playlists_count_2": "{{count}} listas de reprodução",
|
"generic_subscribers_count_plural": "{{count}} inscritos",
|
||||||
"generic_subscribers_count_0": "{{count}} inscrito",
|
"generic_subscriptions_count": "{{count}} inscrição",
|
||||||
"generic_subscribers_count_1": "{{count}} inscritos",
|
"generic_subscriptions_count_plural": "{{count}} inscrições",
|
||||||
"generic_subscribers_count_2": "{{count}} inscritos",
|
"subscriptions_unseen_notifs_count": "{{count}} notificação não vista",
|
||||||
"generic_subscriptions_count_0": "{{count}} inscrição",
|
"subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas",
|
||||||
"generic_subscriptions_count_1": "{{count}} inscrições",
|
"comments_view_x_replies": "Ver {{count}} resposta",
|
||||||
"generic_subscriptions_count_2": "{{count}} inscrições",
|
"comments_view_x_replies_plural": "Ver {{count}} respostas",
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista",
|
"comments_points_count": "{{count}} ponto",
|
||||||
"subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas",
|
"comments_points_count_plural": "{{count}} pontos",
|
||||||
"subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas",
|
|
||||||
"comments_view_x_replies_0": "Ver {{count}} resposta",
|
|
||||||
"comments_view_x_replies_1": "Ver {{count}} respostas",
|
|
||||||
"comments_view_x_replies_2": "Ver {{count}} respostas",
|
|
||||||
"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 você encontrou um erro no Invidious!",
|
"crash_page_you_found_a_bug": "Parece que você encontrou um erro no Invidious!",
|
||||||
"crash_page_before_reporting": "Antes de reportar um erro, verifique se você:",
|
"crash_page_before_reporting": "Antes de reportar um erro, verifique se você:",
|
||||||
"preferences_save_player_pos_label": "Salvar a posição de reprodução: ",
|
"preferences_save_player_pos_label": "Salvar a posição de reprodução: ",
|
||||||
@ -415,9 +400,8 @@
|
|||||||
"crash_page_search_issue": "procurou por um <a href=\"`x`\">erro existente no GitHub</a>",
|
"crash_page_search_issue": "procurou por um <a href=\"`x`\">erro existente 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 (NÃ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 traduza):",
|
||||||
"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>",
|
||||||
"generic_views_count_0": "{{count}} visualização",
|
"generic_views_count": "{{count}} visualização",
|
||||||
"generic_views_count_1": "{{count}} visualizações",
|
"generic_views_count_plural": "{{count}} visualizações",
|
||||||
"generic_views_count_2": "{{count}} visualizações",
|
|
||||||
"preferences_quality_option_dash": "DASH (qualidade adaptável)",
|
"preferences_quality_option_dash": "DASH (qualidade adaptável)",
|
||||||
"preferences_quality_option_hd720": "HD720",
|
"preferences_quality_option_hd720": "HD720",
|
||||||
"preferences_quality_option_small": "Pequeno",
|
"preferences_quality_option_small": "Pequeno",
|
||||||
@ -491,17 +475,6 @@
|
|||||||
"Standard YouTube license": "Licença padrão do YouTube",
|
"Standard YouTube license": "Licença padrão do YouTube",
|
||||||
"Song: ": "Música: ",
|
"Song: ": "Música: ",
|
||||||
"Channel Sponsor": "Patrocinador do Canal",
|
"Channel Sponsor": "Patrocinador do Canal",
|
||||||
"Download is disabled": "Download está desabilitado",
|
"Download is disabled": "Download está desativado",
|
||||||
"Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)"
|
||||||
"generic_button_delete": "Apagar",
|
|
||||||
"generic_button_save": "Salvar",
|
|
||||||
"generic_button_edit": "Editar",
|
|
||||||
"playlist_button_add_items": "Adicionar vídeos",
|
|
||||||
"channel_tab_releases_label": "Lançamentos",
|
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
|
||||||
"generic_button_cancel": "Cancelar",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"generic_channels_count_0": "{{count}} canal",
|
|
||||||
"generic_channels_count_1": "{{count}} canais",
|
|
||||||
"generic_channels_count_2": "{{count}} canais"
|
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"search_filters_features_option_location": "Localização",
|
"search_filters_features_option_location": "Localização",
|
||||||
"search_filters_features_option_four_k": "4K",
|
"search_filters_features_option_four_k": "4K",
|
||||||
"search_filters_features_option_live": "Ao Vivo",
|
"search_filters_features_option_live": "Em direto",
|
||||||
"search_filters_features_option_three_d": "3D",
|
"search_filters_features_option_three_d": "3D",
|
||||||
"search_filters_features_option_c_commons": "Creative Commons",
|
"search_filters_features_option_c_commons": "Creative Commons",
|
||||||
"search_filters_features_option_subtitles": "Legendas",
|
"search_filters_features_option_subtitles": "Legendas",
|
||||||
@ -365,7 +365,7 @@
|
|||||||
"Subscribe": "Subscrever",
|
"Subscribe": "Subscrever",
|
||||||
"Unsubscribe": "Anular subscrição",
|
"Unsubscribe": "Anular subscrição",
|
||||||
"Shared `x` ago": "Partilhado `x` atrás",
|
"Shared `x` ago": "Partilhado `x` atrás",
|
||||||
"LIVE": "AO VIVO",
|
"LIVE": "Em direto",
|
||||||
"search_filters_duration_option_short": "Curto (< 4 minutos)",
|
"search_filters_duration_option_short": "Curto (< 4 minutos)",
|
||||||
"search_filters_duration_option_long": "Longo (> 20 minutos)",
|
"search_filters_duration_option_long": "Longo (> 20 minutos)",
|
||||||
"footer_source_code": "Código-fonte",
|
"footer_source_code": "Código-fonte",
|
||||||
@ -476,13 +476,5 @@
|
|||||||
"Channel Sponsor": "Patrocinador do canal",
|
"Channel Sponsor": "Patrocinador do canal",
|
||||||
"Standard YouTube license": "Licença padrão do YouTube",
|
"Standard YouTube license": "Licença padrão do YouTube",
|
||||||
"Download is disabled": "A descarga está desativada",
|
"Download is disabled": "A descarga está desativada",
|
||||||
"Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)"
|
||||||
"generic_button_delete": "Deletar",
|
|
||||||
"generic_button_edit": "Editar",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
|
||||||
"channel_tab_releases_label": "Lançamentos",
|
|
||||||
"generic_button_save": "Salvar",
|
|
||||||
"generic_button_cancel": "Cancelar",
|
|
||||||
"playlist_button_add_items": "Adicionar vídeos"
|
|
||||||
}
|
}
|
||||||
|
@ -492,16 +492,5 @@
|
|||||||
"Standard YouTube license": "Стандартная лицензия YouTube",
|
"Standard YouTube license": "Стандартная лицензия YouTube",
|
||||||
"Channel Sponsor": "Спонсор канала",
|
"Channel Sponsor": "Спонсор канала",
|
||||||
"Download is disabled": "Загрузка отключена",
|
"Download is disabled": "Загрузка отключена",
|
||||||
"Import YouTube playlist (.csv)": "Импорт плейлиста YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Импорт плейлиста YouTube (.csv)"
|
||||||
"channel_tab_releases_label": "Релизы",
|
|
||||||
"generic_button_delete": "Удалить",
|
|
||||||
"generic_button_edit": "Редактировать",
|
|
||||||
"generic_button_save": "Сохранить",
|
|
||||||
"generic_button_cancel": "Отменить",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"playlist_button_add_items": "Добавить видео",
|
|
||||||
"channel_tab_podcasts_label": "Подкасты",
|
|
||||||
"generic_channels_count_0": "{{count}} канал",
|
|
||||||
"generic_channels_count_1": "{{count}} канала",
|
|
||||||
"generic_channels_count_2": "{{count}} каналов"
|
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,7 @@
|
|||||||
"preferences_quality_option_hd720": "HD720",
|
"preferences_quality_option_hd720": "HD720",
|
||||||
"preferences_quality_dash_option_auto": "ස්වයංක්රීය",
|
"preferences_quality_dash_option_auto": "ස්වයංක්රීය",
|
||||||
"preferences_quality_option_small": "කුඩා",
|
"preferences_quality_option_small": "කුඩා",
|
||||||
"preferences_quality_dash_option_best": "හොඳම",
|
"preferences_quality_dash_option_best": "උසස්",
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
"preferences_quality_dash_option_2160p": "2160p",
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
"preferences_quality_dash_option_1440p": "1440p",
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
"preferences_quality_dash_option_720p": "720p",
|
||||||
@ -119,9 +119,5 @@
|
|||||||
"Only show latest unwatched video from channel: ": "නාලිකාවේ නවතම නැරඹන නොලද වීඩියෝව පමණක් පෙන්වන්න: ",
|
"Only show latest unwatched video from channel: ": "නාලිකාවේ නවතම නැරඹන නොලද වීඩියෝව පමණක් පෙන්වන්න: ",
|
||||||
"preferences_category_data": "දත්ත මනාප",
|
"preferences_category_data": "දත්ත මනාප",
|
||||||
"Clear watch history": "නැරඹුම් ඉතිහාසය මකාදැමීම",
|
"Clear watch history": "නැරඹුම් ඉතිහාසය මකාදැමීම",
|
||||||
"Subscriptions": "දායකත්ව",
|
"Subscriptions": "දායකත්ව"
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"generic_button_save": "සුරකින්න",
|
|
||||||
"generic_button_cancel": "අවලංගු කරන්න",
|
|
||||||
"preferences_quality_dash_option_worst": "නරකම"
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"last": "posledné",
|
"last": "posledné",
|
||||||
"Next page": "Ďalšia strana",
|
"Next page": "Ďalšia strana",
|
||||||
"Previous page": "Predchádzajúca strana",
|
"Previous page": "Predchádzajúca strana",
|
||||||
"Clear watch history?": "Vymazať históriu pozerania?",
|
"Clear watch history?": "Vymazať históriu sledovania?",
|
||||||
"New password": "Nové heslo",
|
"New password": "Nové heslo",
|
||||||
"New passwords must match": "Nové heslá sa musia zhodovať",
|
"New passwords must match": "Nové heslá sa musia zhodovať",
|
||||||
"Authorize token?": "Autorizovať token?",
|
"Authorize token?": "Autorizovať token?",
|
||||||
@ -99,23 +99,5 @@
|
|||||||
"generic_subscriptions_count_1": "{{count}} odbery",
|
"generic_subscriptions_count_1": "{{count}} odbery",
|
||||||
"generic_subscriptions_count_2": "{{count}} odberov",
|
"generic_subscriptions_count_2": "{{count}} odberov",
|
||||||
"Authorize token for `x`?": "Autorizovať token pre `x`?",
|
"Authorize token for `x`?": "Autorizovať token pre `x`?",
|
||||||
"View playlist on YouTube": "Zobraziť playlist na YouTube",
|
"View playlist on YouTube": "Zobraziť playlist na YouTube"
|
||||||
"preferences_quality_dash_option_best": "Najlepšia",
|
|
||||||
"preferences_quality_dash_option_worst": "Najhoršia",
|
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
|
||||||
"preferences_quality_option_hd720": "HD720",
|
|
||||||
"preferences_quality_dash_label": "Preferovaná video kvalita DASH: ",
|
|
||||||
"preferences_quality_option_dash": "DASH (adaptívna kvalita)",
|
|
||||||
"preferences_quality_option_small": "Malá",
|
|
||||||
"preferences_watch_history_label": "Zapnúť históriu pozerania: ",
|
|
||||||
"preferences_quality_dash_option_240p": "240p",
|
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
|
||||||
"preferences_quality_dash_option_auto": "Auto",
|
|
||||||
"preferences_quality_dash_option_144p": "144p",
|
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
|
||||||
"invidious": "Invidious",
|
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
|
||||||
"preferences_quality_dash_option_360p": "360p"
|
|
||||||
}
|
}
|
||||||
|
@ -222,7 +222,7 @@
|
|||||||
"search_filters_date_option_week": "Ta teden",
|
"search_filters_date_option_week": "Ta teden",
|
||||||
"search_filters_type_label": "Vrsta",
|
"search_filters_type_label": "Vrsta",
|
||||||
"search_filters_type_option_all": "Katerakoli vrsta",
|
"search_filters_type_option_all": "Katerakoli vrsta",
|
||||||
"search_filters_type_option_playlist": "Seznam predvajanja",
|
"search_filters_type_option_playlist": "Seznami predvajanja",
|
||||||
"search_filters_features_option_subtitles": "Podnapisi/CC",
|
"search_filters_features_option_subtitles": "Podnapisi/CC",
|
||||||
"search_filters_features_option_location": "Lokacija",
|
"search_filters_features_option_location": "Lokacija",
|
||||||
"footer_donate_page": "Prispevaj",
|
"footer_donate_page": "Prispevaj",
|
||||||
@ -508,17 +508,5 @@
|
|||||||
"Standard YouTube license": "Standardna licenca YouTube",
|
"Standard YouTube license": "Standardna licenca YouTube",
|
||||||
"Channel Sponsor": "Sponzor kanala",
|
"Channel Sponsor": "Sponzor kanala",
|
||||||
"Download is disabled": "Prenos je onemogočen",
|
"Download is disabled": "Prenos je onemogočen",
|
||||||
"Import YouTube playlist (.csv)": "Uvoz seznama predvajanja YouTube (.csv)",
|
"Import YouTube playlist (.csv)": "Uvoz seznama predvajanja YouTube (.csv)"
|
||||||
"generic_button_delete": "Izbriši",
|
|
||||||
"generic_button_edit": "Uredi",
|
|
||||||
"generic_button_save": "Shrani",
|
|
||||||
"generic_button_cancel": "Prekliči",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"playlist_button_add_items": "Dodaj videoposnetke",
|
|
||||||
"channel_tab_podcasts_label": "Poddaje",
|
|
||||||
"channel_tab_releases_label": "Izdaje",
|
|
||||||
"generic_channels_count_0": "{{count}} kanal",
|
|
||||||
"generic_channels_count_1": "{{count}} kanala",
|
|
||||||
"generic_channels_count_2": "{{count}} kanali",
|
|
||||||
"generic_channels_count_3": "{{count}} kanalov"
|
|
||||||
}
|
}
|
||||||
|
@ -257,7 +257,7 @@
|
|||||||
"Video mode": "Mënyrë video",
|
"Video mode": "Mënyrë video",
|
||||||
"channel_tab_videos_label": "Video",
|
"channel_tab_videos_label": "Video",
|
||||||
"search_filters_sort_option_rating": "Vlerësim",
|
"search_filters_sort_option_rating": "Vlerësim",
|
||||||
"search_filters_sort_option_date": "Datë Ngarkimi",
|
"search_filters_sort_option_date": "Datë ngarkimi",
|
||||||
"search_filters_sort_option_views": "Numër parjesh",
|
"search_filters_sort_option_views": "Numër parjesh",
|
||||||
"search_filters_type_label": "Lloj",
|
"search_filters_type_label": "Lloj",
|
||||||
"search_filters_duration_label": "Kohëzgjatje",
|
"search_filters_duration_label": "Kohëzgjatje",
|
||||||
@ -345,7 +345,7 @@
|
|||||||
"View YouTube comments": "Shihni komente Youtube",
|
"View YouTube comments": "Shihni komente Youtube",
|
||||||
"View more comments on Reddit": "Shihni më tepër komente në Reddit",
|
"View more comments on Reddit": "Shihni më tepër komente në Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Shihni `x` koment",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Shihni `x` komente",
|
||||||
"": "Shihni `x` komente"
|
"": "Shihni `x` komente"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Shihni komente Reddit",
|
"View Reddit comments": "Shihni komente Reddit",
|
||||||
@ -462,20 +462,5 @@
|
|||||||
"channel_tab_channels_label": "Kanale",
|
"channel_tab_channels_label": "Kanale",
|
||||||
"Music in this video": "Muzikë në këtë video",
|
"Music in this video": "Muzikë në këtë video",
|
||||||
"channel_tab_shorts_label": "Të shkurtra",
|
"channel_tab_shorts_label": "Të shkurtra",
|
||||||
"channel_tab_streams_label": "Transmetime të drejtpërdrejta",
|
"channel_tab_streams_label": "Transmetime të drejtpërdrejta"
|
||||||
"generic_button_cancel": "Anuloje",
|
|
||||||
"generic_channels_count": "{{count}} kanal",
|
|
||||||
"generic_channels_count_plural": "{{count}} kanale",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"generic_button_delete": "Fshije",
|
|
||||||
"generic_button_save": "Ruaje",
|
|
||||||
"generic_button_edit": "Përpunoni",
|
|
||||||
"playlist_button_add_items": "Shtoni video",
|
|
||||||
"Report statistics: ": "Statistika raportimesh: ",
|
|
||||||
"Download is disabled": "Shkarkimi është i çaktivizuar",
|
|
||||||
"Channel Sponsor": "Sponsor Kanali",
|
|
||||||
"channel_tab_releases_label": "Hedhje në qarkullim",
|
|
||||||
"Song: ": "Pjesë: ",
|
|
||||||
"Import YouTube playlist (.csv)": "Importoni luajlistë YouTube (.csv)",
|
|
||||||
"Standard YouTube license": "Licencë YouTube standarde"
|
|
||||||
}
|
}
|
||||||
|
465
locales/sr.json
465
locales/sr.json
@ -1,90 +1,90 @@
|
|||||||
{
|
{
|
||||||
"LIVE": "UŽIVO",
|
"LIVE": "UŽIVO",
|
||||||
"Shared `x` ago": "Deljeno pre `x`",
|
"Shared `x` ago": "Podeljeno pre `x`",
|
||||||
"Unsubscribe": "Prekini praćenje",
|
"Unsubscribe": "Prekini praćenje",
|
||||||
"Subscribe": "Zaprati",
|
"Subscribe": "Prati",
|
||||||
"View channel on YouTube": "Pogledaj kanal na YouTube-u",
|
"View channel on YouTube": "Pogledaj kanal na YouTube-u",
|
||||||
"View playlist on YouTube": "Pogledaj plejlistu na YouTube-u",
|
"View playlist on YouTube": "Pogledaj spisak izvođenja na YouTube-u",
|
||||||
"newest": "najnovije",
|
"newest": "najnovije",
|
||||||
"oldest": "najstarije",
|
"oldest": "najstarije",
|
||||||
"popular": "popularno",
|
"popular": "popularno",
|
||||||
"last": "poslednje",
|
"last": "poslednje",
|
||||||
"Next page": "Sledeća stranica",
|
"Next page": "Sledeća stranica",
|
||||||
"Previous page": "Prethodna stranica",
|
"Previous page": "Prethodna stranica",
|
||||||
"Clear watch history?": "Očistiti istoriju gledanja?",
|
"Clear watch history?": "Izbrisati povest pregledanja?",
|
||||||
"New password": "Nova lozinka",
|
"New password": "Nova lozinka",
|
||||||
"New passwords must match": "Nove lozinke moraju da se podudaraju",
|
"New passwords must match": "Nove lozinke moraju biti istovetne",
|
||||||
"Authorize token?": "Autorizovati token?",
|
"Authorize token?": "Ovlasti žeton?",
|
||||||
"Authorize token for `x`?": "Autorizovati token za `x`?",
|
"Authorize token for `x`?": "Ovlasti žeton za `x`?",
|
||||||
"Yes": "Da",
|
"Yes": "Da",
|
||||||
"No": "Ne",
|
"No": "Ne",
|
||||||
"Import and Export Data": "Uvoz i izvoz podataka",
|
"Import and Export Data": "Uvoz i Izvoz Podataka",
|
||||||
"Import": "Uvezi",
|
"Import": "Uvezi",
|
||||||
"Import Invidious data": "Uvezi Invidious JSON podatke",
|
"Import Invidious data": "Uvezi podatke sa Invidious-a",
|
||||||
"Import YouTube subscriptions": "Uvezi YouTube/OPML praćenja",
|
"Import YouTube subscriptions": "Uvezi praćenja sa YouTube-a",
|
||||||
"Import FreeTube subscriptions (.db)": "Uvezi FreeTube praćenja (.db)",
|
"Import FreeTube subscriptions (.db)": "Uvezi praćenja sa FreeTube-a (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Uvezi NewPipe praćenja (.json)",
|
"Import NewPipe subscriptions (.json)": "Uvezi praćenja sa NewPipe-a (.json)",
|
||||||
"Import NewPipe data (.zip)": "Uvezi NewPipe podatke (.zip)",
|
"Import NewPipe data (.zip)": "Uvezi podatke sa NewPipe-a (.zip)",
|
||||||
"Export": "Izvezi",
|
"Export": "Izvezi",
|
||||||
"Export subscriptions as OPML": "Izvezi praćenja kao OPML",
|
"Export subscriptions as OPML": "Izvezi praćenja kao OPML datoteku",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Izvezi praćenja kao OPML (za NewPipe i FreeTube)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Izvezi praćenja kao OPML datoteku (za NewPipe i FreeTube)",
|
||||||
"Export data as JSON": "Izvezi Invidious podatke kao JSON",
|
"Export data as JSON": "Izvezi podatke kao JSON datoteku",
|
||||||
"Delete account?": "Izbrisati nalog?",
|
"Delete account?": "Izbrišite nalog?",
|
||||||
"History": "Istorija",
|
"History": "Istorija",
|
||||||
"An alternative front-end to YouTube": "Alternativni front-end za YouTube",
|
"An alternative front-end to YouTube": "Zamenski korisnički sloj za YouTube",
|
||||||
"JavaScript license information": "Informacije o JavaScript licenci",
|
"JavaScript license information": "Izveštaj o JavaScript odobrenju",
|
||||||
"source": "izvor",
|
"source": "izvor",
|
||||||
"Log in": "Prijava",
|
"Log in": "Prijavi se",
|
||||||
"Log in/register": "Prijava/registracija",
|
"Log in/register": "Prijavi se/Otvori nalog",
|
||||||
"User ID": "ID korisnika",
|
"User ID": "Korisnički ID",
|
||||||
"Password": "Lozinka",
|
"Password": "Lozinka",
|
||||||
"Time (h:mm:ss):": "Vreme (č:mm:ss):",
|
"Time (h:mm:ss):": "Vreme (č:mm:ss):",
|
||||||
"Text CAPTCHA": "Tekst CAPTCHA",
|
"Text CAPTCHA": "Znakovni CAPTCHA",
|
||||||
"Image CAPTCHA": "Slika CAPTCHA",
|
"Image CAPTCHA": "Slikovni CAPTCHA",
|
||||||
"Sign In": "Prijava",
|
"Sign In": "Prijava",
|
||||||
"Register": "Registracija",
|
"Register": "Otvori nalog",
|
||||||
"E-mail": "Imejl",
|
"E-mail": "E-pošta",
|
||||||
"Preferences": "Podešavanja",
|
"Preferences": "Podešavanja",
|
||||||
"preferences_category_player": "Podešavanja plejera",
|
"preferences_category_player": "Podešavanja reproduktora",
|
||||||
"preferences_video_loop_label": "Uvek ponavljaj: ",
|
"preferences_video_loop_label": "Uvek ponavljaj: ",
|
||||||
"preferences_autoplay_label": "Automatski pusti: ",
|
"preferences_autoplay_label": "Samopuštanje: ",
|
||||||
"preferences_continue_label": "Podrazumevano pusti sledeće: ",
|
"preferences_continue_label": "Uvek podrazumevano puštaj sledeće: ",
|
||||||
"preferences_continue_autoplay_label": "Automatski pusti sledeći video snimak: ",
|
"preferences_continue_autoplay_label": "Samopuštanje sledećeg video zapisa: ",
|
||||||
"preferences_listen_label": "Podrazumevano uključi samo zvuk: ",
|
"preferences_listen_label": "Uvek podrazumevano uključen samo zvuk: ",
|
||||||
"preferences_local_label": "Proksi video snimci: ",
|
"preferences_local_label": "Prikaz video zapisa preko posrednika: ",
|
||||||
"Playlist privacy": "Privatnost plejliste",
|
"Playlist privacy": "Podešavanja privatnosti plej liste",
|
||||||
"Editing playlist `x`": "Izmenjivanje plejliste `x`",
|
"Editing playlist `x`": "Izmena plej liste `x`",
|
||||||
"Playlist does not exist.": "Plejlista ne postoji.",
|
"Playlist does not exist.": "Nepostojeća plej lista.",
|
||||||
"Erroneous challenge": "Pogrešan izazov",
|
"Erroneous challenge": "Pogrešan izazov",
|
||||||
"Maltese": "Malteški",
|
"Maltese": "Malteški",
|
||||||
"Download": "Preuzmi",
|
"Download": "Preuzmi",
|
||||||
"Download as: ": "Preuzeti kao: ",
|
"Download as: ": "Preuzmi kao: ",
|
||||||
"Bangla": "Bengalski",
|
"Bangla": "Bangla/Bengalski",
|
||||||
"preferences_quality_dash_label": "Preferirani DASH kvalitet video snimka: ",
|
"preferences_quality_dash_label": "Preferirani kvalitet DASH video formata: ",
|
||||||
"Token manager": "Upravljanje tokenima",
|
"Token manager": "Upravljanje žetonima",
|
||||||
"Token": "Token",
|
"Token": "Žeton",
|
||||||
"Import/export": "Uvoz/izvoz",
|
"Import/export": "Uvezi/Izvezi",
|
||||||
"revoke": "opozovi",
|
"revoke": "opozovi",
|
||||||
"search": "pretraga",
|
"search": "pretraga",
|
||||||
"Log out": "Odjava",
|
"Log out": "Odjava",
|
||||||
"Source available here.": "Izvorni kôd je dostupan ovde.",
|
"Source available here.": "Izvorna koda je ovde dostupna.",
|
||||||
"Trending": "U trendu",
|
"Trending": "U trendu",
|
||||||
"Updated `x` ago": "Ažurirano pre `x`",
|
"Updated `x` ago": "Ažurirano pre `x`",
|
||||||
"Delete playlist `x`?": "Izbrisati plejlistu `x`?",
|
"Delete playlist `x`?": "Obriši plej listu `x`?",
|
||||||
"Create playlist": "Napravi plejlistu",
|
"Create playlist": "Napravi plej listu",
|
||||||
"Show less": "Prikaži manje",
|
"Show less": "Prikaži manje",
|
||||||
"Switch Invidious Instance": "Promeni Invidious instancu",
|
"Switch Invidious Instance": "Promeni Invidious instancu",
|
||||||
"Hide annotations": "Sakrij napomene",
|
"Hide annotations": "Sakrij napomene",
|
||||||
"User ID is a required field": "ID korisnika je obavezno polje",
|
"User ID is a required field": "Korisnički ID je obavezno polje",
|
||||||
"Wrong username or password": "Pogrešno korisničko ime ili lozinka",
|
"Wrong username or password": "Pogrešno korisničko ime ili lozinka",
|
||||||
"Please log in": "Molimo, prijavite se",
|
"Please log in": "Molimo vas da se prijavite",
|
||||||
"channel:`x`": "kanal:`x`",
|
"channel:`x`": "kanal:`x`",
|
||||||
"Could not fetch comments": "Nije moguće prikupiti komentare",
|
"Could not fetch comments": "Uzimanje komentara nije uspelo",
|
||||||
"Could not create mix.": "Nije moguće napraviti miks.",
|
"Could not create mix.": "Pravljenje miksa nije uspelo.",
|
||||||
"Empty playlist": "Prazna plejlista",
|
"Empty playlist": "Prazna plej lista",
|
||||||
"Not a playlist.": "Nije plejlista.",
|
"Not a playlist.": "Nije plej lista.",
|
||||||
"Could not pull trending pages.": "Nije moguće povući stranice „U trendu“.",
|
"Could not pull trending pages.": "Učitavanje 'U toku' stranica nije uspelo.",
|
||||||
"Token is expired, please try again": "Token je istekao, pokušajte ponovo",
|
"Token is expired, please try again": "Žeton je istekao, molimo vas da pokušate ponovo",
|
||||||
"English (auto-generated)": "Engleski (automatski generisano)",
|
"English (auto-generated)": "Engleski (automatski generisano)",
|
||||||
"Afrikaans": "Afrikans",
|
"Afrikaans": "Afrikans",
|
||||||
"Albanian": "Albanski",
|
"Albanian": "Albanski",
|
||||||
@ -95,19 +95,19 @@
|
|||||||
"Bulgarian": "Bugarski",
|
"Bulgarian": "Bugarski",
|
||||||
"Burmese": "Burmanski",
|
"Burmese": "Burmanski",
|
||||||
"Catalan": "Katalonski",
|
"Catalan": "Katalonski",
|
||||||
"Cebuano": "Cebuanski",
|
"Cebuano": "Sebuano",
|
||||||
"Chinese (Traditional)": "Kineski (Tradicionalni)",
|
"Chinese (Traditional)": "Kineski (Tradicionalni)",
|
||||||
"Corsican": "Korzikanski",
|
"Corsican": "Korzikanski",
|
||||||
"Danish": "Danski",
|
"Danish": "Danski",
|
||||||
"Kannada": "Kanada",
|
"Kannada": "Kanada (Jezik)",
|
||||||
"Kazakh": "Kazaški",
|
"Kazakh": "Kazaški",
|
||||||
"Russian": "Ruski",
|
"Russian": "Ruski",
|
||||||
"Scottish Gaelic": "Škotski Gelski",
|
"Scottish Gaelic": "Škotski Gelski",
|
||||||
"Sinhala": "Sinhalski",
|
"Sinhala": "Sinhaleški",
|
||||||
"Slovak": "Slovački",
|
"Slovak": "Slovački",
|
||||||
"Spanish": "Španski",
|
"Spanish": "Španski",
|
||||||
"Spanish (Latin America)": "Španski (Latinska Amerika)",
|
"Spanish (Latin America)": "Španski (Južna Amerika)",
|
||||||
"Sundanese": "Sundanski",
|
"Sundanese": "Sundski",
|
||||||
"Swedish": "Švedski",
|
"Swedish": "Švedski",
|
||||||
"Tajik": "Tadžički",
|
"Tajik": "Tadžički",
|
||||||
"Telugu": "Telugu",
|
"Telugu": "Telugu",
|
||||||
@ -116,77 +116,77 @@
|
|||||||
"Urdu": "Urdu",
|
"Urdu": "Urdu",
|
||||||
"Uzbek": "Uzbečki",
|
"Uzbek": "Uzbečki",
|
||||||
"Vietnamese": "Vijetnamski",
|
"Vietnamese": "Vijetnamski",
|
||||||
"Rating: ": "Ocena: ",
|
"Rating: ": "Ocena/e: ",
|
||||||
"View as playlist": "Pogledaj kao plejlistu",
|
"View as playlist": "Pogledaj kao plej listu",
|
||||||
"Default": "Podrazumevano",
|
"Default": "Podrazumevan/o",
|
||||||
"Gaming": "Video igre",
|
"Gaming": "Igrice",
|
||||||
"Movies": "Filmovi",
|
"Movies": "Filmovi",
|
||||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
"(edited)": "(izmenjeno)",
|
"(edited)": "(izmenjeno)",
|
||||||
"YouTube comment permalink": "Trajni link YouTube komentara",
|
"YouTube comment permalink": "YouTube komentar trajna veza",
|
||||||
"Audio mode": "Režim audio snimka",
|
"Audio mode": "Audio mod",
|
||||||
"Playlists": "Plejliste",
|
"Playlists": "Plej liste",
|
||||||
"search_filters_sort_option_relevance": "Relevantnost",
|
"search_filters_sort_option_relevance": "Relevantnost",
|
||||||
"search_filters_sort_option_rating": "Ocena",
|
"search_filters_sort_option_rating": "Ocene",
|
||||||
"search_filters_sort_option_date": "Datum otpremanja",
|
"search_filters_sort_option_date": "Datum otpremanja",
|
||||||
"search_filters_sort_option_views": "Broj pregleda",
|
"search_filters_sort_option_views": "Broj pregleda",
|
||||||
"`x` marked it with a ❤": "`x` je označio/la sa ❤",
|
"`x` marked it with a ❤": "`x` je označio/la ovo sa ❤",
|
||||||
"search_filters_duration_label": "Trajanje",
|
"search_filters_duration_label": "Trajanje",
|
||||||
"search_filters_features_label": "Karakteristike",
|
"search_filters_features_label": "Karakteristike",
|
||||||
"search_filters_date_option_hour": "Poslednji sat",
|
"search_filters_date_option_hour": "Poslednji sat",
|
||||||
"search_filters_date_option_week": "Ove nedelje",
|
"search_filters_date_option_week": "Ove sedmice",
|
||||||
"search_filters_date_option_month": "Ovog meseca",
|
"search_filters_date_option_month": "Ovaj mesec",
|
||||||
"search_filters_date_option_year": "Ove godine",
|
"search_filters_date_option_year": "Ove godine",
|
||||||
"search_filters_type_option_video": "Video snimak",
|
"search_filters_type_option_video": "Video",
|
||||||
"search_filters_type_option_playlist": "Plejlista",
|
"search_filters_type_option_playlist": "Plej lista",
|
||||||
"search_filters_type_option_movie": "Film",
|
"search_filters_type_option_movie": "Film",
|
||||||
"search_filters_duration_option_long": "Dugo (> 20 minuta)",
|
"search_filters_duration_option_long": "Dugo (> 20 minuta)",
|
||||||
"search_filters_features_option_hd": "HD",
|
"search_filters_features_option_hd": "HD",
|
||||||
"search_filters_features_option_c_commons": "Creative Commons",
|
"search_filters_features_option_c_commons": "Creative Commons (Licenca)",
|
||||||
"search_filters_features_option_three_d": "3D",
|
"search_filters_features_option_three_d": "3D",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "Video Visoke Rezolucije",
|
||||||
"next_steps_error_message": "Nakon toga treba da pokušate da: ",
|
"next_steps_error_message": "Nakon čega bi trebali probati: ",
|
||||||
"next_steps_error_message_go_to_youtube": "Odete na YouTube",
|
"next_steps_error_message_go_to_youtube": "Idi na YouTube",
|
||||||
"footer_documentation": "Dokumentacija",
|
"footer_documentation": "Dokumentacija",
|
||||||
"preferences_region_label": "Država sadržaja: ",
|
"preferences_region_label": "Država porekla sadržaja: ",
|
||||||
"preferences_player_style_label": "Stil plejera: ",
|
"preferences_player_style_label": "Stil plejera: ",
|
||||||
"preferences_dark_mode_label": "Tema: ",
|
"preferences_dark_mode_label": "Izgled/Tema: ",
|
||||||
"light": "svetla",
|
"light": "svetlo",
|
||||||
"preferences_thin_mode_label": "Kompaktni režim: ",
|
"preferences_thin_mode_label": "Kompaktni režim: ",
|
||||||
"preferences_category_misc": "Ostala podešavanja",
|
"preferences_category_misc": "Ostala podešavanja",
|
||||||
"preferences_automatic_instance_redirect_label": "Automatsko preusmeravanje instance (povratak na redirect.invidious.io): ",
|
"preferences_automatic_instance_redirect_label": "Automatsko prebacivanje na drugu instancu u slučaju otkazivanja (preči će nazad na redirect.invidious.io): ",
|
||||||
"alphabetically - reverse": "abecedno - obrnuto",
|
"alphabetically - reverse": "po alfabetu - obrnuto",
|
||||||
"Enable web notifications": "Omogući veb obaveštenja",
|
"Enable web notifications": "Omogući obaveštenja u veb pretraživaču",
|
||||||
"`x` is live": "`x` je uživo",
|
"`x` is live": "`x` prenosi uživo",
|
||||||
"Manage tokens": "Upravljaj tokenima",
|
"Manage tokens": "Upravljaj žetonima",
|
||||||
"Watch history": "Istorija gledanja",
|
"Watch history": "Istorija gledanja",
|
||||||
"preferences_feed_menu_label": "Fid meni: ",
|
"preferences_feed_menu_label": "Dovodna stranica: ",
|
||||||
"preferences_show_nick_label": "Prikaži nadimke na vrhu: ",
|
"preferences_show_nick_label": "Prikaži nadimke na vrhu: ",
|
||||||
"CAPTCHA enabled: ": "CAPTCHA omogućena: ",
|
"CAPTCHA enabled: ": "CAPTCHA omogućena: ",
|
||||||
"Registration enabled: ": "Registracija omogućena: ",
|
"Registration enabled: ": "Registracija omogućena: ",
|
||||||
"Subscription manager": "Upravljanje praćenjima",
|
"Subscription manager": "Upravljanje praćenjima",
|
||||||
"Wilson score: ": "Vilsonova ocena: ",
|
"Wilson score: ": "Wilsonova ocena: ",
|
||||||
"Engagement: ": "Angažovanje: ",
|
"Engagement: ": "Angažovanje: ",
|
||||||
"Whitelisted regions: ": "Dostupni regioni: ",
|
"Whitelisted regions: ": "Dozvoljene oblasti: ",
|
||||||
"Shared `x`": "Deljeno `x`",
|
"Shared `x`": "Podeljeno `x`",
|
||||||
"Premieres in `x`": "Premijera u `x`",
|
"Premieres in `x`": "Premera u `x`",
|
||||||
"Premieres `x`": "Premijera `x`",
|
"Premieres `x`": "Premere u `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.": "Hej! Izgleda da ste isključili JavaScript. Kliknite ovde da biste videli komentare, imajte na umu da će možda potrajati malo duže da se učitaju.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hej! Izgleda da ste onemogućili JavaScript. Kliknite ovde da vidite komentare, čuvajte na umu da ovo može da potraje duže dok se ne učitaju.",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Pogledaj `x` komentar",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Prikaži `x` komentar",
|
||||||
"": "Pogledaj`x` komentare"
|
"": "Prikaži `x` komentara"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Pogledaj Reddit komentare",
|
"View Reddit comments": "Prikaži Reddit komentare",
|
||||||
"CAPTCHA is a required field": "CAPTCHA je obavezno polje",
|
"CAPTCHA is a required field": "CAPTCHA je obavezno polje",
|
||||||
"Croatian": "Hrvatski",
|
"Croatian": "Hrvatski",
|
||||||
"Estonian": "Estonski",
|
"Estonian": "Estonski",
|
||||||
"Filipino": "Filipinski",
|
"Filipino": "Filipino",
|
||||||
"French": "Francuski",
|
"French": "Francuski",
|
||||||
"Galician": "Galicijski",
|
"Galician": "Galicijski",
|
||||||
"German": "Nemački",
|
"German": "Nemački",
|
||||||
"Greek": "Grčki",
|
"Greek": "Grčki",
|
||||||
"Hausa": "Hausa",
|
"Hausa": "Hausa",
|
||||||
"Italian": "Italijanski",
|
"Italian": "Talijanski",
|
||||||
"Khmer": "Kmerski",
|
"Khmer": "Kmerski",
|
||||||
"Kurdish": "Kurdski",
|
"Kurdish": "Kurdski",
|
||||||
"Kyrgyz": "Kirgiski",
|
"Kyrgyz": "Kirgiski",
|
||||||
@ -195,68 +195,68 @@
|
|||||||
"Macedonian": "Makedonski",
|
"Macedonian": "Makedonski",
|
||||||
"Malagasy": "Malgaški",
|
"Malagasy": "Malgaški",
|
||||||
"Malay": "Malajski",
|
"Malay": "Malajski",
|
||||||
"Marathi": "Maratski",
|
"Marathi": "Marathi",
|
||||||
"Mongolian": "Mongolski",
|
"Mongolian": "Mongolski",
|
||||||
"Norwegian Bokmål": "Norveški Bokmal",
|
"Norwegian Bokmål": "Norveški Bokmal",
|
||||||
"Nyanja": "Nijandža",
|
"Nyanja": "Čeva",
|
||||||
"Pashto": "Paštunski",
|
"Pashto": "Paštunski",
|
||||||
"Persian": "Persijski",
|
"Persian": "Persijski",
|
||||||
"Punjabi": "Pandžapski",
|
"Punjabi": "Pundžabi",
|
||||||
"Romanian": "Rumunski",
|
"Romanian": "Rumunski",
|
||||||
"Welsh": "Velški",
|
"Welsh": "Velški",
|
||||||
"Western Frisian": "Zapadnofrizijski",
|
"Western Frisian": "Zapadnofrizijski",
|
||||||
"Fallback comments: ": "Rezervni komentari: ",
|
"Fallback comments: ": "Komentari u slučaju otkazivanja: ",
|
||||||
"Popular": "Popularno",
|
"Popular": "Popularno",
|
||||||
"Search": "Pretraga",
|
"Search": "Pretraga",
|
||||||
"About": "O sajtu",
|
"About": "O programu",
|
||||||
"footer_source_code": "Izvorni kôd",
|
"footer_source_code": "Izvorna Koda",
|
||||||
"footer_original_source_code": "Originalni izvorni kôd",
|
"footer_original_source_code": "Originalna Izvorna Koda",
|
||||||
"preferences_related_videos_label": "Prikaži povezane video snimke: ",
|
"preferences_related_videos_label": "Prikaži slične video klipove: ",
|
||||||
"preferences_annotations_label": "Podrazumevano prikaži napomene: ",
|
"preferences_annotations_label": "Prikaži napomene podrazumevano: ",
|
||||||
"preferences_extend_desc_label": "Automatski proširi opis video snimka: ",
|
"preferences_extend_desc_label": "Automatski prikaži ceo opis videa: ",
|
||||||
"preferences_vr_mode_label": "Interaktivni video snimci od 360 stepeni (zahteva WebGl): ",
|
"preferences_vr_mode_label": "Interaktivni video klipovi u 360 stepeni: ",
|
||||||
"preferences_category_visual": "Vizuelna podešavanja",
|
"preferences_category_visual": "Vizuelne preference",
|
||||||
"preferences_captions_label": "Podrazumevani titlovi: ",
|
"preferences_captions_label": "Podrazumevani titl: ",
|
||||||
"Music": "Muzika",
|
"Music": "Muzika",
|
||||||
"search_filters_type_label": "Vrsta",
|
"search_filters_type_label": "Tip",
|
||||||
"Tamil": "Tamilski",
|
"Tamil": "Tamilski",
|
||||||
"Save preferences": "Sačuvaj podešavanja",
|
"Save preferences": "Sačuvaj podešavanja",
|
||||||
"Only show latest unwatched video from channel: ": "Prikaži samo najnoviji neodgledani video snimak sa kanala: ",
|
"Only show latest unwatched video from channel: ": "Prikaži samo poslednje video klipove koji nisu pogledani sa kanala: ",
|
||||||
"Xhosa": "Kosa (Khosa)",
|
"Xhosa": "Kosa (Jezik)",
|
||||||
"search_filters_type_option_channel": "Kanal",
|
"search_filters_type_option_channel": "Kanal",
|
||||||
"Hungarian": "Mađarski",
|
"Hungarian": "Mađarski",
|
||||||
"Maori": "Maorski",
|
"Maori": "Maori (Jezik)",
|
||||||
"Manage subscriptions": "Upravljaj praćenjima",
|
"Manage subscriptions": "Upravljaj zapisima",
|
||||||
"Hindi": "Hindi",
|
"Hindi": "Hindi",
|
||||||
"`x` ago": "pre `x`",
|
"`x` ago": "pre `x`",
|
||||||
"Import/export data": "Uvezi/Izvezi podatke",
|
"Import/export data": "Uvezi/Izvezi podatke",
|
||||||
"`x` uploaded a video": "`x` je otpremio/la video snimak",
|
"`x` uploaded a video": "`x` je otpremio/la video klip",
|
||||||
"Delete account": "Izbriši nalog",
|
"Delete account": "Obriši nalog",
|
||||||
"preferences_default_home_label": "Podrazumevana početna stranica: ",
|
"preferences_default_home_label": "Podrazumevana početna stranica: ",
|
||||||
"Serbian": "Srpski",
|
"Serbian": "Srpski",
|
||||||
"License: ": "Licenca: ",
|
"License: ": "Licenca: ",
|
||||||
"search_filters_features_option_live": "Uživo",
|
"search_filters_features_option_live": "Uživo",
|
||||||
"Report statistics: ": "Izveštavaj statistike: ",
|
"Report statistics: ": "Izveštavaj o statistici: ",
|
||||||
"Only show latest video from channel: ": "Prikaži samo najnoviji video snimak sa kanala: ",
|
"Only show latest video from channel: ": "Prikazuj poslednje video klipove samo sa kanala: ",
|
||||||
"channel name - reverse": "ime kanala - obrnuto",
|
"channel name - reverse": "ime kanala - obrnuto",
|
||||||
"Could not get channel info.": "Nije moguće prikupiti informacije o kanalu.",
|
"Could not get channel info.": "Uzimanje podataka o kanalu nije uspelo.",
|
||||||
"View privacy policy.": "Pogledaj politiku privatnosti.",
|
"View privacy policy.": "Pogledaj izveštaj o privatnosti.",
|
||||||
"Change password": "Promeni lozinku",
|
"Change password": "Promeni lozinku",
|
||||||
"Malayalam": "Malajalamski",
|
"Malayalam": "Malajalam",
|
||||||
"View more comments on Reddit": "Pogledaj više komentara na Reddit-u",
|
"View more comments on Reddit": "Prikaži više komentara na Reddit-u",
|
||||||
"Portuguese": "Portugalski",
|
"Portuguese": "Portugalski",
|
||||||
"View YouTube comments": "Pogledaj YouTube komentare",
|
"View YouTube comments": "Prikaži YouTube komentare",
|
||||||
"published - reverse": "objavljeno - obrnuto",
|
"published - reverse": "objavljeno - obrnuto",
|
||||||
"Dutch": "Holandski",
|
"Dutch": "Holandski",
|
||||||
"preferences_volume_label": "Jačina zvuka plejera: ",
|
"preferences_volume_label": "Jačina zvuka: ",
|
||||||
"preferences_locale_label": "Jezik: ",
|
"preferences_locale_label": "Jezik: ",
|
||||||
"adminprefs_modified_source_code_url_label": "URL adresa do repozitorijuma izmenjenog izvornog koda",
|
"adminprefs_modified_source_code_url_label": "URL veza do skladišta sa Izmenjenom Izvornom Kodom",
|
||||||
"channel_tab_community_label": "Zajednica",
|
"channel_tab_community_label": "Zajednica",
|
||||||
"Video mode": "Režim video snimka",
|
"Video mode": "Video mod",
|
||||||
"Fallback captions: ": "Rezervni titlovi: ",
|
"Fallback captions: ": "Titl u slučaju da glavni nije dostupan: ",
|
||||||
"Private": "Privatno",
|
"Private": "Privatno",
|
||||||
"alphabetically": "abecedno",
|
"alphabetically": "po alfabetu",
|
||||||
"No such user": "Ne postoji korisnik",
|
"No such user": "Nepostojeći korisnik",
|
||||||
"Subscriptions": "Praćenja",
|
"Subscriptions": "Praćenja",
|
||||||
"search_filters_date_option_today": "Danas",
|
"search_filters_date_option_today": "Danas",
|
||||||
"Finnish": "Finski",
|
"Finnish": "Finski",
|
||||||
@ -265,30 +265,30 @@
|
|||||||
"Shona": "Šona",
|
"Shona": "Šona",
|
||||||
"search_filters_features_option_location": "Lokacija",
|
"search_filters_features_option_location": "Lokacija",
|
||||||
"Load more": "Učitaj više",
|
"Load more": "Učitaj više",
|
||||||
"Released under the AGPLv3 on Github.": "Objavljeno pod licencom AGPLv3 na GitHub-u.",
|
"Released under the AGPLv3 on Github.": "Izbačeno pod licencom AGPLv3 na GitHub-u.",
|
||||||
"Slovenian": "Slovenački",
|
"Slovenian": "Slovenački",
|
||||||
"View JavaScript license information.": "Pogledaj informacije o JavaScript licenci.",
|
"View JavaScript license information.": "Pogledaj informacije licence vezane za JavaScript.",
|
||||||
"Chinese (Simplified)": "Kineski (Pojednostavljeni)",
|
"Chinese (Simplified)": "Kineski (Pojednostavljeni)",
|
||||||
"preferences_comments_label": "Podrazumevani komentari: ",
|
"preferences_comments_label": "Podrazumevani komentari: ",
|
||||||
"Incorrect password": "Netačna lozinka",
|
"Incorrect password": "Netačna lozinka",
|
||||||
"Show replies": "Prikaži odgovore",
|
"Show replies": "Prikaži odgovore",
|
||||||
"Invidious Private Feed for `x`": "Invidious privatni fid za `x`",
|
"Invidious Private Feed for `x`": "Invidious Privatni Dovod za `x`",
|
||||||
"Watch on YouTube": "Gledaj na YouTube-u",
|
"Watch on YouTube": "Gledaj na YouTube-u",
|
||||||
"Wrong answer": "Pogrešan odgovor",
|
"Wrong answer": "Pogrešan odgovor",
|
||||||
"preferences_quality_label": "Preferirani kvalitet video snimka: ",
|
"preferences_quality_label": "Preferirani video kvalitet: ",
|
||||||
"Hide replies": "Sakrij odgovore",
|
"Hide replies": "Sakrij odgovore",
|
||||||
"Erroneous CAPTCHA": "Pogrešna CAPTCHA",
|
"Erroneous CAPTCHA": "Pogrešna CAPTCHA",
|
||||||
"Erroneous token": "Pogrešan token",
|
"Erroneous token": "Pogrešan žeton",
|
||||||
"Czech": "Češki",
|
"Czech": "Češki",
|
||||||
"Latin": "Latinski",
|
"Latin": "Latinski",
|
||||||
"channel_tab_videos_label": "Video snimci",
|
"channel_tab_videos_label": "Video klipovi",
|
||||||
"search_filters_features_option_four_k": "4К",
|
"search_filters_features_option_four_k": "4К",
|
||||||
"footer_donate_page": "Doniraj",
|
"footer_donate_page": "Doniraj",
|
||||||
"English": "Engleski",
|
"English": "Engleski",
|
||||||
"Arabic": "Arapski",
|
"Arabic": "Arapski",
|
||||||
"Unlisted": "Po pozivu",
|
"Unlisted": "Nenavedeno",
|
||||||
"Hidden field \"challenge\" is a required field": "Skriveno polje „izazov“ je obavezno polje",
|
"Hidden field \"challenge\" is a required field": "Sakriveno \"challenge\" polje je obavezno",
|
||||||
"Hidden field \"token\" is a required field": "Skriveno polje „token“ je obavezno polje",
|
"Hidden field \"token\" is a required field": "Sakriveno \"token\" polje je obavezno",
|
||||||
"Georgian": "Gruzijski",
|
"Georgian": "Gruzijski",
|
||||||
"Hawaiian": "Havajski",
|
"Hawaiian": "Havajski",
|
||||||
"Hebrew": "Hebrejski",
|
"Hebrew": "Hebrejski",
|
||||||
@ -297,211 +297,68 @@
|
|||||||
"Japanese": "Japanski",
|
"Japanese": "Japanski",
|
||||||
"Javanese": "Javanski",
|
"Javanese": "Javanski",
|
||||||
"Sindhi": "Sindi",
|
"Sindhi": "Sindi",
|
||||||
"Swahili": "Suvali",
|
"Swahili": "Svahili",
|
||||||
"Yiddish": "Jidiš",
|
"Yiddish": "Jidiš",
|
||||||
"Zulu": "Zulu",
|
"Zulu": "Zulu",
|
||||||
"search_filters_features_option_subtitles": "Titlovi/Skriveni titlovi",
|
"search_filters_features_option_subtitles": "Titl/Prevod",
|
||||||
"Password cannot be longer than 55 characters": "Lozinka ne može biti duža od 55 znakova",
|
"Password cannot be longer than 55 characters": "Lozinka ne može biti duža od 55 karaktera",
|
||||||
"This channel does not exist.": "Ovaj kanal ne postoji.",
|
"This channel does not exist.": "Ovaj kanal ne postoji.",
|
||||||
"Belarusian": "Beloruski",
|
"Belarusian": "Beloruski",
|
||||||
"Gujarati": "Gudžarati",
|
"Gujarati": "Gudžarati",
|
||||||
"Haitian Creole": "Haićanski Kreolski",
|
"Haitian Creole": "Haićanski Kreolski",
|
||||||
"Somali": "Somalijski",
|
"Somali": "Somalijski",
|
||||||
"Top": "Top",
|
"Top": "Vrh",
|
||||||
"footer_modfied_source_code": "Izmenjeni izvorni kôd",
|
"footer_modfied_source_code": "Izmenjena Izvorna Koda",
|
||||||
"preferences_category_subscription": "Podešavanja praćenja",
|
"preferences_category_subscription": "Podešavanja praćenja",
|
||||||
"preferences_annotations_subscribed_label": "Podrazumevano prikazati napomene za kanale koje pratite? ",
|
"preferences_annotations_subscribed_label": "Podrazumevano prikazati napomene za kanale koje pratite? ",
|
||||||
"preferences_max_results_label": "Broj video snimaka prikazanih u fidu: ",
|
"preferences_max_results_label": "Broj video klipova prikazanih u dovodnoj listi: ",
|
||||||
"preferences_sort_label": "Sortiraj video snimke po: ",
|
"preferences_sort_label": "Sortiraj video klipove po: ",
|
||||||
"preferences_unseen_only_label": "Prikaži samo neodgledano: ",
|
"preferences_unseen_only_label": "Prikaži samo video klipove koji nisu pogledani: ",
|
||||||
"preferences_notifications_only_label": "Prikaži samo obaveštenja (ako ih ima): ",
|
"preferences_notifications_only_label": "Prikaži samo obaveštenja (ako ih uopšte ima): ",
|
||||||
"preferences_category_data": "Podešavanja podataka",
|
"preferences_category_data": "Podešavanja podataka",
|
||||||
"Clear watch history": "Očisti istoriju gledanja",
|
"Clear watch history": "Obriši istoriju gledanja",
|
||||||
"preferences_category_admin": "Podešavanja administratora",
|
"preferences_category_admin": "Administratorska podešavanja",
|
||||||
"published": "objavljeno",
|
"published": "objavljeno",
|
||||||
"search_filters_sort_label": "Sortiranje po",
|
"search_filters_sort_label": "Poredaj prema",
|
||||||
"search_filters_type_option_show": "Emisija",
|
"search_filters_type_option_show": "Emisija",
|
||||||
"search_filters_duration_option_short": "Kratko (< 4 minuta)",
|
"search_filters_duration_option_short": "Kratko (< 4 minute)",
|
||||||
"Current version: ": "Trenutna verzija: ",
|
"Current version: ": "Trenutna verzija: ",
|
||||||
"Top enabled: ": "Top omogućeno: ",
|
"Top enabled: ": "Vrh omogućen: ",
|
||||||
"Public": "Javno",
|
"Public": "Javno",
|
||||||
"Delete playlist": "Izbriši plejlistu",
|
"Delete playlist": "Obriši plej listu",
|
||||||
"Title": "Naslov",
|
"Title": "Naslov",
|
||||||
"Show annotations": "Prikaži napomene",
|
"Show annotations": "Prikaži napomene",
|
||||||
"Password cannot be empty": "Lozinka ne može biti prazna",
|
"Password cannot be empty": "Lozinka ne može biti prazna",
|
||||||
"Deleted or invalid channel": "Izbrisan ili nevažeći kanal",
|
"Deleted or invalid channel": "Obrisan ili nepostojeći kanal",
|
||||||
"Esperanto": "Esperanto",
|
"Esperanto": "Esperanto",
|
||||||
"Hmong": "Hmong",
|
"Hmong": "Hmong",
|
||||||
"Luxembourgish": "Luksemburški",
|
"Luxembourgish": "Luksemburški",
|
||||||
"Nepali": "Nepalski",
|
"Nepali": "Nepalski",
|
||||||
"Samoan": "Samoanski",
|
"Samoan": "Samoanski",
|
||||||
"News": "Vesti",
|
"News": "Vesti",
|
||||||
"permalink": "trajni link",
|
"permalink": "trajna veza",
|
||||||
"Password is a required field": "Lozinka je obavezno polje",
|
"Password is a required field": "Lozinka je obavezno polje",
|
||||||
"Amharic": "Amharski",
|
"Amharic": "Amharski",
|
||||||
"Indonesian": "Indonezijski",
|
"Indonesian": "Indonežanski",
|
||||||
"Irish": "Irski",
|
"Irish": "Irski",
|
||||||
"Korean": "Korejski",
|
"Korean": "Korejski",
|
||||||
"Southern Sotho": "Južni Soto",
|
"Southern Sotho": "Južni Soto",
|
||||||
"Thai": "Tajski",
|
"Thai": "Tajski",
|
||||||
"preferences_speed_label": "Podrazumevana brzina: ",
|
"preferences_speed_label": "Podrazumevana brzina: ",
|
||||||
"Dark mode: ": "Tamni režim: ",
|
"Dark mode: ": "Tamni režim: ",
|
||||||
"dark": "tamna",
|
"dark": "tamno",
|
||||||
"Redirect homepage to feed: ": "Preusmeri početnu stranicu na fid: ",
|
"Redirect homepage to feed: ": "Prebaci sa početne stranice na dovodnu listu: ",
|
||||||
"channel name": "ime kanala",
|
"channel name": "ime kanala",
|
||||||
"View all playlists": "Pogledaj sve plejliste",
|
"View all playlists": "Pregledaj sve plej liste",
|
||||||
"Show more": "Prikaži više",
|
"Show more": "Prikaži više",
|
||||||
"Genre: ": "Žanr: ",
|
"Genre: ": "Žanr: ",
|
||||||
"Family friendly? ": "Pogodno za porodicu? ",
|
"Family friendly? ": "Pogodno za porodicu? ",
|
||||||
"next_steps_error_message_refresh": "Osvežite",
|
"next_steps_error_message_refresh": "Osveži stranicu",
|
||||||
"youtube": "YouTube",
|
"youtube": "YouTube",
|
||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
"unsubscribe": "prekini praćenje",
|
"unsubscribe": "prekini sa praćenjem",
|
||||||
"Blacklisted regions: ": "Nedostupni regioni: ",
|
"Blacklisted regions: ": "Zabranjene oblasti: ",
|
||||||
"Polish": "Poljski",
|
"Polish": "Poljski",
|
||||||
"Yoruba": "Joruba",
|
"Yoruba": "Joruba",
|
||||||
"search_filters_title": "Filteri",
|
"search_filters_title": "Filter"
|
||||||
"Korean (auto-generated)": "Korejski (automatski generisano)",
|
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
|
||||||
"preferences_quality_dash_option_worst": "Najgore",
|
|
||||||
"channel_tab_podcasts_label": "Podkasti",
|
|
||||||
"preferences_save_player_pos_label": "Sačuvaj poziciju reprodukcije: ",
|
|
||||||
"Spanish (Mexico)": "Španski (Meksiko)",
|
|
||||||
"generic_subscriptions_count_0": "{{count}} praćenje",
|
|
||||||
"generic_subscriptions_count_1": "{{count}} praćenja",
|
|
||||||
"generic_subscriptions_count_2": "{{count}} praćenja",
|
|
||||||
"search_filters_apply_button": "Primeni izabrane filtere",
|
|
||||||
"Download is disabled": "Preuzimanje je onemogućeno",
|
|
||||||
"comments_points_count_0": "{{count}} poen",
|
|
||||||
"comments_points_count_1": "{{count}} poena",
|
|
||||||
"comments_points_count_2": "{{count}} poena",
|
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
|
||||||
"German (auto-generated)": "Nemački (automatski generisano)",
|
|
||||||
"Japanese (auto-generated)": "Japanski (automatski generisano)",
|
|
||||||
"preferences_quality_option_medium": "Srednje",
|
|
||||||
"search_message_change_filters_or_query": "Pokušajte da proširite upit za pretragu i/ili promenite filtere.",
|
|
||||||
"crash_page_before_reporting": "Pre nego što prijavite grešku, uverite se da ste:",
|
|
||||||
"preferences_quality_dash_option_best": "Najbolje",
|
|
||||||
"Channel Sponsor": "Sponzor kanala",
|
|
||||||
"generic_videos_count_0": "{{count}} video snimak",
|
|
||||||
"generic_videos_count_1": "{{count}} video snimka",
|
|
||||||
"generic_videos_count_2": "{{count}} video snimaka",
|
|
||||||
"videoinfo_started_streaming_x_ago": "Započeto strimovanje pre `x`",
|
|
||||||
"videoinfo_youTube_embed_link": "Ugrađeno",
|
|
||||||
"channel_tab_streams_label": "Strimovi uživo",
|
|
||||||
"playlist_button_add_items": "Dodaj video snimke",
|
|
||||||
"generic_count_minutes_0": "{{count}} minut",
|
|
||||||
"generic_count_minutes_1": "{{count}} minuta",
|
|
||||||
"generic_count_minutes_2": "{{count}} minuta",
|
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
|
||||||
"preferences_watch_history_label": "Omogući istoriju gledanja: ",
|
|
||||||
"user_saved_playlists": "Sačuvanih plejlista: `x`",
|
|
||||||
"Spanish (Spain)": "Španski (Španija)",
|
|
||||||
"invidious": "Invidious",
|
|
||||||
"crash_page_refresh": "pokušali da <a href=\"`x`\">osvežite stranicu</a>",
|
|
||||||
"Chinese (Hong Kong)": "Kineski (Hong Kong)",
|
|
||||||
"Artist: ": "Izvođač: ",
|
|
||||||
"generic_count_months_0": "{{count}} mesec",
|
|
||||||
"generic_count_months_1": "{{count}} meseca",
|
|
||||||
"generic_count_months_2": "{{count}} meseci",
|
|
||||||
"search_message_use_another_instance": " Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.",
|
|
||||||
"generic_subscribers_count_0": "{{count}} pratilac",
|
|
||||||
"generic_subscribers_count_1": "{{count}} pratioca",
|
|
||||||
"generic_subscribers_count_2": "{{count}} pratilaca",
|
|
||||||
"download_subtitles": "Titlovi - `x` (.vtt)",
|
|
||||||
"generic_button_save": "Sačuvaj",
|
|
||||||
"crash_page_search_issue": "pretražili <a href=\"`x`\">postojeće izveštaje o problemima na GitHub-u</a>",
|
|
||||||
"generic_button_cancel": "Otkaži",
|
|
||||||
"none": "nijedno",
|
|
||||||
"English (United States)": "Engleski (Sjedinjene Američke Države)",
|
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}} neviđeno obaveštenje",
|
|
||||||
"subscriptions_unseen_notifs_count_1": "{{count}} neviđena obaveštenja",
|
|
||||||
"subscriptions_unseen_notifs_count_2": "{{count}} neviđenih obaveštenja",
|
|
||||||
"Album: ": "Album: ",
|
|
||||||
"preferences_quality_option_dash": "DASH (adaptivni kvalitet)",
|
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
|
||||||
"Video unavailable": "Video snimak nedostupan",
|
|
||||||
"tokens_count_0": "{{count}} token",
|
|
||||||
"tokens_count_1": "{{count}} tokena",
|
|
||||||
"tokens_count_2": "{{count}} tokena",
|
|
||||||
"Chinese (China)": "Kineski (Kina)",
|
|
||||||
"Italian (auto-generated)": "Italijanski (automatski generisano)",
|
|
||||||
"channel_tab_shorts_label": "Shorts",
|
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
|
||||||
"preferences_quality_dash_option_360p": "360p",
|
|
||||||
"search_message_no_results": "Nisu pronađeni rezultati.",
|
|
||||||
"channel_tab_releases_label": "Izdanja",
|
|
||||||
"preferences_quality_dash_option_144p": "144p",
|
|
||||||
"Interlingue": "Interlingva",
|
|
||||||
"Song: ": "Pesma: ",
|
|
||||||
"generic_channels_count_0": "{{count}} kanal",
|
|
||||||
"generic_channels_count_1": "{{count}} kanala",
|
|
||||||
"generic_channels_count_2": "{{count}} kanala",
|
|
||||||
"Chinese (Taiwan)": "Kineski (Tajvan)",
|
|
||||||
"Turkish (auto-generated)": "Turski (automatski generisano)",
|
|
||||||
"Indonesian (auto-generated)": "Indonezijski (automatski generisano)",
|
|
||||||
"Portuguese (auto-generated)": "Portugalski (automatski generisano)",
|
|
||||||
"generic_count_years_0": "{{count}} godina",
|
|
||||||
"generic_count_years_1": "{{count}} godine",
|
|
||||||
"generic_count_years_2": "{{count}} godina",
|
|
||||||
"videoinfo_invidious_embed_link": "Ugrađeni link",
|
|
||||||
"Popular enabled: ": "Popularno omogućeno: ",
|
|
||||||
"Spanish (auto-generated)": "Španski (automatski generisano)",
|
|
||||||
"preferences_quality_option_small": "Malo",
|
|
||||||
"English (United Kingdom)": "Engleski (Ujedinjeno Kraljevstvo)",
|
|
||||||
"channel_tab_playlists_label": "Plejliste",
|
|
||||||
"generic_button_edit": "Izmeni",
|
|
||||||
"generic_playlists_count_0": "{{count}} plejlista",
|
|
||||||
"generic_playlists_count_1": "{{count}} plejliste",
|
|
||||||
"generic_playlists_count_2": "{{count}} plejlista",
|
|
||||||
"preferences_quality_option_hd720": "HD720",
|
|
||||||
"search_filters_features_option_purchased": "Kupljeno",
|
|
||||||
"search_filters_date_option_none": "Bilo koji datum",
|
|
||||||
"preferences_quality_dash_option_auto": "Automatski",
|
|
||||||
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
|
|
||||||
"crash_page_report_issue": "Ako ništa od gorenavedenog nije pomoglo, <a href=\"`x`\">otvorite novi izveštaj o problemu na GitHub-u</a> (po mogućnosti na engleskom) i uključite sledeći tekst u svoju poruku (NE prevodite taj tekst):",
|
|
||||||
"crash_page_switch_instance": "pokušali da <a href=\"`x`\">koristite drugu instancu</a>",
|
|
||||||
"generic_count_weeks_0": "{{count}} nedelja",
|
|
||||||
"generic_count_weeks_1": "{{count}} nedelje",
|
|
||||||
"generic_count_weeks_2": "{{count}} nedelja",
|
|
||||||
"videoinfo_watch_on_youTube": "Gledaj na YouTube-u",
|
|
||||||
"Music in this video": "Muzika u ovom video snimku",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
|
||||||
"generic_count_hours_0": "{{count}} sat",
|
|
||||||
"generic_count_hours_1": "{{count}} sata",
|
|
||||||
"generic_count_hours_2": "{{count}} sati",
|
|
||||||
"French (auto-generated)": "Francuski (automatski generisano)",
|
|
||||||
"crash_page_read_the_faq": "pročitali <a href=\"`x`\">Često Postavljana Pitanja (ČPP)</a>",
|
|
||||||
"user_created_playlists": "Napravljenih plejlista: `x`",
|
|
||||||
"channel_tab_channels_label": "Kanali",
|
|
||||||
"search_filters_type_option_all": "Bilo koja vrsta",
|
|
||||||
"Russian (auto-generated)": "Ruski (automatski generisano)",
|
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
|
||||||
"comments_view_x_replies_0": "Pogledaj {{count}} odgovor",
|
|
||||||
"comments_view_x_replies_1": "Pogledaj {{count}} odgovora",
|
|
||||||
"comments_view_x_replies_2": "Pogledaj {{count}} odgovora",
|
|
||||||
"Portuguese (Brazil)": "Portugalski (Brazil)",
|
|
||||||
"search_filters_features_option_vr180": "VR180",
|
|
||||||
"error_video_not_in_playlist": "Traženi video snimak ne postoji na ovoj plejlisti. <a href=\"`x`\">Kliknite ovde za početnu stranicu plejliste.</a>",
|
|
||||||
"Dutch (auto-generated)": "Holandski (automatski generisano)",
|
|
||||||
"generic_count_days_0": "{{count}} dan",
|
|
||||||
"generic_count_days_1": "{{count}} dana",
|
|
||||||
"generic_count_days_2": "{{count}} dana",
|
|
||||||
"Vietnamese (auto-generated)": "Vijetnamski (automatski generisano)",
|
|
||||||
"search_filters_duration_option_none": "Bilo koje trajanje",
|
|
||||||
"preferences_quality_dash_option_240p": "240p",
|
|
||||||
"Chinese": "Kineski",
|
|
||||||
"generic_button_delete": "Izbriši",
|
|
||||||
"Import YouTube playlist (.csv)": "Uvezi YouTube plejlistu (.csv)",
|
|
||||||
"Standard YouTube license": "Standardna YouTube licenca",
|
|
||||||
"search_filters_duration_option_medium": "Srednje (4 - 20 minuta)",
|
|
||||||
"generic_count_seconds_0": "{{count}} sekunda",
|
|
||||||
"generic_count_seconds_1": "{{count}} sekunde",
|
|
||||||
"generic_count_seconds_2": "{{count}} sekundi",
|
|
||||||
"search_filters_date_label": "Datum otpremanja",
|
|
||||||
"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_1": "{{count}} pregleda",
|
|
||||||
"generic_views_count_2": "{{count}} pregleda"
|
|
||||||
}
|
}
|
||||||
|
@ -1,166 +1,166 @@
|
|||||||
{
|
{
|
||||||
"LIVE": "УЖИВО",
|
"LIVE": "УЖИВО",
|
||||||
"Shared `x` ago": "Дељено пре `x`",
|
"Shared `x` ago": "Подељено пре `x`",
|
||||||
"Unsubscribe": "Прекини праћење",
|
"Unsubscribe": "Прекини праћење",
|
||||||
"Subscribe": "Запрати",
|
"Subscribe": "Прати",
|
||||||
"View channel on YouTube": "Погледај канал на YouTube-у",
|
"View channel on YouTube": "Погледај канал на YouTube-у",
|
||||||
"View playlist on YouTube": "Погледај плејлисту на YouTube-у",
|
"View playlist on YouTube": "Погледај списак извођења на YоуТубе-у",
|
||||||
"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": "Увоз и извоз података",
|
||||||
"Import": "Увези",
|
"Import": "Увези",
|
||||||
"Import Invidious data": "Увези Invidious JSON податке",
|
"Import Invidious data": "Увези податке са Individious-а",
|
||||||
"Import YouTube subscriptions": "Увези YouTube/OPML праћења",
|
"Import YouTube subscriptions": "Увези праћења са YouTube-а",
|
||||||
"Import FreeTube subscriptions (.db)": "Увези FreeTube праћења (.db)",
|
"Import FreeTube subscriptions (.db)": "Увези праћења са FreeTube-а (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Увези NewPipe праћења (.json)",
|
"Import NewPipe subscriptions (.json)": "Увези праћења са NewPipe-а (.json)",
|
||||||
"Import NewPipe data (.zip)": "Увези NewPipe податке (.zip)",
|
"Import NewPipe data (.zip)": "Увези податке са NewPipe-a (.zip)",
|
||||||
"Export": "Извези",
|
"Export": "Извези",
|
||||||
"Export subscriptions as OPML": "Извези праћења као OPML",
|
"Export subscriptions as OPML": "Извези праћења као ОПМЛ датотеку",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Извези праћења као OPML (за NewPipe и FreeTube)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Извези праћења као ОПМЛ датотеку (за NewPipe и FreeTube)",
|
||||||
"Export data as JSON": "Извези Invidious податке као JSON",
|
"Export data as JSON": "Извези податке као 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 одобрењу",
|
||||||
"source": "извор",
|
"source": "извор",
|
||||||
"Log in": "Пријава",
|
"Log in": "Пријави се",
|
||||||
"Log in/register": "Пријава/регистрација",
|
"Log in/register": "Пријави се/Отворите налог",
|
||||||
"User ID": "ID корисника",
|
"User ID": "Кориснички ИД",
|
||||||
"Password": "Лозинка",
|
"Password": "Лозинка",
|
||||||
"Time (h:mm:ss):": "Време (ч:мм:сс):",
|
"Time (h:mm:ss):": "Време (ч:мм:сс):",
|
||||||
"Text CAPTCHA": "Текст CAPTCHA",
|
"Text CAPTCHA": "Знаковни ЦАПТЧА",
|
||||||
"Image CAPTCHA": "Слика CAPTCHA",
|
"Image CAPTCHA": "Сликовни CAPTCHA",
|
||||||
"Sign In": "Пријава",
|
"Sign In": "Пријава",
|
||||||
"Register": "Регистрација",
|
"Register": "Отвори налог",
|
||||||
"E-mail": "Имејл",
|
"E-mail": "Е-пошта",
|
||||||
"Preferences": "Подешавања",
|
"Preferences": "Подешавања",
|
||||||
"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": "Приказ видео записа преко посредника: ",
|
||||||
"preferences_speed_label": "Подразумевана брзина: ",
|
"preferences_speed_label": "Подразумевана брзина: ",
|
||||||
"preferences_quality_label": "Преферирани квалитет видео снимка: ",
|
"preferences_quality_label": "Преферирани видео квалитет: ",
|
||||||
"preferences_volume_label": "Јачина звука плејера: ",
|
"preferences_volume_label": "Јачина звука: ",
|
||||||
"preferences_comments_label": "Подразумевани коментари: ",
|
"preferences_comments_label": "Подразумевани коментари: ",
|
||||||
"youtube": "YouTube",
|
"youtube": "YouTube",
|
||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
"preferences_captions_label": "Подразумевани титлови: ",
|
"preferences_captions_label": "Подразумевани титл: ",
|
||||||
"Fallback captions: ": "Резервни титлови: ",
|
"Fallback captions: ": "Титл у случају да главни није доступан: ",
|
||||||
"preferences_related_videos_label": "Прикажи повезане видео снимке: ",
|
"preferences_related_videos_label": "Прикажи сличне видео клипове: ",
|
||||||
"preferences_annotations_label": "Подразумевано прикажи напомене: ",
|
"preferences_annotations_label": "Прикажи напомене подразумевано: ",
|
||||||
"preferences_category_visual": "Визуелна подешавања",
|
"preferences_category_visual": "Визуелне преференце",
|
||||||
"preferences_player_style_label": "Стил плејера: ",
|
"preferences_player_style_label": "Стил плејера: ",
|
||||||
"Dark mode: ": "Тамни режим: ",
|
"Dark mode: ": "Тамни режим: ",
|
||||||
"preferences_dark_mode_label": "Тема: ",
|
"preferences_dark_mode_label": "Изглед/Тема: ",
|
||||||
"dark": "тамна",
|
"dark": "тамно",
|
||||||
"light": "светла",
|
"light": "светло",
|
||||||
"preferences_thin_mode_label": "Компактни режим: ",
|
"preferences_thin_mode_label": "Компактни режим: ",
|
||||||
"preferences_category_subscription": "Подешавања праћења",
|
"preferences_category_subscription": "Подешавања праћења",
|
||||||
"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": "Прикажи само видео клипове који нису погледани: ",
|
||||||
"preferences_notifications_only_label": "Прикажи само обавештења (ако их има): ",
|
"preferences_notifications_only_label": "Прикажи само обавештења (ако их уопште има): ",
|
||||||
"Enable web notifications": "Омогући веб обавештења",
|
"Enable web notifications": "Омогући обавештења у веб претраживачу",
|
||||||
"`x` uploaded a video": "`x` је отпремио/ла видео снимак",
|
"`x` uploaded a video": "`x` је отпремио/ла видео клип",
|
||||||
"`x` is live": "`x` је уживо",
|
"`x` is live": "`x` преноси уживо",
|
||||||
"preferences_category_data": "Подешавања података",
|
"preferences_category_data": "Подешавања података",
|
||||||
"Clear watch history": "Очисти историју гледања",
|
"Clear watch history": "Обриши историју гледања",
|
||||||
"Import/export data": "Увези/Извези податке",
|
"Import/export data": "Увези/Извези податке",
|
||||||
"Change password": "Промени лозинку",
|
"Change password": "Промени лозинку",
|
||||||
"Manage subscriptions": "Управљај праћењима",
|
"Manage subscriptions": "Управљај записима",
|
||||||
"Manage tokens": "Управљај токенима",
|
"Manage tokens": "Управљај жетонима",
|
||||||
"Watch history": "Историја гледања",
|
"Watch history": "Историја гледања",
|
||||||
"Delete account": "Избриши налог",
|
"Delete account": "Обриши налог",
|
||||||
"preferences_category_admin": "Подешавања администратора",
|
"preferences_category_admin": "Администраторска подешавања",
|
||||||
"preferences_default_home_label": "Подразумевана почетна страница: ",
|
"preferences_default_home_label": "Подразумевана почетна страница: ",
|
||||||
"preferences_feed_menu_label": "Фид мени: ",
|
"preferences_feed_menu_label": "Доводна страница: ",
|
||||||
"CAPTCHA enabled: ": "CAPTCHA омогућена: ",
|
"CAPTCHA enabled: ": "CAPTCHA омогућена: ",
|
||||||
"Login enabled: ": "Пријава омогућена: ",
|
"Login enabled: ": "Пријава омогућена: ",
|
||||||
"Registration enabled: ": "Регистрација омогућена: ",
|
"Registration enabled: ": "Регистрација омогућена: ",
|
||||||
"Save preferences": "Сачувај подешавања",
|
"Save preferences": "Сачувај подешавања",
|
||||||
"Subscription manager": "Управљање праћењима",
|
"Subscription manager": "Управљање праћењима",
|
||||||
"Token manager": "Управљање токенима",
|
"Token manager": "Управљање жетонима",
|
||||||
"Token": "Токен",
|
"Token": "Жетон",
|
||||||
"Import/export": "Увоз/извоз",
|
"Import/export": "Увези/Извези",
|
||||||
"unsubscribe": "прекини праћење",
|
"unsubscribe": "прекини са праћењем",
|
||||||
"revoke": "опозови",
|
"revoke": "опозови",
|
||||||
"Subscriptions": "Праћења",
|
"Subscriptions": "Праћења",
|
||||||
"search": "претрага",
|
"search": "претрага",
|
||||||
"Log out": "Одјава",
|
"Log out": "Одјава",
|
||||||
"Source available here.": "Изворни кôд је доступан овде.",
|
"Source available here.": "Изворна кода је овде доступна.",
|
||||||
"View JavaScript license information.": "Погледај информације о JavaScript лиценци.",
|
"View JavaScript license information.": "Погледај информације лиценце везане за JavaScript.",
|
||||||
"View privacy policy.": "Погледај политику приватности.",
|
"View privacy policy.": "Погледај извештај о приватности.",
|
||||||
"Trending": "У тренду",
|
"Trending": "У тренду",
|
||||||
"Public": "Јавно",
|
"Public": "Јавно",
|
||||||
"Unlisted": "По позиву",
|
"Unlisted": "Ненаведено",
|
||||||
"Private": "Приватно",
|
"Private": "Приватно",
|
||||||
"View all playlists": "Погледај све плејлисте",
|
"View all playlists": "Прегледај све плеј листе",
|
||||||
"Updated `x` ago": "Ажурирано пре `x`",
|
"Updated `x` ago": "Ажурирано пре `x`",
|
||||||
"Delete playlist `x`?": "Избрисати плејлисту `x`?",
|
"Delete playlist `x`?": "Обриши плеј листу `x`?",
|
||||||
"Delete playlist": "Избриши плејлисту",
|
"Delete playlist": "Обриши плеј листу",
|
||||||
"Create playlist": "Направи плејлисту",
|
"Create playlist": "Направи плеј листу",
|
||||||
"Title": "Наслов",
|
"Title": "Наслов",
|
||||||
"Playlist privacy": "Приватност плејлисте",
|
"Playlist privacy": "Подешавања приватности плеј листе",
|
||||||
"Editing playlist `x`": "Измењивање плејлисте `x`",
|
"Editing playlist `x`": "Измена плеј листе `x`",
|
||||||
"Watch on YouTube": "Гледај на YouTube-у",
|
"Watch on YouTube": "Гледај на YouTube-у",
|
||||||
"Hide annotations": "Сакриј напомене",
|
"Hide annotations": "Сакриј напомене",
|
||||||
"Show annotations": "Прикажи напомене",
|
"Show annotations": "Прикажи напомене",
|
||||||
"Genre: ": "Жанр: ",
|
"Genre: ": "Жанр: ",
|
||||||
"License: ": "Лиценца: ",
|
"License: ": "Лиценца: ",
|
||||||
"Engagement: ": "Ангажовање: ",
|
"Engagement: ": "Ангажовање: ",
|
||||||
"Whitelisted regions: ": "Доступни региони: ",
|
"Whitelisted regions: ": "Дозвољене области: ",
|
||||||
"Blacklisted regions: ": "Недоступни региони: ",
|
"Blacklisted regions: ": "Забрањене области: ",
|
||||||
"Premieres in `x`": "Премијера у `x`",
|
"Premieres in `x`": "Премера у `x`",
|
||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Хеј! Изгледа да сте искључили JavaScript. Кликните овде да бисте видели коментаре, имајте на уму да ће можда потрајати мало дуже да се учитају.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Хеј! Изгледа да сте онемогућили JavaScript. Кликните овде да видите коментаре, чувајте на уму да ово може да потраје дуже док се не учитају.",
|
||||||
"View YouTube comments": "Погледај YouTube коментаре",
|
"View YouTube comments": "Прикажи YouTube коментаре",
|
||||||
"View more comments on Reddit": "Погледај више коментара на Reddit-у",
|
"View more comments on Reddit": "Прикажи више коментара на Reddit-у",
|
||||||
"View Reddit comments": "Погледај Reddit коментаре",
|
"View Reddit comments": "Прикажи Reddit коментаре",
|
||||||
"Hide replies": "Сакриј одговоре",
|
"Hide replies": "Сакриј одговоре",
|
||||||
"Show replies": "Прикажи одговоре",
|
"Show replies": "Прикажи одговоре",
|
||||||
"Incorrect password": "Нетачна лозинка",
|
"Incorrect password": "Нетачна лозинка",
|
||||||
"Current version: ": "Тренутна верзија: ",
|
"Current version: ": "Тренутна верзија: ",
|
||||||
"Wilson score: ": "Вилсонова оцена: ",
|
"Wilson score: ": "Wилсонова оцена: ",
|
||||||
"Burmese": "Бурмански",
|
"Burmese": "Бурмански",
|
||||||
"preferences_quality_dash_label": "Преферирани DASH квалитет видео снимка: ",
|
"preferences_quality_dash_label": "Преферирани квалитет DASH видео формата: ",
|
||||||
"Erroneous token": "Погрешан токен",
|
"Erroneous token": "Погрешан жетон",
|
||||||
"CAPTCHA is a required field": "CAPTCHA је обавезно поље",
|
"CAPTCHA is a required field": "CAPTCHA је обавезно поље",
|
||||||
"No such user": "Не постоји корисник",
|
"No such user": "Непостојећи корисник",
|
||||||
"Chinese (Traditional)": "Кинески (Традиционални)",
|
"Chinese (Traditional)": "Кинески (Традиционални)",
|
||||||
"adminprefs_modified_source_code_url_label": "URL адреса до репозиторијума измењеног изворног кода",
|
"adminprefs_modified_source_code_url_label": "УРЛ веза до складишта са Измењеном Изворном Кодом",
|
||||||
"Lao": "Лаоски",
|
"Lao": "Лаоски",
|
||||||
"Czech": "Чешки",
|
"Czech": "Чешки",
|
||||||
"Kannada": "Канада",
|
"Kannada": "Канада (Језик)",
|
||||||
"Polish": "Пољски",
|
"Polish": "Пољски",
|
||||||
"Cebuano": "Цебуански",
|
"Cebuano": "Себуано",
|
||||||
"preferences_show_nick_label": "Прикажи надимке на врху: ",
|
"preferences_show_nick_label": "Прикажи надимке на врху: ",
|
||||||
"Report statistics: ": "Извештавај статистике: ",
|
"Report statistics: ": "Извештавај о статистици: ",
|
||||||
"Show more": "Прикажи више",
|
"Show more": "Прикажи више",
|
||||||
"Wrong answer": "Погрешан одговор",
|
"Wrong answer": "Погрешан одговор",
|
||||||
"Hidden field \"token\" is a required field": "Скривено поље „токен“ је обавезно поље",
|
"Hidden field \"token\" is a required field": "Сакривено \"token\" поље је обавезно",
|
||||||
"English": "Енглески",
|
"English": "Енглески",
|
||||||
"Albanian": "Албански",
|
"Albanian": "Албански",
|
||||||
"Amharic": "Амхарски",
|
"Amharic": "Амхарски",
|
||||||
@ -176,38 +176,38 @@
|
|||||||
"Georgian": "Грузијски",
|
"Georgian": "Грузијски",
|
||||||
"Greek": "Грчки",
|
"Greek": "Грчки",
|
||||||
"Hausa": "Хауса",
|
"Hausa": "Хауса",
|
||||||
"search_filters_type_option_video": "Видео снимак",
|
"search_filters_type_option_video": "Видео",
|
||||||
"search_filters_type_option_playlist": "Плејлиста",
|
"search_filters_type_option_playlist": "Плеј листа",
|
||||||
"search_filters_type_option_movie": "Филм",
|
"search_filters_type_option_movie": "Филм",
|
||||||
"search_filters_duration_option_long": "Дуго (> 20 минута)",
|
"search_filters_duration_option_long": "Дуго (> 20 минута)",
|
||||||
"search_filters_features_option_c_commons": "Creative Commons",
|
"search_filters_features_option_c_commons": "Creative Commons (Лиценца)",
|
||||||
"search_filters_features_option_live": "Уживо",
|
"search_filters_features_option_live": "Уживо",
|
||||||
"search_filters_features_option_location": "Локација",
|
"search_filters_features_option_location": "Локација",
|
||||||
"next_steps_error_message": "Након тога би требало да покушате да: ",
|
"next_steps_error_message": "Након чега би требали пробати: ",
|
||||||
"footer_donate_page": "Донирај",
|
"footer_donate_page": "Донирај",
|
||||||
"footer_documentation": "Документација",
|
"footer_documentation": "Документација",
|
||||||
"footer_modfied_source_code": "Измењени изворни кôд",
|
"footer_modfied_source_code": "Измењена Изворна Кода",
|
||||||
"preferences_region_label": "Држава садржаја: ",
|
"preferences_region_label": "Држава порекла садржаја: ",
|
||||||
"preferences_category_misc": "Остала подешавања",
|
"preferences_category_misc": "Остала подешавања",
|
||||||
"User ID is a required field": "ID корисника је обавезно поље",
|
"User ID is a required field": "Кориснички ИД је обавезно поље",
|
||||||
"Password is a required field": "Лозинка је обавезно поље",
|
"Password is a required field": "Лозинка је обавезно поље",
|
||||||
"Wrong username or password": "Погрешно корисничко име или лозинка",
|
"Wrong username or password": "Погрешно корисничко име или лозинка",
|
||||||
"Password cannot be empty": "Лозинка не може бити празна",
|
"Password cannot be empty": "Лозинка не може бити празна",
|
||||||
"Password cannot be longer than 55 characters": "Лозинка не може бити дужа од 55 знакова",
|
"Password cannot be longer than 55 characters": "Лозинка не може бити дужа од 55 карактера",
|
||||||
"Invidious Private Feed for `x`": "Invidious приватни фид за `x`",
|
"Invidious Private Feed for `x`": "Инвидиоус Приватни Довод за `x`",
|
||||||
"Deleted or invalid channel": "Избрисан или неважећи канал",
|
"Deleted or invalid channel": "Обрисан или непостојећи канал",
|
||||||
"This channel does not exist.": "Овај канал не постоји.",
|
"This channel does not exist.": "Овај канал не постоји.",
|
||||||
"Could not create mix.": "Није могуће направити микс.",
|
"Could not create mix.": "Прављење микса није успело.",
|
||||||
"Empty playlist": "Празна плејлиста",
|
"Empty playlist": "Празна плеј листа",
|
||||||
"Not a playlist.": "Није плејлиста.",
|
"Not a playlist.": "Није плеј листа.",
|
||||||
"Playlist does not exist.": "Плејлиста не постоји.",
|
"Playlist does not exist.": "Непостојећа плеј листа.",
|
||||||
"Could not pull trending pages.": "Није могуће повући странице „У тренду“.",
|
"Could not pull trending pages.": "Учитавање 'У току' страница није успело.",
|
||||||
"Hidden field \"challenge\" is a required field": "Скривено поље „изазов“ је обавезно поље",
|
"Hidden field \"challenge\" is a required field": "Сакривено \"challenge\" поље је обавезно",
|
||||||
"Telugu": "Телугу",
|
"Telugu": "Телугу",
|
||||||
"Turkish": "Турски",
|
"Turkish": "Турски",
|
||||||
"Urdu": "Урду",
|
"Urdu": "Урду",
|
||||||
"Western Frisian": "Западнофризијски",
|
"Western Frisian": "Западнофрисијски",
|
||||||
"Xhosa": "Коса (Кхоса)",
|
"Xhosa": "Коса (Језик)",
|
||||||
"Yiddish": "Јидиш",
|
"Yiddish": "Јидиш",
|
||||||
"Hawaiian": "Хавајски",
|
"Hawaiian": "Хавајски",
|
||||||
"Hmong": "Хмонг",
|
"Hmong": "Хмонг",
|
||||||
@ -217,58 +217,58 @@
|
|||||||
"Khmer": "Кмерски",
|
"Khmer": "Кмерски",
|
||||||
"Kyrgyz": "Киргиски",
|
"Kyrgyz": "Киргиски",
|
||||||
"Macedonian": "Македонски",
|
"Macedonian": "Македонски",
|
||||||
"Maori": "Маорски",
|
"Maori": "Маори (Језик)",
|
||||||
"Marathi": "Маратски",
|
"Marathi": "Маратхи",
|
||||||
"Nepali": "Непалски",
|
"Nepali": "Непалски",
|
||||||
"Norwegian Bokmål": "Норвешки Бокмал",
|
"Norwegian Bokmål": "Норвешки Бокмал",
|
||||||
"Nyanja": "Нијанџа",
|
"Nyanja": "Чева",
|
||||||
"Russian": "Руски",
|
"Russian": "Руски",
|
||||||
"Scottish Gaelic": "Шкотски Гелски",
|
"Scottish Gaelic": "Шкотски Гелски",
|
||||||
"Shona": "Шона",
|
"Shona": "Шона",
|
||||||
"Slovak": "Словачки",
|
"Slovak": "Словачки",
|
||||||
"Spanish (Latin America)": "Шпански (Латинска Америка)",
|
"Spanish (Latin America)": "Шпански (Јужна Америка)",
|
||||||
"Sundanese": "Сундански",
|
"Sundanese": "Сундски",
|
||||||
"Swahili": "Сували",
|
"Swahili": "Свахили",
|
||||||
"Tajik": "Таџички",
|
"Tajik": "Таџички",
|
||||||
"Search": "Претрага",
|
"Search": "Претрага",
|
||||||
"Rating: ": "Оцена: ",
|
"Rating: ": "Ocena/e: ",
|
||||||
"Default": "Подразумевано",
|
"Default": "Подразумеван/о",
|
||||||
"News": "Вести",
|
"News": "Вести",
|
||||||
"Download": "Преузми",
|
"Download": "Преузми",
|
||||||
"(edited)": "(измењено)",
|
"(edited)": "(измењено)",
|
||||||
"`x` marked it with a ❤": "`x` је означио/ла са ❤",
|
"`x` marked it with a ❤": "`x` је означио/ла ово са ❤",
|
||||||
"Audio mode": "Режим аудио снимка",
|
"Audio mode": "Аудио мод",
|
||||||
"channel_tab_videos_label": "Видео снимци",
|
"channel_tab_videos_label": "Видео клипови",
|
||||||
"search_filters_sort_option_views": "Број прегледа",
|
"search_filters_sort_option_views": "Број прегледа",
|
||||||
"search_filters_features_label": "Карактеристике",
|
"search_filters_features_label": "Карактеристике",
|
||||||
"search_filters_date_option_today": "Данас",
|
"search_filters_date_option_today": "Данас",
|
||||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
"preferences_locale_label": "Језик: ",
|
"preferences_locale_label": "Језик: ",
|
||||||
"Persian": "Персијски",
|
"Persian": "Перзијски",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"": "Погледај `x` коментаре",
|
"": "Прикажи `x` коментара",
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Погледај `x` коментар"
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Прикажи `x` коментар"
|
||||||
},
|
},
|
||||||
"search_filters_type_option_channel": "Канал",
|
"search_filters_type_option_channel": "Канал",
|
||||||
"Haitian Creole": "Хаићански Креолски",
|
"Haitian Creole": "Хаићански Креолски",
|
||||||
"Armenian": "Јерменски",
|
"Armenian": "Јерменски",
|
||||||
"next_steps_error_message_go_to_youtube": "Одете на YouTube",
|
"next_steps_error_message_go_to_youtube": "Иди на YouTube",
|
||||||
"Indonesian": "Индонезијски",
|
"Indonesian": "Индонежански",
|
||||||
"preferences_vr_mode_label": "Интерактивни видео снимци од 360 степени (захтева WebGL): ",
|
"preferences_vr_mode_label": "Интерактивни видео клипови у 360 степени: ",
|
||||||
"Switch Invidious Instance": "Промени Invidious инстанцу",
|
"Switch Invidious Instance": "Промени Invidious инстанцу",
|
||||||
"Portuguese": "Португалски",
|
"Portuguese": "Португалски",
|
||||||
"search_filters_date_option_week": "Ове недеље",
|
"search_filters_date_option_week": "Ове седмице",
|
||||||
"search_filters_type_option_show": "Емисија",
|
"search_filters_type_option_show": "Емисија",
|
||||||
"Fallback comments: ": "Резервни коментари: ",
|
"Fallback comments: ": "Коментари у случају отказивања: ",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "Видео Високе Резолуције",
|
||||||
"About": "О сајту",
|
"About": "О програму",
|
||||||
"Kazakh": "Казашки",
|
"Kazakh": "Казашки",
|
||||||
"Shared `x`": "Дељено `x`",
|
"Shared `x`": "Подељено `x`",
|
||||||
"Playlists": "Плејлисте",
|
"Playlists": "Плеј листе",
|
||||||
"Yoruba": "Јоруба",
|
"Yoruba": "Јоруба",
|
||||||
"Erroneous challenge": "Погрешан изазов",
|
"Erroneous challenge": "Погрешан изазов",
|
||||||
"Danish": "Дански",
|
"Danish": "Дански",
|
||||||
"Could not get channel info.": "Није могуће прикупити информације о каналу.",
|
"Could not get channel info.": "Узимање података о каналу није успело.",
|
||||||
"search_filters_features_option_hd": "HD",
|
"search_filters_features_option_hd": "HD",
|
||||||
"Slovenian": "Словеначки",
|
"Slovenian": "Словеначки",
|
||||||
"Load more": "Учитај више",
|
"Load more": "Учитај више",
|
||||||
@ -276,53 +276,53 @@
|
|||||||
"Luxembourgish": "Луксембуршки",
|
"Luxembourgish": "Луксембуршки",
|
||||||
"Mongolian": "Монголски",
|
"Mongolian": "Монголски",
|
||||||
"Latvian": "Летонски",
|
"Latvian": "Летонски",
|
||||||
"channel:`x`": "канал:`x`",
|
"channel:`x`": "kanal:`x`",
|
||||||
"Southern Sotho": "Јужни Сото",
|
"Southern Sotho": "Јужни Сото",
|
||||||
"Popular": "Популарно",
|
"Popular": "Популарно",
|
||||||
"Gujarati": "Гуџарати",
|
"Gujarati": "Гуџарати",
|
||||||
"search_filters_date_option_year": "Ове године",
|
"search_filters_date_option_year": "Ове године",
|
||||||
"Irish": "Ирски",
|
"Irish": "Ирски",
|
||||||
"YouTube comment permalink": "Трајни линк YouTube коментара",
|
"YouTube comment permalink": "YouTube коментар трајна веза",
|
||||||
"Malagasy": "Малгашки",
|
"Malagasy": "Малгашки",
|
||||||
"Token is expired, please try again": "Токен је истекао, покушајте поново",
|
"Token is expired, please try again": "Жетон је истекао, молимо вас да покушате поново",
|
||||||
"search_filters_duration_option_short": "Кратко (< 4 минута)",
|
"search_filters_duration_option_short": "Кратко (< 4 минуте)",
|
||||||
"Samoan": "Самоански",
|
"Samoan": "Самоански",
|
||||||
"Tamil": "Тамилски",
|
"Tamil": "Тамилски",
|
||||||
"Ukrainian": "Украјински",
|
"Ukrainian": "Украјински",
|
||||||
"permalink": "трајни линк",
|
"permalink": "трајна веза",
|
||||||
"Pashto": "Паштунски",
|
"Pashto": "Паштунски",
|
||||||
"channel_tab_community_label": "Заједница",
|
"channel_tab_community_label": "Заједница",
|
||||||
"Sindhi": "Синди",
|
"Sindhi": "Синди",
|
||||||
"Could not fetch comments": "Није могуће прикупити коментаре",
|
"Could not fetch comments": "Узимање коментара није успело",
|
||||||
"Bangla": "Бенгалски",
|
"Bangla": "Бангла/Бенгалски",
|
||||||
"Uzbek": "Узбечки",
|
"Uzbek": "Узбечки",
|
||||||
"Lithuanian": "Литвански",
|
"Lithuanian": "Литвански",
|
||||||
"Icelandic": "Исландски",
|
"Icelandic": "Исландски",
|
||||||
"Thai": "Тајски",
|
"Thai": "Тајски",
|
||||||
"search_filters_date_option_month": "Овог месеца",
|
"search_filters_date_option_month": "Овај месец",
|
||||||
"search_filters_type_label": "Врста",
|
"search_filters_type_label": "Тип",
|
||||||
"search_filters_date_option_hour": "Последњи сат",
|
"search_filters_date_option_hour": "Последњи сат",
|
||||||
"Spanish": "Шпански",
|
"Spanish": "Шпански",
|
||||||
"search_filters_sort_option_date": "Датум отпремања",
|
"search_filters_sort_option_date": "Датум отпремања",
|
||||||
"View as playlist": "Погледај као плејлисту",
|
"View as playlist": "Погледај као плеј листу",
|
||||||
"search_filters_sort_option_relevance": "Релевантност",
|
"search_filters_sort_option_relevance": "Релевантност",
|
||||||
"Estonian": "Естонски",
|
"Estonian": "Естонски",
|
||||||
"Sinhala": "Синхалски",
|
"Sinhala": "Синхалешки",
|
||||||
"Corsican": "Корзикански",
|
"Corsican": "Корзикански",
|
||||||
"Filipino": "Филипински",
|
"Filipino": "Филипино",
|
||||||
"Gaming": "Видео игре",
|
"Gaming": "Игрице",
|
||||||
"Movies": "Филмови",
|
"Movies": "Филмови",
|
||||||
"search_filters_sort_option_rating": "Оцена",
|
"search_filters_sort_option_rating": "Оцене",
|
||||||
"Top enabled: ": "Топ омогућено: ",
|
"Top enabled: ": "Врх омогућен: ",
|
||||||
"Released under the AGPLv3 on Github.": "Објављено под лиценцом AGPLv3 на GitHub-у.",
|
"Released under the AGPLv3 on Github.": "Избачено под лиценцом AGPLv3 на GitHub-у.",
|
||||||
"Afrikaans": "Африканс",
|
"Afrikaans": "Африканс",
|
||||||
"preferences_automatic_instance_redirect_label": "Аутоматско преусмеравање инстанце (повратак на redirect.invidious.io): ",
|
"preferences_automatic_instance_redirect_label": "Аутоматско пребацивање на другу инстанцу у случају отказивања (пречи ће назад на редирецт.инвидиоус.ио): ",
|
||||||
"Please log in": "Молимо, пријавите се",
|
"Please log in": "Молимо вас да се пријавите",
|
||||||
"English (auto-generated)": "Енглески (аутоматски генерисано)",
|
"English (auto-generated)": "Енглески (аутоматски генерисано)",
|
||||||
"Hindi": "Хинди",
|
"Hindi": "Хинди",
|
||||||
"Italian": "Италијански",
|
"Italian": "Талијански",
|
||||||
"Malayalam": "Малајаламски",
|
"Malayalam": "Малајалам",
|
||||||
"Punjabi": "Панџапски",
|
"Punjabi": "Пунџаби",
|
||||||
"Somali": "Сомалијски",
|
"Somali": "Сомалијски",
|
||||||
"Vietnamese": "Вијетнамски",
|
"Vietnamese": "Вијетнамски",
|
||||||
"Welsh": "Велшки",
|
"Welsh": "Велшки",
|
||||||
@ -330,25 +330,25 @@
|
|||||||
"Maltese": "Малтешки",
|
"Maltese": "Малтешки",
|
||||||
"Swedish": "Шведски",
|
"Swedish": "Шведски",
|
||||||
"Music": "Музика",
|
"Music": "Музика",
|
||||||
"Download as: ": "Преузети као: ",
|
"Download as: ": "Преузми као: ",
|
||||||
"search_filters_duration_label": "Трајање",
|
"search_filters_duration_label": "Трајање",
|
||||||
"search_filters_sort_label": "Сортирање по",
|
"search_filters_sort_label": "Поредај према",
|
||||||
"search_filters_features_option_subtitles": "Титлови/Скривени титлови",
|
"search_filters_features_option_subtitles": "Титл/Превод",
|
||||||
"preferences_extend_desc_label": "Аутоматски прошири опис видео снимка: ",
|
"preferences_extend_desc_label": "Аутоматски прикажи цео опис видеа: ",
|
||||||
"Show less": "Прикажи мање",
|
"Show less": "Прикажи мање",
|
||||||
"Family friendly? ": "Погодно за породицу? ",
|
"Family friendly? ": "Погодно за породицу? ",
|
||||||
"Premieres `x`": "Премијера `x`",
|
"Premieres `x`": "Премерe у `x`",
|
||||||
"Bosnian": "Босански",
|
"Bosnian": "Босански",
|
||||||
"Catalan": "Каталонски",
|
"Catalan": "Каталонски",
|
||||||
"Japanese": "Јапански",
|
"Japanese": "Јапански",
|
||||||
"Latin": "Латински",
|
"Latin": "Латински",
|
||||||
"next_steps_error_message_refresh": "Освежите",
|
"next_steps_error_message_refresh": "Освежи страницу",
|
||||||
"footer_original_source_code": "Оригинални изворни кôд",
|
"footer_original_source_code": "Оригинална Изворна Кода",
|
||||||
"Romanian": "Румунски",
|
"Romanian": "Румунски",
|
||||||
"Serbian": "Српски",
|
"Serbian": "Српски",
|
||||||
"Top": "Топ",
|
"Top": "Врх",
|
||||||
"Video mode": "Режим видео снимка",
|
"Video mode": "Видео мод",
|
||||||
"footer_source_code": "Изворни кôд",
|
"footer_source_code": "Изворна Кода",
|
||||||
"search_filters_features_option_three_d": "3D",
|
"search_filters_features_option_three_d": "3D",
|
||||||
"search_filters_features_option_four_k": "4K",
|
"search_filters_features_option_four_k": "4K",
|
||||||
"Erroneous CAPTCHA": "Погрешна CAPTCHA",
|
"Erroneous CAPTCHA": "Погрешна CAPTCHA",
|
||||||
@ -360,148 +360,5 @@
|
|||||||
"Korean": "Корејски",
|
"Korean": "Корејски",
|
||||||
"Kurdish": "Курдски",
|
"Kurdish": "Курдски",
|
||||||
"Malay": "Малајски",
|
"Malay": "Малајски",
|
||||||
"search_filters_title": "Филтери",
|
"search_filters_title": "Филтер"
|
||||||
"Korean (auto-generated)": "Корејски (аутоматски генерисано)",
|
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
|
||||||
"preferences_quality_dash_option_worst": "Најгоре",
|
|
||||||
"channel_tab_podcasts_label": "Подкасти",
|
|
||||||
"preferences_save_player_pos_label": "Сачувај позицију репродукције: ",
|
|
||||||
"Spanish (Mexico)": "Шпански (Мексико)",
|
|
||||||
"generic_subscriptions_count_0": "{{count}} праћење",
|
|
||||||
"generic_subscriptions_count_1": "{{count}} праћења",
|
|
||||||
"generic_subscriptions_count_2": "{{count}} праћења",
|
|
||||||
"search_filters_apply_button": "Примени изабране филтере",
|
|
||||||
"Download is disabled": "Преузимање је онемогућено",
|
|
||||||
"comments_points_count_0": "{{count}} поен",
|
|
||||||
"comments_points_count_1": "{{count}} поена",
|
|
||||||
"comments_points_count_2": "{{count}} поена",
|
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
|
||||||
"German (auto-generated)": "Немачки (аутоматски генерисано)",
|
|
||||||
"Japanese (auto-generated)": "Јапански (аутоматски генерисано)",
|
|
||||||
"preferences_quality_option_medium": "Средње",
|
|
||||||
"search_message_change_filters_or_query": "Покушајте да проширите упит за претрагу и/или промените филтере.",
|
|
||||||
"crash_page_before_reporting": "Пре него што пријавите грешку, уверите се да сте:",
|
|
||||||
"preferences_quality_dash_option_best": "Најбоље",
|
|
||||||
"Channel Sponsor": "Спонзор канала",
|
|
||||||
"generic_videos_count_0": "{{count}} видео снимак",
|
|
||||||
"generic_videos_count_1": "{{count}} видео снимка",
|
|
||||||
"generic_videos_count_2": "{{count}} видео снимака",
|
|
||||||
"videoinfo_started_streaming_x_ago": "Започето стримовање пре `x`",
|
|
||||||
"videoinfo_youTube_embed_link": "Уграђено",
|
|
||||||
"channel_tab_streams_label": "Стримови уживо",
|
|
||||||
"playlist_button_add_items": "Додај видео снимке",
|
|
||||||
"generic_count_minutes_0": "{{count}} минут",
|
|
||||||
"generic_count_minutes_1": "{{count}} минута",
|
|
||||||
"generic_count_minutes_2": "{{count}} минута",
|
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
|
||||||
"preferences_watch_history_label": "Омогући историју гледања: ",
|
|
||||||
"user_saved_playlists": "Сачуваних плејлиста: `x`",
|
|
||||||
"Spanish (Spain)": "Шпански (Шпанија)",
|
|
||||||
"invidious": "Invidious",
|
|
||||||
"crash_page_refresh": "покушали да <a href=\"`x`\">освежите страницу</a>",
|
|
||||||
"Chinese (Hong Kong)": "Кинески (Хонг Конг)",
|
|
||||||
"Artist: ": "Извођач: ",
|
|
||||||
"generic_count_months_0": "{{count}} месец",
|
|
||||||
"generic_count_months_1": "{{count}} месеца",
|
|
||||||
"generic_count_months_2": "{{count}} месеци",
|
|
||||||
"search_message_use_another_instance": " Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.",
|
|
||||||
"generic_subscribers_count_0": "{{count}} пратилац",
|
|
||||||
"generic_subscribers_count_1": "{{count}} пратиоца",
|
|
||||||
"generic_subscribers_count_2": "{{count}} пратилаца",
|
|
||||||
"download_subtitles": "Титлови - `x` (.vtt)",
|
|
||||||
"generic_button_save": "Сачувај",
|
|
||||||
"crash_page_search_issue": "претражили <a href=\"`x`\">постојеће извештаје о проблемима на GitHub-у</a>",
|
|
||||||
"generic_button_cancel": "Откажи",
|
|
||||||
"none": "ниједно",
|
|
||||||
"English (United States)": "Енглески (Сједињене Америчке Државе)",
|
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}} невиђено обавештење",
|
|
||||||
"subscriptions_unseen_notifs_count_1": "{{count}} невиђена обавештења",
|
|
||||||
"subscriptions_unseen_notifs_count_2": "{{count}} невиђених обавештења",
|
|
||||||
"Album: ": "Албум: ",
|
|
||||||
"preferences_quality_option_dash": "DASH (адаптивни квалитет)",
|
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
|
||||||
"Video unavailable": "Видео снимак недоступан",
|
|
||||||
"tokens_count_0": "{{count}} токен",
|
|
||||||
"tokens_count_1": "{{count}} токена",
|
|
||||||
"tokens_count_2": "{{count}} токена",
|
|
||||||
"Chinese (China)": "Кинески (Кина)",
|
|
||||||
"Italian (auto-generated)": "Италијански (аутоматски генерисано)",
|
|
||||||
"channel_tab_shorts_label": "Shorts",
|
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
|
||||||
"preferences_quality_dash_option_360p": "360p",
|
|
||||||
"search_message_no_results": "Нису пронађени резултати.",
|
|
||||||
"channel_tab_releases_label": "Издања",
|
|
||||||
"preferences_quality_dash_option_144p": "144p",
|
|
||||||
"Interlingue": "Интерлингва",
|
|
||||||
"Song: ": "Песма: ",
|
|
||||||
"generic_channels_count_0": "{{count}} канал",
|
|
||||||
"generic_channels_count_1": "{{count}} канала",
|
|
||||||
"generic_channels_count_2": "{{count}} канала",
|
|
||||||
"Chinese (Taiwan)": "Кинески (Тајван)",
|
|
||||||
"Turkish (auto-generated)": "Турски (аутоматски генерисано)",
|
|
||||||
"Indonesian (auto-generated)": "Индонезијски (аутоматски генерисано)",
|
|
||||||
"Portuguese (auto-generated)": "Португалски (аутоматски генерисано)",
|
|
||||||
"generic_count_years_0": "{{count}} година",
|
|
||||||
"generic_count_years_1": "{{count}} године",
|
|
||||||
"generic_count_years_2": "{{count}} година",
|
|
||||||
"videoinfo_invidious_embed_link": "Уграђени линк",
|
|
||||||
"Popular enabled: ": "Популарно омогућено: ",
|
|
||||||
"Spanish (auto-generated)": "Шпански (аутоматски генерисано)",
|
|
||||||
"preferences_quality_option_small": "Мало",
|
|
||||||
"English (United Kingdom)": "Енглески (Уједињено Краљевство)",
|
|
||||||
"channel_tab_playlists_label": "Плејлисте",
|
|
||||||
"generic_button_edit": "Измени",
|
|
||||||
"generic_playlists_count_0": "{{count}} плејлиста",
|
|
||||||
"generic_playlists_count_1": "{{count}} плејлисте",
|
|
||||||
"generic_playlists_count_2": "{{count}} плејлиста",
|
|
||||||
"preferences_quality_option_hd720": "HD720",
|
|
||||||
"search_filters_features_option_purchased": "Купљено",
|
|
||||||
"search_filters_date_option_none": "Било који датум",
|
|
||||||
"preferences_quality_dash_option_auto": "Аутоматски",
|
|
||||||
"Cantonese (Hong Kong)": "Кантонски (Хонг Конг)",
|
|
||||||
"crash_page_report_issue": "Ако ништа од горенаведеног није помогло, <a href=\"`x`\">отворите нови извештај о проблему на GitHub-у</a> (по могућности на енглеском) и укључите следећи текст у своју поруку (НЕ преводите тај текст):",
|
|
||||||
"crash_page_switch_instance": "покушали да <a href=\"`x`\">користите другу инстанцу</a>",
|
|
||||||
"generic_count_weeks_0": "{{count}} недеља",
|
|
||||||
"generic_count_weeks_1": "{{count}} недеље",
|
|
||||||
"generic_count_weeks_2": "{{count}} недеља",
|
|
||||||
"videoinfo_watch_on_youTube": "Гледај на YouTube-у",
|
|
||||||
"Music in this video": "Музика у овом видео снимку",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
|
||||||
"generic_count_hours_0": "{{count}} сат",
|
|
||||||
"generic_count_hours_1": "{{count}} сата",
|
|
||||||
"generic_count_hours_2": "{{count}} сати",
|
|
||||||
"French (auto-generated)": "Француски (аутоматски генерисано)",
|
|
||||||
"crash_page_read_the_faq": "прочитали <a href=\"`x`\">Често Постављана Питања (ЧПП)</a>",
|
|
||||||
"user_created_playlists": "Направљених плејлиста: `x`",
|
|
||||||
"channel_tab_channels_label": "Канали",
|
|
||||||
"search_filters_type_option_all": "Било која врста",
|
|
||||||
"Russian (auto-generated)": "Руски (аутоматски генерисано)",
|
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
|
||||||
"comments_view_x_replies_0": "Погледај {{count}} одговор",
|
|
||||||
"comments_view_x_replies_1": "Погледај {{count}} одговора",
|
|
||||||
"comments_view_x_replies_2": "Погледај {{count}} одговора",
|
|
||||||
"Portuguese (Brazil)": "Португалски (Бразил)",
|
|
||||||
"search_filters_features_option_vr180": "VR180",
|
|
||||||
"error_video_not_in_playlist": "Тражени видео снимак не постоји на овој плејлисти. <a href=\"`x`\">Кликните овде за почетну страницу плејлисте.</a>",
|
|
||||||
"Dutch (auto-generated)": "Холандски (аутоматски генерисано)",
|
|
||||||
"generic_count_days_0": "{{count}} дан",
|
|
||||||
"generic_count_days_1": "{{count}} дана",
|
|
||||||
"generic_count_days_2": "{{count}} дана",
|
|
||||||
"Vietnamese (auto-generated)": "Вијетнамски (аутоматски генерисано)",
|
|
||||||
"search_filters_duration_option_none": "Било које трајање",
|
|
||||||
"preferences_quality_dash_option_240p": "240p",
|
|
||||||
"Chinese": "Кинески",
|
|
||||||
"generic_button_delete": "Избриши",
|
|
||||||
"Import YouTube playlist (.csv)": "Увези YouTube плејлисту (.csv)",
|
|
||||||
"Standard YouTube license": "Стандардна YouTube лиценца",
|
|
||||||
"search_filters_duration_option_medium": "Средње (4 - 20 минута)",
|
|
||||||
"generic_count_seconds_0": "{{count}} секунда",
|
|
||||||
"generic_count_seconds_1": "{{count}} секунде",
|
|
||||||
"generic_count_seconds_2": "{{count}} секунди",
|
|
||||||
"search_filters_date_label": "Датум отпремања",
|
|
||||||
"crash_page_you_found_a_bug": "Изгледа да сте пронашли грешку у Invidious-у!",
|
|
||||||
"generic_views_count_0": "{{count}} преглед",
|
|
||||||
"generic_views_count_1": "{{count}} прегледа",
|
|
||||||
"generic_views_count_2": "{{count}} прегледа"
|
|
||||||
}
|
}
|
||||||
|
@ -476,15 +476,5 @@
|
|||||||
"Song: ": "Şarkı: ",
|
"Song: ": "Şarkı: ",
|
||||||
"Standard YouTube license": "Standart YouTube lisansı",
|
"Standard YouTube license": "Standart YouTube lisansı",
|
||||||
"Download is disabled": "İndirme devre dışı",
|
"Download is disabled": "İndirme devre dışı",
|
||||||
"Import YouTube playlist (.csv)": "YouTube Oynatma Listesini İçe Aktar (.csv)",
|
"Import YouTube playlist (.csv)": "YouTube Oynatma Listesini İçe Aktar (.csv)"
|
||||||
"generic_button_delete": "Sil",
|
|
||||||
"generic_button_edit": "Düzenle",
|
|
||||||
"generic_button_save": "Kaydet",
|
|
||||||
"generic_button_cancel": "İptal",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_releases_label": "Yayınlar",
|
|
||||||
"playlist_button_add_items": "Video ekle",
|
|
||||||
"channel_tab_podcasts_label": "Podcast'ler",
|
|
||||||
"generic_channels_count": "{{count}} kanal",
|
|
||||||
"generic_channels_count_plural": "{{count}} kanal"
|
|
||||||
}
|
}
|
||||||
|
@ -492,16 +492,5 @@
|
|||||||
"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)"
|
||||||
"channel_tab_podcasts_label": "Подкасти",
|
|
||||||
"playlist_button_add_items": "Додати відео",
|
|
||||||
"generic_button_cancel": "Скасувати",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_releases_label": "Випуски",
|
|
||||||
"generic_button_delete": "Видалити",
|
|
||||||
"generic_button_edit": "Змінити",
|
|
||||||
"generic_button_save": "Зберегти",
|
|
||||||
"generic_channels_count_0": "{{count}} канал",
|
|
||||||
"generic_channels_count_1": "{{count}} канали",
|
|
||||||
"generic_channels_count_2": "{{count}} каналів"
|
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"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 theo dõi",
|
||||||
"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 theo dõi",
|
||||||
"Subscribe": "Theo dõi",
|
"Subscribe": "Theo dõi",
|
||||||
"View channel on YouTube": "Xem kênh trên YouTube",
|
"View channel on YouTube": "Xem kênh trên YouTube",
|
||||||
@ -71,7 +71,7 @@
|
|||||||
"Dark mode: ": "Chế độ tối: ",
|
"Dark mode: ": "Chế độ tối: ",
|
||||||
"preferences_dark_mode_label": "Chủ đề: ",
|
"preferences_dark_mode_label": "Chủ đề: ",
|
||||||
"dark": "tối",
|
"dark": "tối",
|
||||||
"light": "sáng",
|
"light": "ánh sáng",
|
||||||
"preferences_thin_mode_label": "Chế độ mỏng: ",
|
"preferences_thin_mode_label": "Chế độ mỏng: ",
|
||||||
"preferences_category_misc": "Tùy chọn khác",
|
"preferences_category_misc": "Tùy chọn khác",
|
||||||
"preferences_automatic_instance_redirect_label": "Tự động chuyển hướng phiên bản (dự phòng về redirect.invidious.io): ",
|
"preferences_automatic_instance_redirect_label": "Tự động chuyển hướng phiên bản (dự phòng về redirect.invidious.io): ",
|
||||||
@ -120,7 +120,7 @@
|
|||||||
"View JavaScript license information.": "Xem thông tin giấy phép JavaScript.",
|
"View JavaScript license information.": "Xem thông tin giấy phép JavaScript.",
|
||||||
"View privacy policy.": "Xem chính sách bảo mật.",
|
"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 cộng",
|
||||||
"Unlisted": "Không hiển thị",
|
"Unlisted": "Không hiển thị",
|
||||||
"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",
|
||||||
@ -182,17 +182,17 @@
|
|||||||
"Amharic": "Amharic",
|
"Amharic": "Amharic",
|
||||||
"Arabic": "Tiếng Ả Rập",
|
"Arabic": "Tiếng Ả Rập",
|
||||||
"Armenian": "Tiếng Armenia",
|
"Armenian": "Tiếng Armenia",
|
||||||
"Azerbaijani": "Tiếng Azerbaijan",
|
"Azerbaijani": "Azerbaijan",
|
||||||
"Bangla": "Tiếng Bengal",
|
"Bangla": "Bangla",
|
||||||
"Basque": "Tiếng Basque",
|
"Basque": "Tiếng Basque",
|
||||||
"Belarusian": "Tiếng Belarus",
|
"Belarusian": "Người Belarus",
|
||||||
"Bosnian": "Tiếng Bosnia",
|
"Bosnian": "Tiếng Bosnia",
|
||||||
"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": "Cebuano",
|
||||||
"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)": "Truyền thống Trung Hoa)",
|
||||||
"Corsican": "Corsican",
|
"Corsican": "Corsican",
|
||||||
"Croatian": "Tiếng Croatia",
|
"Croatian": "Tiếng Croatia",
|
||||||
"Czech": "Tiếng Séc",
|
"Czech": "Tiếng Séc",
|
||||||
@ -219,22 +219,22 @@
|
|||||||
"Igbo": "Igbo",
|
"Igbo": "Igbo",
|
||||||
"Indonesian": "Tiếng Indonesia",
|
"Indonesian": "Tiếng Indonesia",
|
||||||
"Irish": "Tiếng Ailen",
|
"Irish": "Tiếng Ailen",
|
||||||
"Italian": "Tiếng Ý",
|
"Italian": "Người Ý",
|
||||||
"Japanese": "Tiếng Nhật",
|
"Japanese": "Tiếng Nhật",
|
||||||
"Javanese": "Tiếng Java",
|
"Javanese": "Tiếng Java",
|
||||||
"Kannada": "Tiếng Kannada",
|
"Kannada": "Tiếng Kannada",
|
||||||
"Kazakh": "Tiếng Kazakh",
|
"Kazakh": "Tiếng Kazakh",
|
||||||
"Khmer": "Tiếng Khmer",
|
"Khmer": "Tiếng Khmer",
|
||||||
"Korean": "Tiếng Hàn",
|
"Korean": "Hàn Quốc",
|
||||||
"Kurdish": "Tiếng Kurd",
|
"Kurdish": "Tiếng Kurd",
|
||||||
"Kyrgyz": "Tiếng Kyrgyz",
|
"Kyrgyz": "Kyrgyz",
|
||||||
"Lao": "Tiếng Lào",
|
"Lao": "Lào",
|
||||||
"Latin": "Tiếng Latin",
|
"Latin": "Latin",
|
||||||
"Latvian": "Tiếng Latvia",
|
"Latvian": "Tiếng Latvia",
|
||||||
"Lithuanian": "Tiếng Litva",
|
"Lithuanian": "Tiếng Litva",
|
||||||
"Luxembourgish": "Tiếng Luxembourg",
|
"Luxembourgish": "Tiếng Luxembourg",
|
||||||
"Macedonian": "Tiếng Macedonian",
|
"Macedonian": "Người Macedonian",
|
||||||
"Malagasy": "Tiếng Malagasy",
|
"Malagasy": "Malagasy",
|
||||||
"Malay": "Tiếng Mã Lai",
|
"Malay": "Tiếng Mã Lai",
|
||||||
"Malayalam": "Tiếng Malayalam",
|
"Malayalam": "Tiếng Malayalam",
|
||||||
"Maltese": "Cây nho",
|
"Maltese": "Cây nho",
|
||||||
@ -364,7 +364,7 @@
|
|||||||
"Import/export": "Xuất/nhập dữ liệu",
|
"Import/export": "Xuất/nhập dữ liệu",
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
"preferences_quality_dash_option_4320p": "4320p",
|
||||||
"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}} thuê bao",
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
"preferences_quality_dash_option_1440p": "1440p",
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
"preferences_quality_dash_option_480p": "480p",
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
"preferences_quality_dash_option_2160p": "2160p",
|
||||||
@ -383,9 +383,5 @@
|
|||||||
"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 cuối cùng ",
|
||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Xin chào! Có vẻ như bạn đã tắt JavaScript. Bấm vào đây để xem bình luận, lưu ý rằng thời gian tải có thể lâu hơn.",
|
"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)",
|
|
||||||
"generic_button_cancel": "Hủy",
|
|
||||||
"Chinese": "Tiếng Trung",
|
|
||||||
"generic_button_delete": "Xóa"
|
|
||||||
}
|
}
|
||||||
|
@ -460,15 +460,5 @@
|
|||||||
"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)"
|
||||||
"Import YouTube watch history (.json)": "导入 YouTube 观看历史(.json)",
|
|
||||||
"generic_button_cancel": "取消",
|
|
||||||
"playlist_button_add_items": "添加视频",
|
|
||||||
"generic_button_delete": "删除",
|
|
||||||
"channel_tab_podcasts_label": "播客",
|
|
||||||
"generic_button_edit": "编辑",
|
|
||||||
"generic_button_save": "保存",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"channel_tab_releases_label": "公告",
|
|
||||||
"generic_channels_count_0": "{{count}} 个频道"
|
|
||||||
}
|
}
|
||||||
|
@ -460,15 +460,5 @@
|
|||||||
"Song: ": "歌曲: ",
|
"Song: ": "歌曲: ",
|
||||||
"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)"
|
||||||
"Import YouTube watch history (.json)": "匯入 YouTube 觀看歷史 (.json)",
|
|
||||||
"generic_button_cancel": "取消",
|
|
||||||
"generic_button_edit": "編輯",
|
|
||||||
"generic_button_save": "儲存",
|
|
||||||
"generic_button_rss": "RSS",
|
|
||||||
"generic_button_delete": "刪除",
|
|
||||||
"playlist_button_add_items": "新增影片",
|
|
||||||
"channel_tab_podcasts_label": "Podcast",
|
|
||||||
"channel_tab_releases_label": "發布",
|
|
||||||
"generic_channels_count_0": "{{count}} 個頻道"
|
|
||||||
}
|
}
|
||||||
|
11
shard.lock
11
shard.lock
@ -1,9 +1,5 @@
|
|||||||
version: 2.0
|
version: 2.0
|
||||||
shards:
|
shards:
|
||||||
ameba:
|
|
||||||
git: https://github.com/crystal-ameba/ameba.git
|
|
||||||
version: 1.5.0
|
|
||||||
|
|
||||||
athena-negotiation:
|
athena-negotiation:
|
||||||
git: https://github.com/athena-framework/negotiation.git
|
git: https://github.com/athena-framework/negotiation.git
|
||||||
version: 0.1.1
|
version: 0.1.1
|
||||||
@ -28,6 +24,10 @@ shards:
|
|||||||
git: https://github.com/jeromegn/kilt.git
|
git: https://github.com/jeromegn/kilt.git
|
||||||
version: 0.6.1
|
version: 0.6.1
|
||||||
|
|
||||||
|
lsquic:
|
||||||
|
git: https://github.com/iv-org/lsquic.cr.git
|
||||||
|
version: 2.18.1-2
|
||||||
|
|
||||||
pg:
|
pg:
|
||||||
git: https://github.com/will/crystal-pg.git
|
git: https://github.com/will/crystal-pg.git
|
||||||
version: 0.24.0
|
version: 0.24.0
|
||||||
@ -48,3 +48,6 @@ shards:
|
|||||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||||
version: 0.18.0
|
version: 0.18.0
|
||||||
|
|
||||||
|
ameba:
|
||||||
|
git: https://github.com/crystal-ameba/ameba.git
|
||||||
|
version: 0.14.3
|
||||||
|
@ -3,7 +3,7 @@ version: 0.20.1
|
|||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Omar Roth <omarroth@protonmail.com>
|
- Omar Roth <omarroth@protonmail.com>
|
||||||
- Invidious team
|
- Invidous team
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
invidious:
|
invidious:
|
||||||
@ -25,6 +25,9 @@ dependencies:
|
|||||||
protodec:
|
protodec:
|
||||||
github: iv-org/protodec
|
github: iv-org/protodec
|
||||||
version: ~> 0.1.5
|
version: ~> 0.1.5
|
||||||
|
lsquic:
|
||||||
|
github: iv-org/lsquic.cr
|
||||||
|
version: ~> 2.18.1-2
|
||||||
athena-negotiation:
|
athena-negotiation:
|
||||||
github: athena-framework/negotiation
|
github: athena-framework/negotiation
|
||||||
version: ~> 0.1.1
|
version: ~> 0.1.1
|
||||||
@ -35,7 +38,7 @@ development_dependencies:
|
|||||||
version: ~> 0.10.4
|
version: ~> 0.10.4
|
||||||
ameba:
|
ameba:
|
||||||
github: crystal-ameba/ameba
|
github: crystal-ameba/ameba
|
||||||
version: ~> 1.5.0
|
version: ~> 0.14.3
|
||||||
|
|
||||||
crystal: ">= 1.0.0, < 2.0.0"
|
crystal: ">= 1.0.0, < 2.0.0"
|
||||||
|
|
||||||
|
@ -1,64 +0,0 @@
|
|||||||
require "../../spec_helper.cr"
|
|
||||||
|
|
||||||
MockLines = [
|
|
||||||
{
|
|
||||||
"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
|
|
||||||
it "correctly builds a vtt file" do
|
|
||||||
result = WebVTT.build do |vtt|
|
|
||||||
MockLines.each do |line|
|
|
||||||
vtt.cue(line["start_time"], line["end_time"], line["text"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(result).to eq([
|
|
||||||
"WEBVTT",
|
|
||||||
"",
|
|
||||||
"00:00:01.000 --> 00:00:02.000",
|
|
||||||
"Line 1",
|
|
||||||
"",
|
|
||||||
"00:00:02.000 --> 00:00:03.000",
|
|
||||||
"Line 2",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
].join('\n'))
|
|
||||||
end
|
|
||||||
|
|
||||||
it "correctly builds a vtt file with setting fields" do
|
|
||||||
setting_fields = {
|
|
||||||
"Kind" => "captions",
|
|
||||||
"Language" => "en",
|
|
||||||
}
|
|
||||||
|
|
||||||
result = WebVTT.build(setting_fields) do |vtt|
|
|
||||||
MockLines.each do |line|
|
|
||||||
vtt.cue(line["start_time"], line["end_time"], line["text"])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
expect(result).to eq([
|
|
||||||
"WEBVTT",
|
|
||||||
"Kind: captions",
|
|
||||||
"Language: en",
|
|
||||||
"",
|
|
||||||
"00:00:01.000 --> 00:00:02.000",
|
|
||||||
"Line 1",
|
|
||||||
"",
|
|
||||||
"00:00:02.000 --> 00:00:03.000",
|
|
||||||
"Line 2",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
].join('\n'))
|
|
||||||
end
|
|
||||||
end
|
|
@ -15,15 +15,12 @@ FORM_TESTS = {
|
|||||||
"ar" => I18next::Plurals::PluralForms::Special_Arabic,
|
"ar" => I18next::Plurals::PluralForms::Special_Arabic,
|
||||||
"be" => I18next::Plurals::PluralForms::Dual_Slavic,
|
"be" => I18next::Plurals::PluralForms::Dual_Slavic,
|
||||||
"cy" => I18next::Plurals::PluralForms::Special_Welsh,
|
"cy" => I18next::Plurals::PluralForms::Special_Welsh,
|
||||||
"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,
|
"fr" => I18next::Plurals::PluralForms::Single_gt_one,
|
||||||
"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,
|
||||||
"hr" => I18next::Plurals::PluralForms::Special_Hungarian_Serbian,
|
|
||||||
"is" => I18next::Plurals::PluralForms::Special_Icelandic,
|
"is" => I18next::Plurals::PluralForms::Special_Icelandic,
|
||||||
"it" => I18next::Plurals::PluralForms::Special_Spanish_Italian,
|
|
||||||
"jv" => I18next::Plurals::PluralForms::Special_Javanese,
|
"jv" => I18next::Plurals::PluralForms::Special_Javanese,
|
||||||
"kw" => I18next::Plurals::PluralForms::Special_Cornish,
|
"kw" => I18next::Plurals::PluralForms::Special_Cornish,
|
||||||
"lt" => I18next::Plurals::PluralForms::Special_Lithuanian,
|
"lt" => I18next::Plurals::PluralForms::Special_Lithuanian,
|
||||||
@ -34,12 +31,12 @@ FORM_TESTS = {
|
|||||||
"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::Single_gt_one,
|
||||||
"pt-BR" => I18next::Plurals::PluralForms::Special_French_Portuguese,
|
"pt-PT" => I18next::Plurals::PluralForms::Single_not_one,
|
||||||
|
"pt-BR" => I18next::Plurals::PluralForms::Single_gt_one,
|
||||||
"ro" => I18next::Plurals::PluralForms::Special_Romanian,
|
"ro" => I18next::Plurals::PluralForms::Special_Romanian,
|
||||||
|
"su" => I18next::Plurals::PluralForms::None,
|
||||||
"sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak,
|
"sk" => I18next::Plurals::PluralForms::Special_Czech_Slovak,
|
||||||
"sl" => I18next::Plurals::PluralForms::Special_Slovenian,
|
"sl" => I18next::Plurals::PluralForms::Special_Slovenian,
|
||||||
"su" => I18next::Plurals::PluralForms::None,
|
|
||||||
"sr" => I18next::Plurals::PluralForms::Special_Hungarian_Serbian,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SUFFIX_TESTS = {
|
SUFFIX_TESTS = {
|
||||||
@ -76,18 +73,10 @@ SUFFIX_TESTS = {
|
|||||||
{num: 1, suffix: ""},
|
{num: 1, suffix: ""},
|
||||||
{num: 10, suffix: "_plural"},
|
{num: 10, suffix: "_plural"},
|
||||||
],
|
],
|
||||||
"es" => [
|
"fr" => [
|
||||||
{num: 0, suffix: "_plural"},
|
{num: 0, suffix: ""},
|
||||||
{num: 1, suffix: ""},
|
{num: 1, suffix: ""},
|
||||||
{num: 10, suffix: "_plural"},
|
{num: 10, suffix: "_plural"},
|
||||||
{num: 6_000_000, suffix: "_plural"},
|
|
||||||
],
|
|
||||||
"fr" => [
|
|
||||||
{num: 0, suffix: "_0"},
|
|
||||||
{num: 1, suffix: "_0"},
|
|
||||||
{num: 10, suffix: "_2"},
|
|
||||||
{num: 4_000_000, suffix: "_1"},
|
|
||||||
{num: 6_260_000, suffix: "_2"},
|
|
||||||
],
|
],
|
||||||
"ga" => [
|
"ga" => [
|
||||||
{num: 1, suffix: "_0"},
|
{num: 1, suffix: "_0"},
|
||||||
@ -166,24 +155,31 @@ SUFFIX_TESTS = {
|
|||||||
{num: 1, suffix: "_0"},
|
{num: 1, suffix: "_0"},
|
||||||
{num: 5, suffix: "_2"},
|
{num: 5, suffix: "_2"},
|
||||||
],
|
],
|
||||||
"pt-BR" => [
|
"pt" => [
|
||||||
{num: 0, suffix: "_0"},
|
{num: 0, suffix: ""},
|
||||||
{num: 1, suffix: "_0"},
|
{num: 1, suffix: ""},
|
||||||
{num: 10, suffix: "_2"},
|
{num: 10, suffix: "_plural"},
|
||||||
{num: 42, suffix: "_2"},
|
],
|
||||||
{num: 9_000_000, suffix: "_1"},
|
"pt-PT" => [
|
||||||
],
|
{num: 0, suffix: "_plural"},
|
||||||
"pt-PT" => [
|
{num: 1, suffix: ""},
|
||||||
|
{num: 10, suffix: "_plural"},
|
||||||
|
],
|
||||||
|
"pt-BR" => [
|
||||||
{num: 0, suffix: ""},
|
{num: 0, suffix: ""},
|
||||||
{num: 1, suffix: ""},
|
{num: 1, suffix: ""},
|
||||||
{num: 10, suffix: "_plural"},
|
{num: 10, suffix: "_plural"},
|
||||||
{num: 9_000_000, suffix: "_plural"},
|
|
||||||
],
|
],
|
||||||
"ro" => [
|
"ro" => [
|
||||||
{num: 0, suffix: "_1"},
|
{num: 0, suffix: "_1"},
|
||||||
{num: 1, suffix: "_0"},
|
{num: 1, suffix: "_0"},
|
||||||
{num: 20, suffix: "_2"},
|
{num: 20, suffix: "_2"},
|
||||||
],
|
],
|
||||||
|
"su" => [
|
||||||
|
{num: 0, suffix: "_0"},
|
||||||
|
{num: 1, suffix: "_0"},
|
||||||
|
{num: 10, suffix: "_0"},
|
||||||
|
],
|
||||||
"sk" => [
|
"sk" => [
|
||||||
{num: 0, suffix: "_2"},
|
{num: 0, suffix: "_2"},
|
||||||
{num: 1, suffix: "_0"},
|
{num: 1, suffix: "_0"},
|
||||||
@ -195,18 +191,6 @@ SUFFIX_TESTS = {
|
|||||||
{num: 2, suffix: "_2"},
|
{num: 2, suffix: "_2"},
|
||||||
{num: 3, suffix: "_3"},
|
{num: 3, suffix: "_3"},
|
||||||
],
|
],
|
||||||
"su" => [
|
|
||||||
{num: 0, suffix: "_0"},
|
|
||||||
{num: 1, suffix: "_0"},
|
|
||||||
{num: 10, suffix: "_0"},
|
|
||||||
],
|
|
||||||
"sr" => [
|
|
||||||
{num: 1, suffix: "_0"},
|
|
||||||
{num: 51, suffix: "_0"},
|
|
||||||
{num: 32, suffix: "_1"},
|
|
||||||
{num: 100, suffix: "_2"},
|
|
||||||
{num: 100_000, suffix: "_2"},
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Spectator.describe "i18next_Plural_Resolver" do
|
Spectator.describe "i18next_Plural_Resolver" do
|
||||||
|
@ -3,6 +3,18 @@ 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")
|
||||||
|
@ -90,7 +90,7 @@ SOFTWARE = {
|
|||||||
"branch" => "#{CURRENT_BRANCH}",
|
"branch" => "#{CURRENT_BRANCH}",
|
||||||
}
|
}
|
||||||
|
|
||||||
YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
|
YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size, use_quic: CONFIG.use_quic)
|
||||||
|
|
||||||
# CLI
|
# CLI
|
||||||
Kemal.config.extra_options do |parser|
|
Kemal.config.extra_options do |parser|
|
||||||
@ -103,14 +103,6 @@ Kemal.config.extra_options do |parser|
|
|||||||
exit
|
exit
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
parser.on("-f THREADS", "--feed-threads=THREADS", "Number of threads for refreshing feeds (default: #{CONFIG.feed_threads})") do |number|
|
|
||||||
begin
|
|
||||||
CONFIG.feed_threads = number.to_i
|
|
||||||
rescue ex
|
|
||||||
puts "THREADS must be integer"
|
|
||||||
exit
|
|
||||||
end
|
|
||||||
end
|
|
||||||
parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: #{CONFIG.output})") do |output|
|
parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: #{CONFIG.output})") do |output|
|
||||||
CONFIG.output = output
|
CONFIG.output = output
|
||||||
end
|
end
|
||||||
@ -159,10 +151,6 @@ if CONFIG.channel_threads > 0
|
|||||||
Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB)
|
Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB)
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.feed_threads > 0
|
|
||||||
Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB)
|
|
||||||
end
|
|
||||||
|
|
||||||
DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling)
|
DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling)
|
||||||
if CONFIG.decrypt_polling
|
if CONFIG.decrypt_polling
|
||||||
Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new
|
Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new
|
||||||
|
@ -18,8 +18,8 @@ record AboutChannel,
|
|||||||
|
|
||||||
def get_about_info(ucid, locale) : AboutChannel
|
def get_about_info(ucid, locale) : AboutChannel
|
||||||
begin
|
begin
|
||||||
# Fetch channel information from channel home page
|
# "EgVhYm91dA==" is the base64-encoded protobuf object {"2:string":"about"}
|
||||||
initdata = YoutubeAPI.browse(browse_id: ucid, params: "")
|
initdata = YoutubeAPI.browse(browse_id: ucid, params: "EgVhYm91dA==")
|
||||||
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).splat}}
|
{{*@type.instance_vars.map(&.name)}}
|
||||||
}
|
}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
|
@ -24,33 +24,7 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
|
|||||||
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode)
|
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
|
def extract_channel_community(items, *, ucid, locale, format, thin_mode)
|
||||||
object = {
|
|
||||||
"2:string" => "community",
|
|
||||||
"25:embedded" => {
|
|
||||||
"22:string" => post_id.to_s,
|
|
||||||
},
|
|
||||||
"45:embedded" => {
|
|
||||||
"2:varint" => 1_i64,
|
|
||||||
"3:varint" => 1_i64,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
params = object.try { |i| Protodec::Any.cast_json(i) }
|
|
||||||
.try { |i| Protodec::Any.from_json(i) }
|
|
||||||
.try { |i| Base64.urlsafe_encode(i) }
|
|
||||||
.try { |i| URI.encode_www_form(i) }
|
|
||||||
|
|
||||||
initial_data = YoutubeAPI.browse(ucid, params: params)
|
|
||||||
|
|
||||||
items = [] of JSON::Any
|
|
||||||
extract_items(initial_data) do |item|
|
|
||||||
items << item
|
|
||||||
end
|
|
||||||
|
|
||||||
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode, is_single_post: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_single_post : Bool = false)
|
|
||||||
if message = items[0]["messageRenderer"]?
|
if message = items[0]["messageRenderer"]?
|
||||||
error_message = (message["text"]["simpleText"]? ||
|
error_message = (message["text"]["simpleText"]? ||
|
||||||
message["text"]["runs"]?.try &.[0]?.try &.["text"]?)
|
message["text"]["runs"]?.try &.[0]?.try &.["text"]?)
|
||||||
@ -65,9 +39,6 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
|
|||||||
response = JSON.build do |json|
|
response = JSON.build do |json|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "authorId", ucid
|
json.field "authorId", ucid
|
||||||
if is_single_post
|
|
||||||
json.field "singlePost", true
|
|
||||||
end
|
|
||||||
json.field "comments" do
|
json.field "comments" do
|
||||||
json.array do
|
json.array do
|
||||||
items.each do |post|
|
items.each do |post|
|
||||||
@ -269,10 +240,8 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if !is_single_post
|
if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token")
|
||||||
if cont = items.dig?(-1, "continuationItemRenderer", "continuationEndpoint", "continuationCommand", "token")
|
json.field "continuation", extract_channel_community_cursor(cont.as_s)
|
||||||
json.field "continuation", extract_channel_community_cursor(cont.as_s)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -62,6 +62,12 @@ 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
|
||||||
|
|
||||||
|
@ -13,51 +13,6 @@ module Invidious::Comments
|
|||||||
|
|
||||||
client_config = YoutubeAPI::ClientConfig.new(region: region)
|
client_config = YoutubeAPI::ClientConfig.new(region: region)
|
||||||
response = YoutubeAPI.next(continuation: ctoken, client_config: client_config)
|
response = YoutubeAPI.next(continuation: ctoken, client_config: client_config)
|
||||||
return parse_youtube(id, response, format, locale, thin_mode, sort_by)
|
|
||||||
end
|
|
||||||
|
|
||||||
def fetch_community_post_comments(ucid, post_id)
|
|
||||||
object = {
|
|
||||||
"2:string" => "community",
|
|
||||||
"25:embedded" => {
|
|
||||||
"22:string" => post_id,
|
|
||||||
},
|
|
||||||
"45:embedded" => {
|
|
||||||
"2:varint" => 1_i64,
|
|
||||||
"3:varint" => 1_i64,
|
|
||||||
},
|
|
||||||
"53:embedded" => {
|
|
||||||
"4:embedded" => {
|
|
||||||
"6:varint" => 0_i64,
|
|
||||||
"27:varint" => 1_i64,
|
|
||||||
"29:string" => post_id,
|
|
||||||
"30:string" => ucid,
|
|
||||||
},
|
|
||||||
"8:string" => "comments-section",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
object_parsed = object.try { |i| Protodec::Any.cast_json(i) }
|
|
||||||
.try { |i| Protodec::Any.from_json(i) }
|
|
||||||
.try { |i| Base64.urlsafe_encode(i) }
|
|
||||||
|
|
||||||
object2 = {
|
|
||||||
"80226972:embedded" => {
|
|
||||||
"2:string" => ucid,
|
|
||||||
"3:string" => object_parsed,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation = object2.try { |i| Protodec::Any.cast_json(i) }
|
|
||||||
.try { |i| Protodec::Any.from_json(i) }
|
|
||||||
.try { |i| Base64.urlsafe_encode(i) }
|
|
||||||
.try { |i| URI.encode_www_form(i) }
|
|
||||||
|
|
||||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
|
||||||
return initial_data
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", isPost = false)
|
|
||||||
contents = nil
|
contents = nil
|
||||||
|
|
||||||
if on_response_received_endpoints = response["onResponseReceivedEndpoints"]?
|
if on_response_received_endpoints = response["onResponseReceivedEndpoints"]?
|
||||||
@ -113,11 +68,7 @@ module Invidious::Comments
|
|||||||
json.field "commentCount", comment_count
|
json.field "commentCount", comment_count
|
||||||
end
|
end
|
||||||
|
|
||||||
if isPost
|
json.field "videoId", id
|
||||||
json.field "postId", id
|
|
||||||
else
|
|
||||||
json.field "videoId", id
|
|
||||||
end
|
|
||||||
|
|
||||||
json.field "comments" do
|
json.field "comments" do
|
||||||
json.array do
|
json.array do
|
||||||
|
@ -48,7 +48,7 @@ struct ConfigPreferences
|
|||||||
def to_tuple
|
def to_tuple
|
||||||
{% begin %}
|
{% begin %}
|
||||||
{
|
{
|
||||||
{{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}}
|
{{*@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }}}
|
||||||
}
|
}
|
||||||
{% end %}
|
{% end %}
|
||||||
end
|
end
|
||||||
@ -62,8 +62,6 @@ class Config
|
|||||||
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
|
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
|
||||||
@[YAML::Field(converter: Preferences::TimeSpanConverter)]
|
@[YAML::Field(converter: Preferences::TimeSpanConverter)]
|
||||||
property channel_refresh_interval : Time::Span = 30.minutes
|
property channel_refresh_interval : Time::Span = 30.minutes
|
||||||
# Number of threads to use for updating feeds
|
|
||||||
property feed_threads : Int32 = 1
|
|
||||||
# Log file path or STDOUT
|
# Log file path or STDOUT
|
||||||
property output : String = "STDOUT"
|
property output : String = "STDOUT"
|
||||||
# Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
|
# Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
|
||||||
@ -126,13 +124,16 @@ class Config
|
|||||||
property host_binding : String = "0.0.0.0"
|
property host_binding : String = "0.0.0.0"
|
||||||
# Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
|
# Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
|
||||||
property pool_size : Int32 = 100
|
property pool_size : Int32 = 100
|
||||||
|
# Use quic transport for youtube api
|
||||||
# Use Innertube's transcripts API instead of timedtext for closed captions
|
property use_quic : Bool = false
|
||||||
property use_innertube_for_captions : Bool = false
|
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -23,24 +23,6 @@ module Invidious::Frontend::Comments
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
END_HTML
|
END_HTML
|
||||||
elsif comments["authorId"]? && !comments["singlePost"]?
|
|
||||||
# for posts we should display a link to the post
|
|
||||||
replies_count_text = translate_count(locale,
|
|
||||||
"comments_view_x_replies",
|
|
||||||
child["replyCount"].as_i64 || 0,
|
|
||||||
NumberFormatting::Separator
|
|
||||||
)
|
|
||||||
|
|
||||||
replies_html = <<-END_HTML
|
|
||||||
<div class="pure-g">
|
|
||||||
<div class="pure-u-1-24"></div>
|
|
||||||
<div class="pure-u-23-24">
|
|
||||||
<p>
|
|
||||||
<a href="/post/#{child["commentId"]}?ucid=#{comments["authorId"]}">#{replies_count_text}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
END_HTML
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if !thin_mode
|
if !thin_mode
|
||||||
|
@ -7,7 +7,7 @@ module Invidious::Frontend::WatchPage
|
|||||||
getter full_videos : Array(Hash(String, JSON::Any))
|
getter full_videos : Array(Hash(String, JSON::Any))
|
||||||
getter video_streams : Array(Hash(String, JSON::Any))
|
getter video_streams : Array(Hash(String, JSON::Any))
|
||||||
getter audio_streams : Array(Hash(String, JSON::Any))
|
getter audio_streams : Array(Hash(String, JSON::Any))
|
||||||
getter captions : Array(Invidious::Videos::Captions::Metadata)
|
getter captions : Array(Invidious::Videos::Caption)
|
||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
@full_videos,
|
@full_videos,
|
||||||
@ -42,7 +42,8 @@ module Invidious::Frontend::WatchPage
|
|||||||
str << translate(locale, "Download as: ")
|
str << translate(locale, "Download as: ")
|
||||||
str << "</label>\n"
|
str << "</label>\n"
|
||||||
|
|
||||||
str << "\t\t<select name='download_widget' id='download_widget'>\n"
|
# TODO: remove inline style
|
||||||
|
str << "\t\t<select style=\"width:100%\" name='download_widget' id='download_widget'>\n"
|
||||||
|
|
||||||
# Non-DASH videos (audio+video)
|
# Non-DASH videos (audio+video)
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
macro error_template(*args)
|
macro error_template(*args)
|
||||||
error_template_helper(env, {{args.splat}})
|
error_template_helper(env, {{*args}})
|
||||||
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.splat}})
|
error_atom_helper(env, {{*args}})
|
||||||
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.splat}})
|
error_json_helper(env, {{*args}})
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_json_helper(
|
def error_json_helper(
|
||||||
|
@ -208,20 +208,3 @@ 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
|
|
||||||
|
@ -35,27 +35,19 @@ module I18next::Plurals
|
|||||||
Special_Slovenian = 21
|
Special_Slovenian = 21
|
||||||
Special_Hebrew = 22
|
Special_Hebrew = 22
|
||||||
Special_Odia = 23
|
Special_Odia = 23
|
||||||
|
|
||||||
# Mixed v3/v4 rules in Weblate
|
|
||||||
# `es`, `pt` and `pt-PT` doesn't seem to have been refreshed
|
|
||||||
# by weblate yet, but I suspect it will happen one day.
|
|
||||||
# See: https://github.com/translate/translate/issues/4873
|
|
||||||
Special_French_Portuguese
|
|
||||||
Special_Hungarian_Serbian
|
|
||||||
Special_Spanish_Italian
|
|
||||||
end
|
end
|
||||||
|
|
||||||
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", "fil", "fr", "gun", "ln", "mfe", "mg",
|
||||||
"mi", "oc", "pt", "tg", "tl", "ti", "tr", "uz", "wa",
|
"mi", "oc", "pt", "pt-BR", "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", "es", "et", "eu", "fi", "fo", "fur", "fy", "gl", "gu", "ha", "hi",
|
||||||
"hu", "hy", "ia", "kk", "kn", "ku", "lb", "mai", "ml", "mn", "mr",
|
"hu", "hy", "ia", "it", "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", "pt-PT", "rm", "sco", "se", "si", "so", "son", "sq", "sv", "sw",
|
||||||
"ta", "te", "tk", "ur", "yo",
|
"ta", "te", "tk", "ur", "yo",
|
||||||
],
|
],
|
||||||
PluralForms::None => [
|
PluralForms::None => [
|
||||||
@ -63,7 +55,7 @@ module I18next::Plurals
|
|||||||
"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 => [
|
||||||
"be", "bs", "cnr", "dz", "ru", "uk",
|
"be", "bs", "cnr", "dz", "hr", "ru", "sr", "uk",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,12 +81,6 @@ module I18next::Plurals
|
|||||||
"ro" => PluralForms::Special_Romanian,
|
"ro" => PluralForms::Special_Romanian,
|
||||||
"sk" => PluralForms::Special_Czech_Slovak,
|
"sk" => PluralForms::Special_Czech_Slovak,
|
||||||
"sl" => PluralForms::Special_Slovenian,
|
"sl" => PluralForms::Special_Slovenian,
|
||||||
# Mixed v3/v4 rules
|
|
||||||
"fr" => PluralForms::Special_French_Portuguese,
|
|
||||||
"hr" => PluralForms::Special_Hungarian_Serbian,
|
|
||||||
"it" => PluralForms::Special_Spanish_Italian,
|
|
||||||
"pt-BR" => PluralForms::Special_French_Portuguese,
|
|
||||||
"sr" => PluralForms::Special_Hungarian_Serbian,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# These are the v1 and v2 compatible suffixes.
|
# These are the v1 and v2 compatible suffixes.
|
||||||
@ -164,8 +150,9 @@ module I18next::Plurals
|
|||||||
end
|
end
|
||||||
|
|
||||||
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$/)
|
# except for pt-BR and pt-PT which needs to be kept as-is.
|
||||||
|
if !locale.matches?(/^pt-(BR|PT)$/)
|
||||||
locale = locale.split('-')[0]
|
locale = locale.split('-')[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -259,10 +246,6 @@ module I18next::Plurals
|
|||||||
when .special_slovenian? then return special_slovenian(count)
|
when .special_slovenian? then return special_slovenian(count)
|
||||||
when .special_hebrew? then return special_hebrew(count)
|
when .special_hebrew? then return special_hebrew(count)
|
||||||
when .special_odia? then return special_odia(count)
|
when .special_odia? then return special_odia(count)
|
||||||
# Mixed v3/v4 forms
|
|
||||||
when .special_spanish_italian? then return special_cldr_Spanish_Italian(count)
|
|
||||||
when .special_french_portuguese? then return special_cldr_French_Portuguese(count)
|
|
||||||
when .special_hungarian_serbian? then return special_cldr_Hungarian_Serbian(count)
|
|
||||||
else
|
else
|
||||||
# default, if nothing matched above
|
# default, if nothing matched above
|
||||||
return 0_u8
|
return 0_u8
|
||||||
@ -524,42 +507,5 @@ module I18next::Plurals
|
|||||||
def self.special_odia(count : Int) : UInt8
|
def self.special_odia(count : Int) : UInt8
|
||||||
return (count == 1) ? 0_u8 : 1_u8
|
return (count == 1) ? 0_u8 : 1_u8
|
||||||
end
|
end
|
||||||
|
|
||||||
# -------------------
|
|
||||||
# "v3.5" rules
|
|
||||||
# -------------------
|
|
||||||
|
|
||||||
# Plural form for Spanish & Italian languages
|
|
||||||
#
|
|
||||||
# This rule is mostly compliant to CLDR v42
|
|
||||||
#
|
|
||||||
def self.special_cldr_Spanish_Italian(count : Int) : UInt8
|
|
||||||
return 0_u8 if (count == 1) # one
|
|
||||||
return 1_u8 if (count != 0 && count % 1_000_000 == 0) # many
|
|
||||||
return 2_u8 # other
|
|
||||||
end
|
|
||||||
|
|
||||||
# Plural form for French and Portuguese
|
|
||||||
#
|
|
||||||
# This rule is mostly compliant to CLDR v42
|
|
||||||
#
|
|
||||||
def self.special_cldr_French_Portuguese(count : Int) : UInt8
|
|
||||||
return 0_u8 if (count == 0 || count == 1) # one
|
|
||||||
return 1_u8 if (count % 1_000_000 == 0) # many
|
|
||||||
return 2_u8 # other
|
|
||||||
end
|
|
||||||
|
|
||||||
# Plural form for Hungarian and Serbian
|
|
||||||
#
|
|
||||||
# This rule is mostly compliant to CLDR v42
|
|
||||||
#
|
|
||||||
def self.special_cldr_Hungarian_Serbian(count : Int) : UInt8
|
|
||||||
n_mod_10 = count % 10
|
|
||||||
n_mod_100 = count % 100
|
|
||||||
|
|
||||||
return 0_u8 if (n_mod_10 == 1 && n_mod_100 != 11) # one
|
|
||||||
return 1_u8 if (2 <= n_mod_10 <= 4 && (n_mod_100 < 12 || 14 < n_mod_100)) # few
|
|
||||||
return 2_u8 # other
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -186,7 +186,6 @@ struct SearchChannel
|
|||||||
property author_thumbnail : String
|
property author_thumbnail : String
|
||||||
property subscriber_count : Int32
|
property subscriber_count : Int32
|
||||||
property video_count : Int32
|
property video_count : Int32
|
||||||
property channel_handle : String?
|
|
||||||
property description_html : String
|
property description_html : String
|
||||||
property auto_generated : Bool
|
property auto_generated : Bool
|
||||||
property author_verified : Bool
|
property author_verified : Bool
|
||||||
@ -215,7 +214,6 @@ struct SearchChannel
|
|||||||
json.field "autoGenerated", self.auto_generated
|
json.field "autoGenerated", self.auto_generated
|
||||||
json.field "subCount", self.subscriber_count
|
json.field "subCount", self.subscriber_count
|
||||||
json.field "videoCount", self.video_count
|
json.field "videoCount", self.video_count
|
||||||
json.field "channelHandle", self.channel_handle
|
|
||||||
|
|
||||||
json.field "description", html_to_content(self.description_html)
|
json.field "description", html_to_content(self.description_html)
|
||||||
json.field "descriptionHtml", self.description_html
|
json.field "descriptionHtml", self.description_html
|
||||||
@ -234,25 +232,6 @@ struct SearchChannel
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct SearchHashtag
|
|
||||||
include DB::Serializable
|
|
||||||
|
|
||||||
property title : String
|
|
||||||
property url : String
|
|
||||||
property video_count : Int64
|
|
||||||
property channel_count : Int64
|
|
||||||
|
|
||||||
def to_json(locale : String?, json : JSON::Builder)
|
|
||||||
json.object do
|
|
||||||
json.field "type", "hashtag"
|
|
||||||
json.field "title", self.title
|
|
||||||
json.field "url", self.url
|
|
||||||
json.field "videoCount", self.video_count
|
|
||||||
json.field "channelCount", self.channel_count
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Category
|
class Category
|
||||||
include DB::Serializable
|
include DB::Serializable
|
||||||
|
|
||||||
@ -295,4 +274,4 @@ struct Continuation
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category
|
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | Category
|
||||||
|
@ -1,67 +0,0 @@
|
|||||||
# Namespace for logic relating to generating WebVTT files
|
|
||||||
#
|
|
||||||
# Probably not compliant to WebVTT's specs but it is enough for Invidious.
|
|
||||||
module WebVTT
|
|
||||||
# A WebVTT builder generates WebVTT files
|
|
||||||
private class Builder
|
|
||||||
def initialize(@io : IO)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Writes an vtt cue with the specified time stamp and contents
|
|
||||||
def cue(start_time : Time::Span, end_time : Time::Span, text : String)
|
|
||||||
timestamp(start_time, end_time)
|
|
||||||
@io << text
|
|
||||||
@io << "\n\n"
|
|
||||||
end
|
|
||||||
|
|
||||||
private def timestamp(start_time : Time::Span, end_time : Time::Span)
|
|
||||||
timestamp_component(start_time)
|
|
||||||
@io << " --> "
|
|
||||||
timestamp_component(end_time)
|
|
||||||
|
|
||||||
@io << '\n'
|
|
||||||
end
|
|
||||||
|
|
||||||
private def timestamp_component(timestamp : Time::Span)
|
|
||||||
@io << timestamp.hours.to_s.rjust(2, '0')
|
|
||||||
@io << ':' << timestamp.minutes.to_s.rjust(2, '0')
|
|
||||||
@io << ':' << timestamp.seconds.to_s.rjust(2, '0')
|
|
||||||
@io << '.' << timestamp.milliseconds.to_s.rjust(3, '0')
|
|
||||||
end
|
|
||||||
|
|
||||||
def document(setting_fields : Hash(String, String)? = nil, &)
|
|
||||||
@io << "WEBVTT\n"
|
|
||||||
|
|
||||||
if setting_fields
|
|
||||||
setting_fields.each do |name, value|
|
|
||||||
@io << name << ": " << value << '\n'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@io << '\n'
|
|
||||||
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Returns the resulting `String` of writing WebVTT to the yielded `WebVTT::Builder`
|
|
||||||
#
|
|
||||||
# ```
|
|
||||||
# string = WebVTT.build do |vtt|
|
|
||||||
# vtt.cue(Time::Span.new(seconds: 1), Time::Span.new(seconds: 2), "Line 1")
|
|
||||||
# vtt.cue(Time::Span.new(seconds: 2), Time::Span.new(seconds: 3), "Line 2")
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# string # => "WEBVTT\n\n00:00:01.000 --> 00:00:02.000\nLine 1\n\n00:00:02.000 --> 00:00:03.000\nLine 2\n\n"
|
|
||||||
# ```
|
|
||||||
#
|
|
||||||
# Accepts an optional settings fields hash to add settings attribute to the resulting vtt file.
|
|
||||||
def self.build(setting_fields : Hash(String, String)? = nil, &)
|
|
||||||
String.build do |str|
|
|
||||||
builder = Builder.new(str)
|
|
||||||
builder.document(setting_fields) do
|
|
||||||
yield builder
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
135
src/invidious/jobs/bypass_captcha_job.cr
Normal file
135
src/invidious/jobs/bypass_captcha_job.cr
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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
|
@ -1,75 +0,0 @@
|
|||||||
class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob
|
|
||||||
private getter db : DB::Database
|
|
||||||
|
|
||||||
def initialize(@db)
|
|
||||||
end
|
|
||||||
|
|
||||||
def begin
|
|
||||||
max_fibers = CONFIG.feed_threads
|
|
||||||
active_fibers = 0
|
|
||||||
active_channel = ::Channel(Bool).new
|
|
||||||
|
|
||||||
loop do
|
|
||||||
db.query("SELECT email FROM users WHERE feed_needs_update = true OR feed_needs_update IS NULL") do |rs|
|
|
||||||
rs.each do
|
|
||||||
email = rs.read(String)
|
|
||||||
view_name = "subscriptions_#{sha256(email)}"
|
|
||||||
|
|
||||||
if active_fibers >= max_fibers
|
|
||||||
if active_channel.receive
|
|
||||||
active_fibers -= 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
active_fibers += 1
|
|
||||||
spawn do
|
|
||||||
begin
|
|
||||||
# Drop outdated views
|
|
||||||
column_array = Invidious::Database.get_column_array(db, view_name)
|
|
||||||
ChannelVideo.type_array.each_with_index do |name, i|
|
|
||||||
if name != column_array[i]?
|
|
||||||
LOGGER.info("RefreshFeedsJob: DROP MATERIALIZED VIEW #{view_name}")
|
|
||||||
db.exec("DROP MATERIALIZED VIEW #{view_name}")
|
|
||||||
raise "view does not exist"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if !db.query_one("SELECT pg_get_viewdef('#{view_name}')", as: String).includes? "WHERE ((cv.ucid = ANY (u.subscriptions))"
|
|
||||||
LOGGER.info("RefreshFeedsJob: Materialized view #{view_name} is out-of-date, recreating...")
|
|
||||||
db.exec("DROP MATERIALIZED VIEW #{view_name}")
|
|
||||||
end
|
|
||||||
|
|
||||||
db.exec("REFRESH MATERIALIZED VIEW #{view_name}")
|
|
||||||
db.exec("UPDATE users SET feed_needs_update = false WHERE email = $1", email)
|
|
||||||
rescue ex
|
|
||||||
# Rename old views
|
|
||||||
begin
|
|
||||||
legacy_view_name = "subscriptions_#{sha256(email)[0..7]}"
|
|
||||||
|
|
||||||
db.exec("SELECT * FROM #{legacy_view_name} LIMIT 0")
|
|
||||||
LOGGER.info("RefreshFeedsJob: RENAME MATERIALIZED VIEW #{legacy_view_name}")
|
|
||||||
db.exec("ALTER MATERIALIZED VIEW #{legacy_view_name} RENAME TO #{view_name}")
|
|
||||||
rescue ex
|
|
||||||
begin
|
|
||||||
# While iterating through, we may have an email stored from a deleted account
|
|
||||||
if db.query_one?("SELECT true FROM users WHERE email = $1", email, as: Bool)
|
|
||||||
LOGGER.info("RefreshFeedsJob: CREATE #{view_name}")
|
|
||||||
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(email)}")
|
|
||||||
db.exec("UPDATE users SET feed_needs_update = false WHERE email = $1", email)
|
|
||||||
end
|
|
||||||
rescue ex
|
|
||||||
LOGGER.error("RefreshFeedJobs: REFRESH #{email} : #{ex.message}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
active_channel.send(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
sleep 5.seconds
|
|
||||||
Fiber.yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -18,13 +18,6 @@ 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
|
||||||
@ -37,7 +30,7 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
|
|||||||
|
|
||||||
loop do
|
loop do
|
||||||
refresh_stats
|
refresh_stats
|
||||||
sleep 10.minute
|
sleep 1.minute
|
||||||
Fiber.yield
|
Fiber.yield
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -63,8 +56,5 @@ class Invidious::Jobs::StatisticsRefreshJob < Invidious::Jobs::BaseJob
|
|||||||
"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
|
||||||
|
@ -39,7 +39,6 @@ module Invidious::JSONify::APIv1
|
|||||||
json.field "author", video.author
|
json.field "author", video.author
|
||||||
json.field "authorId", video.ucid
|
json.field "authorId", video.ucid
|
||||||
json.field "authorUrl", "/channel/#{video.ucid}"
|
json.field "authorUrl", "/channel/#{video.ucid}"
|
||||||
json.field "authorVerified", video.author_verified
|
|
||||||
|
|
||||||
json.field "authorThumbnails" do
|
json.field "authorThumbnails" do
|
||||||
json.array do
|
json.array do
|
||||||
|
@ -89,7 +89,6 @@ struct Playlist
|
|||||||
property views : Int64
|
property views : Int64
|
||||||
property updated : Time
|
property updated : Time
|
||||||
property thumbnail : String?
|
property thumbnail : String?
|
||||||
property subtitle : String?
|
|
||||||
|
|
||||||
def to_json(offset, json : JSON::Builder, video_id : String? = nil)
|
def to_json(offset, json : JSON::Builder, video_id : String? = nil)
|
||||||
json.object do
|
json.object do
|
||||||
@ -101,7 +100,6 @@ struct Playlist
|
|||||||
json.field "author", self.author
|
json.field "author", self.author
|
||||||
json.field "authorId", self.ucid
|
json.field "authorId", self.ucid
|
||||||
json.field "authorUrl", "/channel/#{self.ucid}"
|
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||||
json.field "subtitle", self.subtitle
|
|
||||||
|
|
||||||
json.field "authorThumbnails" do
|
json.field "authorThumbnails" do
|
||||||
json.array do
|
json.array do
|
||||||
@ -358,8 +356,6 @@ def fetch_playlist(plid : String)
|
|||||||
updated = Time.utc
|
updated = Time.utc
|
||||||
video_count = 0
|
video_count = 0
|
||||||
|
|
||||||
subtitle = extract_text(initial_data.dig?("header", "playlistHeaderRenderer", "subtitle"))
|
|
||||||
|
|
||||||
playlist_info["stats"]?.try &.as_a.each do |stat|
|
playlist_info["stats"]?.try &.as_a.each do |stat|
|
||||||
text = stat["runs"]?.try &.as_a.map(&.["text"].as_s).join("") || stat["simpleText"]?.try &.as_s
|
text = stat["runs"]?.try &.as_a.map(&.["text"].as_s).join("") || stat["simpleText"]?.try &.as_s
|
||||||
next if !text
|
next if !text
|
||||||
@ -401,7 +397,6 @@ def fetch_playlist(plid : String)
|
|||||||
views: views,
|
views: views,
|
||||||
updated: updated,
|
updated: updated,
|
||||||
thumbnail: thumbnail,
|
thumbnail: thumbnail,
|
||||||
subtitle: subtitle,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -123,10 +123,8 @@ module Invidious::Routes::Account
|
|||||||
return error_template(400, ex)
|
return error_template(400, ex)
|
||||||
end
|
end
|
||||||
|
|
||||||
view_name = "subscriptions_#{sha256(user.email)}"
|
|
||||||
Invidious::Database::Users.delete(user)
|
Invidious::Database::Users.delete(user)
|
||||||
Invidious::Database::SessionIDs.delete(email: user.email)
|
Invidious::Database::SessionIDs.delete(email: user.email)
|
||||||
PG_DB.exec("DROP MATERIALIZED VIEW #{view_name}")
|
|
||||||
|
|
||||||
env.request.cookies.each do |cookie|
|
env.request.cookies.each do |cookie|
|
||||||
cookie.expires = Time.utc(1990, 1, 1)
|
cookie.expires = Time.utc(1990, 1, 1)
|
||||||
|
@ -343,59 +343,6 @@ module Invidious::Routes::API::V1::Channels
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.post(env)
|
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
id = env.params.url["id"].to_s
|
|
||||||
ucid = env.params.query["ucid"]?
|
|
||||||
|
|
||||||
thin_mode = env.params.query["thin_mode"]?
|
|
||||||
thin_mode = thin_mode == "true"
|
|
||||||
|
|
||||||
format = env.params.query["format"]?
|
|
||||||
format ||= "json"
|
|
||||||
|
|
||||||
if ucid.nil?
|
|
||||||
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
|
||||||
return error_json(400, "Invalid post ID") if response["error"]?
|
|
||||||
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
|
|
||||||
else
|
|
||||||
ucid = ucid.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
|
||||||
fetch_channel_community_post(ucid, id, locale, format, thin_mode)
|
|
||||||
rescue ex
|
|
||||||
return error_json(500, ex)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.post_comments(env)
|
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
|
||||||
|
|
||||||
env.response.content_type = "application/json"
|
|
||||||
|
|
||||||
id = env.params.url["id"]
|
|
||||||
|
|
||||||
thin_mode = env.params.query["thin_mode"]?
|
|
||||||
thin_mode = thin_mode == "true"
|
|
||||||
|
|
||||||
format = env.params.query["format"]?
|
|
||||||
format ||= "json"
|
|
||||||
|
|
||||||
continuation = env.params.query["continuation"]?
|
|
||||||
|
|
||||||
case continuation
|
|
||||||
when nil, ""
|
|
||||||
ucid = env.params.query["ucid"]
|
|
||||||
comments = Comments.fetch_community_post_comments(ucid, id)
|
|
||||||
else
|
|
||||||
comments = YoutubeAPI.browse(continuation: continuation)
|
|
||||||
end
|
|
||||||
return Comments.parse_youtube(id, comments, format, locale, thin_mode, isPost: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.channels(env)
|
def self.channels(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
ucid = env.params.url["ucid"]
|
ucid = env.params.url["ucid"]
|
||||||
|
@ -6,22 +6,6 @@ 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
|
||||||
@ -178,20 +162,17 @@ module Invidious::Routes::API::V1::Misc
|
|||||||
resolved_url = YoutubeAPI.resolve_url(url.as(String))
|
resolved_url = YoutubeAPI.resolve_url(url.as(String))
|
||||||
endpoint = resolved_url["endpoint"]
|
endpoint = resolved_url["endpoint"]
|
||||||
pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || ""
|
pageType = endpoint.dig?("commandMetadata", "webCommandMetadata", "webPageType").try &.as_s || ""
|
||||||
if pageType == "WEB_PAGE_TYPE_UNKNOWN"
|
if resolved_ucid = endpoint.dig?("watchEndpoint", "videoId")
|
||||||
|
elsif resolved_ucid = endpoint.dig?("browseEndpoint", "browseId")
|
||||||
|
elsif pageType == "WEB_PAGE_TYPE_UNKNOWN"
|
||||||
return error_json(400, "Unknown url")
|
return error_json(400, "Unknown url")
|
||||||
end
|
end
|
||||||
|
|
||||||
sub_endpoint = endpoint["watchEndpoint"]? || endpoint["browseEndpoint"]? || endpoint
|
|
||||||
params = sub_endpoint.try &.dig?("params")
|
|
||||||
rescue ex
|
rescue ex
|
||||||
return error_json(500, ex)
|
return error_json(500, ex)
|
||||||
end
|
end
|
||||||
JSON.build do |json|
|
JSON.build do |json|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]?
|
json.field "ucid", resolved_ucid.try &.as_s || ""
|
||||||
json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]?
|
|
||||||
json.field "params", params.try &.as_s
|
|
||||||
json.field "pageType", pageType
|
json.field "pageType", pageType
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -87,67 +87,70 @@ module Invidious::Routes::API::V1::Videos
|
|||||||
caption = caption[0]
|
caption = caption[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.use_innertube_for_captions
|
url = URI.parse("#{caption.base_url}&tlang=#{tlang}").request_target
|
||||||
params = Invidious::Videos::Transcript.generate_param(id, caption.language_code, caption.auto_generated)
|
|
||||||
initial_data = YoutubeAPI.get_transcript(params)
|
|
||||||
|
|
||||||
webvtt = Invidious::Videos::Transcript.convert_transcripts_to_vtt(initial_data, caption.language_code)
|
# Auto-generated captions often have cues that aren't aligned properly with the video,
|
||||||
else
|
# as well as some other markup that makes it cumbersome, so we try to fix that here
|
||||||
# Timedtext API handling
|
if caption.name.includes? "auto-generated"
|
||||||
url = URI.parse("#{caption.base_url}&tlang=#{tlang}").request_target
|
caption_xml = YT_POOL.client &.get(url).body
|
||||||
|
|
||||||
# Auto-generated captions often have cues that aren't aligned properly with the video,
|
if caption_xml.starts_with?("<?xml")
|
||||||
# as well as some other markup that makes it cumbersome, so we try to fix that here
|
webvtt = caption.timedtext_to_vtt(caption_xml, tlang)
|
||||||
if caption.name.includes? "auto-generated"
|
else
|
||||||
caption_xml = YT_POOL.client &.get(url).body
|
caption_xml = XML.parse(caption_xml)
|
||||||
|
|
||||||
settings_field = {
|
webvtt = String.build do |str|
|
||||||
"Kind" => "captions",
|
str << <<-END_VTT
|
||||||
"Language" => "#{tlang || caption.language_code}",
|
WEBVTT
|
||||||
}
|
Kind: captions
|
||||||
|
Language: #{tlang || caption.language_code}
|
||||||
|
|
||||||
if caption_xml.starts_with?("<?xml")
|
|
||||||
webvtt = caption.timedtext_to_vtt(caption_xml, tlang)
|
|
||||||
else
|
|
||||||
caption_xml = XML.parse(caption_xml)
|
|
||||||
|
|
||||||
webvtt = WebVTT.build(settings_field) do |webvtt|
|
END_VTT
|
||||||
caption_nodes = caption_xml.xpath_nodes("//transcript/text")
|
|
||||||
caption_nodes.each_with_index do |node, i|
|
|
||||||
start_time = node["start"].to_f.seconds
|
|
||||||
duration = node["dur"]?.try &.to_f.seconds
|
|
||||||
duration ||= start_time
|
|
||||||
|
|
||||||
if caption_nodes.size > i + 1
|
caption_nodes = caption_xml.xpath_nodes("//transcript/text")
|
||||||
end_time = caption_nodes[i + 1]["start"].to_f.seconds
|
caption_nodes.each_with_index do |node, i|
|
||||||
else
|
start_time = node["start"].to_f.seconds
|
||||||
end_time = start_time + duration
|
duration = node["dur"]?.try &.to_f.seconds
|
||||||
end
|
duration ||= start_time
|
||||||
|
|
||||||
text = HTML.unescape(node.content)
|
if caption_nodes.size > i + 1
|
||||||
text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "")
|
end_time = caption_nodes[i + 1]["start"].to_f.seconds
|
||||||
text = text.gsub(/<\/font>/, "")
|
else
|
||||||
if md = text.match(/(?<name>.*) : (?<text>.*)/)
|
end_time = start_time + duration
|
||||||
text = "<v #{md["name"]}>#{md["text"]}</v>"
|
|
||||||
end
|
|
||||||
|
|
||||||
webvtt.cue(start_time, end_time, text)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
start_time = "#{start_time.hours.to_s.rjust(2, '0')}:#{start_time.minutes.to_s.rjust(2, '0')}:#{start_time.seconds.to_s.rjust(2, '0')}.#{start_time.milliseconds.to_s.rjust(3, '0')}"
|
||||||
|
end_time = "#{end_time.hours.to_s.rjust(2, '0')}:#{end_time.minutes.to_s.rjust(2, '0')}:#{end_time.seconds.to_s.rjust(2, '0')}.#{end_time.milliseconds.to_s.rjust(3, '0')}"
|
||||||
|
|
||||||
|
text = HTML.unescape(node.content)
|
||||||
|
text = text.gsub(/<font color="#[a-fA-F0-9]{6}">/, "")
|
||||||
|
text = text.gsub(/<\/font>/, "")
|
||||||
|
if md = text.match(/(?<name>.*) : (?<text>.*)/)
|
||||||
|
text = "<v #{md["name"]}>#{md["text"]}</v>"
|
||||||
|
end
|
||||||
|
|
||||||
|
str << <<-END_CUE
|
||||||
|
#{start_time} --> #{end_time}
|
||||||
|
#{text}
|
||||||
|
|
||||||
|
|
||||||
|
END_CUE
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
# Some captions have "align:[start/end]" and "position:[num]%"
|
||||||
|
# attributes. Those are causing issues with VideoJS, which is unable
|
||||||
|
# to properly align the captions on the video, so we remove them.
|
||||||
|
#
|
||||||
|
# See: https://github.com/iv-org/invidious/issues/2391
|
||||||
|
webvtt = YT_POOL.client &.get("#{url}&format=vtt").body
|
||||||
|
if webvtt.starts_with?("<?xml")
|
||||||
|
webvtt = caption.timedtext_to_vtt(webvtt)
|
||||||
else
|
else
|
||||||
webvtt = YT_POOL.client &.get("#{url}&fmt=vtt").body
|
webvtt = YT_POOL.client &.get("#{url}&format=vtt").body
|
||||||
|
.gsub(/([0-9:.]{12} --> [0-9:.]{12}).+/, "\\1")
|
||||||
if webvtt.starts_with?("<?xml")
|
|
||||||
webvtt = caption.timedtext_to_vtt(webvtt)
|
|
||||||
else
|
|
||||||
# Some captions have "align:[start/end]" and "position:[num]%"
|
|
||||||
# attributes. Those are causing issues with VideoJS, which is unable
|
|
||||||
# to properly align the captions on the video, so we remove them.
|
|
||||||
#
|
|
||||||
# See: https://github.com/iv-org/invidious/issues/2391
|
|
||||||
webvtt = webvtt.gsub(/([0-9:.]{12} --> [0-9:.]{12}).+/, "\\1")
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -204,7 +207,11 @@ module Invidious::Routes::API::V1::Videos
|
|||||||
storyboard = storyboard[0]
|
storyboard = storyboard[0]
|
||||||
end
|
end
|
||||||
|
|
||||||
WebVTT.build do |vtt|
|
String.build do |str|
|
||||||
|
str << <<-END_VTT
|
||||||
|
WEBVTT
|
||||||
|
END_VTT
|
||||||
|
|
||||||
start_time = 0.milliseconds
|
start_time = 0.milliseconds
|
||||||
end_time = storyboard[:interval].milliseconds
|
end_time = storyboard[:interval].milliseconds
|
||||||
|
|
||||||
@ -216,8 +223,12 @@ module Invidious::Routes::API::V1::Videos
|
|||||||
|
|
||||||
storyboard[:storyboard_height].times do |j|
|
storyboard[:storyboard_height].times do |j|
|
||||||
storyboard[:storyboard_width].times do |k|
|
storyboard[:storyboard_width].times do |k|
|
||||||
current_cue_url = "#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}"
|
str << <<-END_CUE
|
||||||
vtt.cue(start_time, end_time, current_cue_url)
|
#{start_time}.000 --> #{end_time}.000
|
||||||
|
#{url}#xywh=#{storyboard[:width] * k},#{storyboard[:height] * j},#{storyboard[:width] - 2},#{storyboard[:height]}
|
||||||
|
|
||||||
|
|
||||||
|
END_CUE
|
||||||
|
|
||||||
start_time += storyboard[:interval].milliseconds
|
start_time += storyboard[:interval].milliseconds
|
||||||
end_time += storyboard[:interval].milliseconds
|
end_time += storyboard[:interval].milliseconds
|
||||||
|
@ -1,12 +1,6 @@
|
|||||||
{% skip_file if flag?(:api_only) %}
|
{% skip_file if flag?(:api_only) %}
|
||||||
|
|
||||||
module Invidious::Routes::Channels
|
module Invidious::Routes::Channels
|
||||||
# Redirection for unsupported routes ("tabs")
|
|
||||||
def self.redirect_home(env)
|
|
||||||
ucid = env.params.url["ucid"]
|
|
||||||
return env.redirect "/channel/#{URI.encode_www_form(ucid)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.home(env)
|
def self.home(env)
|
||||||
self.videos(env)
|
self.videos(env)
|
||||||
end
|
end
|
||||||
@ -165,11 +159,6 @@ module Invidious::Routes::Channels
|
|||||||
end
|
end
|
||||||
locale, user, subscriptions, continuation, ucid, channel = data
|
locale, user, subscriptions, continuation, ucid, channel = data
|
||||||
|
|
||||||
# redirect to post page
|
|
||||||
if lb = env.params.query["lb"]?
|
|
||||||
env.redirect "/post/#{URI.encode_www_form(lb)}?ucid=#{URI.encode_www_form(ucid)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode
|
thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode
|
||||||
thin_mode = thin_mode == "true"
|
thin_mode = thin_mode == "true"
|
||||||
|
|
||||||
@ -198,44 +187,6 @@ module Invidious::Routes::Channels
|
|||||||
templated "community"
|
templated "community"
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.post(env)
|
|
||||||
# /post/{postId}
|
|
||||||
id = env.params.url["id"]
|
|
||||||
ucid = env.params.query["ucid"]?
|
|
||||||
|
|
||||||
prefs = env.get("preferences").as(Preferences)
|
|
||||||
|
|
||||||
locale = prefs.locale
|
|
||||||
|
|
||||||
thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode
|
|
||||||
thin_mode = thin_mode == "true"
|
|
||||||
|
|
||||||
nojs = env.params.query["nojs"]?
|
|
||||||
|
|
||||||
nojs ||= "0"
|
|
||||||
nojs = nojs == "1"
|
|
||||||
|
|
||||||
if !ucid.nil?
|
|
||||||
ucid = ucid.to_s
|
|
||||||
post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
|
|
||||||
else
|
|
||||||
# resolve the url to get the author's UCID
|
|
||||||
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
|
||||||
return error_template(400, "Invalid post ID") if response["error"]?
|
|
||||||
|
|
||||||
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
|
|
||||||
post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
|
|
||||||
end
|
|
||||||
|
|
||||||
post_response = JSON.parse(post_response)
|
|
||||||
|
|
||||||
if nojs
|
|
||||||
comments = Comments.fetch_community_post_comments(ucid, id)
|
|
||||||
comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, isPost: true))["contentHtml"]
|
|
||||||
end
|
|
||||||
templated "post"
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.channels(env)
|
def self.channels(env)
|
||||||
data = self.fetch_basic_information(env)
|
data = self.fetch_basic_information(env)
|
||||||
return data if !data.is_a?(Tuple)
|
return data if !data.is_a?(Tuple)
|
||||||
@ -266,11 +217,6 @@ module Invidious::Routes::Channels
|
|||||||
env.redirect "/channel/#{ucid}"
|
env.redirect "/channel/#{ucid}"
|
||||||
end
|
end
|
||||||
|
|
||||||
private KNOWN_TABS = {
|
|
||||||
"home", "videos", "shorts", "streams", "podcasts",
|
|
||||||
"releases", "playlists", "community", "channels", "about",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Redirects brand url channels to a normal /channel/:ucid route
|
# Redirects brand url channels to a normal /channel/:ucid route
|
||||||
def self.brand_redirect(env)
|
def self.brand_redirect(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
@ -281,10 +227,7 @@ module Invidious::Routes::Channels
|
|||||||
yt_url_params = URI::Params.encode(env.params.query.to_h.select(["a", "u", "user"]))
|
yt_url_params = URI::Params.encode(env.params.query.to_h.select(["a", "u", "user"]))
|
||||||
|
|
||||||
# Retrieves URL params that only Invidious uses
|
# Retrieves URL params that only Invidious uses
|
||||||
invidious_url_params = env.params.query.dup
|
invidious_url_params = URI::Params.encode(env.params.query.to_h.select!(["a", "u", "user"]))
|
||||||
invidious_url_params.delete_all("a")
|
|
||||||
invidious_url_params.delete_all("u")
|
|
||||||
invidious_url_params.delete_all("user")
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}")
|
resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}")
|
||||||
@ -293,17 +236,14 @@ module Invidious::Routes::Channels
|
|||||||
return error_template(404, translate(locale, "This channel does not exist."))
|
return error_template(404, translate(locale, "This channel does not exist."))
|
||||||
end
|
end
|
||||||
|
|
||||||
selected_tab = env.params.url["tab"]?
|
selected_tab = env.request.path.split("/")[-1]
|
||||||
|
if {"home", "videos", "shorts", "streams", "playlists", "community", "channels", "about"}.includes? selected_tab
|
||||||
if KNOWN_TABS.includes? selected_tab
|
|
||||||
url = "/channel/#{ucid}/#{selected_tab}"
|
url = "/channel/#{ucid}/#{selected_tab}"
|
||||||
else
|
else
|
||||||
url = "/channel/#{ucid}"
|
url = "/channel/#{ucid}"
|
||||||
end
|
end
|
||||||
|
|
||||||
url += "?#{invidious_url_params}" if !invidious_url_params.empty?
|
env.redirect url
|
||||||
|
|
||||||
return env.redirect url
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Handles redirects for the /profile endpoint
|
# Handles redirects for the /profile endpoint
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
module Invidious::Routes::ErrorRoutes
|
module Invidious::Routes::ErrorRoutes
|
||||||
def self.error_404(env)
|
def self.error_404(env)
|
||||||
# Workaround for #3117
|
|
||||||
if HOST_URL.empty? && env.request.path.starts_with?("/v1/storyboards/sb")
|
|
||||||
return env.redirect "#{env.request.path[15..]}?#{env.params.query}"
|
|
||||||
end
|
|
||||||
|
|
||||||
if md = env.request.path.match(/^\/(?<id>([a-zA-Z0-9_-]{11})|(\w+))$/)
|
if md = env.request.path.match(/^\/(?<id>([a-zA-Z0-9_-]{11})|(\w+))$/)
|
||||||
item = md["id"]
|
item = md["id"]
|
||||||
|
|
||||||
|
@ -3,7 +3,17 @@ module Invidious::Routes::Images
|
|||||||
def self.ggpht(env)
|
def self.ggpht(env)
|
||||||
url = env.request.path.lchop("/ggpht")
|
url = env.request.path.lchop("/ggpht")
|
||||||
|
|
||||||
headers = HTTP::Headers.new
|
headers = (
|
||||||
|
{% unless flag?(:disable_quic) %}
|
||||||
|
if CONFIG.use_quic
|
||||||
|
HTTP::Headers{":authority" => "yt3.ggpht.com"}
|
||||||
|
else
|
||||||
|
HTTP::Headers.new
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
HTTP::Headers.new
|
||||||
|
{% end %}
|
||||||
|
)
|
||||||
|
|
||||||
REQUEST_HEADERS_WHITELIST.each do |header|
|
REQUEST_HEADERS_WHITELIST.each do |header|
|
||||||
if env.request.headers[header]?
|
if env.request.headers[header]?
|
||||||
@ -32,9 +42,22 @@ module Invidious::Routes::Images
|
|||||||
}
|
}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp|
|
{% unless flag?(:disable_quic) %}
|
||||||
return request_proc.call(resp)
|
if CONFIG.use_quic
|
||||||
end
|
YT_POOL.client &.get(url, headers) do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
# This can likely be optimized into a (small) pool sometime in the future.
|
||||||
|
HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -55,6 +78,10 @@ module Invidious::Routes::Images
|
|||||||
|
|
||||||
headers = HTTP::Headers.new
|
headers = HTTP::Headers.new
|
||||||
|
|
||||||
|
{% unless flag?(:disable_quic) %}
|
||||||
|
headers[":authority"] = "#{authority}.ytimg.com"
|
||||||
|
{% end %}
|
||||||
|
|
||||||
REQUEST_HEADERS_WHITELIST.each do |header|
|
REQUEST_HEADERS_WHITELIST.each do |header|
|
||||||
if env.request.headers[header]?
|
if env.request.headers[header]?
|
||||||
headers[header] = env.request.headers[header]
|
headers[header] = env.request.headers[header]
|
||||||
@ -80,9 +107,22 @@ module Invidious::Routes::Images
|
|||||||
}
|
}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp|
|
{% unless flag?(:disable_quic) %}
|
||||||
return request_proc.call(resp)
|
if CONFIG.use_quic
|
||||||
end
|
YT_POOL.client &.get(url, headers) do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
# This can likely be optimized into a (small) pool sometime in the future.
|
||||||
|
HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -93,7 +133,17 @@ module Invidious::Routes::Images
|
|||||||
name = env.params.url["name"]
|
name = env.params.url["name"]
|
||||||
url = env.request.resource
|
url = env.request.resource
|
||||||
|
|
||||||
headers = HTTP::Headers.new
|
headers = (
|
||||||
|
{% unless flag?(:disable_quic) %}
|
||||||
|
if CONFIG.use_quic
|
||||||
|
HTTP::Headers{":authority" => "i9.ytimg.com"}
|
||||||
|
else
|
||||||
|
HTTP::Headers.new
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
HTTP::Headers.new
|
||||||
|
{% end %}
|
||||||
|
)
|
||||||
|
|
||||||
REQUEST_HEADERS_WHITELIST.each do |header|
|
REQUEST_HEADERS_WHITELIST.each do |header|
|
||||||
if env.request.headers[header]?
|
if env.request.headers[header]?
|
||||||
@ -119,9 +169,22 @@ module Invidious::Routes::Images
|
|||||||
}
|
}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp|
|
{% unless flag?(:disable_quic) %}
|
||||||
return request_proc.call(resp)
|
if CONFIG.use_quic
|
||||||
end
|
YT_POOL.client &.get(url, headers) do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
# This can likely be optimized into a (small) pool sometime in the future.
|
||||||
|
HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -160,16 +223,41 @@ module Invidious::Routes::Images
|
|||||||
id = env.params.url["id"]
|
id = env.params.url["id"]
|
||||||
name = env.params.url["name"]
|
name = env.params.url["name"]
|
||||||
|
|
||||||
headers = HTTP::Headers.new
|
headers = (
|
||||||
|
{% unless flag?(:disable_quic) %}
|
||||||
|
if CONFIG.use_quic
|
||||||
|
HTTP::Headers{":authority" => "i.ytimg.com"}
|
||||||
|
else
|
||||||
|
HTTP::Headers.new
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
HTTP::Headers.new
|
||||||
|
{% end %}
|
||||||
|
)
|
||||||
|
|
||||||
if name == "maxres.jpg"
|
if name == "maxres.jpg"
|
||||||
build_thumbnails(id).each do |thumb|
|
build_thumbnails(id).each do |thumb|
|
||||||
thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg"
|
thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg"
|
||||||
# This can likely be optimized into a (small) pool sometime in the future.
|
# Logic here is short enough that manually typing them out should be fine.
|
||||||
if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200
|
{% unless flag?(:disable_quic) %}
|
||||||
name = thumb[:url] + ".jpg"
|
if CONFIG.use_quic
|
||||||
break
|
if YT_POOL.client &.head(thumbnail_resource_path, headers).status_code == 200
|
||||||
end
|
name = thumb[:url] + ".jpg"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200
|
||||||
|
name = thumb[:url] + ".jpg"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
# This can likely be optimized into a (small) pool sometime in the future.
|
||||||
|
if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200
|
||||||
|
name = thumb[:url] + ".jpg"
|
||||||
|
break
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -199,10 +287,22 @@ module Invidious::Routes::Images
|
|||||||
}
|
}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
# This can likely be optimized into a (small) pool sometime in the future.
|
{% unless flag?(:disable_quic) %}
|
||||||
HTTP::Client.get("https://i.ytimg.com#{url}") do |resp|
|
if CONFIG.use_quic
|
||||||
return request_proc.call(resp)
|
YT_POOL.client &.get(url, headers) do |resp|
|
||||||
end
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
HTTP::Client.get("https://i.ytimg.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{% else %}
|
||||||
|
# This can likely be optimized into a (small) pool sometime in the future.
|
||||||
|
HTTP::Client.get("https://i.ytimg.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -160,9 +160,6 @@ module Invidious::Routes::Login
|
|||||||
Invidious::Database::Users.insert(user)
|
Invidious::Database::Users.insert(user)
|
||||||
Invidious::Database::SessionIDs.insert(sid, email)
|
Invidious::Database::SessionIDs.insert(sid, email)
|
||||||
|
|
||||||
view_name = "subscriptions_#{sha256(user.email)}"
|
|
||||||
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
|
|
||||||
|
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||||
|
|
||||||
if env.request.cookies["PREFS"]?
|
if env.request.cookies["PREFS"]?
|
||||||
|
@ -319,15 +319,6 @@ module Invidious::Routes::PreferencesRoute
|
|||||||
response: error_template(415, "Invalid playlist file uploaded")
|
response: error_template(415, "Invalid playlist file uploaded")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
when "import_youtube_wh"
|
|
||||||
filename = part.filename || ""
|
|
||||||
success = Invidious::User::Import.from_youtube_wh(user, body, filename, type)
|
|
||||||
|
|
||||||
if !success
|
|
||||||
haltf(env, status_code: 415,
|
|
||||||
response: error_template(415, "Invalid watch history file uploaded")
|
|
||||||
)
|
|
||||||
end
|
|
||||||
when "import_freetube"
|
when "import_freetube"
|
||||||
Invidious::User::Import.from_freetube(user, body)
|
Invidious::User::Import.from_freetube(user, body)
|
||||||
when "import_newpipe_subscriptions"
|
when "import_newpipe_subscriptions"
|
||||||
|
@ -80,14 +80,9 @@ 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"
|
||||||
|
@ -30,6 +30,14 @@ module Invidious::Routes::Watch
|
|||||||
return env.redirect "/"
|
return env.redirect "/"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
embed_link = "/embed/#{id}"
|
||||||
|
if env.params.query.size > 1
|
||||||
|
embed_params = HTTP::Params.parse(env.params.query.to_s)
|
||||||
|
embed_params.delete_all("v")
|
||||||
|
embed_link += "?"
|
||||||
|
embed_link += embed_params.to_s
|
||||||
|
end
|
||||||
|
|
||||||
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
|
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
|
||||||
continuation = process_continuation(env.params.query, plid, id)
|
continuation = process_continuation(env.params.query, plid, id)
|
||||||
|
|
||||||
|
@ -124,42 +124,28 @@ module Invidious::Routing
|
|||||||
get "/channel/:ucid/community", Routes::Channels, :community
|
get "/channel/:ucid/community", Routes::Channels, :community
|
||||||
get "/channel/:ucid/channels", Routes::Channels, :channels
|
get "/channel/:ucid/channels", Routes::Channels, :channels
|
||||||
get "/channel/:ucid/about", Routes::Channels, :about
|
get "/channel/:ucid/about", Routes::Channels, :about
|
||||||
|
|
||||||
get "/channel/:ucid/live", Routes::Channels, :live
|
get "/channel/:ucid/live", Routes::Channels, :live
|
||||||
get "/user/:user/live", Routes::Channels, :live
|
get "/user/:user/live", Routes::Channels, :live
|
||||||
get "/c/:user/live", Routes::Channels, :live
|
get "/c/:user/live", Routes::Channels, :live
|
||||||
get "/post/:id", Routes::Channels, :post
|
|
||||||
|
|
||||||
# Channel catch-all, to redirect future routes to the channel's home
|
{"", "/videos", "/shorts", "/streams", "/playlists", "/community", "/about"}.each do |path|
|
||||||
# NOTE: defined last in order to be processed after the other routes
|
# /c/LinusTechTips
|
||||||
get "/channel/:ucid/*", Routes::Channels, :redirect_home
|
get "/c/:user#{path}", Routes::Channels, :brand_redirect
|
||||||
|
# /user/linustechtips | Not always the same as /c/
|
||||||
# /c/LinusTechTips
|
get "/user/:user#{path}", Routes::Channels, :brand_redirect
|
||||||
get "/c/:user", Routes::Channels, :brand_redirect
|
# /@LinusTechTips | Handle
|
||||||
get "/c/:user/:tab", Routes::Channels, :brand_redirect
|
get "/@:user#{path}", Routes::Channels, :brand_redirect
|
||||||
|
# /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow
|
||||||
# /user/linustechtips (Not always the same as /c/)
|
get "/attribution_link#{path}", Routes::Channels, :brand_redirect
|
||||||
get "/user/:user", Routes::Channels, :brand_redirect
|
# /profile?user=linustechtips
|
||||||
get "/user/:user/:tab", Routes::Channels, :brand_redirect
|
get "/profile/#{path}", Routes::Channels, :profile
|
||||||
|
end
|
||||||
# /@LinusTechTips (Handle)
|
|
||||||
get "/@:user", Routes::Channels, :brand_redirect
|
|
||||||
get "/@:user/:tab", Routes::Channels, :brand_redirect
|
|
||||||
|
|
||||||
# /attribution_link?a=anything&u=/channel/UCZYTClx2T1of7BRZ86-8fow
|
|
||||||
get "/attribution_link", Routes::Channels, :brand_redirect
|
|
||||||
get "/attribution_link/:tab", Routes::Channels, :brand_redirect
|
|
||||||
|
|
||||||
# /profile?user=linustechtips
|
|
||||||
get "/profile", Routes::Channels, :profile
|
|
||||||
get "/profile/*", Routes::Channels, :profile
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def register_watch_routes
|
def register_watch_routes
|
||||||
get "/watch", Routes::Watch, :handle
|
get "/watch", Routes::Watch, :handle
|
||||||
post "/watch_ajax", Routes::Watch, :mark_watched
|
post "/watch_ajax", Routes::Watch, :mark_watched
|
||||||
get "/watch/:id", Routes::Watch, :redirect
|
get "/watch/:id", Routes::Watch, :redirect
|
||||||
get "/live/:id", Routes::Watch, :redirect
|
|
||||||
get "/shorts/:id", Routes::Watch, :redirect
|
get "/shorts/:id", Routes::Watch, :redirect
|
||||||
get "/clip/:clip", Routes::Watch, :clip
|
get "/clip/:clip", Routes::Watch, :clip
|
||||||
get "/w/:id", Routes::Watch, :redirect
|
get "/w/:id", Routes::Watch, :redirect
|
||||||
@ -254,10 +240,6 @@ module Invidious::Routing
|
|||||||
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
|
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
# Posts
|
|
||||||
get "/api/v1/post/:id", {{namespace}}::Channels, :post
|
|
||||||
get "/api/v1/post/:id/comments", {{namespace}}::Channels, :post_comments
|
|
||||||
|
|
||||||
# 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community
|
# 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community
|
||||||
get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect
|
get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect
|
||||||
get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect
|
get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect
|
||||||
@ -267,7 +249,6 @@ module Invidious::Routing
|
|||||||
get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions
|
get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions
|
||||||
get "/api/v1/hashtag/:hashtag", {{namespace}}::Search, :hashtag
|
get "/api/v1/hashtag/:hashtag", {{namespace}}::Search, :hashtag
|
||||||
|
|
||||||
|
|
||||||
# Authenticated
|
# Authenticated
|
||||||
|
|
||||||
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr
|
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr
|
||||||
|
@ -37,18 +37,18 @@ module Invidious::Search
|
|||||||
|
|
||||||
# Search inside of user subscriptions
|
# Search inside of user subscriptions
|
||||||
def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo)
|
def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo)
|
||||||
view_name = "subscriptions_#{sha256(user.email)}"
|
|
||||||
|
|
||||||
return PG_DB.query_all("
|
return PG_DB.query_all("
|
||||||
SELECT id,title,published,updated,ucid,author,length_seconds
|
SELECT id,title,published,updated,ucid,author,length_seconds
|
||||||
FROM (
|
FROM (
|
||||||
SELECT *,
|
SELECT cv.*,
|
||||||
to_tsvector(#{view_name}.title) ||
|
to_tsvector(cv.title) ||
|
||||||
to_tsvector(#{view_name}.author)
|
to_tsvector(cv.author) AS document
|
||||||
as document
|
FROM channel_videos cv
|
||||||
FROM #{view_name}
|
JOIN users ON cv.ucid = any(users.subscriptions)
|
||||||
) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;",
|
WHERE users.email = $1 AND published > now() - interval '1 month'
|
||||||
query.text, (query.page - 1) * 20,
|
ORDER BY published
|
||||||
|
) v_search WHERE v_search.document @@ plainto_tsquery($2) LIMIT 20 OFFSET $3;",
|
||||||
|
user.email, query.text, (query.page - 1) * 20,
|
||||||
as: ChannelVideo
|
as: ChannelVideo
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
@ -133,7 +133,7 @@ struct Invidious::User
|
|||||||
next if !video_id
|
next if !video_id
|
||||||
|
|
||||||
begin
|
begin
|
||||||
video = get_video(video_id, false)
|
video = get_video(video_id)
|
||||||
rescue ex
|
rescue ex
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
@ -218,26 +218,6 @@ struct Invidious::User
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def from_youtube_wh(user : User, body : String, filename : String, type : String) : Bool
|
|
||||||
extension = filename.split(".").last
|
|
||||||
|
|
||||||
if extension == "json" || type == "application/json"
|
|
||||||
data = JSON.parse(body)
|
|
||||||
watched = data.as_a.compact_map do |item|
|
|
||||||
next unless url = item["titleUrl"]?
|
|
||||||
next unless match = url.as_s.match(/\?v=(?<video_id>[a-zA-Z0-9_-]+)$/)
|
|
||||||
match["video_id"]
|
|
||||||
end
|
|
||||||
watched.reverse! # YouTube have newest first
|
|
||||||
user.watched += watched
|
|
||||||
user.watched.uniq!
|
|
||||||
Invidious::Database::Users.update_watch_history(user)
|
|
||||||
return true
|
|
||||||
else
|
|
||||||
return false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Freetube
|
# Freetube
|
||||||
# -------------------
|
# -------------------
|
||||||
@ -248,12 +228,8 @@ struct Invidious::User
|
|||||||
subs = matches.map(&.["channel_id"])
|
subs = matches.map(&.["channel_id"])
|
||||||
|
|
||||||
if subs.empty?
|
if subs.empty?
|
||||||
profiles = body.split('\n', remove_empty: true)
|
data = JSON.parse(body)["subscriptions"]
|
||||||
profiles.each do |profile|
|
subs = data.as_a.map(&.["id"].as_s)
|
||||||
if data = JSON.parse(profile)["subscriptions"]?
|
|
||||||
subs += data.as_a.map(&.["id"].as_s)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
user.subscriptions += subs
|
user.subscriptions += subs
|
||||||
|
@ -27,7 +27,6 @@ def get_subscription_feed(user, max_results = 40, page = 1)
|
|||||||
offset = (page - 1) * limit
|
offset = (page - 1) * limit
|
||||||
|
|
||||||
notifications = Invidious::Database::Users.select_notifications(user)
|
notifications = Invidious::Database::Users.select_notifications(user)
|
||||||
view_name = "subscriptions_#{sha256(user.email)}"
|
|
||||||
|
|
||||||
if user.preferences.notifications_only && !notifications.empty?
|
if user.preferences.notifications_only && !notifications.empty?
|
||||||
# Only show notifications
|
# Only show notifications
|
||||||
@ -53,33 +52,39 @@ def get_subscription_feed(user, max_results = 40, page = 1)
|
|||||||
# Show latest video from a channel that a user hasn't watched
|
# Show latest video from a channel that a user hasn't watched
|
||||||
# "unseen_only" isn't really correct here, more accurate would be "unwatched_only"
|
# "unseen_only" isn't really correct here, more accurate would be "unwatched_only"
|
||||||
|
|
||||||
if user.watched.empty?
|
# "SELECT cv.* FROM channel_videos cv JOIN users ON cv.ucid = any(users.subscriptions) WHERE users.email = $1 AND published > now() - interval '1 month' ORDER BY published DESC"
|
||||||
values = "'{}'"
|
# "SELECT DISTINCT ON (cv.ucid) cv.* FROM channel_videos cv JOIN users ON cv.ucid = any(users.subscriptions) WHERE users.email = ? AND NOT cv.id = any(users.watched) AND published > now() - interval '1 month' ORDER BY ucid, published DESC"
|
||||||
else
|
videos = PG_DB.query_all("SELECT DISTINCT ON (cv.ucid) cv.* " \
|
||||||
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
|
"FROM channel_videos cv " \
|
||||||
end
|
"JOIN users ON cv.ucid = any(users.subscriptions) " \
|
||||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY ucid, published DESC", as: ChannelVideo)
|
"WHERE users.email = $1 AND NOT cv.id = any(users.watched) AND published > now() - interval '1 month' " \
|
||||||
|
"ORDER BY ucid, published DESC", user.email, as: ChannelVideo)
|
||||||
else
|
else
|
||||||
# Show latest video from each channel
|
# Show latest video from each channel
|
||||||
|
|
||||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} ORDER BY ucid, published DESC", as: ChannelVideo)
|
videos = PG_DB.query_all("SELECT DISTINCT ON (cv.ucid) cv.* " \
|
||||||
|
"FROM channel_videos cv " \
|
||||||
|
"JOIN users ON cv.ucid = any(users.subscriptions) " \
|
||||||
|
"WHERE users.email = $1 AND published > now() - interval '1 month' " \
|
||||||
|
"ORDER BY ucid, published DESC", user.email, as: ChannelVideo)
|
||||||
end
|
end
|
||||||
|
|
||||||
videos.sort_by!(&.published).reverse!
|
videos.sort_by!(&.published).reverse!
|
||||||
else
|
else
|
||||||
if user.preferences.unseen_only
|
if user.preferences.unseen_only
|
||||||
# Only show unwatched
|
# Only show unwatched
|
||||||
|
videos = PG_DB.query_all("SELECT cv.* " \
|
||||||
if user.watched.empty?
|
"FROM channel_videos cv " \
|
||||||
values = "'{}'"
|
"JOIN users ON cv.ucid = any(users.subscriptions) " \
|
||||||
else
|
"WHERE users.email = $1 AND NOT cv.id = any(users.watched) AND published > now() - interval '1 month' " \
|
||||||
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
|
"ORDER BY published DESC LIMIT $2 OFFSET $3", user.email, limit, offset, as: ChannelVideo)
|
||||||
end
|
|
||||||
videos = PG_DB.query_all("SELECT * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
|
|
||||||
else
|
else
|
||||||
# Sort subscriptions as normal
|
# Sort subscriptions as normal
|
||||||
|
videos = PG_DB.query_all("SELECT cv.* " \
|
||||||
videos = PG_DB.query_all("SELECT * FROM #{view_name} ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
|
"FROM channel_videos cv " \
|
||||||
|
"JOIN users ON cv.ucid = any(users.subscriptions) " \
|
||||||
|
"WHERE users.email = $1 AND published > now() - interval '1 month' " \
|
||||||
|
"ORDER BY published DESC LIMIT $2 OFFSET $3", user.email, limit, offset, as: ChannelVideo)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ struct Video
|
|||||||
property updated : Time
|
property updated : Time
|
||||||
|
|
||||||
@[DB::Field(ignore: true)]
|
@[DB::Field(ignore: true)]
|
||||||
@captions = [] of Invidious::Videos::Captions::Metadata
|
@captions = [] of Invidious::Videos::Caption
|
||||||
|
|
||||||
@[DB::Field(ignore: true)]
|
@[DB::Field(ignore: true)]
|
||||||
property adaptive_fmts : Array(Hash(String, JSON::Any))?
|
property adaptive_fmts : Array(Hash(String, JSON::Any))?
|
||||||
@ -215,9 +215,9 @@ struct Video
|
|||||||
keywords.includes? "YouTube Red"
|
keywords.includes? "YouTube Red"
|
||||||
end
|
end
|
||||||
|
|
||||||
def captions : Array(Invidious::Videos::Captions::Metadata)
|
def captions : Array(Invidious::Videos::Caption)
|
||||||
if @captions.empty? && @info.has_key?("captions")
|
if @captions.empty? && @info.has_key?("captions")
|
||||||
@captions = Invidious::Videos::Captions::Metadata.from_yt_json(info["captions"])
|
@captions = Invidious::Videos::Caption.from_yt_json(info["captions"])
|
||||||
end
|
end
|
||||||
|
|
||||||
return @captions
|
return @captions
|
||||||
@ -227,22 +227,8 @@ struct Video
|
|||||||
info.dig?("streamingData", "hlsManifestUrl").try &.as_s
|
info.dig?("streamingData", "hlsManifestUrl").try &.as_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def dash_manifest_url : String?
|
def dash_manifest_url
|
||||||
raw_dash_url = info.dig?("streamingData", "dashManifestUrl").try &.as_s
|
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?
|
||||||
|
@ -1,89 +1,100 @@
|
|||||||
require "json"
|
require "json"
|
||||||
|
|
||||||
module Invidious::Videos
|
module Invidious::Videos
|
||||||
module Captions
|
struct Caption
|
||||||
struct Metadata
|
property name : String
|
||||||
property name : String
|
property language_code : String
|
||||||
property language_code : String
|
property base_url : String
|
||||||
property base_url : String
|
|
||||||
|
|
||||||
property auto_generated : Bool
|
def initialize(@name, @language_code, @base_url)
|
||||||
|
end
|
||||||
|
|
||||||
def initialize(@name, @language_code, @base_url, @auto_generated)
|
# Parse the JSON structure from Youtube
|
||||||
|
def self.from_yt_json(container : JSON::Any) : Array(Caption)
|
||||||
|
caption_tracks = container
|
||||||
|
.dig?("playerCaptionsTracklistRenderer", "captionTracks")
|
||||||
|
.try &.as_a
|
||||||
|
|
||||||
|
captions_list = [] of Caption
|
||||||
|
return captions_list if caption_tracks.nil?
|
||||||
|
|
||||||
|
caption_tracks.each do |caption|
|
||||||
|
name = caption["name"]["simpleText"]? || caption["name"]["runs"][0]["text"]
|
||||||
|
name = name.to_s.split(" - ")[0]
|
||||||
|
|
||||||
|
language_code = caption["languageCode"].to_s
|
||||||
|
base_url = caption["baseUrl"].to_s
|
||||||
|
|
||||||
|
captions_list << Caption.new(name, language_code, base_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Parse the JSON structure from Youtube
|
return captions_list
|
||||||
def self.from_yt_json(container : JSON::Any) : Array(Captions::Metadata)
|
end
|
||||||
caption_tracks = container
|
|
||||||
.dig?("playerCaptionsTracklistRenderer", "captionTracks")
|
|
||||||
.try &.as_a
|
|
||||||
|
|
||||||
captions_list = [] of Captions::Metadata
|
def timedtext_to_vtt(timedtext : String, tlang = nil) : String
|
||||||
return captions_list if caption_tracks.nil?
|
# In the future, we could just directly work with the url. This is more of a POC
|
||||||
|
cues = [] of XML::Node
|
||||||
|
tree = XML.parse(timedtext)
|
||||||
|
tree = tree.children.first
|
||||||
|
|
||||||
caption_tracks.each do |caption|
|
tree.children.each do |item|
|
||||||
name = caption["name"]["simpleText"]? || caption["name"]["runs"][0]["text"]
|
if item.name == "body"
|
||||||
name = name.to_s.split(" - ")[0]
|
item.children.each do |cue|
|
||||||
|
if cue.name == "p" && !(cue.children.size == 1 && cue.children[0].content == "\n")
|
||||||
language_code = caption["languageCode"].to_s
|
cues << cue
|
||||||
base_url = caption["baseUrl"].to_s
|
|
||||||
|
|
||||||
auto_generated = (caption["kind"]? == "asr")
|
|
||||||
|
|
||||||
captions_list << Captions::Metadata.new(name, language_code, base_url, auto_generated)
|
|
||||||
end
|
|
||||||
|
|
||||||
return captions_list
|
|
||||||
end
|
|
||||||
|
|
||||||
def timedtext_to_vtt(timedtext : String, tlang = nil) : String
|
|
||||||
# In the future, we could just directly work with the url. This is more of a POC
|
|
||||||
cues = [] of XML::Node
|
|
||||||
tree = XML.parse(timedtext)
|
|
||||||
tree = tree.children.first
|
|
||||||
|
|
||||||
tree.children.each do |item|
|
|
||||||
if item.name == "body"
|
|
||||||
item.children.each do |cue|
|
|
||||||
if cue.name == "p" && !(cue.children.size == 1 && cue.children[0].content == "\n")
|
|
||||||
cues << cue
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
break
|
|
||||||
end
|
end
|
||||||
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
settings_field = {
|
|
||||||
"Kind" => "captions",
|
|
||||||
"Language" => "#{tlang || @language_code}",
|
|
||||||
}
|
|
||||||
|
|
||||||
result = WebVTT.build(settings_field) do |vtt|
|
|
||||||
cues.each_with_index do |node, i|
|
|
||||||
start_time = node["t"].to_f.milliseconds
|
|
||||||
|
|
||||||
duration = node["d"]?.try &.to_f.milliseconds
|
|
||||||
|
|
||||||
duration ||= start_time
|
|
||||||
|
|
||||||
if cues.size > i + 1
|
|
||||||
end_time = cues[i + 1]["t"].to_f.milliseconds
|
|
||||||
else
|
|
||||||
end_time = start_time + duration
|
|
||||||
end
|
|
||||||
|
|
||||||
text = String.build do |io|
|
|
||||||
node.children.each do |s|
|
|
||||||
io << s.content
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
vtt.cue(start_time, end_time, text)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return result
|
|
||||||
end
|
end
|
||||||
|
result = String.build do |result|
|
||||||
|
result << <<-END_VTT
|
||||||
|
WEBVTT
|
||||||
|
Kind: captions
|
||||||
|
Language: #{tlang || @language_code}
|
||||||
|
|
||||||
|
|
||||||
|
END_VTT
|
||||||
|
|
||||||
|
result << "\n\n"
|
||||||
|
|
||||||
|
cues.each_with_index do |node, i|
|
||||||
|
start_time = node["t"].to_f.milliseconds
|
||||||
|
|
||||||
|
duration = node["d"]?.try &.to_f.milliseconds
|
||||||
|
|
||||||
|
duration ||= start_time
|
||||||
|
|
||||||
|
if cues.size > i + 1
|
||||||
|
end_time = cues[i + 1]["t"].to_f.milliseconds
|
||||||
|
else
|
||||||
|
end_time = start_time + duration
|
||||||
|
end
|
||||||
|
|
||||||
|
# start_time
|
||||||
|
result << start_time.hours.to_s.rjust(2, '0')
|
||||||
|
result << ':' << start_time.minutes.to_s.rjust(2, '0')
|
||||||
|
result << ':' << start_time.seconds.to_s.rjust(2, '0')
|
||||||
|
result << '.' << start_time.milliseconds.to_s.rjust(3, '0')
|
||||||
|
|
||||||
|
result << " --> "
|
||||||
|
|
||||||
|
# end_time
|
||||||
|
result << end_time.hours.to_s.rjust(2, '0')
|
||||||
|
result << ':' << end_time.minutes.to_s.rjust(2, '0')
|
||||||
|
result << ':' << end_time.seconds.to_s.rjust(2, '0')
|
||||||
|
result << '.' << end_time.milliseconds.to_s.rjust(3, '0')
|
||||||
|
|
||||||
|
result << "\n"
|
||||||
|
|
||||||
|
node.children.each do |s|
|
||||||
|
result << s.content
|
||||||
|
end
|
||||||
|
result << "\n"
|
||||||
|
result << "\n"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
# List of all caption languages available on Youtube.
|
# List of all caption languages available on Youtube.
|
||||||
|
@ -55,7 +55,8 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
|
|||||||
client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region)
|
client_config = YoutubeAPI::ClientConfig.new(proxy_region: proxy_region)
|
||||||
|
|
||||||
# Fetch data from the player endpoint
|
# Fetch data from the player endpoint
|
||||||
player_response = YoutubeAPI.player(video_id: video_id, params: "", client_config: client_config)
|
# 8AEB param is used to fetch YouTube stories
|
||||||
|
player_response = YoutubeAPI.player(video_id: video_id, params: "8AEB", client_config: client_config)
|
||||||
|
|
||||||
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
||||||
|
|
||||||
@ -78,11 +79,6 @@ 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>"),
|
||||||
@ -123,9 +119,6 @@ def extract_video_info(video_id : String, proxy_region : String? = nil)
|
|||||||
|
|
||||||
# Replace player response and reset reason
|
# Replace player response and reset reason
|
||||||
if !new_player_response.nil?
|
if !new_player_response.nil?
|
||||||
# Preserve storyboard data before replacement
|
|
||||||
new_player_response["storyboards"] = player_response["storyboards"] if player_response["storyboards"]?
|
|
||||||
|
|
||||||
player_response = new_player_response
|
player_response = new_player_response
|
||||||
params.delete("reason")
|
params.delete("reason")
|
||||||
end
|
end
|
||||||
@ -142,8 +135,8 @@ end
|
|||||||
|
|
||||||
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
|
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
||||||
# 2AMBCgIQBg is a workaround for streaming URLs that returns a 403.
|
# 8AEB param is used to fetch YouTube stories
|
||||||
response = YoutubeAPI.player(video_id: id, params: "2AMBCgIQBg", client_config: client_config)
|
response = YoutubeAPI.player(video_id: id, params: "8AEB", client_config: client_config)
|
||||||
|
|
||||||
playability_status = response["playabilityStatus"]["status"]
|
playability_status = response["playabilityStatus"]["status"]
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
||||||
|
@ -1,77 +0,0 @@
|
|||||||
module Invidious::Videos
|
|
||||||
# Namespace for methods primarily relating to Transcripts
|
|
||||||
module Transcript
|
|
||||||
record TranscriptLine, start_ms : Time::Span, end_ms : Time::Span, line : String
|
|
||||||
|
|
||||||
def self.generate_param(video_id : String, language_code : String, auto_generated : Bool) : String
|
|
||||||
kind = auto_generated ? "asr" : ""
|
|
||||||
|
|
||||||
object = {
|
|
||||||
"1:0:string" => video_id,
|
|
||||||
|
|
||||||
"2:base64" => {
|
|
||||||
"1:string" => kind,
|
|
||||||
"2:string" => language_code,
|
|
||||||
"3:string" => "",
|
|
||||||
},
|
|
||||||
|
|
||||||
"3:varint" => 1_i64,
|
|
||||||
"5:string" => "engagement-panel-searchable-transcript-search-panel",
|
|
||||||
"6:varint" => 1_i64,
|
|
||||||
"7:varint" => 1_i64,
|
|
||||||
"8:varint" => 1_i64,
|
|
||||||
}
|
|
||||||
|
|
||||||
params = object.try { |i| Protodec::Any.cast_json(i) }
|
|
||||||
.try { |i| Protodec::Any.from_json(i) }
|
|
||||||
.try { |i| Base64.urlsafe_encode(i) }
|
|
||||||
.try { |i| URI.encode_www_form(i) }
|
|
||||||
|
|
||||||
return params
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.convert_transcripts_to_vtt(initial_data : Hash(String, JSON::Any), target_language : String) : String
|
|
||||||
# Convert into array of TranscriptLine
|
|
||||||
lines = self.parse(initial_data)
|
|
||||||
|
|
||||||
settings_field = {
|
|
||||||
"Kind" => "captions",
|
|
||||||
"Language" => target_language,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Taken from Invidious::Videos::Captions::Metadata.timedtext_to_vtt()
|
|
||||||
vtt = WebVTT.build(settings_field) do |vtt|
|
|
||||||
lines.each do |line|
|
|
||||||
vtt.cue(line.start_ms, line.end_ms, line.line)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return vtt
|
|
||||||
end
|
|
||||||
|
|
||||||
private def self.parse(initial_data : Hash(String, JSON::Any))
|
|
||||||
body = initial_data.dig("actions", 0, "updateEngagementPanelAction", "content", "transcriptRenderer",
|
|
||||||
"content", "transcriptSearchPanelRenderer", "body", "transcriptSegmentListRenderer",
|
|
||||||
"initialSegments").as_a
|
|
||||||
|
|
||||||
lines = [] of TranscriptLine
|
|
||||||
body.each do |line|
|
|
||||||
# Transcript section headers. They are not apart of the captions and as such we can safely skip them.
|
|
||||||
if line.as_h.has_key?("transcriptSectionHeaderRenderer")
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
line = line["transcriptSegmentRenderer"]
|
|
||||||
|
|
||||||
start_ms = line["startMs"].as_s.to_i.millisecond
|
|
||||||
end_ms = line["endMs"].as_s.to_i.millisecond
|
|
||||||
|
|
||||||
text = extract_text(line["snippet"]) || ""
|
|
||||||
|
|
||||||
lines << TranscriptLine.new(start_ms, end_ms, text)
|
|
||||||
end
|
|
||||||
|
|
||||||
return lines
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -26,7 +26,7 @@
|
|||||||
<p><%= error_message %></p>
|
<p><%= error_message %></p>
|
||||||
</div>
|
</div>
|
||||||
<% else %>
|
<% else %>
|
||||||
<div class="h-box pure-g comments" id="comments">
|
<div class="h-box pure-g" id="comments">
|
||||||
<%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %>
|
<%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user