mirror of
https://github.com/iv-org/invidious.git
synced 2025-05-27 18:32:37 -04:00
Compare commits
261 commits
v2.2024111
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4b37d47ebb | ||
![]() |
2c857b5ab6 | ||
![]() |
00299ca4a0 | ||
![]() |
9d18c8699f | ||
![]() |
475bf7448a | ||
![]() |
50e0a4361b | ||
![]() |
6bfb61e9b4 | ||
![]() |
ef07c542dc | ||
![]() |
a9180aa6c1 | ||
![]() |
4b2f9ffffc | ||
![]() |
64ad97f308 | ||
![]() |
d5cb653fd1 | ||
![]() |
0b23dd12e1 | ||
![]() |
23d66338cd | ||
![]() |
df41cb9588 | ||
![]() |
49ada0aae9 | ||
![]() |
f6a41ce90d | ||
![]() |
f7aefd5fb1 | ||
![]() |
6376fd55db | ||
![]() |
9e172d8371 | ||
![]() |
8d0834005f | ||
![]() |
9f192d4f74 | ||
![]() |
ee7b8b6c61 | ||
![]() |
b9097d0a3b | ||
![]() |
be469304de | ||
![]() |
b6b245586a | ||
![]() |
88195113bf | ||
![]() |
4d381aca60 | ||
![]() |
a5904ecce2 | ||
![]() |
42125dfadd | ||
![]() |
5953f7286f | ||
![]() |
31556d0f88 | ||
![]() |
7bd1abecde | ||
![]() |
583195ccbd | ||
![]() |
f96e476ed9 | ||
![]() |
9186020f94 | ||
![]() |
546a799f0b | ||
![]() |
2d8326c63d | ||
![]() |
9c9a8592e0 | ||
![]() |
435106b7de | ||
![]() |
6a9ed48d5d | ||
![]() |
a5b97a5850 | ||
![]() |
f8f6eb74f5 | ||
![]() |
1e73f4e382 | ||
![]() |
476bc51b0e | ||
![]() |
96b226b130 | ||
![]() |
3b87bf2675 | ||
![]() |
0dff773a07 | ||
![]() |
03f89be929 | ||
![]() |
d4eb2a9741 | ||
![]() |
6fe21a7523 | ||
![]() |
aab6ff4bb6 | ||
![]() |
20cf913a4e | ||
![]() |
1492453c60 | ||
![]() |
401bc110d6 | ||
![]() |
30ae222bf2 | ||
![]() |
81ca831439 | ||
![]() |
8feea29607 | ||
![]() |
c4944ee061 | ||
![]() |
406277b16f | ||
![]() |
7259c63648 | ||
![]() |
73f524fccd | ||
![]() |
03e06b239b | ||
![]() |
c304ea6db3 | ||
![]() |
b120abdcc5 | ||
![]() |
9e3c0dfd85 | ||
![]() |
25eade589f | ||
![]() |
35896d086b | ||
![]() |
d1bc15b8bf | ||
![]() |
1f028fee0f | ||
![]() |
2c1400c41e | ||
![]() |
8fd0b82c38 | ||
![]() |
7579adc3a3 | ||
![]() |
d567c6be6e | ||
![]() |
05b99df49a | ||
![]() |
6c063436d4 | ||
![]() |
0c07e9d27a | ||
![]() |
23ff6135bb | ||
![]() |
7b27585454 | ||
![]() |
f7810ba007 | ||
![]() |
c288005bfd | ||
![]() |
aae5ba01c2 | ||
![]() |
dd16f15aae | ||
![]() |
180d77276b | ||
![]() |
0e0a95430a | ||
![]() |
9de69c0052 | ||
![]() |
dbeee71457 | ||
![]() |
94cb80ea81 | ||
![]() |
409d12a81e | ||
![]() |
70ff463cc6 | ||
![]() |
e23d0d13be | ||
![]() |
5c8b4eb379 | ||
![]() |
dd2e999402 | ||
![]() |
adcdb8cb92 | ||
![]() |
fe4fa0480a | ||
![]() |
dbbcacc955 | ||
![]() |
58ad848d56 | ||
![]() |
f9b9e85ee4 | ||
![]() |
6ac74f4362 | ||
![]() |
9fbe3944b0 | ||
![]() |
c5e9447f41 | ||
![]() |
3e329410d1 | ||
![]() |
74dfda150e | ||
![]() |
e60f53154e | ||
![]() |
3d77635a5c | ||
![]() |
d0433c8386 | ||
![]() |
4ea4878d1a | ||
![]() |
1f0a89fb5f | ||
![]() |
f95f87e448 | ||
![]() |
49afbf2a14 | ||
![]() |
d853b9f6dc | ||
![]() |
d70681538a | ||
![]() |
05c5448bc1 | ||
![]() |
e2df12b7d6 | ||
![]() |
29219c46a1 | ||
![]() |
a77f083a0a | ||
![]() |
eaf47385c5 | ||
![]() |
1fb8d3f583 | ||
![]() |
26b15d6e35 | ||
![]() |
786e3e0550 | ||
![]() |
104553fdc4 | ||
![]() |
ae670d5b2d | ||
![]() |
b2c14f1a2a | ||
![]() |
b899bc959e | ||
![]() |
74dc6795cd | ||
![]() |
5404b67bef | ||
![]() |
7b59ccf645 | ||
![]() |
cc6c39d0e6 | ||
![]() |
37f3c285d7 | ||
![]() |
106086c766 | ||
![]() |
0980867d42 | ||
![]() |
3abc377d56 | ||
![]() |
4a0a6f7ed5 | ||
![]() |
3056e1767e | ||
![]() |
0846faa6f6 | ||
![]() |
943c42e47b | ||
![]() |
fc7b5120db | ||
![]() |
d4d6a4b172 | ||
![]() |
e0cb54f7e0 | ||
![]() |
844e1bdf43 | ||
![]() |
aacfbb09da | ||
![]() |
f57b4b5e4f | ||
![]() |
b1422b7434 | ||
![]() |
f56e4012fe | ||
![]() |
7d5b2ec7b6 | ||
![]() |
cad64e420c | ||
![]() |
f181ae3cb0 | ||
![]() |
0fd480bae2 | ||
![]() |
afb0aad7d3 | ||
![]() |
6816ded0fa | ||
![]() |
0546a73bfa | ||
![]() |
164d764d55 | ||
![]() |
4a31da4000 | ||
![]() |
831017f403 | ||
![]() |
52daafe047 | ||
![]() |
dca130ca6f | ||
![]() |
086c6209ab | ||
![]() |
0d398c9d1a | ||
![]() |
dc38bcdf17 | ||
![]() |
d5442d45bc | ||
![]() |
d4f0560e80 | ||
![]() |
eae3c42dab | ||
![]() |
c0131d8646 | ||
![]() |
21fd717701 | ||
![]() |
8ee73aa0c1 | ||
![]() |
6e3ec10d76 | ||
![]() |
d95ae7e6a5 | ||
![]() |
d36f372bd1 | ||
![]() |
58c65e921f | ||
![]() |
5d9ed95ffd | ||
![]() |
033e42a981 | ||
![]() |
bfa6da2474 | ||
![]() |
097b4f0433 | ||
![]() |
e1378702af | ||
![]() |
b13f77b5af | ||
![]() |
b4a6193642 | ||
![]() |
525dea1e2a | ||
![]() |
f9885cca8e | ||
![]() |
047ead8080 | ||
![]() |
275318dae2 | ||
![]() |
48d2250024 | ||
![]() |
5f8130fd03 | ||
![]() |
b4e930f3bc | ||
![]() |
d7f5cdc2f9 | ||
![]() |
04b0742293 | ||
![]() |
1838ac4c99 | ||
![]() |
8729f01075 | ||
![]() |
6dd89bd401 | ||
![]() |
bba1769f4b | ||
![]() |
6b0e4e6817 | ||
![]() |
6abee5de99 | ||
![]() |
9892604758 | ||
![]() |
3ac8978e96 | ||
![]() |
e7a93fcc18 | ||
![]() |
aa33d9b7ec | ||
![]() |
7a15318fbc | ||
![]() |
5fa87cc27c | ||
![]() |
1333fed26c | ||
![]() |
eed14d08a8 | ||
![]() |
b0c7dd9771 | ||
![]() |
dbdf2ad23a | ||
![]() |
dbd96c77e4 | ||
![]() |
e453a2a682 | ||
![]() |
7e4b3b182a | ||
![]() |
3850739d7f | ||
![]() |
9d91ac3b88 | ||
![]() |
5d0149844f | ||
![]() |
b526f48120 | ||
![]() |
e8cd631b2d | ||
![]() |
69ff6def5f | ||
![]() |
26dc9dc99c | ||
![]() |
2d6b46c926 | ||
![]() |
cab02d4959 | ||
![]() |
5f590dda80 | ||
![]() |
b2f5b1eb68 | ||
![]() |
7693f61e44 | ||
![]() |
7214fdaff4 | ||
![]() |
7b7197cde8 | ||
![]() |
3c6019edd0 | ||
![]() |
6861148290 | ||
![]() |
03f9962a47 | ||
![]() |
d098e5ae9b | ||
![]() |
4c486634e2 | ||
![]() |
3bced4e12b | ||
![]() |
0d22af6564 | ||
![]() |
2a6a32e667 | ||
![]() |
50da6cf3e7 | ||
![]() |
7388e4ca72 | ||
![]() |
be216fff94 | ||
![]() |
019807256f | ||
![]() |
a0d24190b8 | ||
![]() |
2b2d67fcfa | ||
![]() |
76369eb599 | ||
![]() |
6236cea33e | ||
![]() |
e8c2388589 | ||
![]() |
995df2d296 | ||
![]() |
c0d75bc52f | ||
![]() |
e307fcc9a1 | ||
![]() |
bae8bab3ff | ||
![]() |
fa59f41f7b | ||
![]() |
20ca1ebcc0 | ||
![]() |
b0b4f09b3a | ||
![]() |
48af0af9d5 | ||
![]() |
f9460e31bc | ||
![]() |
b7a252b096 | ||
![]() |
6b929da0e1 | ||
![]() |
21122db3a7 | ||
![]() |
c9a843c7fe | ||
![]() |
275501aad3 | ||
![]() |
5cdbc184c7 | ||
![]() |
9996d00cb1 | ||
![]() |
9a617ae087 | ||
![]() |
c257882a1f | ||
![]() |
58bad6180f | ||
![]() |
509bace7d1 | ||
![]() |
07c52cba3d | ||
![]() |
04ba7b0d58 | ||
![]() |
4788a3b4a9 | ||
![]() |
7fe2af735d | ||
![]() |
905582db66 | ||
![]() |
78773d7326 |
124 changed files with 2514 additions and 935 deletions
.github
CHANGELOG.mdREADME.mdassets
css
js
config
docker
locales
ar.jsonbg.jsonca.jsoncs.jsoncy.jsonde.jsonel.jsonen-US.jsones.jsonfa.jsonfi.jsonfr.jsonhr.jsonis.jsonit.jsonja.jsonko.jsonlv.jsonnb-NO.jsonnl.jsonpl.jsonpt-BR.jsonpt-PT.jsonpt.jsonru.jsonsl.jsonsq.jsonsr.jsonsr_Cyrl.jsonsv-SE.jsonta.jsontok.jsontr.jsonuk.jsonvi.jsonzh-CN.jsonzh-TW.json
scripts
shard.lockshard.ymlsrc
ext
invidious.crinvidious
channels
config.crdatabase
frontend
helpers
jobs
jsonify/api_v1
mixes.crplaylists.crroutes
account.cr
routing.crapi
before_all.crchannels.crembed.crfeeds.crimages.crlogin.crmisc.crplaylists.crsearch.crsubscriptions.crvideo_playback.crwatch.cr
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -10,8 +10,10 @@ assignees: ''
|
||||||
<!--
|
<!--
|
||||||
BEFORE TRYING TO REPORT A BUG:
|
BEFORE TRYING TO REPORT A BUG:
|
||||||
|
|
||||||
* Read the FAQ!
|
* Read the FAQ: https://docs.invidious.io/faq/!
|
||||||
* Use the search function to check if there is already an issue open for your problem!
|
* Use the search function to check if there is already an issue open for your problem: https://github.com/search?q=repo%3Aiv-org%2Finvidious+replace+me+with+your+bug&type=issues!
|
||||||
|
|
||||||
|
MAKE SURE TO FOLLOW THE TWO STEPS ABOVE BEFORE REPORTING A BUG. A BUG THAT ALREADY EXIST WILL IMMEDIATELY CLOSED.
|
||||||
|
|
||||||
If you want to suggest a new feature please use "Feature request" instead
|
If you want to suggest a new feature please use "Feature request" instead
|
||||||
If you want to suggest an enhancement to an existing feature please use "Enhancement" instead
|
If you want to suggest an enhancement to an existing feature please use "Enhancement" instead
|
||||||
|
|
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "docker"
|
||||||
|
directory: "/docker"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
17
.github/workflows/build-nightly-container.yml
vendored
17
.github/workflows/build-nightly-container.yml
vendored
|
@ -23,19 +23,6 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Crystal
|
|
||||||
uses: crystal-lang/install-crystal@v1.8.2
|
|
||||||
with:
|
|
||||||
crystal: 1.12.2
|
|
||||||
|
|
||||||
- name: Run lint
|
|
||||||
run: |
|
|
||||||
if ! crystal tool format --check; then
|
|
||||||
crystal tool format
|
|
||||||
git diff
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -63,7 +50,7 @@ jobs:
|
||||||
quay.expires-after=12w
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker AMD64 image for Push Event
|
- name: Build and push Docker AMD64 image for Push Event
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
|
@ -88,7 +75,7 @@ jobs:
|
||||||
quay.expires-after=12w
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker ARM64 image for Push Event
|
- name: Build and push Docker ARM64 image for Push Event
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
|
|
17
.github/workflows/build-stable-container.yml
vendored
17
.github/workflows/build-stable-container.yml
vendored
|
@ -14,19 +14,6 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Crystal
|
|
||||||
uses: crystal-lang/install-crystal@v1.8.2
|
|
||||||
with:
|
|
||||||
crystal: 1.12.2
|
|
||||||
|
|
||||||
- name: Run lint
|
|
||||||
run: |
|
|
||||||
if ! crystal tool format --check; then
|
|
||||||
crystal tool format
|
|
||||||
git diff
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
with:
|
||||||
|
@ -56,7 +43,7 @@ jobs:
|
||||||
quay.expires-after=12w
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker AMD64 image for Push Event
|
- name: Build and push Docker AMD64 image for Push Event
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
|
@ -82,7 +69,7 @@ jobs:
|
||||||
quay.expires-after=12w
|
quay.expires-after=12w
|
||||||
|
|
||||||
- name: Build and push Docker ARM64 image for Push Event
|
- name: Build and push Docker ARM64 image for Push Event
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
|
|
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
|
@ -38,11 +38,11 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
stable: [true]
|
stable: [true]
|
||||||
crystal:
|
crystal:
|
||||||
- 1.10.1
|
- 1.12.2
|
||||||
- 1.11.2
|
- 1.13.3
|
||||||
- 1.12.1
|
- 1.14.1
|
||||||
- 1.13.2
|
- 1.15.1
|
||||||
- 1.14.0
|
- 1.16.3
|
||||||
include:
|
include:
|
||||||
- crystal: nightly
|
- crystal: nightly
|
||||||
stable: false
|
stable: false
|
||||||
|
@ -58,12 +58,12 @@ jobs:
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.8.0
|
uses: crystal-lang/install-crystal@v1.8.2
|
||||||
with:
|
with:
|
||||||
crystal: ${{ matrix.crystal }}
|
crystal: ${{ matrix.crystal }}
|
||||||
|
|
||||||
- name: Cache Shards
|
- name: Cache Shards
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
./lib
|
./lib
|
||||||
|
@ -114,7 +114,7 @@ jobs:
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build Docker ARM64 image
|
- name: Build Docker ARM64 image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile.arm64
|
file: docker/Dockerfile.arm64
|
||||||
|
@ -136,17 +136,18 @@ jobs:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.8.0
|
id: lint_step_install_crystal
|
||||||
|
uses: crystal-lang/install-crystal@v1.8.2
|
||||||
with:
|
with:
|
||||||
crystal: latest
|
crystal: latest
|
||||||
|
|
||||||
- name: Cache Shards
|
- name: Cache Shards
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
./lib
|
./lib
|
||||||
./bin
|
./bin
|
||||||
key: shards-${{ hashFiles('shard.lock') }}
|
key: shards-${{ hashFiles('shard.lock') }}-${{ steps.lint_step_install_crystal.outputs.crystal }}
|
||||||
|
|
||||||
- name: Install Shards
|
- name: Install Shards
|
||||||
run: |
|
run: |
|
||||||
|
|
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v9
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 730
|
days-before-stale: 730
|
||||||
|
|
108
CHANGELOG.md
108
CHANGELOG.md
|
@ -1,5 +1,113 @@
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
## vX.Y.0 (future)
|
||||||
|
|
||||||
|
## v2.20250517.0
|
||||||
|
|
||||||
|
Inverse fallback for the YouTube client from TVHTML then MWEB. Fixes https://github.com/iv-org/invidious/issues/5273
|
||||||
|
|
||||||
|
## v2.20250504.0
|
||||||
|
|
||||||
|
Small release with quick workaround fix for issue #4251 (Nil assertion failed).
|
||||||
|
|
||||||
|
PR: https://github.com/iv-org/invidious/issues/5262
|
||||||
|
|
||||||
|
## v2.20250314.0
|
||||||
|
|
||||||
|
### Wrap-up
|
||||||
|
|
||||||
|
This release brings the long awaited feature of supporting multiple audio tracks in a video, some bug fixes and UX improvements, and many other things primarily oriented to self-hosting instances, and developers using the API.
|
||||||
|
|
||||||
|
The `Community` channel tab has been replaced by `Posts` in light of YouTube changes, but the URL remains the same.
|
||||||
|
|
||||||
|
Tamil is now available as an interface language
|
||||||
|
|
||||||
|
Automatic instance redirects will no longer have the chance to annoyingly redirect to the same instance you're on.
|
||||||
|
|
||||||
|
Due to their requirements for video playback, Invidious will log warning messages when either inv-sig-helper, `po_token` or `visitor_data` is not configured
|
||||||
|
|
||||||
|
Invidious is now able to listen through a UNIX socket
|
||||||
|
|
||||||
|
User notifications are now batched for each channel
|
||||||
|
|
||||||
|
**The minimum Crystal version supported by Invidious now `1.12.0`**
|
||||||
|
|
||||||
|
### New features & important changes
|
||||||
|
|
||||||
|
#### For users
|
||||||
|
|
||||||
|
* Invidious now supports videos with multiple audio tracks allowing you to select which one you want to hear with!
|
||||||
|
* Channel pages now have a proper previous page button
|
||||||
|
* RSS feeds for channels will no longer contain the channel's profile picture
|
||||||
|
* Support for channel `courses` page has been added
|
||||||
|
* `Community` tabs has been replaced with `Posts` to comply with YouTube changes
|
||||||
|
* Tamil is now an available interface language.
|
||||||
|
|
||||||
|
#### For instance owners
|
||||||
|
* Invidious is now able to listen on a UNIX socket
|
||||||
|
* User notifications are now batched by channels, significantly reducing database load.
|
||||||
|
* **`1.12.0` is now the oldest Crystal version that Invidious supports**
|
||||||
|
* The example config will no longer force an http proxy to be configured
|
||||||
|
* Invidious will now warn when any top-level config option must be set to a custom value, instead of just `HMAC_KEY`
|
||||||
|
* Due to their requirements for video playback, Invidious will log warning messages when either inv-sig-helper, `po_token` or `visitor_data` is not configured
|
||||||
|
|
||||||
|
#### For developers
|
||||||
|
* Invidious is now compliant to Crystal 1.15 formatting rules, which are incompatible with earlier versions.
|
||||||
|
* `/api/v1/transcripts/{id}` has been added to the API to allow for fetching the transcripts for a video. The arguments are the same as the captions endpoint.
|
||||||
|
* `author_thumbnail` field has been added to videos in the various paged api endpoints
|
||||||
|
* `published` field has been added to the API response for a video's related videos.
|
||||||
|
* Docker builds now uses the Crystal compiler cache, reducing build times on repeated builds significantly.
|
||||||
|
* Invidious ajax action handlers has undergone a clean up and may face compatibility issues with code that depends on these endpoints.
|
||||||
|
* The versions of Crystal that we test in CI/CD are now: `1.12.1`, `1.13.2`, `1.14.0`, `1.15.0`
|
||||||
|
|
||||||
|
### Bugs fixed
|
||||||
|
|
||||||
|
#### User-side
|
||||||
|
* Local video listen mode is now preserved when clicking on a video in the sidebar playlist widget
|
||||||
|
* Automatic instance redirects will no longer redirect to the same instance the user is on
|
||||||
|
* Fix some thumbnails responses returning 404
|
||||||
|
* Videos: Fix missing host parameter on playback URLs when `local=true`
|
||||||
|
* Fix HLS being used for non-livestream videos
|
||||||
|
* Fix timeupdate event errors when required elements are missing
|
||||||
|
* User: Ensure IO is properly closed when importing NewPipe subscriptions
|
||||||
|
|
||||||
|
#### For instance owners
|
||||||
|
* Fix http proxy configuration being forced by the standard example config
|
||||||
|
|
||||||
|
#### API
|
||||||
|
* `/api/v1/videos/{id}` will no longer return an occasional empty JSON response
|
||||||
|
|
||||||
|
### Full list of pull requests merged since the last release (newest first)
|
||||||
|
* Make Invidious compliant to Crystal 1.15 formatting rules (https://github.com/iv-org/invidious/pull/5014, by @syeopite)
|
||||||
|
* Remove formatter check on container workflows (https://github.com/iv-org/invidious/pull/5153, by @syeopite)
|
||||||
|
* Videos: Fix missing host parameter on playback URLs when `local=true` (https://github.com/iv-org/invidious/pull/4992, by @SamantazFox)
|
||||||
|
* Remove stdlib override for proxy initialization (https://github.com/iv-org/invidious/pull/5065, by @syeopite)
|
||||||
|
* Add support for author thumbnails in search api for videos (https://github.com/iv-org/invidious/pull/5072, thanks @ChunkyProgrammer)
|
||||||
|
* Skip route if resp got closed by before handlers (https://github.com/iv-org/invidious/pull/5073, by @syeopite)
|
||||||
|
* Fix video thumbnails in mixes (https://github.com/iv-org/invidious/pull/5116, thanks @iBicha)
|
||||||
|
* CI: Drop support for versions prior to 1.12 and add 1.15.0 (https://github.com/iv-org/invidious/pull/5148, by @syeopite)
|
||||||
|
* [Continuing #5094] Set language info for dash audio streams and sort (https://github.com/iv-org/invidious/pull/5149, thanks @giuliano-macedo)
|
||||||
|
* Warn when any top-level config is "CHANGE_ME!!" (https://github.com/iv-org/invidious/pull/5150, by @syeopite)
|
||||||
|
* Comment out http_proxy in example config (https://github.com/iv-org/invidious/pull/5151, by @syeopite)
|
||||||
|
* API: Add a 'published' video parameter for related videos (https://github.com/iv-org/invidious/pull/4149, thanks @RadoslavL)
|
||||||
|
* Ensure IO is properly closed when importing NewPipe subscriptions (https://github.com/iv-org/invidious/pull/4346, thanks @ChunkyProgrammer)
|
||||||
|
* Carry over audio-only mode in playlist links (https://github.com/iv-org/invidious/pull/4784, thanks @krystof1119)
|
||||||
|
* Routes: Clean ajax actions handlers (https://github.com/iv-org/invidious/pull/5036, by @SamantazFox)
|
||||||
|
* Frontend: Add a first page and previous page buttons for channel navigation (https://github.com/iv-org/invidious/pull/4123, thanks @RadoslavL)
|
||||||
|
* RSS: Channel + Playlist improvements (https://github.com/iv-org/invidious/pull/4298, thanks @ChunkyProgrammer)
|
||||||
|
* Batch user notifications together (https://github.com/iv-org/invidious/pull/4486, thanks @999eagle)
|
||||||
|
* JS: Update timeupdate event making it more defensive to prevent errors (https://github.com/iv-org/invidious/pull/4782, thanks @PMK)
|
||||||
|
* Add API endpoint for fetching transcripts from YouTube by (https://github.com/iv-org/invidious/pull/4788, by @syeopite)
|
||||||
|
* Translations update from Hosted Weblate by (https://github.com/iv-org/invidious/pull/4989, thanks to our many translators)
|
||||||
|
* Add the ability to listen on UNIX sockets (https://github.com/iv-org/invidious/pull/5112, thanks @Caian)
|
||||||
|
* Pick a different instance upon redirect (https://github.com/iv-org/invidious/pull/5154, thanks @epicsam123)
|
||||||
|
* Add Courses to channel page and channel API (https://github.com/iv-org/invidious/pull/5158, thanks @ChunkyProgrammer)
|
||||||
|
* fix /api/v1/videos/:id returns 200 with no content (https://github.com/iv-org/invidious/pull/5162, thanks @Drikanis)
|
||||||
|
* Use Crystal compiler cache in docker builds (https://github.com/iv-org/invidious/pull/5163, by @syeopite)
|
||||||
|
* Channels: Fix community tab by (https://github.com/iv-org/invidious/pull/5183, thanks @Fijxu)
|
||||||
|
* Fix typo in `src/invidious/routes/images.cr` (https://github.com/iv-org/invidious/pull/5184, by @syeopite)
|
||||||
|
* Fix an issue with the HLS manifest check for livestream videos (https://github.com/iv-org/invidious/pull/5189, thanks @alexmaras)
|
||||||
|
* Warn when `po_token`, `visitor_data` and/or `inv-sig-helper` is not configured (https://github.com/iv-org/invidious/pull/5202, by @syeopite)
|
||||||
## v2.20241110.0
|
## v2.20241110.0
|
||||||
|
|
||||||
### Wrap-up
|
### Wrap-up
|
||||||
|
|
14
README.md
14
README.md
|
@ -81,9 +81,9 @@
|
||||||
- [Available in many languages](locales/), thanks to [our translators](#contribute)
|
- [Available in many languages](locales/), thanks to [our translators](#contribute)
|
||||||
|
|
||||||
**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 YouTube and NewPipe
|
||||||
- Export subscriptions to NewPipe and Freetube
|
- Export subscriptions to NewPipe and FreeTube
|
||||||
- Import/Export Invidious user data
|
- Import/Export Invidious user data
|
||||||
|
|
||||||
**Technical features**
|
**Technical features**
|
||||||
|
@ -95,11 +95,11 @@
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
**Using invidious:**
|
**Using Invidious:**
|
||||||
|
|
||||||
- [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now!
|
- [Select a public instance from the list](https://instances.invidious.io) and start watching videos right now!
|
||||||
|
|
||||||
**Hosting invidious:**
|
**Hosting Invidious:**
|
||||||
|
|
||||||
- [Follow the installation instructions](https://docs.invidious.io/installation/)
|
- [Follow the installation instructions](https://docs.invidious.io/installation/)
|
||||||
|
|
||||||
|
@ -114,8 +114,8 @@ https://github.com/iv-org/documentation
|
||||||
### Extensions
|
### Extensions
|
||||||
|
|
||||||
We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get),
|
We highly recommend the use of [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect#get),
|
||||||
a browser extension that automatically redirects Youtube URLs to any Invidious instance and replaces
|
a browser extension that automatically redirects YouTube URLs to any Invidious instance and replaces
|
||||||
embedded youtube videos on other websites with invidious.
|
embedded YouTube videos on other websites with Invidious.
|
||||||
|
|
||||||
The documentation contains a list of browser extensions that we recommended to use along with Invidious.
|
The documentation contains a list of browser extensions that we recommended to use along with Invidious.
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ We use [Weblate](https://weblate.org) to manage Invidious translations.
|
||||||
You can suggest new translations and/or correction here: https://hosted.weblate.org/engage/invidious/.
|
You can suggest new translations and/or correction here: https://hosted.weblate.org/engage/invidious/.
|
||||||
|
|
||||||
Creating an account is not required, but recommended, especially if you want to contribute regularly.
|
Creating an account is not required, but recommended, especially if you want to contribute regularly.
|
||||||
Weblate also allows you to log-in with major SSO providers like Github, Gitlab, BitBucket, Google, ...
|
Weblate also allows you to log-in with major SSO providers like GitHub, GitLab, BitBucket, Google, ...
|
||||||
|
|
||||||
|
|
||||||
## Projects using Invidious
|
## Projects using Invidious
|
||||||
|
|
|
@ -550,6 +550,10 @@ span > select {
|
||||||
color: #565d64;
|
color: #565d64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.light-theme .error-card {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light) {
|
||||||
.no-theme a:hover,
|
.no-theme a:hover,
|
||||||
.no-theme a:active,
|
.no-theme a:active,
|
||||||
|
@ -596,6 +600,10 @@ span > select {
|
||||||
.light-theme .pure-menu-heading {
|
.light-theme .pure-menu-heading {
|
||||||
color: #565d64;
|
color: #565d64;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-theme .error-card {
|
||||||
|
border: 1px solid black;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -658,6 +666,10 @@ body.dark-theme {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark-theme .error-card {
|
||||||
|
border: 1px solid #5e5e5e;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.no-theme a:hover,
|
.no-theme a:hover,
|
||||||
.no-theme a:active,
|
.no-theme a:active,
|
||||||
|
@ -719,6 +731,10 @@ body.dark-theme {
|
||||||
.no-theme footer a {
|
.no-theme footer a {
|
||||||
color: #adadad !important;
|
color: #adadad !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-theme .error-card {
|
||||||
|
border: 1px solid #5e5e5e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -816,3 +832,57 @@ h1, h2, h3, h4, h5, p,
|
||||||
#download_widget {
|
#download_widget {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.error-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 25px;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card > .explanation {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: max-content 1fr;
|
||||||
|
grid-template-rows: 1fr max-content;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 10px;
|
||||||
|
row-gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card > .explanation > i {
|
||||||
|
color: #f44;
|
||||||
|
font-size: 24px;
|
||||||
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card > .explanation > h4 {
|
||||||
|
grid-area: 1 / 2 / 2 / 3;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card > .explanation > p {
|
||||||
|
grid-area: 2 / 2 / 3 / 3;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card details {
|
||||||
|
margin-top: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card summary {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-card pre {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-issue-template {
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.12345);
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
summary {
|
#filters-collapse summary {
|
||||||
/* This should hide the marker */
|
/* This should hide the marker */
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
@ -8,10 +8,10 @@ summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary::-webkit-details-marker,
|
#filters-collapse summary::-webkit-details-marker,
|
||||||
summary::marker { display: none; }
|
#filters-collapse summary::marker { display: none; }
|
||||||
|
|
||||||
summary:before {
|
#filters-collapse summary:before {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
content: "[ + ]";
|
content: "[ + ]";
|
||||||
margin: -2px 10px 0 10px;
|
margin: -2px 10px 0 10px;
|
||||||
|
@ -20,7 +20,7 @@ summary:before {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
details[open] > summary:before { content: "[ − ]"; }
|
#filters-collapse details[open] > summary:before { content: "[ − ]"; }
|
||||||
|
|
||||||
|
|
||||||
#filters-box {
|
#filters-box {
|
||||||
|
|
|
@ -91,7 +91,7 @@
|
||||||
var count = document.getElementById('count');
|
var count = document.getElementById('count');
|
||||||
count.textContent--;
|
count.textContent--;
|
||||||
|
|
||||||
var url = '/token_ajax?action_revoke_token=1&redirect=false' +
|
var url = '/token_ajax?action=revoke_token&redirect=false' +
|
||||||
'&referer=' + encodeURIComponent(location.href) +
|
'&referer=' + encodeURIComponent(location.href) +
|
||||||
'&session=' + target.getAttribute('data-session');
|
'&session=' + target.getAttribute('data-session');
|
||||||
|
|
||||||
|
@ -111,7 +111,7 @@
|
||||||
var count = document.getElementById('count');
|
var count = document.getElementById('count');
|
||||||
count.textContent--;
|
count.textContent--;
|
||||||
|
|
||||||
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
|
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
|
||||||
'&referer=' + encodeURIComponent(location.href) +
|
'&referer=' + encodeURIComponent(location.href) +
|
||||||
'&c=' + target.getAttribute('data-ucid');
|
'&c=' + target.getAttribute('data-ucid');
|
||||||
|
|
||||||
|
|
93
assets/js/pagination.js
Normal file
93
assets/js/pagination.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const CURRENT_CONTINUATION = (new URL(document.location)).searchParams.get("continuation");
|
||||||
|
const CONT_CACHE_KEY = `continuation_cache_${encodeURIComponent(window.location.pathname)}`;
|
||||||
|
|
||||||
|
function get_data(){
|
||||||
|
return JSON.parse(sessionStorage.getItem(CONT_CACHE_KEY)) || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function save_data(){
|
||||||
|
const prev_data = get_data();
|
||||||
|
prev_data.push(CURRENT_CONTINUATION);
|
||||||
|
|
||||||
|
sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data));
|
||||||
|
}
|
||||||
|
|
||||||
|
function button_press(){
|
||||||
|
let prev_data = get_data();
|
||||||
|
if (!prev_data.length) return null;
|
||||||
|
|
||||||
|
// Sanity check. Nowhere should the current continuation token exist in the cache
|
||||||
|
// but it can happen when using the browser's back feature. As such we'd need to travel
|
||||||
|
// back to the point where the current continuation token first appears in order to
|
||||||
|
// account for the rewind.
|
||||||
|
const conflict_at = prev_data.indexOf(CURRENT_CONTINUATION);
|
||||||
|
if (conflict_at != -1) {
|
||||||
|
prev_data.length = conflict_at;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prev_ctoken = prev_data.pop();
|
||||||
|
|
||||||
|
// On the first page, the stored continuation token is null.
|
||||||
|
if (prev_ctoken === null) {
|
||||||
|
sessionStorage.removeItem(CONT_CACHE_KEY);
|
||||||
|
let url = set_continuation();
|
||||||
|
window.location.href = url;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionStorage.setItem(CONT_CACHE_KEY, JSON.stringify(prev_data));
|
||||||
|
let url = set_continuation(prev_ctoken);
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Method to set the current page's continuation token
|
||||||
|
// Removes the continuation parameter when a continuation token is not given
|
||||||
|
function set_continuation(prev_ctoken = null){
|
||||||
|
let url = window.location.href.split('?')[0];
|
||||||
|
let params = window.location.href.split('?')[1];
|
||||||
|
let url_params = new URLSearchParams(params);
|
||||||
|
|
||||||
|
if (prev_ctoken) {
|
||||||
|
url_params.set("continuation", prev_ctoken);
|
||||||
|
} else {
|
||||||
|
url_params.delete('continuation');
|
||||||
|
};
|
||||||
|
|
||||||
|
if(Array.from(url_params).length > 0){
|
||||||
|
return `${url}?${url_params.toString()}`;
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener('DOMContentLoaded', function(){
|
||||||
|
const pagination_data = JSON.parse(document.getElementById('pagination-data').textContent);
|
||||||
|
const next_page_containers = document.getElementsByClassName("page-next-container");
|
||||||
|
|
||||||
|
for (let container of next_page_containers){
|
||||||
|
const next_page_button = container.getElementsByClassName("pure-button")
|
||||||
|
|
||||||
|
// exists?
|
||||||
|
if (next_page_button.length > 0){
|
||||||
|
next_page_button[0].addEventListener("click", save_data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add previous page buttons when not on the first page
|
||||||
|
if (CURRENT_CONTINUATION) {
|
||||||
|
const prev_page_containers = document.getElementsByClassName("page-prev-container")
|
||||||
|
|
||||||
|
for (let container of prev_page_containers) {
|
||||||
|
if (pagination_data.is_rtl) {
|
||||||
|
container.innerHTML = `<button class="pure-button pure-button-secondary">${pagination_data.prev_page} <i class="icon ion-ios-arrow-forward"></i></button>`
|
||||||
|
} else {
|
||||||
|
container.innerHTML = `<button class="pure-button pure-button-secondary"><i class="icon ion-ios-arrow-back"></i> ${pagination_data.prev_page}</button>`
|
||||||
|
}
|
||||||
|
container.getElementsByClassName("pure-button")[0].addEventListener("click", button_press);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -134,26 +134,32 @@ player.on('timeupdate', function () {
|
||||||
// YouTube links
|
// YouTube links
|
||||||
|
|
||||||
let elem_yt_watch = document.getElementById('link-yt-watch');
|
let elem_yt_watch = document.getElementById('link-yt-watch');
|
||||||
|
if (elem_yt_watch) {
|
||||||
|
let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url');
|
||||||
|
elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch);
|
||||||
|
}
|
||||||
|
|
||||||
let elem_yt_embed = document.getElementById('link-yt-embed');
|
let elem_yt_embed = document.getElementById('link-yt-embed');
|
||||||
|
if (elem_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');
|
||||||
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
|
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
|
||||||
|
}
|
||||||
elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch);
|
|
||||||
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
|
|
||||||
|
|
||||||
// Invidious links
|
// Invidious links
|
||||||
|
|
||||||
let domain = window.location.origin;
|
let domain = window.location.origin;
|
||||||
|
|
||||||
let elem_iv_embed = document.getElementById('link-iv-embed');
|
let elem_iv_embed = document.getElementById('link-iv-embed');
|
||||||
|
if (elem_iv_embed) {
|
||||||
|
let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url');
|
||||||
|
elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain);
|
||||||
|
}
|
||||||
|
|
||||||
let elem_iv_other = document.getElementById('link-iv-other');
|
let elem_iv_other = document.getElementById('link-iv-other');
|
||||||
|
if (elem_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');
|
||||||
let base_url_iv_other = elem_iv_other.getAttribute('data-base-url');
|
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain);
|
||||||
|
}
|
||||||
elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain);
|
|
||||||
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ function add_playlist_video(target) {
|
||||||
var select = target.parentNode.children[0].children[1];
|
var select = target.parentNode.children[0].children[1];
|
||||||
var option = select.children[select.selectedIndex];
|
var option = select.children[select.selectedIndex];
|
||||||
|
|
||||||
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
var url = '/playlist_ajax?action=add_video&redirect=false' +
|
||||||
'&video_id=' + target.getAttribute('data-id') +
|
'&video_id=' + target.getAttribute('data-id') +
|
||||||
'&playlist_id=' + option.getAttribute('data-plid');
|
'&playlist_id=' + option.getAttribute('data-plid');
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ function add_playlist_item(target) {
|
||||||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
tile.style.display = 'none';
|
tile.style.display = 'none';
|
||||||
|
|
||||||
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
var url = '/playlist_ajax?action=add_video&redirect=false' +
|
||||||
'&video_id=' + target.getAttribute('data-id') +
|
'&video_id=' + target.getAttribute('data-id') +
|
||||||
'&playlist_id=' + target.getAttribute('data-plid');
|
'&playlist_id=' + target.getAttribute('data-plid');
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ function remove_playlist_item(target) {
|
||||||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
tile.style.display = 'none';
|
tile.style.display = 'none';
|
||||||
|
|
||||||
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
|
var url = '/playlist_ajax?action=remove_video&redirect=false' +
|
||||||
'&set_video_id=' + target.getAttribute('data-index') +
|
'&set_video_id=' + target.getAttribute('data-index') +
|
||||||
'&playlist_id=' + target.getAttribute('data-plid');
|
'&playlist_id=' + target.getAttribute('data-plid');
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ function subscribe() {
|
||||||
subscribe_button.onclick = unsubscribe;
|
subscribe_button.onclick = unsubscribe;
|
||||||
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
||||||
|
|
||||||
var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
|
var url = '/subscription_ajax?action=create_subscription_to_channel&redirect=false' +
|
||||||
'&c=' + subscribe_data.ucid;
|
'&c=' + subscribe_data.ucid;
|
||||||
|
|
||||||
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
|
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
|
||||||
|
@ -32,7 +32,7 @@ function unsubscribe() {
|
||||||
subscribe_button.onclick = subscribe;
|
subscribe_button.onclick = subscribe;
|
||||||
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
||||||
|
|
||||||
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
|
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
|
||||||
'&c=' + subscribe_data.ucid;
|
'&c=' + subscribe_data.ucid;
|
||||||
|
|
||||||
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {
|
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {
|
||||||
|
|
|
@ -67,6 +67,10 @@ function get_playlist(plid) {
|
||||||
'&format=html&hl=' + video_data.preferences.locale;
|
'&format=html&hl=' + video_data.preferences.locale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (video_data.params.listen) {
|
||||||
|
plid_url += '&listen=1'
|
||||||
|
}
|
||||||
|
|
||||||
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
|
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
|
||||||
on200: function (response) {
|
on200: function (response) {
|
||||||
playlist.innerHTML = response.playlistHtml;
|
playlist.innerHTML = response.playlistHtml;
|
||||||
|
|
|
@ -6,7 +6,7 @@ function mark_watched(target) {
|
||||||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||||
tile.style.display = 'none';
|
tile.style.display = 'none';
|
||||||
|
|
||||||
var url = '/watch_ajax?action_mark_watched=1&redirect=false' +
|
var url = '/watch_ajax?action=mark_watched&redirect=false' +
|
||||||
'&id=' + target.getAttribute('data-id');
|
'&id=' + target.getAttribute('data-id');
|
||||||
|
|
||||||
helpers.xhr('POST', url, {payload: payload}, {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
|
@ -22,7 +22,7 @@ function mark_unwatched(target) {
|
||||||
var count = document.getElementById('count');
|
var count = document.getElementById('count');
|
||||||
count.textContent--;
|
count.textContent--;
|
||||||
|
|
||||||
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
|
var url = '/watch_ajax?action=mark_unwatched&redirect=false' +
|
||||||
'&id=' + target.getAttribute('data-id');
|
'&id=' + target.getAttribute('data-id');
|
||||||
|
|
||||||
helpers.xhr('POST', url, {payload: payload}, {
|
helpers.xhr('POST', url, {payload: payload}, {
|
||||||
|
|
|
@ -54,6 +54,53 @@ db:
|
||||||
##
|
##
|
||||||
#signature_server:
|
#signature_server:
|
||||||
|
|
||||||
|
##
|
||||||
|
## Invidious companion is an external program
|
||||||
|
## for loading the video streams from YouTube servers.
|
||||||
|
##
|
||||||
|
## When this setting is commented out, Invidious companion is not used.
|
||||||
|
## Otherwise, Invidious will proxy the requests to Invidious companion.
|
||||||
|
##
|
||||||
|
## Note: multiple URL can be configured. In this case, invidious will
|
||||||
|
## randomly pick one every time video data needs to be retrieved. This
|
||||||
|
## URL is then kept in the video metadata cache to allow video playback
|
||||||
|
## to work. Once said cache has expired, requesting that video's data
|
||||||
|
## again will cause a new companion URL to be picked.
|
||||||
|
##
|
||||||
|
## The parameter private_url needs to be configured for the internal
|
||||||
|
## communication between the companion and Invidious.
|
||||||
|
## And public_url is the public URL from which companion is listening
|
||||||
|
## to the requests from the user(s).
|
||||||
|
##
|
||||||
|
## If you are using a reverse proxy then you will probably need to
|
||||||
|
## configure the public_url to be the same as the domain used for Invidious.
|
||||||
|
## Also apply when used from an external IP address (without a domain).
|
||||||
|
## Examples: https://MYINVIDIOUSDOMAIN or http://192.168.1.100:8282
|
||||||
|
##
|
||||||
|
## Both parameter can have identical URL when Invidious is hosted in
|
||||||
|
## an internal network or at home or locally (localhost).
|
||||||
|
##
|
||||||
|
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
|
||||||
|
## Default: <none>
|
||||||
|
##
|
||||||
|
#invidious_companion:
|
||||||
|
# - private_url: "http://localhost:8282"
|
||||||
|
# public_url: "http://localhost:8282"
|
||||||
|
|
||||||
|
##
|
||||||
|
## API key for Invidious companion, used for securing the communication
|
||||||
|
## between Invidious and Invidious companion.
|
||||||
|
## The key needs to be exactly 16 characters long.
|
||||||
|
##
|
||||||
|
## Note: This parameter is mandatory when Invidious companion is enabled
|
||||||
|
## and should be a random string.
|
||||||
|
## Such random string can be generated on linux with the following
|
||||||
|
## command: `pwgen 16 1`
|
||||||
|
##
|
||||||
|
## Accepted values: a string (of length 16)
|
||||||
|
## Default: <none>
|
||||||
|
##
|
||||||
|
#invidious_companion_key: "CHANGE_ME!!"
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
#
|
#
|
||||||
|
@ -130,6 +177,20 @@ https_only: false
|
||||||
##
|
##
|
||||||
#hsts: true
|
#hsts: true
|
||||||
|
|
||||||
|
##
|
||||||
|
## Path and permissions of a UNIX socket to listen on for incoming connections.
|
||||||
|
##
|
||||||
|
## Note: Enabling socket will make invidious stop listening on the address
|
||||||
|
## specified by 'host_binding' and 'port'.
|
||||||
|
##
|
||||||
|
## Accepted values: Any path to a new file (that doesn't exist yet) and its
|
||||||
|
## permissions following the UNIX octal convention.
|
||||||
|
## Default: <none>
|
||||||
|
##
|
||||||
|
#socket_binding:
|
||||||
|
# path: /tmp/invidious.sock
|
||||||
|
# permissions: 777
|
||||||
|
|
||||||
|
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
# Network (outbound)
|
# Network (outbound)
|
||||||
|
@ -178,11 +239,11 @@ https_only: false
|
||||||
##
|
##
|
||||||
## If unset, then no HTTP proxy will be used.
|
## If unset, then no HTTP proxy will be used.
|
||||||
##
|
##
|
||||||
http_proxy:
|
#http_proxy:
|
||||||
user:
|
# user:
|
||||||
password:
|
# password:
|
||||||
host:
|
# host:
|
||||||
port:
|
# port:
|
||||||
|
|
||||||
|
|
||||||
##
|
##
|
||||||
|
@ -797,9 +858,9 @@ default_user_preferences:
|
||||||
## Default video quality.
|
## Default video quality.
|
||||||
##
|
##
|
||||||
## Accepted values: dash, hd720, medium, small
|
## Accepted values: dash, hd720, medium, small
|
||||||
## Default: hd720
|
## Default: dash
|
||||||
##
|
##
|
||||||
#quality: hd720
|
#quality: dash
|
||||||
|
|
||||||
##
|
##
|
||||||
## Default dash video quality.
|
## Default dash video quality.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM crystallang/crystal:1.12.2-alpine AS builder
|
FROM crystallang/crystal:1.16.3-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-static yaml-static
|
RUN apk add --no-cache sqlite-static yaml-static
|
||||||
|
|
||||||
|
@ -21,7 +21,7 @@ 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 --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
|
||||||
crystal build ./src/invidious.cr \
|
crystal build ./src/invidious.cr \
|
||||||
--release \
|
--release \
|
||||||
--static --warnings all \
|
--static --warnings all \
|
||||||
|
@ -32,7 +32,7 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
||||||
--link-flags "-lxml2 -llzma"; \
|
--link-flags "-lxml2 -llzma"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.21
|
||||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
FROM alpine:3.20 AS builder
|
FROM alpine:3.21 AS builder
|
||||||
RUN apk add --no-cache 'crystal=1.12.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \
|
RUN apk add --no-cache 'crystal=1.14.0-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \
|
||||||
zlib-static openssl-libs-static openssl-dev musl-dev xz-static
|
zlib-static openssl-libs-static openssl-dev musl-dev xz-static
|
||||||
|
|
||||||
ARG release
|
ARG release
|
||||||
|
@ -22,7 +22,7 @@ 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 --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
|
||||||
crystal build ./src/invidious.cr \
|
crystal build ./src/invidious.cr \
|
||||||
--release \
|
--release \
|
||||||
--static --warnings all \
|
--static --warnings all \
|
||||||
|
@ -33,7 +33,7 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
||||||
--link-flags "-lxml2 -llzma"; \
|
--link-flags "-lxml2 -llzma"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.21
|
||||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
|
|
|
@ -154,8 +154,8 @@
|
||||||
"View YouTube comments": "عرض تعليقات اليوتيوب",
|
"View YouTube comments": "عرض تعليقات اليوتيوب",
|
||||||
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع ريديت",
|
"View more comments on Reddit": "عرض المزيد من التعليقات على\\من موقع ريديت",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليقات",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "عرض `x` تعليق",
|
||||||
"": "عرض `x` تعليقات."
|
"": "عرض `x` تعليقات"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "عرض تعليقات ريديت",
|
"View Reddit comments": "عرض تعليقات ريديت",
|
||||||
"Hide replies": "إخفاء الردود",
|
"Hide replies": "إخفاء الردود",
|
||||||
|
@ -559,10 +559,15 @@
|
||||||
"toggle_theme": "تبديل الموضوع",
|
"toggle_theme": "تبديل الموضوع",
|
||||||
"Add to playlist": "أضف إلى قائمة التشغيل",
|
"Add to playlist": "أضف إلى قائمة التشغيل",
|
||||||
"Add to playlist: ": "أضف إلى قائمة التشغيل: ",
|
"Add to playlist: ": "أضف إلى قائمة التشغيل: ",
|
||||||
"Answer": "الرد",
|
"Answer": "اجابة",
|
||||||
"Search for videos": "ابحث عن مقاطع الفيديو",
|
"Search for videos": "ابحث عن مقاطع الفيديو",
|
||||||
"The Popular feed has been disabled by the administrator.": "تم تعطيل الخلاصة الشائعة من قبل المسؤول.",
|
"The Popular feed has been disabled by the administrator.": "تم تعطيل الخلاصة الشائعة من قبل المسؤول.",
|
||||||
"carousel_slide": "الشريحة {{current}} من {{total}}",
|
"carousel_slide": "الشريحة {{current}} من {{total}}",
|
||||||
"carousel_skip": "تخطي الكاروسيل",
|
"carousel_skip": "تخطي الكاروسيل",
|
||||||
"carousel_go_to": "انتقل إلى الشريحة `x`"
|
"carousel_go_to": "انتقل إلى الشريحة `x`",
|
||||||
|
"preferences_preload_label": "التحميل المسبق لبيانات الفيديو: ",
|
||||||
|
"Filipino (auto-generated)": "الفلبينية (المولدة تلقائيًا)",
|
||||||
|
"channel_tab_courses_label": "الدورات",
|
||||||
|
"channel_tab_posts_label": "المنشورات",
|
||||||
|
"First page": "الصفحة الأولى"
|
||||||
}
|
}
|
||||||
|
|
|
@ -403,7 +403,7 @@
|
||||||
"comments_view_x_replies": "Виж {{count}} отговор",
|
"comments_view_x_replies": "Виж {{count}} отговор",
|
||||||
"comments_view_x_replies_plural": "Виж {{count}} отговора",
|
"comments_view_x_replies_plural": "Виж {{count}} отговора",
|
||||||
"footer_original_source_code": "Оригинален изходен код",
|
"footer_original_source_code": "Оригинален изходен код",
|
||||||
"Import YouTube subscriptions": "Импортиране на YouTube/OPML абонаменти",
|
"Import YouTube subscriptions": "Импортиране на YouTube-CSV/OPML абонаменти",
|
||||||
"Lithuanian": "Литовски",
|
"Lithuanian": "Литовски",
|
||||||
"Nyanja": "Нянджа",
|
"Nyanja": "Нянджа",
|
||||||
"Updated `x` ago": "Актуализирано преди `x`",
|
"Updated `x` ago": "Актуализирано преди `x`",
|
||||||
|
@ -493,5 +493,8 @@
|
||||||
"Add to playlist: ": "Добави към плейлист: ",
|
"Add to playlist: ": "Добави към плейлист: ",
|
||||||
"Answer": "Отговор",
|
"Answer": "Отговор",
|
||||||
"Search for videos": "Търсене на видеа",
|
"Search for videos": "Търсене на видеа",
|
||||||
"The Popular feed has been disabled by the administrator.": "Популярната страница е деактивирана от администратора."
|
"The Popular feed has been disabled by the administrator.": "Популярната страница е деактивирана от администратора.",
|
||||||
|
"Filipino (auto-generated)": "Филипински (автоматично генериран)",
|
||||||
|
"preferences_preload_label": "Предварително заредете видео данни: ",
|
||||||
|
"First page": "Първа страница"
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,7 +204,7 @@
|
||||||
"View JavaScript license information.": "Consulta la informació de la llicència de JavaScript.",
|
"View JavaScript license information.": "Consulta la informació de la llicència de JavaScript.",
|
||||||
"Playlist privacy": "Privacitat de la llista de reproducció",
|
"Playlist privacy": "Privacitat de la llista de reproducció",
|
||||||
"search_message_no_results": "No s'han trobat resultats.",
|
"search_message_no_results": "No s'han trobat resultats.",
|
||||||
"search_message_use_another_instance": " També es pot <a href=\"`x`\">buscar en una altra instància</a>.",
|
"search_message_use_another_instance": "També es pot <a href=\"`x`\">cercar en una altra instància</a>.",
|
||||||
"Genre: ": "Gènere: ",
|
"Genre: ": "Gènere: ",
|
||||||
"Hidden field \"challenge\" is a required field": "El camp ocult \"repte\" és un camp obligatori",
|
"Hidden field \"challenge\" is a required field": "El camp ocult \"repte\" és un camp obligatori",
|
||||||
"Burmese": "Birmà",
|
"Burmese": "Birmà",
|
||||||
|
@ -489,5 +489,16 @@
|
||||||
"generic_button_delete": "Suprimeix",
|
"generic_button_delete": "Suprimeix",
|
||||||
"Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)",
|
"Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)",
|
||||||
"Answer": "Resposta",
|
"Answer": "Resposta",
|
||||||
"toggle_theme": "Commuta el tema"
|
"toggle_theme": "Commuta el tema",
|
||||||
|
"Add to playlist": "Afegeix a la llista de reproducció",
|
||||||
|
"Add to playlist: ": "Afegeix a la llista de reproducció: ",
|
||||||
|
"Search for videos": "Cercar vídeos",
|
||||||
|
"carousel_slide": "Diapositiva {{current}} de {{total}}",
|
||||||
|
"preferences_preload_label": "Precarregar dades del vídeo: ",
|
||||||
|
"carousel_go_to": "Anar a la diapositiva `x`",
|
||||||
|
"First page": "Primera pàgina",
|
||||||
|
"Filipino (auto-generated)": "Filipí (generat automàticament)",
|
||||||
|
"channel_tab_courses_label": "Cursos",
|
||||||
|
"channel_tab_posts_label": "Missatges",
|
||||||
|
"carousel_skip": "Saltar l'exhibició"
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@
|
||||||
"Family friendly? ": "Vhodné pro rodiny? ",
|
"Family friendly? ": "Vhodné pro rodiny? ",
|
||||||
"Engagement: ": "Zapojení: ",
|
"Engagement: ": "Zapojení: ",
|
||||||
"English": "Angličtina",
|
"English": "Angličtina",
|
||||||
"English (auto-generated)": "Angličtina (automaticky generováno)",
|
"English (auto-generated)": "Angličtina (vytvořeno automaticky)",
|
||||||
"Afrikaans": "Afrikánština",
|
"Afrikaans": "Afrikánština",
|
||||||
"Albanian": "Albánština",
|
"Albanian": "Albánština",
|
||||||
"Amharic": "Amharština",
|
"Amharic": "Amharština",
|
||||||
|
@ -294,8 +294,8 @@
|
||||||
"Chinese (China)": "Čínština (Čína)",
|
"Chinese (China)": "Čínština (Čína)",
|
||||||
"Chinese (Hong Kong)": "Čínština (Hong Kong)",
|
"Chinese (Hong Kong)": "Čínština (Hong Kong)",
|
||||||
"Chinese (Taiwan)": "Čínština (Taiwan)",
|
"Chinese (Taiwan)": "Čínština (Taiwan)",
|
||||||
"Portuguese (auto-generated)": "Portugalština (automaticky generováno)",
|
"Portuguese (auto-generated)": "Portugalština (vytvořeno automaticky)",
|
||||||
"Spanish (auto-generated)": "Španělština (automaticky generováno)",
|
"Spanish (auto-generated)": "Španělština (vytvořeno automaticky)",
|
||||||
"Spanish (Mexico)": "Španělština (Mexiko)",
|
"Spanish (Mexico)": "Španělština (Mexiko)",
|
||||||
"Spanish (Spain)": "Španělština (Španělsko)",
|
"Spanish (Spain)": "Španělština (Španělsko)",
|
||||||
"generic_count_years_0": "{{count}} rokem",
|
"generic_count_years_0": "{{count}} rokem",
|
||||||
|
@ -352,13 +352,13 @@
|
||||||
"comments_points_count_0": "{{count}} bod",
|
"comments_points_count_0": "{{count}} bod",
|
||||||
"comments_points_count_1": "{{count}} body",
|
"comments_points_count_1": "{{count}} body",
|
||||||
"comments_points_count_2": "{{count}} bodů",
|
"comments_points_count_2": "{{count}} bodů",
|
||||||
"German (auto-generated)": "Němčina (automaticky generováno)",
|
"German (auto-generated)": "Němčina (vytvořeno automaticky)",
|
||||||
"Indonesian (auto-generated)": "Indonéština (automaticky generováno)",
|
"Indonesian (auto-generated)": "Indonéština (vytvořeno automaticky)",
|
||||||
"Interlingue": "Interlingue",
|
"Interlingue": "Interlingue",
|
||||||
"Italian (auto-generated)": "Italština (automaticky generováno)",
|
"Italian (auto-generated)": "Italština (vytvořeno automaticky)",
|
||||||
"Japanese (auto-generated)": "Japonština (automaticky generováno)",
|
"Japanese (auto-generated)": "Japonština (vytvořeno automaticky)",
|
||||||
"Korean (auto-generated)": "Korejština (automaticky generováno)",
|
"Korean (auto-generated)": "Korejština (vytvořeno automaticky)",
|
||||||
"Russian (auto-generated)": "Ruština (automaticky generováno)",
|
"Russian (auto-generated)": "Ruština (vytvořeno automaticky)",
|
||||||
"generic_count_months_0": "{{count}} měsícem",
|
"generic_count_months_0": "{{count}} měsícem",
|
||||||
"generic_count_months_1": "{{count}} měsíci",
|
"generic_count_months_1": "{{count}} měsíci",
|
||||||
"generic_count_months_2": "{{count}} měsíci",
|
"generic_count_months_2": "{{count}} měsíci",
|
||||||
|
@ -371,7 +371,7 @@
|
||||||
"footer_documentation": "Dokumentace",
|
"footer_documentation": "Dokumentace",
|
||||||
"next_steps_error_message_refresh": "Obnovit stránku",
|
"next_steps_error_message_refresh": "Obnovit stránku",
|
||||||
"Chinese": "Čínština",
|
"Chinese": "Čínština",
|
||||||
"Dutch (auto-generated)": "Nizozemština (automaticky generováno)",
|
"Dutch (auto-generated)": "Nizozemština (vytvořeno automaticky)",
|
||||||
"Erroneous token": "Chybný token",
|
"Erroneous token": "Chybný token",
|
||||||
"tokens_count_0": "{{count}} token",
|
"tokens_count_0": "{{count}} token",
|
||||||
"tokens_count_1": "{{count}} tokeny",
|
"tokens_count_1": "{{count}} tokeny",
|
||||||
|
@ -380,9 +380,9 @@
|
||||||
"Token is expired, please try again": "Token vypršel, zkuste to prosím znovu",
|
"Token is expired, please try again": "Token vypršel, zkuste to prosím znovu",
|
||||||
"English (United States)": "Angličtina (Spojené státy)",
|
"English (United States)": "Angličtina (Spojené státy)",
|
||||||
"Cantonese (Hong Kong)": "Kantonština (Hong Kong)",
|
"Cantonese (Hong Kong)": "Kantonština (Hong Kong)",
|
||||||
"French (auto-generated)": "Francouzština (automaticky generováno)",
|
"French (auto-generated)": "Francouzština (vytvořeno automaticky)",
|
||||||
"Turkish (auto-generated)": "Turečtina (automaticky generováno)",
|
"Turkish (auto-generated)": "Turečtina (vytvořeno automaticky)",
|
||||||
"Vietnamese (auto-generated)": "Vietnamština (automaticky generováno)",
|
"Vietnamese (auto-generated)": "Vietnamština (vytvořeno automaticky)",
|
||||||
"Current version: ": "Aktuální verze: ",
|
"Current version: ": "Aktuální verze: ",
|
||||||
"next_steps_error_message": "Měli byste zkusit: ",
|
"next_steps_error_message": "Měli byste zkusit: ",
|
||||||
"footer_donate_page": "Přispět",
|
"footer_donate_page": "Přispět",
|
||||||
|
@ -513,5 +513,10 @@
|
||||||
"The Popular feed has been disabled by the administrator.": "Kategorie Populární byla zakázána administrátorem.",
|
"The Popular feed has been disabled by the administrator.": "Kategorie Populární byla zakázána administrátorem.",
|
||||||
"carousel_slide": "Snímek {{current}} z {{total}}",
|
"carousel_slide": "Snímek {{current}} z {{total}}",
|
||||||
"carousel_skip": "Přeskočit galerii",
|
"carousel_skip": "Přeskočit galerii",
|
||||||
"carousel_go_to": "Přejít na snímek `x`"
|
"carousel_go_to": "Přejít na snímek `x`",
|
||||||
|
"preferences_preload_label": "Předem načíst data videa: ",
|
||||||
|
"Filipino (auto-generated)": "Filipínština (vytvořeno automaticky)",
|
||||||
|
"First page": "První stránka",
|
||||||
|
"channel_tab_courses_label": "Kurzy",
|
||||||
|
"channel_tab_posts_label": "Příspěvky"
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,7 @@
|
||||||
"An alternative front-end to YouTube": "Pen blaen amgen i YouTube",
|
"An alternative front-end to YouTube": "Pen blaen amgen i YouTube",
|
||||||
"source": "ffynhonnell",
|
"source": "ffynhonnell",
|
||||||
"Log in": "Mewngofnodi",
|
"Log in": "Mewngofnodi",
|
||||||
"Log in/register": "Mewngofnodi/Cofrestru",
|
"Log in/register": "Mewngofnodi/cofrestru",
|
||||||
"User ID": "Enw defnyddiwr",
|
"User ID": "Enw defnyddiwr",
|
||||||
"preferences_quality_option_dash": "DASH (ansawdd addasol)",
|
"preferences_quality_option_dash": "DASH (ansawdd addasol)",
|
||||||
"Sign In": "Mewngofnodi",
|
"Sign In": "Mewngofnodi",
|
||||||
|
@ -381,5 +381,32 @@
|
||||||
"channel_tab_channels_label": "Sianeli",
|
"channel_tab_channels_label": "Sianeli",
|
||||||
"channel_tab_community_label": "Cymuned",
|
"channel_tab_community_label": "Cymuned",
|
||||||
"channel_tab_shorts_label": "Fideos byrion",
|
"channel_tab_shorts_label": "Fideos byrion",
|
||||||
"channel_tab_videos_label": "Fideos"
|
"channel_tab_videos_label": "Fideos",
|
||||||
|
"generic_playlists_count_0": "{{count}} rhestr chwarae",
|
||||||
|
"generic_playlists_count_1": "{{count}} rhestr chwarae",
|
||||||
|
"generic_playlists_count_2": "{{count}} rhestri chwarae",
|
||||||
|
"generic_playlists_count_3": "{{count}} rhestri chwarae",
|
||||||
|
"generic_playlists_count_4": "{{count}} rhestri chwarae",
|
||||||
|
"generic_playlists_count_5": "{{count}} rhestri chwarae",
|
||||||
|
"New passwords must match": "Rhaid i'r cyfrineiriau newydd cyfateb â'i gilydd",
|
||||||
|
"last": "diwethaf",
|
||||||
|
"First page": "Tudalen gyntaf",
|
||||||
|
"preferences_preload_label": "Cynlwytho data fideo: ",
|
||||||
|
"preferences_extend_desc_label": "Ymestyn disgrifiad fideo'n awtomatig: ",
|
||||||
|
"preferences_vr_mode_label": "Fideos rhyngweithiol 360 gradd (angen WebGL): ",
|
||||||
|
"preferences_video_loop_label": "Doleniwch bob amser: ",
|
||||||
|
"Top enabled: ": "Tudalen fideos brig wedi'i alluogi: ",
|
||||||
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Allforio tanysgrifiadau ar fformat OPML (i NewPipe a FreeTube)",
|
||||||
|
"Export subscriptions as OPML": "Allforio tanysgrifiadau ar fformat OPML",
|
||||||
|
"preferences_annotations_subscribed_label": "Ddangos nodiadau sianeli tanysgrifiwyd fel rhagosodiad? ",
|
||||||
|
"Redirect homepage to feed: ": "Ailgyfeirio tudalen gartref i'r borthiant: ",
|
||||||
|
"preferences_feed_menu_label": "Dewislen porthiant: ",
|
||||||
|
"Login enabled: ": "Mewngofnodi wedi'i alluogi: ",
|
||||||
|
"tokens_count_0": "",
|
||||||
|
"tokens_count_1": "tocyn",
|
||||||
|
"tokens_count_2": "",
|
||||||
|
"tokens_count_3": "",
|
||||||
|
"tokens_count_4": "tocynnau",
|
||||||
|
"tokens_count_5": "",
|
||||||
|
"Source available here.": "Tarddle ar gael yma."
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"last": "neueste",
|
"last": "neueste",
|
||||||
"Next page": "Nächste Seite",
|
"Next page": "Nächste Seite",
|
||||||
"Previous page": "Vorherige Seite",
|
"Previous page": "Vorherige Seite",
|
||||||
|
"First page": "Erste Seite",
|
||||||
"Clear watch history?": "Verlauf löschen?",
|
"Clear watch history?": "Verlauf löschen?",
|
||||||
"New password": "Neues Passwort",
|
"New password": "Neues Passwort",
|
||||||
"New passwords must match": "Neue Passwörter müssen übereinstimmen",
|
"New passwords must match": "Neue Passwörter müssen übereinstimmen",
|
||||||
|
@ -490,12 +491,15 @@
|
||||||
"generic_channels_count_plural": "{{count}} Kanäle",
|
"generic_channels_count_plural": "{{count}} Kanäle",
|
||||||
"Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)",
|
"Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)",
|
||||||
"Answer": "Antwort",
|
"Answer": "Antwort",
|
||||||
"The Popular feed has been disabled by the administrator.": "Der Angesagt-Feed wurde vom Administrator deaktiviert.",
|
"The Popular feed has been disabled by the administrator.": "Der Feed für beliebte Inhalte wurde vom Administrator deaktiviert.",
|
||||||
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
|
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
|
||||||
"Search for videos": "Nach Videos suchen",
|
"Search for videos": "Nach Videos suchen",
|
||||||
"toggle_theme": "Thema wechseln",
|
"toggle_theme": "Thema wechseln",
|
||||||
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
|
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
|
||||||
"carousel_go_to": "Zu Folie `x` gehen",
|
"carousel_go_to": "Zu Element `x` springen",
|
||||||
"carousel_slide": "Folie {{current}} von {{total}}",
|
"carousel_slide": "Seite {{current}} von {{total}}",
|
||||||
"carousel_skip": "Karussell überspringen"
|
"carousel_skip": "Galerie überspringen",
|
||||||
|
"Filipino (auto-generated)": "Philippinisch (automatisch generiert)",
|
||||||
|
"channel_tab_courses_label": "Kurse",
|
||||||
|
"channel_tab_posts_label": "Beiträge"
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
"Import and Export Data": "Εισαγωγή και Εξαγωγή Δεδομένων",
|
"Import and Export Data": "Εισαγωγή και Εξαγωγή Δεδομένων",
|
||||||
"Import": "Εισαγωγή",
|
"Import": "Εισαγωγή",
|
||||||
"Import Invidious data": "Εsαγωγή δεδομένων Invidious JSON",
|
"Import Invidious data": "Εsαγωγή δεδομένων Invidious JSON",
|
||||||
"Import YouTube subscriptions": "Εισαγωγή συνδρομών YouTube/OPML",
|
"Import YouTube subscriptions": "Εισαγωγή συνδρομών YouTube απο CVS/OPML",
|
||||||
"Import FreeTube subscriptions (.db)": "Εισαγωγή συνδρομών FreeTube (.db)",
|
"Import FreeTube subscriptions (.db)": "Εισαγωγή συνδρομών FreeTube (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Εισαγωγή συνδρομών NewPipe (.json)",
|
"Import NewPipe subscriptions (.json)": "Εισαγωγή συνδρομών NewPipe (.json)",
|
||||||
"Import NewPipe data (.zip)": "Εισαγωγή δεδομένων NewPipe (.zip)",
|
"Import NewPipe data (.zip)": "Εισαγωγή δεδομένων NewPipe (.zip)",
|
||||||
|
@ -455,7 +455,7 @@
|
||||||
"channel_tab_streams_label": "Ζωντανή μετάδοση",
|
"channel_tab_streams_label": "Ζωντανή μετάδοση",
|
||||||
"playlist_button_add_items": "Προσθήκη βίντεο",
|
"playlist_button_add_items": "Προσθήκη βίντεο",
|
||||||
"Artist: ": "Καλλιτέχνης: ",
|
"Artist: ": "Καλλιτέχνης: ",
|
||||||
"search_message_use_another_instance": " Μπορείτε επίσης <a href=\"`x`\">να αναζητήσετε σε άλλο instance</a>.",
|
"search_message_use_another_instance": "Μπορείτε επίσης <a href=\"`x`\">να αναζητήσετε σε άλλο instance</a>.",
|
||||||
"generic_button_save": "Αποθήκευση",
|
"generic_button_save": "Αποθήκευση",
|
||||||
"generic_button_cancel": "Ακύρωση",
|
"generic_button_cancel": "Ακύρωση",
|
||||||
"subscriptions_unseen_notifs_count": "{{count}} μη αναγνωσμένη ειδοποίηση",
|
"subscriptions_unseen_notifs_count": "{{count}} μη αναγνωσμένη ειδοποίηση",
|
||||||
|
@ -490,9 +490,16 @@
|
||||||
"Search for videos": "Αναζήτηση βίντεο",
|
"Search for videos": "Αναζήτηση βίντεο",
|
||||||
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
|
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
|
||||||
"Answer": "Απάντηση",
|
"Answer": "Απάντηση",
|
||||||
"Add to playlist": "Λίιστα αναπαραγωγής",
|
"Add to playlist": "Προσθήκη στην λίστα αναπαραγωγής",
|
||||||
"Add to playlist: ": "Λίστα αναπαραγωγής: ",
|
"Add to playlist: ": "Προσθήκη στην λίστα αναπαραγωγής : ",
|
||||||
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
|
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
|
||||||
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
|
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
|
||||||
"toggle_theme": "Αλλαγή θέματος"
|
"toggle_theme": "Αλλαγή θέματος",
|
||||||
|
"Import YouTube watch history (.json)": "Εισαγωγή ιστορικού προβολής YouTube (.json)",
|
||||||
|
"Filipino (auto-generated)": "Φιλιππινέζικα (αυτόματη παραγωγή)",
|
||||||
|
"preferences_preload_label": "Προφόρτιση δεδομένων βίντεο: ",
|
||||||
|
"carousel_skip": "Αποφυγή εμφάνισης εικόνων",
|
||||||
|
"First page": "Πρώτη σελίδα",
|
||||||
|
"channel_tab_courses_label": "Μαθήματα",
|
||||||
|
"channel_tab_posts_label": "Δημοσιεύσεις"
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"last": "last",
|
"last": "last",
|
||||||
"Next page": "Next page",
|
"Next page": "Next page",
|
||||||
"Previous page": "Previous page",
|
"Previous page": "Previous page",
|
||||||
|
"First page": "First page",
|
||||||
"Clear watch history?": "Clear watch history?",
|
"Clear watch history?": "Clear watch history?",
|
||||||
"New password": "New password",
|
"New password": "New password",
|
||||||
"New passwords must match": "New passwords must match",
|
"New passwords must match": "New passwords must match",
|
||||||
|
@ -63,8 +64,6 @@
|
||||||
"User ID": "User ID",
|
"User ID": "User ID",
|
||||||
"Password": "Password",
|
"Password": "Password",
|
||||||
"Time (h:mm:ss):": "Time (h:mm:ss):",
|
"Time (h:mm:ss):": "Time (h:mm:ss):",
|
||||||
"Text CAPTCHA": "Text CAPTCHA",
|
|
||||||
"Image CAPTCHA": "Image CAPTCHA",
|
|
||||||
"Sign In": "Sign In",
|
"Sign In": "Sign In",
|
||||||
"Register": "Register",
|
"Register": "Register",
|
||||||
"E-mail": "E-mail",
|
"E-mail": "E-mail",
|
||||||
|
@ -492,11 +491,16 @@
|
||||||
"channel_tab_streams_label": "Livestreams",
|
"channel_tab_streams_label": "Livestreams",
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
"channel_tab_podcasts_label": "Podcasts",
|
||||||
"channel_tab_releases_label": "Releases",
|
"channel_tab_releases_label": "Releases",
|
||||||
|
"channel_tab_courses_label": "Courses",
|
||||||
"channel_tab_playlists_label": "Playlists",
|
"channel_tab_playlists_label": "Playlists",
|
||||||
"channel_tab_community_label": "Community",
|
"channel_tab_community_label": "Community",
|
||||||
|
"channel_tab_posts_label": "Posts",
|
||||||
"channel_tab_channels_label": "Channels",
|
"channel_tab_channels_label": "Channels",
|
||||||
"toggle_theme": "Toggle Theme",
|
"toggle_theme": "Toggle Theme",
|
||||||
"carousel_slide": "Slide {{current}} of {{total}}",
|
"carousel_slide": "Slide {{current}} of {{total}}",
|
||||||
"carousel_skip": "Skip the Carousel",
|
"carousel_skip": "Skip the Carousel",
|
||||||
"carousel_go_to": "Go to slide `x`"
|
"carousel_go_to": "Go to slide `x`",
|
||||||
|
"timeline_parse_error_placeholder_heading": "Unable to parse item",
|
||||||
|
"timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:",
|
||||||
|
"timeline_parse_error_show_technical_details": "Show technical details"
|
||||||
}
|
}
|
||||||
|
|
|
@ -187,10 +187,10 @@
|
||||||
"Hidden field \"token\" is a required field": "El campo oculto «símbolo» es un campo obligatorio",
|
"Hidden field \"token\" is a required field": "El campo oculto «símbolo» es un campo obligatorio",
|
||||||
"Erroneous challenge": "Desafío no válido",
|
"Erroneous challenge": "Desafío no válido",
|
||||||
"Erroneous token": "Símbolo no válido",
|
"Erroneous token": "Símbolo no válido",
|
||||||
"No such user": "Usuario no existe",
|
"No such user": "El usuario no existe",
|
||||||
"Token is expired, please try again": "El símbolo ha caducado, inténtelo de nuevo",
|
"Token is expired, please try again": "El símbolo ha caducado, inténtelo de nuevo",
|
||||||
"English": "Inglés",
|
"English": "Inglés",
|
||||||
"English (auto-generated)": "Inglés (generado automáticamente)",
|
"English (auto-generated)": "Inglés (generados automáticamente)",
|
||||||
"Afrikaans": "Afrikáans",
|
"Afrikaans": "Afrikáans",
|
||||||
"Albanian": "Albanés",
|
"Albanian": "Albanés",
|
||||||
"Amharic": "Amárico",
|
"Amharic": "Amárico",
|
||||||
|
@ -276,7 +276,7 @@
|
||||||
"Somali": "Somalí",
|
"Somali": "Somalí",
|
||||||
"Southern Sotho": "Sesoto",
|
"Southern Sotho": "Sesoto",
|
||||||
"Spanish": "Español",
|
"Spanish": "Español",
|
||||||
"Spanish (Latin America)": "Español (Hispanoamérica)",
|
"Spanish (Latin America)": "Español (Latinoamérica)",
|
||||||
"Sundanese": "Sondanés",
|
"Sundanese": "Sondanés",
|
||||||
"Swahili": "Suajili",
|
"Swahili": "Suajili",
|
||||||
"Swedish": "Sueco",
|
"Swedish": "Sueco",
|
||||||
|
@ -412,8 +412,8 @@
|
||||||
"generic_count_weeks_1": "{{count}} semanas",
|
"generic_count_weeks_1": "{{count}} semanas",
|
||||||
"generic_count_weeks_2": "{{count}} semanas",
|
"generic_count_weeks_2": "{{count}} semanas",
|
||||||
"generic_playlists_count_0": "{{count}} lista de reproducción",
|
"generic_playlists_count_0": "{{count}} lista de reproducción",
|
||||||
"generic_playlists_count_1": "{{count}} listas de reproducciones",
|
"generic_playlists_count_1": "{{count}} listas de reproducción",
|
||||||
"generic_playlists_count_2": "{{count}} listas de reproducciones",
|
"generic_playlists_count_2": "{{count}} listas de reproducción",
|
||||||
"generic_videos_count_0": "{{count}} video",
|
"generic_videos_count_0": "{{count}} video",
|
||||||
"generic_videos_count_1": "{{count}} videos",
|
"generic_videos_count_1": "{{count}} videos",
|
||||||
"generic_videos_count_2": "{{count}} videos",
|
"generic_videos_count_2": "{{count}} videos",
|
||||||
|
@ -463,7 +463,7 @@
|
||||||
"Chinese (Hong Kong)": "Chino (Hong Kong)",
|
"Chinese (Hong Kong)": "Chino (Hong Kong)",
|
||||||
"Chinese (China)": "Chino (China)",
|
"Chinese (China)": "Chino (China)",
|
||||||
"Korean (auto-generated)": "Coreano (generados automáticamente)",
|
"Korean (auto-generated)": "Coreano (generados automáticamente)",
|
||||||
"Spanish (Mexico)": "Español (Méjico)",
|
"Spanish (Mexico)": "Español (México)",
|
||||||
"Spanish (auto-generated)": "Español (generados automáticamente)",
|
"Spanish (auto-generated)": "Español (generados automáticamente)",
|
||||||
"preferences_watch_history_label": "Habilitar historial de reproducciones: ",
|
"preferences_watch_history_label": "Habilitar historial de reproducciones: ",
|
||||||
"search_message_no_results": "No se han encontrado resultados.",
|
"search_message_no_results": "No se han encontrado resultados.",
|
||||||
|
@ -500,7 +500,7 @@
|
||||||
"generic_button_cancel": "Cancelar",
|
"generic_button_cancel": "Cancelar",
|
||||||
"generic_button_rss": "RSS",
|
"generic_button_rss": "RSS",
|
||||||
"channel_tab_podcasts_label": "Podcasts",
|
"channel_tab_podcasts_label": "Podcasts",
|
||||||
"channel_tab_releases_label": "Publicaciones",
|
"channel_tab_releases_label": "Lanzamientos",
|
||||||
"generic_channels_count_0": "{{count}} canal",
|
"generic_channels_count_0": "{{count}} canal",
|
||||||
"generic_channels_count_1": "{{count}} canales",
|
"generic_channels_count_1": "{{count}} canales",
|
||||||
"generic_channels_count_2": "{{count}} canales",
|
"generic_channels_count_2": "{{count}} canales",
|
||||||
|
@ -513,5 +513,10 @@
|
||||||
"The Popular feed has been disabled by the administrator.": "El feed Popular ha sido desactivado por el administrador.",
|
"The Popular feed has been disabled by the administrator.": "El feed Popular ha sido desactivado por el administrador.",
|
||||||
"carousel_slide": "Diapositiva {{current}} de {{total}}",
|
"carousel_slide": "Diapositiva {{current}} de {{total}}",
|
||||||
"carousel_skip": "Saltar el carrusel",
|
"carousel_skip": "Saltar el carrusel",
|
||||||
"carousel_go_to": "Ir a la diapositiva `x`"
|
"carousel_go_to": "Ir a la diapositiva `x`",
|
||||||
|
"preferences_preload_label": "Precargar datos del vídeo: ",
|
||||||
|
"Filipino (auto-generated)": "Filipino (generados automáticamente)",
|
||||||
|
"channel_tab_posts_label": "Publicaciones",
|
||||||
|
"First page": "Primera página",
|
||||||
|
"channel_tab_courses_label": "Cursos"
|
||||||
}
|
}
|
||||||
|
|
|
@ -496,5 +496,6 @@
|
||||||
"crash_page_search_issue": "دنبال <a href=\"`x`\"> گشتیم بین مشکلات در گیت هاب </a>",
|
"crash_page_search_issue": "دنبال <a href=\"`x`\"> گشتیم بین مشکلات در گیت هاب </a>",
|
||||||
"crash_page_report_issue": "اگر هیچ یک از روش های بالا کمکی نکردند لطفا <a href=\"`x`\"> (ترجیحا به انگلیسی) یک سوال جدید در گیت هاب بپرسید و </a> طوری که سوالتون شامل متن زیر باشه:",
|
"crash_page_report_issue": "اگر هیچ یک از روش های بالا کمکی نکردند لطفا <a href=\"`x`\"> (ترجیحا به انگلیسی) یک سوال جدید در گیت هاب بپرسید و </a> طوری که سوالتون شامل متن زیر باشه:",
|
||||||
"channel_tab_releases_label": "آثار",
|
"channel_tab_releases_label": "آثار",
|
||||||
"toggle_theme": "تغییر وضعیت تم"
|
"toggle_theme": "تغییر وضعیت تم",
|
||||||
|
"preferences_preload_label": "پیش بار کردن دادههای ویدیو: "
|
||||||
}
|
}
|
||||||
|
|
|
@ -460,7 +460,7 @@
|
||||||
"search_filters_apply_button": "Ota valitut suodattimet käyttöön",
|
"search_filters_apply_button": "Ota valitut suodattimet käyttöön",
|
||||||
"search_filters_date_label": "Latausaika",
|
"search_filters_date_label": "Latausaika",
|
||||||
"search_filters_duration_option_medium": "Keskipituinen (4 - 20 minuuttia)",
|
"search_filters_duration_option_medium": "Keskipituinen (4 - 20 minuuttia)",
|
||||||
"search_message_use_another_instance": " Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.",
|
"search_message_use_another_instance": "Voit myös <a href=\"`x`\">hakea toisella instanssilla</a>.",
|
||||||
"search_filters_date_option_none": "Milloin tahansa",
|
"search_filters_date_option_none": "Milloin tahansa",
|
||||||
"search_filters_type_option_all": "Mikä tahansa tyyppi",
|
"search_filters_type_option_all": "Mikä tahansa tyyppi",
|
||||||
"Popular enabled: ": "Suosittu käytössä: ",
|
"Popular enabled: ": "Suosittu käytössä: ",
|
||||||
|
@ -496,5 +496,6 @@
|
||||||
"generic_channels_count_plural": "{{count}} kanavaa",
|
"generic_channels_count_plural": "{{count}} kanavaa",
|
||||||
"The Popular feed has been disabled by the administrator.": "Järjestelmänvalvoja on poistanut Suositut-syötteen.",
|
"The Popular feed has been disabled by the administrator.": "Järjestelmänvalvoja on poistanut Suositut-syötteen.",
|
||||||
"Import YouTube watch history (.json)": "Tuo Youtube-katseluhistoria (.json)",
|
"Import YouTube watch history (.json)": "Tuo Youtube-katseluhistoria (.json)",
|
||||||
"toggle_theme": "Vaihda teemaa"
|
"toggle_theme": "Vaihda teemaa",
|
||||||
|
"preferences_preload_label": "Esilataa video data. "
|
||||||
}
|
}
|
||||||
|
|
|
@ -505,7 +505,7 @@
|
||||||
"channel_tab_releases_label": "Parutions",
|
"channel_tab_releases_label": "Parutions",
|
||||||
"channel_tab_podcasts_label": "Émissions audio",
|
"channel_tab_podcasts_label": "Émissions audio",
|
||||||
"Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)",
|
"Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)",
|
||||||
"Add to playlist: ": "Ajouter à la playlist : ",
|
"Add to playlist: ": "Ajouter à la playlist : ",
|
||||||
"Add to playlist": "Ajouter à la playlist",
|
"Add to playlist": "Ajouter à la playlist",
|
||||||
"Answer": "Répondre",
|
"Answer": "Répondre",
|
||||||
"Search for videos": "Rechercher des vidéos",
|
"Search for videos": "Rechercher des vidéos",
|
||||||
|
@ -513,5 +513,10 @@
|
||||||
"carousel_skip": "Passez le carrousel",
|
"carousel_skip": "Passez le carrousel",
|
||||||
"carousel_slide": "Diapositive {{current}} sur {{total}}",
|
"carousel_slide": "Diapositive {{current}} sur {{total}}",
|
||||||
"carousel_go_to": "Aller à la diapositive `x`",
|
"carousel_go_to": "Aller à la diapositive `x`",
|
||||||
"toggle_theme": "Changer le Thème"
|
"toggle_theme": "Changer le Thème",
|
||||||
|
"Filipino (auto-generated)": "Philippines (automatiquement générer)",
|
||||||
|
"preferences_preload_label": "Précharger les données de la vidéo : ",
|
||||||
|
"First page": "Première page",
|
||||||
|
"channel_tab_courses_label": "Cours",
|
||||||
|
"channel_tab_posts_label": "Messages"
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,5 +513,7 @@
|
||||||
"toggle_theme": "Uklj./Isklj. temu",
|
"toggle_theme": "Uklj./Isklj. temu",
|
||||||
"carousel_slide": "Kadar {{current}} od {{total}}",
|
"carousel_slide": "Kadar {{current}} od {{total}}",
|
||||||
"carousel_go_to": "Idi na kadar `x`",
|
"carousel_go_to": "Idi na kadar `x`",
|
||||||
"carousel_skip": "Preskoči vrtuljak"
|
"carousel_skip": "Preskoči vrtuljak",
|
||||||
|
"Filipino (auto-generated)": "Filipinski (automatski generirano)",
|
||||||
|
"preferences_preload_label": "Unaprijed učitaj podatke videa: "
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"LIVE": "BEINT",
|
"LIVE": "BEINT",
|
||||||
"Shared `x` ago": "Deilt fyrir `x` síðan",
|
"Shared `x` ago": "Deilt fyrir `x` síðan",
|
||||||
"Unsubscribe": "Afskrá",
|
"Unsubscribe": "Afskrá",
|
||||||
"Subscribe": "Áskrifa",
|
"Subscribe": "Setja í áskrift",
|
||||||
"View channel on YouTube": "Skoða rás á YouTube",
|
"View channel on YouTube": "Skoða rás á YouTube",
|
||||||
"View playlist on YouTube": "Skoða spilunarlista á YouTube",
|
"View playlist on YouTube": "Skoða spilunarlista á YouTube",
|
||||||
"newest": "nýjasta",
|
"newest": "nýjasta",
|
||||||
|
@ -14,8 +14,8 @@
|
||||||
"Clear watch history?": "Hreinsa áhorfsferil?",
|
"Clear watch history?": "Hreinsa áhorfsferil?",
|
||||||
"New password": "Nýtt lykilorð",
|
"New password": "Nýtt lykilorð",
|
||||||
"New passwords must match": "Nýtt lykilorð verður að passa",
|
"New passwords must match": "Nýtt lykilorð verður að passa",
|
||||||
"Authorize token?": "Leyfa teikn?",
|
"Authorize token?": "Auðkenna teikn?",
|
||||||
"Authorize token for `x`?": "Leyfa teikn fyrir `x`?",
|
"Authorize token for `x`?": "Auðkenna teikn fyrir `x`?",
|
||||||
"Yes": "Já",
|
"Yes": "Já",
|
||||||
"No": "Nei",
|
"No": "Nei",
|
||||||
"Import and Export Data": "Inn- og útflutningur gagna",
|
"Import and Export Data": "Inn- og útflutningur gagna",
|
||||||
|
@ -36,17 +36,17 @@
|
||||||
"source": "uppruni",
|
"source": "uppruni",
|
||||||
"Log in": "Skrá inn",
|
"Log in": "Skrá inn",
|
||||||
"Log in/register": "Innskráning/nýskráning",
|
"Log in/register": "Innskráning/nýskráning",
|
||||||
"User ID": "Notandakenni",
|
"User ID": "Auðkenni notanda",
|
||||||
"Password": "Lykilorð",
|
"Password": "Lykilorð",
|
||||||
"Time (h:mm:ss):": "Tími (h:mm: ss):",
|
"Time (h:mm:ss):": "Tími (h:mm: ss):",
|
||||||
"Text CAPTCHA": "Texta CAPTCHA",
|
"Text CAPTCHA": "CAPTCHA-texti",
|
||||||
"Image CAPTCHA": "Mynd CAPTCHA",
|
"Image CAPTCHA": "CAPTCHA-mynd",
|
||||||
"Sign In": "Skrá inn",
|
"Sign In": "Skrá inn",
|
||||||
"Register": "Nýskrá",
|
"Register": "Nýskrá",
|
||||||
"E-mail": "Tölvupóstur",
|
"E-mail": "Tölvupóstur",
|
||||||
"Preferences": "Kjörstillingar",
|
"Preferences": "Kjörstillingar",
|
||||||
"preferences_category_player": "Kjörstillingar spilara",
|
"preferences_category_player": "Kjörstillingar spilara",
|
||||||
"preferences_video_loop_label": "Alltaf lykkja: ",
|
"preferences_video_loop_label": "Alltaf endurtaka: ",
|
||||||
"preferences_autoplay_label": "Sjálfvirk spilun: ",
|
"preferences_autoplay_label": "Sjálfvirk spilun: ",
|
||||||
"preferences_continue_label": "Spila næst sjálfgefið: ",
|
"preferences_continue_label": "Spila næst sjálfgefið: ",
|
||||||
"preferences_continue_autoplay_label": "Spila næsta myndskeið sjálfkrafa: ",
|
"preferences_continue_autoplay_label": "Spila næsta myndskeið sjálfkrafa: ",
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
"preferences_unseen_only_label": "Sýna aðeins óséð: ",
|
"preferences_unseen_only_label": "Sýna aðeins óséð: ",
|
||||||
"preferences_notifications_only_label": "Sýna aðeins tilkynningar (ef einhverjar eru): ",
|
"preferences_notifications_only_label": "Sýna aðeins tilkynningar (ef einhverjar eru): ",
|
||||||
"Enable web notifications": "Virkja veftilkynningar",
|
"Enable web notifications": "Virkja veftilkynningar",
|
||||||
"`x` uploaded a video": "`x` hlóð upp myndband",
|
"`x` uploaded a video": "`x` sendi inn myndskeið",
|
||||||
"`x` is live": "`x` er í beinni",
|
"`x` is live": "`x` er í beinni",
|
||||||
"preferences_category_data": "Gagnastillingar",
|
"preferences_category_data": "Gagnastillingar",
|
||||||
"Clear watch history": "Hreinsa áhorfsferil",
|
"Clear watch history": "Hreinsa áhorfsferil",
|
||||||
|
@ -104,8 +104,8 @@
|
||||||
"Registration enabled: ": "Nýskráning virkjuð? ",
|
"Registration enabled: ": "Nýskráning virkjuð? ",
|
||||||
"Report statistics: ": "Skrá tölfræði? ",
|
"Report statistics: ": "Skrá tölfræði? ",
|
||||||
"Save preferences": "Vista stillingar",
|
"Save preferences": "Vista stillingar",
|
||||||
"Subscription manager": "Áskriftarstjóri",
|
"Subscription manager": "Áskriftastýring",
|
||||||
"Token manager": "Teiknastjórnun",
|
"Token manager": "Teiknastýring",
|
||||||
"Token": "Teikn",
|
"Token": "Teikn",
|
||||||
"Import/export": "Flytja inn/út",
|
"Import/export": "Flytja inn/út",
|
||||||
"unsubscribe": "afskrá",
|
"unsubscribe": "afskrá",
|
||||||
|
@ -233,7 +233,7 @@
|
||||||
"Korean": "Kóreska",
|
"Korean": "Kóreska",
|
||||||
"Kurdish": "Kúrdíska",
|
"Kurdish": "Kúrdíska",
|
||||||
"Kyrgyz": "Kirgisíska",
|
"Kyrgyz": "Kirgisíska",
|
||||||
"Lao": "Laó",
|
"Lao": "Laóska",
|
||||||
"Latin": "Latína",
|
"Latin": "Latína",
|
||||||
"Latvian": "Lettneska",
|
"Latvian": "Lettneska",
|
||||||
"Lithuanian": "Litháíska",
|
"Lithuanian": "Litháíska",
|
||||||
|
@ -295,18 +295,18 @@
|
||||||
"View as playlist": "Skoða sem spilunarlista",
|
"View as playlist": "Skoða sem spilunarlista",
|
||||||
"Default": "Sjálfgefið",
|
"Default": "Sjálfgefið",
|
||||||
"Music": "Tónlist",
|
"Music": "Tónlist",
|
||||||
"Gaming": "Tólvuleikja",
|
"Gaming": "Spilun leikja",
|
||||||
"News": "Fréttir",
|
"News": "Fréttir",
|
||||||
"Movies": "Kvikmyndir",
|
"Movies": "Kvikmyndir",
|
||||||
"Download": "Niðurhal",
|
"Download": "Niðurhal",
|
||||||
"Download as: ": "Niðurhala sem: ",
|
"Download as: ": "Sækja sem: ",
|
||||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
"(edited)": "(breytt)",
|
"(edited)": "(breytt)",
|
||||||
"YouTube comment permalink": "YouTube ummæli varanlegur tengill",
|
"YouTube comment permalink": "Varanlegur tengill á YouTube-ummæli",
|
||||||
"permalink": "Varanlegur tengill",
|
"permalink": "Varanlegur tengill",
|
||||||
"`x` marked it with a ❤": "`x` merkti það með ❤",
|
"`x` marked it with a ❤": "`x` merkti það með ❤",
|
||||||
"Audio mode": "Hljóð ham",
|
"Audio mode": "Hljóðhamur",
|
||||||
"Video mode": "Myndband ham",
|
"Video mode": "Myndhamur",
|
||||||
"channel_tab_videos_label": "Myndskeið",
|
"channel_tab_videos_label": "Myndskeið",
|
||||||
"Playlists": "Spilunarlistar",
|
"Playlists": "Spilunarlistar",
|
||||||
"channel_tab_community_label": "Samfélag",
|
"channel_tab_community_label": "Samfélag",
|
||||||
|
@ -388,7 +388,7 @@
|
||||||
"crash_page_before_reporting": "Áður en þú tilkynnir villu, gakktu úr skugga um að þú hafir:",
|
"crash_page_before_reporting": "Áður en þú tilkynnir villu, gakktu úr skugga um að þú hafir:",
|
||||||
"crash_page_switch_instance": "reynt að <a href=\"`x`\">nota annað tilvik</a>",
|
"crash_page_switch_instance": "reynt að <a href=\"`x`\">nota annað tilvik</a>",
|
||||||
"crash_page_report_issue": "Ef ekkert af ofantöldu hjálpaði, ættirðu að <a href=\"`x`\">opna nýja verkbeiðni (issue) á GitHub</a> (helst á ensku) og láta fylgja eftirfarandi texta í skilaboðunum þínum (alls EKKI þýða þennan texta):",
|
"crash_page_report_issue": "Ef ekkert af ofantöldu hjálpaði, ættirðu að <a href=\"`x`\">opna nýja verkbeiðni (issue) á GitHub</a> (helst á ensku) og láta fylgja eftirfarandi texta í skilaboðunum þínum (alls EKKI þýða þennan texta):",
|
||||||
"channel_tab_shorts_label": "Stuttmyndir",
|
"channel_tab_shorts_label": "Símamyndir",
|
||||||
"carousel_slide": "Skyggna {{current}} af {{total}}",
|
"carousel_slide": "Skyggna {{current}} af {{total}}",
|
||||||
"carousel_go_to": "Fara á skyggnu `x`",
|
"carousel_go_to": "Fara á skyggnu `x`",
|
||||||
"channel_tab_streams_label": "Bein streymi",
|
"channel_tab_streams_label": "Bein streymi",
|
||||||
|
@ -401,8 +401,8 @@
|
||||||
"English (United Kingdom)": "Enska (Bretland)",
|
"English (United Kingdom)": "Enska (Bretland)",
|
||||||
"English (United States)": "Enska (Bandarísk)",
|
"English (United States)": "Enska (Bandarísk)",
|
||||||
"Vietnamese (auto-generated)": "Víetnamska (sjálfvirkt útbúið)",
|
"Vietnamese (auto-generated)": "Víetnamska (sjálfvirkt útbúið)",
|
||||||
"generic_count_months": "{{count}} mánuður",
|
"generic_count_months": "{{count}} mánuði",
|
||||||
"generic_count_months_plural": "{{count}} mánuðir",
|
"generic_count_months_plural": "{{count}} mánuðum",
|
||||||
"search_filters_sort_option_rating": "Einkunn",
|
"search_filters_sort_option_rating": "Einkunn",
|
||||||
"videoinfo_youTube_embed_link": "Ívefja",
|
"videoinfo_youTube_embed_link": "Ívefja",
|
||||||
"error_video_not_in_playlist": "Umbeðið myndskeið fyrirfinnst ekki í þessum spilunarlista. <a href=\"`x`\">Smelltu hér til að fara á heimasíðu spilunarlistans.</a>",
|
"error_video_not_in_playlist": "Umbeðið myndskeið fyrirfinnst ekki í þessum spilunarlista. <a href=\"`x`\">Smelltu hér til að fara á heimasíðu spilunarlistans.</a>",
|
||||||
|
@ -429,11 +429,11 @@
|
||||||
"Spanish (auto-generated)": "Spænska (sjálfvirkt útbúið)",
|
"Spanish (auto-generated)": "Spænska (sjálfvirkt útbúið)",
|
||||||
"Spanish (Mexico)": "Spænska (Mexíkó)",
|
"Spanish (Mexico)": "Spænska (Mexíkó)",
|
||||||
"generic_count_hours": "{{count}} klukkustund",
|
"generic_count_hours": "{{count}} klukkustund",
|
||||||
"generic_count_hours_plural": "{{count}} klukkustundir",
|
"generic_count_hours_plural": "{{count}} klukkustundum",
|
||||||
"generic_count_years": "{{count}} ár",
|
"generic_count_years": "{{count}} ári",
|
||||||
"generic_count_years_plural": "{{count}} ár",
|
"generic_count_years_plural": "{{count}} árum",
|
||||||
"generic_count_weeks": "{{count}} vika",
|
"generic_count_weeks": "{{count}} viku",
|
||||||
"generic_count_weeks_plural": "{{count}} vikur",
|
"generic_count_weeks_plural": "{{count}} vikum",
|
||||||
"search_filters_date_option_none": "Hvaða dagsetning sem er",
|
"search_filters_date_option_none": "Hvaða dagsetning sem er",
|
||||||
"Channel Sponsor": "Styrktaraðili rásar",
|
"Channel Sponsor": "Styrktaraðili rásar",
|
||||||
"search_filters_date_option_week": "Í þessari viku",
|
"search_filters_date_option_week": "Í þessari viku",
|
||||||
|
@ -476,8 +476,8 @@
|
||||||
"preferences_quality_dash_option_144p": "144p",
|
"preferences_quality_dash_option_144p": "144p",
|
||||||
"invidious": "Invidious",
|
"invidious": "Invidious",
|
||||||
"Korean (auto-generated)": "Kóreska (sjálfvirkt útbúið)",
|
"Korean (auto-generated)": "Kóreska (sjálfvirkt útbúið)",
|
||||||
"generic_count_days": "{{count}} dagur",
|
"generic_count_days": "{{count}} degi",
|
||||||
"generic_count_days_plural": "{{count}} dagar",
|
"generic_count_days_plural": "{{count}} dögum",
|
||||||
"search_filters_date_option_today": "Í dag",
|
"search_filters_date_option_today": "Í dag",
|
||||||
"search_filters_type_label": "Tegund",
|
"search_filters_type_label": "Tegund",
|
||||||
"search_filters_type_option_all": "Hvaða tegund sem er",
|
"search_filters_type_option_all": "Hvaða tegund sem er",
|
||||||
|
@ -496,5 +496,10 @@
|
||||||
"footer_documentation": "Leiðbeiningar",
|
"footer_documentation": "Leiðbeiningar",
|
||||||
"channel_tab_channels_label": "Rásir",
|
"channel_tab_channels_label": "Rásir",
|
||||||
"Import YouTube playlist (.csv)": "Flytja inn YouTube spilunarlista (.csv)",
|
"Import YouTube playlist (.csv)": "Flytja inn YouTube spilunarlista (.csv)",
|
||||||
"preferences_quality_option_dash": "DASH (aðlaganleg gæði)"
|
"preferences_quality_option_dash": "DASH (aðlaganleg gæði)",
|
||||||
|
"preferences_preload_label": "Forhlaða gögnum myndskeiðs: ",
|
||||||
|
"Filipino (auto-generated)": "Filippínska (sjálfvirkt útbúin)",
|
||||||
|
"channel_tab_posts_label": "Færslur",
|
||||||
|
"First page": "Fyrsta síða",
|
||||||
|
"channel_tab_courses_label": "Kennsluefni"
|
||||||
}
|
}
|
||||||
|
|
|
@ -469,8 +469,8 @@
|
||||||
"Spanish (auto-generated)": "Spagnolo (generati automaticamente)",
|
"Spanish (auto-generated)": "Spagnolo (generati automaticamente)",
|
||||||
"Spanish (Mexico)": "Spagnolo (Messico)",
|
"Spanish (Mexico)": "Spagnolo (Messico)",
|
||||||
"Spanish (Spain)": "Spagnolo (Spagna)",
|
"Spanish (Spain)": "Spagnolo (Spagna)",
|
||||||
"Turkish (auto-generated)": "Turco (auto-generato)",
|
"Turkish (auto-generated)": "Turco (generati automaticamente)",
|
||||||
"Vietnamese (auto-generated)": "Vietnamita (auto-generato)",
|
"Vietnamese (auto-generated)": "Vietnamita (generati automaticamente)",
|
||||||
"search_filters_date_label": "Data caricamento",
|
"search_filters_date_label": "Data caricamento",
|
||||||
"search_filters_date_option_none": "Qualunque data",
|
"search_filters_date_option_none": "Qualunque data",
|
||||||
"search_filters_type_option_all": "Qualunque tipo",
|
"search_filters_type_option_all": "Qualunque tipo",
|
||||||
|
@ -513,5 +513,10 @@
|
||||||
"The Popular feed has been disabled by the administrator.": "La sezione dei contenuti popolari è stata disabilitata dall'amministratore.",
|
"The Popular feed has been disabled by the administrator.": "La sezione dei contenuti popolari è stata disabilitata dall'amministratore.",
|
||||||
"carousel_slide": "Fotogramma {{current}} di {{total}}",
|
"carousel_slide": "Fotogramma {{current}} di {{total}}",
|
||||||
"carousel_skip": "Salta la galleria",
|
"carousel_skip": "Salta la galleria",
|
||||||
"carousel_go_to": "Vai al fotogramma `x`"
|
"carousel_go_to": "Vai al fotogramma `x`",
|
||||||
|
"preferences_preload_label": "Precarica dati video: ",
|
||||||
|
"Filipino (auto-generated)": "Filippino (generati automaticamente)",
|
||||||
|
"First page": "Prima pagina",
|
||||||
|
"channel_tab_courses_label": "Corsi",
|
||||||
|
"channel_tab_posts_label": "Post"
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
"No": "いいえ",
|
"No": "いいえ",
|
||||||
"Import and Export Data": "データのインポートとエクスポート",
|
"Import and Export Data": "データのインポートとエクスポート",
|
||||||
"Import": "インポート",
|
"Import": "インポート",
|
||||||
"Import Invidious data": "Invidious JSONデータをインポート",
|
"Import Invidious data": "Invidious JSON データをインポート",
|
||||||
"Import YouTube subscriptions": "YouTube/OPML 登録チャンネルをインポート",
|
"Import YouTube subscriptions": "YouTube/OPML 登録チャンネルをインポート",
|
||||||
"Import FreeTube subscriptions (.db)": "FreeTube 登録チャンネルをインポート (.db)",
|
"Import FreeTube subscriptions (.db)": "FreeTube 登録チャンネルをインポート (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "NewPipe 登録チャンネルをインポート (.json)",
|
"Import NewPipe subscriptions (.json)": "NewPipe 登録チャンネルをインポート (.json)",
|
||||||
|
@ -68,7 +68,7 @@
|
||||||
"preferences_related_videos_label": "関連動画を表示: ",
|
"preferences_related_videos_label": "関連動画を表示: ",
|
||||||
"preferences_annotations_label": "最初からアノテーションを表示: ",
|
"preferences_annotations_label": "最初からアノテーションを表示: ",
|
||||||
"preferences_extend_desc_label": "動画の説明文を自動的に拡張: ",
|
"preferences_extend_desc_label": "動画の説明文を自動的に拡張: ",
|
||||||
"preferences_vr_mode_label": "対話的な360°動画 (WebGLが必要): ",
|
"preferences_vr_mode_label": "対話的な 360° 動画 (WebGL が必要): ",
|
||||||
"preferences_category_visual": "外観設定",
|
"preferences_category_visual": "外観設定",
|
||||||
"preferences_player_style_label": "プレイヤーのスタイル: ",
|
"preferences_player_style_label": "プレイヤーのスタイル: ",
|
||||||
"Dark mode: ": "ダークモード: ",
|
"Dark mode: ": "ダークモード: ",
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
"light": "ライト",
|
"light": "ライト",
|
||||||
"preferences_thin_mode_label": "最小モード: ",
|
"preferences_thin_mode_label": "最小モード: ",
|
||||||
"preferences_category_misc": "ほかの設定",
|
"preferences_category_misc": "ほかの設定",
|
||||||
"preferences_automatic_instance_redirect_label": "インスタンスの自動転送 (redirect.invidious.ioにフォールバック): ",
|
"preferences_automatic_instance_redirect_label": "インスタンスの自動転送 (redirect.invidious.io にフォールバック): ",
|
||||||
"preferences_category_subscription": "登録チャンネル設定",
|
"preferences_category_subscription": "登録チャンネル設定",
|
||||||
"preferences_annotations_subscribed_label": "最初から登録チャンネルのアノテーションを表示 ",
|
"preferences_annotations_subscribed_label": "最初から登録チャンネルのアノテーションを表示 ",
|
||||||
"Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ",
|
"Redirect homepage to feed: ": "ホームからフィードにリダイレクト: ",
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}}件の未読通知",
|
"subscriptions_unseen_notifs_count_0": "{{count}}件の未読通知",
|
||||||
"search": "検索",
|
"search": "検索",
|
||||||
"Log out": "ログアウト",
|
"Log out": "ログアウト",
|
||||||
"Released under the AGPLv3 on Github.": "GitHub上でAGPLv3の元で公開",
|
"Released under the AGPLv3 on Github.": "GitHub 上で AGPLv3 の元で公開",
|
||||||
"Source available here.": "ソースはここで閲覧可能です。",
|
"Source available here.": "ソースはここで閲覧可能です。",
|
||||||
"View JavaScript license information.": "JavaScriptライセンス情報",
|
"View JavaScript license information.": "JavaScriptライセンス情報",
|
||||||
"View privacy policy.": "個人情報保護方針",
|
"View privacy policy.": "個人情報保護方針",
|
||||||
|
@ -143,8 +143,8 @@
|
||||||
"Editing playlist `x`": "再生リスト `x` を編集中",
|
"Editing playlist `x`": "再生リスト `x` を編集中",
|
||||||
"Show more": "もっと見る",
|
"Show more": "もっと見る",
|
||||||
"Show less": "表示を少なく",
|
"Show less": "表示を少なく",
|
||||||
"Watch on YouTube": "YouTubeで視聴",
|
"Watch on YouTube": "YouTube で視聴",
|
||||||
"Switch Invidious Instance": "Invidiousインスタンスの変更",
|
"Switch Invidious Instance": "Invidious インスタンスの変更",
|
||||||
"Hide annotations": "アノテーションを隠す",
|
"Hide annotations": "アノテーションを隠す",
|
||||||
"Show annotations": "アノテーションを表示",
|
"Show annotations": "アノテーションを表示",
|
||||||
"Genre: ": "ジャンル: ",
|
"Genre: ": "ジャンル: ",
|
||||||
|
@ -330,7 +330,7 @@
|
||||||
"(edited)": "(編集済み)",
|
"(edited)": "(編集済み)",
|
||||||
"YouTube comment permalink": "YouTube コメントのパーマリンク",
|
"YouTube comment permalink": "YouTube コメントのパーマリンク",
|
||||||
"permalink": "パーマリンク",
|
"permalink": "パーマリンク",
|
||||||
"`x` marked it with a ❤": "`x` が❤を送りました",
|
"`x` marked it with a ❤": "`x` が ❤ を送りました",
|
||||||
"Audio mode": "音声モード",
|
"Audio mode": "音声モード",
|
||||||
"Video mode": "動画モード",
|
"Video mode": "動画モード",
|
||||||
"channel_tab_videos_label": "動画",
|
"channel_tab_videos_label": "動画",
|
||||||
|
@ -343,7 +343,7 @@
|
||||||
"search_filters_type_label": "種類",
|
"search_filters_type_label": "種類",
|
||||||
"search_filters_duration_label": "再生時間",
|
"search_filters_duration_label": "再生時間",
|
||||||
"search_filters_features_label": "特徴",
|
"search_filters_features_label": "特徴",
|
||||||
"search_filters_sort_label": "順番",
|
"search_filters_sort_label": "並べ替え",
|
||||||
"search_filters_date_option_hour": "1時間以内",
|
"search_filters_date_option_hour": "1時間以内",
|
||||||
"search_filters_date_option_today": "今日",
|
"search_filters_date_option_today": "今日",
|
||||||
"search_filters_date_option_week": "今週",
|
"search_filters_date_option_week": "今週",
|
||||||
|
@ -365,13 +365,13 @@
|
||||||
"Current version: ": "現在のバージョン: ",
|
"Current version: ": "現在のバージョン: ",
|
||||||
"next_steps_error_message": "以下をお試しください: ",
|
"next_steps_error_message": "以下をお試しください: ",
|
||||||
"next_steps_error_message_refresh": "再読み込み",
|
"next_steps_error_message_refresh": "再読み込み",
|
||||||
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
|
"next_steps_error_message_go_to_youtube": "YouTube を開く",
|
||||||
"search_filters_duration_option_short": "4分未満",
|
"search_filters_duration_option_short": "4分未満",
|
||||||
"footer_documentation": "説明書",
|
"footer_documentation": "説明書",
|
||||||
"footer_source_code": "ソースコード",
|
"footer_source_code": "ソースコード",
|
||||||
"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": "寄付する",
|
||||||
|
@ -399,7 +399,7 @@
|
||||||
"preferences_quality_dash_option_worst": "最低",
|
"preferences_quality_dash_option_worst": "最低",
|
||||||
"preferences_quality_dash_option_best": "最高",
|
"preferences_quality_dash_option_best": "最高",
|
||||||
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
|
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
|
||||||
"videoinfo_watch_on_youTube": "YouTubeで視聴",
|
"videoinfo_watch_on_youTube": "YouTube で視聴",
|
||||||
"user_created_playlists": "`x`個の作成した再生リスト",
|
"user_created_playlists": "`x`個の作成した再生リスト",
|
||||||
"Video unavailable": "動画は利用できません",
|
"Video unavailable": "動画は利用できません",
|
||||||
"Chinese": "中国語",
|
"Chinese": "中国語",
|
||||||
|
@ -446,7 +446,7 @@
|
||||||
"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 を作成し (できれば英語で) 、メッセージに次のテキストを含めてください (テキストは翻訳しない) 。",
|
||||||
"crash_page_search_issue": "<a href=\"`x`\">GitHub の既存の問題 (issue)</a> を検索",
|
"crash_page_search_issue": "<a href=\"`x`\">GitHub の既存の問題 (issue)</a> を検索",
|
||||||
"channel_tab_streams_label": "ライブ",
|
"channel_tab_streams_label": "ライブ",
|
||||||
"channel_tab_playlists_label": "再生リスト",
|
"channel_tab_playlists_label": "再生リスト",
|
||||||
|
@ -479,5 +479,10 @@
|
||||||
"carousel_go_to": "スライド`x`を表示",
|
"carousel_go_to": "スライド`x`を表示",
|
||||||
"carousel_slide": "スライド{{current}} / 全{{total}}個中",
|
"carousel_slide": "スライド{{current}} / 全{{total}}個中",
|
||||||
"carousel_skip": "画像のスライド表示をスキップ",
|
"carousel_skip": "画像のスライド表示をスキップ",
|
||||||
"toggle_theme": "テーマの切り替え"
|
"toggle_theme": "テーマの切り替え",
|
||||||
|
"preferences_preload_label": "動画データを事前に読み込む: ",
|
||||||
|
"Filipino (auto-generated)": "フィリピノ語 (自動生成)",
|
||||||
|
"First page": "最初のページ",
|
||||||
|
"channel_tab_posts_label": "投稿",
|
||||||
|
"channel_tab_courses_label": "コース"
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
"Next page": "다음 페이지",
|
"Next page": "다음 페이지",
|
||||||
"last": "마지막",
|
"last": "마지막",
|
||||||
"Shared `x` ago": "`x` 전",
|
"Shared `x` ago": "`x` 전",
|
||||||
"popular": "인기",
|
"popular": "인기순",
|
||||||
"oldest": "과거순",
|
"oldest": "과거순",
|
||||||
"newest": "최신순",
|
"newest": "최신순",
|
||||||
"View playlist on YouTube": "유튜브에서 재생목록 보기",
|
"View playlist on YouTube": "유튜브에서 재생목록 보기",
|
||||||
|
@ -419,7 +419,7 @@
|
||||||
"Portuguese (Brazil)": "포르투갈어 (브라질)",
|
"Portuguese (Brazil)": "포르투갈어 (브라질)",
|
||||||
"search_message_no_results": "결과가 없습니다.",
|
"search_message_no_results": "결과가 없습니다.",
|
||||||
"search_message_change_filters_or_query": "필터를 변경하시거나 검색어를 넓게 시도해보세요.",
|
"search_message_change_filters_or_query": "필터를 변경하시거나 검색어를 넓게 시도해보세요.",
|
||||||
"search_message_use_another_instance": " <a href=\"`x`\">다른 인스턴스에서 검색</a>할 수도 있습니다.",
|
"search_message_use_another_instance": "<a href=\"`x`\">다른 인스턴스에서 검색</a>할 수도 있습니다.",
|
||||||
"English (United States)": "영어 (미국)",
|
"English (United States)": "영어 (미국)",
|
||||||
"Chinese": "중국어",
|
"Chinese": "중국어",
|
||||||
"Chinese (China)": "중국어 (중국)",
|
"Chinese (China)": "중국어 (중국)",
|
||||||
|
@ -479,5 +479,10 @@
|
||||||
"carousel_go_to": "`x` 슬라이드로 이동",
|
"carousel_go_to": "`x` 슬라이드로 이동",
|
||||||
"Search for videos": "비디오 검색",
|
"Search for videos": "비디오 검색",
|
||||||
"toggle_theme": "테마 전환",
|
"toggle_theme": "테마 전환",
|
||||||
"carousel_slide": "{{total}}의 슬라이드 {{current}}"
|
"carousel_slide": "{{total}}의 슬라이드 {{current}}",
|
||||||
|
"preferences_preload_label": "비디오 데이터 사전 로드: ",
|
||||||
|
"First page": "첫 페이지",
|
||||||
|
"Filipino (auto-generated)": "Filipino (auto-generated)",
|
||||||
|
"channel_tab_posts_label": "게시글",
|
||||||
|
"channel_tab_courses_label": "코스"
|
||||||
}
|
}
|
||||||
|
|
69
locales/lv.json
Normal file
69
locales/lv.json
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
{
|
||||||
|
"generic_channels_count_0": "{{count}} kanāli",
|
||||||
|
"generic_channels_count_1": "{{count}} kanāls",
|
||||||
|
"generic_channels_count_2": "{{count}} kanāli",
|
||||||
|
"Add to playlist": "Pievienot atskaņošanas sarakstam",
|
||||||
|
"Answer": "Atbildēt",
|
||||||
|
"generic_subscribers_count_0": "{{count}} abonenti",
|
||||||
|
"generic_subscribers_count_1": "{{count}} abonents",
|
||||||
|
"generic_subscribers_count_2": "{{count}} abonenti",
|
||||||
|
"generic_button_delete": "Dzēst",
|
||||||
|
"generic_button_edit": "Rediģēt",
|
||||||
|
"generic_button_save": "Saglabāt",
|
||||||
|
"generic_button_cancel": "Atcelt",
|
||||||
|
"generic_button_rss": "RSS",
|
||||||
|
"Unsubscribe": "Pārtraukt abonementu",
|
||||||
|
"View playlist on YouTube": "Skatīt atskaņošanas sarakstu YouTube vietnē",
|
||||||
|
"New password": "Jaunā parole",
|
||||||
|
"Yes": "Jā",
|
||||||
|
"No": "Nē",
|
||||||
|
"Import and Export Data": "Ievietot un izgūt datus",
|
||||||
|
"Import": "Ievietot",
|
||||||
|
"Import Invidious data": "Ievietot Invidious JSON datus",
|
||||||
|
"Delete account?": "Vai dzēst kontu?",
|
||||||
|
"History": "Vēsture",
|
||||||
|
"User ID": "Lietotāja ID",
|
||||||
|
"Password": "Parole",
|
||||||
|
"Import YouTube subscriptions": "Ievietot YouTube CSV vai OPML abonementus",
|
||||||
|
"E-mail": "E-pasts",
|
||||||
|
"Preferences": "Iestatījumi",
|
||||||
|
"preferences_category_player": "Atskaņotāja iestatījumi",
|
||||||
|
"preferences_quality_option_hd720": "HD - 720p",
|
||||||
|
"preferences_quality_option_medium": "Vidēja",
|
||||||
|
"preferences_quality_dash_option_worst": "Vissliktākā",
|
||||||
|
"preferences_quality_dash_option_2160p": "2160p (4K)",
|
||||||
|
"preferences_quality_dash_option_1080p": "1080p (Full HD)",
|
||||||
|
"preferences_quality_dash_option_720p": "720p (HD)",
|
||||||
|
"preferences_quality_dash_option_1440p": "1440p (2.5K, QHD)",
|
||||||
|
"preferences_quality_dash_option_480p": "480p (SD)",
|
||||||
|
"preferences_quality_dash_option_360p": "360p",
|
||||||
|
"preferences_quality_dash_option_240p": "240p",
|
||||||
|
"preferences_quality_dash_option_144p": "144p",
|
||||||
|
"preferences_volume_label": "Atskaņošanas skaļums: ",
|
||||||
|
"reddit": "Reddit",
|
||||||
|
"invidious": "Invidious",
|
||||||
|
"Bangla": "Bengāļu",
|
||||||
|
"Basque": "Basku",
|
||||||
|
"Cebuano": "Sebuāņu",
|
||||||
|
"Chinese (Traditional)": "Ķīniešu (tradicionālā)",
|
||||||
|
"Corsican": "Korsikāņu",
|
||||||
|
"Croatian": "Horvātu",
|
||||||
|
"Galician": "Galisiešu",
|
||||||
|
"Georgian": "Gruzīnu",
|
||||||
|
"Gujarati": "Gudžaratu",
|
||||||
|
"German": "Vācu",
|
||||||
|
"Greek": "Grieķu",
|
||||||
|
"Haitian Creole": "Haitiešu",
|
||||||
|
"Hausa": "Hausu",
|
||||||
|
"Hawaiian": "Havajiešu",
|
||||||
|
"Export data as JSON": "Izgūt Invidious datus JSON formātā",
|
||||||
|
"preferences_quality_dash_option_4320p": "4320p (8K)",
|
||||||
|
"Time (h:mm:ss):": "Laiks (h:mm:ss):",
|
||||||
|
"Chinese (Simplified)": "Ķīniešu (vienkāršotā)",
|
||||||
|
"preferences_quality_dash_option_best": "Vislabākā",
|
||||||
|
"preferences_quality_option_small": "Zema",
|
||||||
|
"youtube": "YouTube",
|
||||||
|
"Add to playlist: ": "Pievienot atskaņošanas sarakstam: ",
|
||||||
|
"Subscribe": "Abonēt",
|
||||||
|
"View channel on YouTube": "Skatīt kanālu YouTube vietnē"
|
||||||
|
}
|
|
@ -496,5 +496,6 @@
|
||||||
"Add to playlist": "Legg til i spilleliste",
|
"Add to playlist": "Legg til i spilleliste",
|
||||||
"Add to playlist: ": "Legg til i spilleliste: ",
|
"Add to playlist: ": "Legg til i spilleliste: ",
|
||||||
"The Popular feed has been disabled by the administrator.": "Populært-kilden er koblet ut av administratoren.",
|
"The Popular feed has been disabled by the administrator.": "Populært-kilden er koblet ut av administratoren.",
|
||||||
"toggle_theme": "Endre utseende"
|
"toggle_theme": "Endre utseende",
|
||||||
|
"preferences_preload_label": "Last videodata på forhånd: "
|
||||||
}
|
}
|
||||||
|
|
|
@ -496,5 +496,10 @@
|
||||||
"Answer": "Antwoorden",
|
"Answer": "Antwoorden",
|
||||||
"Search for videos": "Naar video's zoeken",
|
"Search for videos": "Naar video's zoeken",
|
||||||
"carousel_skip": "Carousel overslaan",
|
"carousel_skip": "Carousel overslaan",
|
||||||
"toggle_theme": "Thema omschakelen"
|
"toggle_theme": "Thema omschakelen",
|
||||||
|
"preferences_preload_label": "Videogegevens vooraf laden: ",
|
||||||
|
"Filipino (auto-generated)": "Filipijns (automatisch gegenereerd)",
|
||||||
|
"channel_tab_courses_label": "Cursussen",
|
||||||
|
"First page": "Eerste pagina",
|
||||||
|
"channel_tab_posts_label": "Gepost"
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,5 +513,10 @@
|
||||||
"Add to playlist: ": "Dodaj do playlisty: ",
|
"Add to playlist: ": "Dodaj do playlisty: ",
|
||||||
"carousel_slide": "Slajd {{current}} z {{total}}",
|
"carousel_slide": "Slajd {{current}} z {{total}}",
|
||||||
"carousel_skip": "Pomiń karuzelę",
|
"carousel_skip": "Pomiń karuzelę",
|
||||||
"carousel_go_to": "Przejdź do slajdu `x`"
|
"carousel_go_to": "Przejdź do slajdu `x`",
|
||||||
|
"preferences_preload_label": "Wstępne ładowanie danych wideo: ",
|
||||||
|
"Filipino (auto-generated)": "filipiński (wygenerowany automatycznie)",
|
||||||
|
"First page": "Pierwsza strona",
|
||||||
|
"channel_tab_posts_label": "Posty",
|
||||||
|
"channel_tab_courses_label": "Kursy"
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,5 +513,10 @@
|
||||||
"Answer": "Resposta",
|
"Answer": "Resposta",
|
||||||
"carousel_slide": "Slide {{current}} de {{total}}",
|
"carousel_slide": "Slide {{current}} de {{total}}",
|
||||||
"carousel_skip": "Ignorar carrossel",
|
"carousel_skip": "Ignorar carrossel",
|
||||||
"carousel_go_to": "Ir ao slide `x`"
|
"carousel_go_to": "Ir ao slide `x`",
|
||||||
|
"preferences_preload_label": "Pré-carregar dados do vídeo: ",
|
||||||
|
"Filipino (auto-generated)": "Filipino (gerado automaticamente)",
|
||||||
|
"channel_tab_posts_label": "Postagens",
|
||||||
|
"First page": "Primeira página",
|
||||||
|
"channel_tab_courses_label": "Cursos"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,27 @@
|
||||||
{
|
{
|
||||||
"LIVE": "Em direto",
|
"LIVE": "Direto",
|
||||||
"Shared `x` ago": "Partilhado `x` atrás",
|
"Shared `x` ago": "Partilhado `x` atrás",
|
||||||
"Unsubscribe": "Anular subscrição",
|
"Unsubscribe": "Anular subscrição",
|
||||||
"Subscribe": "Subscrever",
|
"Subscribe": "Subscrever",
|
||||||
"View channel on YouTube": "Ver canal no YouTube",
|
"View channel on YouTube": "Ver canal no YouTube",
|
||||||
"View playlist on YouTube": "Ver lista de reprodução no YouTube",
|
"View playlist on YouTube": "Ver lista de reprodução no YouTube",
|
||||||
"newest": "mais recentes",
|
"newest": "recentes",
|
||||||
"oldest": "mais antigos",
|
"oldest": "antigos",
|
||||||
"popular": "popular",
|
"popular": "populares",
|
||||||
"last": "últimos",
|
"last": "últimos",
|
||||||
"Next page": "Próxima página",
|
"Next page": "Página seguinte",
|
||||||
"Previous page": "Página anterior",
|
"Previous page": "Página anterior",
|
||||||
"Clear watch history?": "Limpar histórico de reprodução?",
|
"Clear watch history?": "Limpar histórico de reprodução?",
|
||||||
"New password": "Nova palavra-chave",
|
"New password": "Nova palavra-passe",
|
||||||
"New passwords must match": "As novas palavra-chaves devem corresponder",
|
"New passwords must match": "As novas palavras-passe devem ser iguais",
|
||||||
"Authorize token?": "Autorizar token?",
|
"Authorize token?": "Autorizar 'token'?",
|
||||||
"Authorize token for `x`?": "Autorizar token para `x`?",
|
"Authorize token for `x`?": "Autorizar 'token' para `x`?",
|
||||||
"Yes": "Sim",
|
"Yes": "Sim",
|
||||||
"No": "Não",
|
"No": "Não",
|
||||||
"Import and Export Data": "Importar e exportar dados",
|
"Import and Export Data": "Importar e exportar dados",
|
||||||
"Import": "Importar",
|
"Import": "Importar",
|
||||||
"Import Invidious data": "Importar dados JSON do Invidious",
|
"Import Invidious data": "Importar dados JSON do Invidious",
|
||||||
"Import YouTube subscriptions": "Importar subscrições do YouTube/OPML",
|
"Import YouTube subscriptions": "Importar via YouTube csv ou subscrição OPML",
|
||||||
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
|
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
|
||||||
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
|
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
|
||||||
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
|
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
|
||||||
|
@ -32,38 +32,38 @@
|
||||||
"Delete account?": "Eliminar conta?",
|
"Delete account?": "Eliminar conta?",
|
||||||
"History": "Histórico",
|
"History": "Histórico",
|
||||||
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
|
"An alternative front-end to YouTube": "Uma interface alternativa ao YouTube",
|
||||||
"JavaScript license information": "Informação de licença do JavaScript",
|
"JavaScript license information": "Informação da licença JavaScript",
|
||||||
"source": "código-fonte",
|
"source": "fonte",
|
||||||
"Log in": "Iniciar sessão",
|
"Log in": "Iniciar sessão",
|
||||||
"Log in/register": "Iniciar sessão/registar",
|
"Log in/register": "Iniciar sessão/registar",
|
||||||
"User ID": "Utilizador",
|
"User ID": "Utilizador",
|
||||||
"Password": "Palavra-chave",
|
"Password": "Palavra-passe",
|
||||||
"Time (h:mm:ss):": "Tempo (h:mm:ss):",
|
"Time (h:mm:ss):": "Tempo (h:mm:ss):",
|
||||||
"Text CAPTCHA": "Texto CAPTCHA",
|
"Text CAPTCHA": "Texto CAPTCHA",
|
||||||
"Image CAPTCHA": "Imagem CAPTCHA",
|
"Image CAPTCHA": "Imagem CAPTCHA",
|
||||||
"Sign In": "Iniciar sessão",
|
"Sign In": "Entrar",
|
||||||
"Register": "Registar",
|
"Register": "Registar",
|
||||||
"E-mail": "E-mail",
|
"E-mail": "E-mail",
|
||||||
"Preferences": "Preferências",
|
"Preferences": "Preferências",
|
||||||
"preferences_category_player": "Preferências do reprodutor",
|
"preferences_category_player": "Preferências do reprodutor",
|
||||||
"preferences_video_loop_label": "Repetir sempre: ",
|
"preferences_video_loop_label": "Repetir sempre: ",
|
||||||
"preferences_autoplay_label": "Reprodução automática: ",
|
"preferences_autoplay_label": "Reprodução automática: ",
|
||||||
"preferences_continue_label": "Reproduzir sempre o próximo: ",
|
"preferences_continue_label": "Reproduzir sempre o seguinte: ",
|
||||||
"preferences_continue_autoplay_label": "Reproduzir próximo vídeo automaticamente: ",
|
"preferences_continue_autoplay_label": "Reproduzir próximo vídeo automaticamente: ",
|
||||||
"preferences_listen_label": "Apenas áudio: ",
|
"preferences_listen_label": "Apenas áudio: ",
|
||||||
"preferences_local_label": "Usar proxy nos vídeos: ",
|
"preferences_local_label": "Usar proxy nos vídeos: ",
|
||||||
"preferences_speed_label": "Velocidade preferida: ",
|
"preferences_speed_label": "Velocidade preferida: ",
|
||||||
"preferences_quality_label": "Qualidade de vídeo preferida: ",
|
"preferences_quality_label": "Qualidade de vídeo preferida: ",
|
||||||
"preferences_volume_label": "Volume da reprodução: ",
|
"preferences_volume_label": "Volume de reprodução: ",
|
||||||
"preferences_comments_label": "Preferência dos comentários: ",
|
"preferences_comments_label": "Comentários padrão: ",
|
||||||
"youtube": "YouTube",
|
"youtube": "YouTube",
|
||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
"preferences_captions_label": "Legendas predefinidas: ",
|
"preferences_captions_label": "Legendas padrão: ",
|
||||||
"Fallback captions: ": "Legendas alternativas: ",
|
"Fallback captions: ": "Legendas alternativas: ",
|
||||||
"preferences_related_videos_label": "Mostrar vídeos relacionados: ",
|
"preferences_related_videos_label": "Mostrar vídeos relacionados: ",
|
||||||
"preferences_annotations_label": "Mostrar anotações sempre: ",
|
"preferences_annotations_label": "Mostrar anotações sempre: ",
|
||||||
"preferences_extend_desc_label": "Estender automaticamente a descrição do vídeo: ",
|
"preferences_extend_desc_label": "Expandir automaticamente a descrição do vídeo: ",
|
||||||
"preferences_vr_mode_label": "Vídeos interativos de 360 graus (necessita de WebGL): ",
|
"preferences_vr_mode_label": "Vídeos interativos de 360 graus (requer WebGL): ",
|
||||||
"preferences_category_visual": "Preferências visuais",
|
"preferences_category_visual": "Preferências visuais",
|
||||||
"preferences_player_style_label": "Estilo do reprodutor: ",
|
"preferences_player_style_label": "Estilo do reprodutor: ",
|
||||||
"Dark mode: ": "Modo escuro: ",
|
"Dark mode: ": "Modo escuro: ",
|
||||||
|
@ -74,9 +74,9 @@
|
||||||
"preferences_category_misc": "Preferências diversas",
|
"preferences_category_misc": "Preferências diversas",
|
||||||
"preferences_automatic_instance_redirect_label": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ",
|
"preferences_automatic_instance_redirect_label": "Redirecionamento de instância automática (solução de último recurso para redirect.invidious.io): ",
|
||||||
"preferences_category_subscription": "Preferências de subscrições",
|
"preferences_category_subscription": "Preferências de subscrições",
|
||||||
"preferences_annotations_subscribed_label": "Mostrar sempre anotações aos canais subscritos: ",
|
"preferences_annotations_subscribed_label": "Mostrar sempre anotações nos canais subscritos: ",
|
||||||
"Redirect homepage to feed: ": "Redirecionar página inicial para subscrições: ",
|
"Redirect homepage to feed: ": "Redirecionar página inicial para subscrições: ",
|
||||||
"preferences_max_results_label": "Quantidade de vídeos nas subscrições: ",
|
"preferences_max_results_label": "Número de vídeos nas subscrições: ",
|
||||||
"preferences_sort_label": "Ordenar vídeos por: ",
|
"preferences_sort_label": "Ordenar vídeos por: ",
|
||||||
"published": "publicado",
|
"published": "publicado",
|
||||||
"published - reverse": "publicado - inverso",
|
"published - reverse": "publicado - inverso",
|
||||||
|
@ -88,19 +88,19 @@
|
||||||
"Only show latest unwatched video from channel: ": "Mostrar apenas vídeos mais recentes não visualizados do canal: ",
|
"Only show latest unwatched video from channel: ": "Mostrar apenas vídeos mais recentes não visualizados do canal: ",
|
||||||
"preferences_unseen_only_label": "Mostrar apenas vídeos não visualizados: ",
|
"preferences_unseen_only_label": "Mostrar apenas vídeos não visualizados: ",
|
||||||
"preferences_notifications_only_label": "Mostrar apenas notificações (se existirem): ",
|
"preferences_notifications_only_label": "Mostrar apenas notificações (se existirem): ",
|
||||||
"Enable web notifications": "Ativar notificações pela web",
|
"Enable web notifications": "Ativar notificações web",
|
||||||
"`x` uploaded a video": "`x` publicou um novo vídeo",
|
"`x` uploaded a video": "`x` publicou um vídeo",
|
||||||
"`x` is live": "`x` está em direto",
|
"`x` is live": "`x` está em direto",
|
||||||
"preferences_category_data": "Preferências de dados",
|
"preferences_category_data": "Preferências de dados",
|
||||||
"Clear watch history": "Limpar histórico de reprodução",
|
"Clear watch history": "Limpar histórico de reprodução",
|
||||||
"Import/export data": "Importar / exportar dados",
|
"Import/export data": "Importar/exportar dados",
|
||||||
"Change password": "Alterar palavra-chave",
|
"Change password": "Alterar palavra-passe",
|
||||||
"Manage subscriptions": "Gerir as subscrições",
|
"Manage subscriptions": "Gerir subscrições",
|
||||||
"Manage tokens": "Gerir tokens",
|
"Manage tokens": "Gerir tokens",
|
||||||
"Watch history": "Histórico de reprodução",
|
"Watch history": "Histórico de reprodução",
|
||||||
"Delete account": "Eliminar conta",
|
"Delete account": "Eliminar conta",
|
||||||
"preferences_category_admin": "Preferências de administrador",
|
"preferences_category_admin": "Preferências de administrador",
|
||||||
"preferences_default_home_label": "Página inicial predefinida: ",
|
"preferences_default_home_label": "Página inicial padrão: ",
|
||||||
"preferences_feed_menu_label": "Menu de subscrições: ",
|
"preferences_feed_menu_label": "Menu de subscrições: ",
|
||||||
"preferences_show_nick_label": "Mostrar nome de utilizador em cima: ",
|
"preferences_show_nick_label": "Mostrar nome de utilizador em cima: ",
|
||||||
"Top enabled: ": "Destaques ativados: ",
|
"Top enabled: ": "Destaques ativados: ",
|
||||||
|
@ -109,28 +109,29 @@
|
||||||
"Registration enabled: ": "Registar ativado: ",
|
"Registration enabled: ": "Registar ativado: ",
|
||||||
"Report statistics: ": "Relatório de estatísticas: ",
|
"Report statistics: ": "Relatório de estatísticas: ",
|
||||||
"Save preferences": "Guardar preferências",
|
"Save preferences": "Guardar preferências",
|
||||||
"Subscription manager": "Gerir subscrições",
|
"Subscription manager": "Gestor de subscrições",
|
||||||
"Token manager": "Gerir tokens",
|
"Token manager": "Gestor de tokens",
|
||||||
"Token": "Token",
|
"Token": "Token",
|
||||||
"tokens_count": "{{count}} token",
|
"tokens_count_0": "{{count}} token",
|
||||||
"tokens_count_plural": "{{count}} tokens",
|
"tokens_count_1": "{{count}} tokens",
|
||||||
"Import/export": "Importar / exportar",
|
"tokens_count_2": "{{count}} tokens",
|
||||||
|
"Import/export": "Importar/exportar",
|
||||||
"unsubscribe": "anular subscrição",
|
"unsubscribe": "anular subscrição",
|
||||||
"revoke": "revogar",
|
"revoke": "revogar",
|
||||||
"Subscriptions": "Subscrições",
|
"Subscriptions": "Subscrições",
|
||||||
"search": "pesquisar",
|
"search": "pesquisar",
|
||||||
"Log out": "Terminar sessão",
|
"Log out": "Terminar sessão",
|
||||||
"Released under the AGPLv3 on Github.": "Lançado sob a AGPLv3 no GitHub.",
|
"Released under the AGPLv3 on Github.": "Disponibilizada sob a AGPLv3 no GitHub.",
|
||||||
"Source available here.": "Código-fonte disponível aqui.",
|
"Source available here.": "Código-fonte disponível aqui.",
|
||||||
"View JavaScript license information.": "Ver informações da licença do JavaScript.",
|
"View JavaScript license information.": "Ver informações da licença JavaScript.",
|
||||||
"View privacy policy.": "Ver a política de privacidade.",
|
"View privacy policy.": "Ver política de privacidade.",
|
||||||
"Trending": "Tendências",
|
"Trending": "Tendências",
|
||||||
"Public": "Público",
|
"Public": "Público",
|
||||||
"Unlisted": "Não listado",
|
"Unlisted": "Não listado",
|
||||||
"Private": "Privado",
|
"Private": "Privado",
|
||||||
"View all playlists": "Ver todas as listas de reprodução",
|
"View all playlists": "Ver todas as listas de reprodução",
|
||||||
"Updated `x` ago": "Atualizado `x` atrás",
|
"Updated `x` ago": "Atualizado há `x`",
|
||||||
"Delete playlist `x`?": "Eliminar a lista de reprodução `x`?",
|
"Delete playlist `x`?": "Eliminar lista de reprodução `x`?",
|
||||||
"Delete playlist": "Eliminar lista de reprodução",
|
"Delete playlist": "Eliminar lista de reprodução",
|
||||||
"Create playlist": "Criar lista de reprodução",
|
"Create playlist": "Criar lista de reprodução",
|
||||||
"Title": "Título",
|
"Title": "Título",
|
||||||
|
@ -139,7 +140,7 @@
|
||||||
"Show more": "Mostrar mais",
|
"Show more": "Mostrar mais",
|
||||||
"Show less": "Mostrar menos",
|
"Show less": "Mostrar menos",
|
||||||
"Watch on YouTube": "Ver no YouTube",
|
"Watch on YouTube": "Ver no YouTube",
|
||||||
"Switch Invidious Instance": "Mudar a instância do Invidious",
|
"Switch Invidious Instance": "Alterar instância Invidious",
|
||||||
"Hide annotations": "Ocultar anotações",
|
"Hide annotations": "Ocultar anotações",
|
||||||
"Show annotations": "Mostrar anotações",
|
"Show annotations": "Mostrar anotações",
|
||||||
"Genre: ": "Género: ",
|
"Genre: ": "Género: ",
|
||||||
|
@ -150,27 +151,27 @@
|
||||||
"Whitelisted regions: ": "Regiões permitidas: ",
|
"Whitelisted regions: ": "Regiões permitidas: ",
|
||||||
"Blacklisted regions: ": "Regiões bloqueadas: ",
|
"Blacklisted regions: ": "Regiões bloqueadas: ",
|
||||||
"Shared `x`": "Partilhado `x`",
|
"Shared `x`": "Partilhado `x`",
|
||||||
"Premieres in `x`": "Estreias em `x`",
|
"Premieres in `x`": "Estreia a `x`",
|
||||||
"Premieres `x`": "Estreias `x`",
|
"Premieres `x`": "Estreia `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.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, entretanto eles podem levar mais tempo para carregar.",
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Olá! Parece que o JavaScript está desativado. Clique aqui para ver os comentários, mas tenha e conta que podem levar mais tempo para carregar.",
|
||||||
"View YouTube comments": "Ver comentários do YouTube",
|
"View YouTube comments": "Ver comentários do YouTube",
|
||||||
"View more comments on Reddit": "Ver mais comentários no Reddit",
|
"View more comments on Reddit": "Ver mais comentários no Reddit",
|
||||||
"View `x` comments": {
|
"View `x` comments": {
|
||||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentários",
|
"([^.,0-9]|^)1([^.,0-9]|$)": "Ver `x` comentário",
|
||||||
"": "Ver `x` comentários"
|
"": "Ver `x` comentários"
|
||||||
},
|
},
|
||||||
"View Reddit comments": "Ver comentários do Reddit",
|
"View Reddit comments": "Ver comentários do Reddit",
|
||||||
"Hide replies": "Ocultar respostas",
|
"Hide replies": "Ocultar respostas",
|
||||||
"Show replies": "Mostrar respostas",
|
"Show replies": "Mostrar respostas",
|
||||||
"Incorrect password": "Palavra-chave incorreta",
|
"Incorrect password": "Palavra-passe incorreta",
|
||||||
"Wrong answer": "Resposta errada",
|
"Wrong answer": "Resposta errada",
|
||||||
"Erroneous CAPTCHA": "CAPTCHA inválido",
|
"Erroneous CAPTCHA": "CAPTCHA inválido",
|
||||||
"CAPTCHA is a required field": "CAPTCHA é um campo obrigatório",
|
"CAPTCHA is a required field": "CAPTCHA é um campo obrigatório",
|
||||||
"User ID is a required field": "O nome de utilizador é um campo obrigatório",
|
"User ID is a required field": "O nome de utilizador é um campo obrigatório",
|
||||||
"Password is a required field": "Palavra-chave é um campo obrigatório",
|
"Password is a required field": "Palavra-passe é um campo obrigatório",
|
||||||
"Wrong username or password": "Nome de utilizador ou palavra-chave incorreto",
|
"Wrong username or password": "Nome de utilizador ou palavra-passe incorreta",
|
||||||
"Password cannot be empty": "A palavra-chave não pode estar vazia",
|
"Password cannot be empty": "A palavra-passe não pode estar vazia",
|
||||||
"Password cannot be longer than 55 characters": "A palavra-chave não pode ser superior a 55 caracteres",
|
"Password cannot be longer than 55 characters": "A palavra-passe não pode ter mais do que 55 caracteres",
|
||||||
"Please log in": "Por favor, inicie sessão",
|
"Please log in": "Por favor, inicie sessão",
|
||||||
"Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`",
|
"Invidious Private Feed for `x`": "Feed Privado do Invidious para `x`",
|
||||||
"channel:`x`": "canal:`x`",
|
"channel:`x`": "canal:`x`",
|
||||||
|
@ -180,20 +181,20 @@
|
||||||
"Could not fetch comments": "Não foi possível obter os comentários",
|
"Could not fetch comments": "Não foi possível obter os comentários",
|
||||||
"`x` ago": "`x` atrás",
|
"`x` ago": "`x` atrás",
|
||||||
"Load more": "Carregar mais",
|
"Load more": "Carregar mais",
|
||||||
"Could not create mix.": "Não foi possível criar a mistura.",
|
"Could not create mix.": "Não foi possível criar o mix.",
|
||||||
"Empty playlist": "Lista de reprodução vazia",
|
"Empty playlist": "Lista de reprodução vazia",
|
||||||
"Not a playlist.": "Não é uma lista de reprodução.",
|
"Not a playlist.": "Não é uma lista de reprodução.",
|
||||||
"Playlist does not exist.": "A lista de reprodução não existe.",
|
"Playlist does not exist.": "A lista de reprodução não existe.",
|
||||||
"Could not pull trending pages.": "Não foi possível obter as páginas de tendências.",
|
"Could not pull trending pages.": "Não foi possível obter a página de tendências.",
|
||||||
"Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório",
|
"Hidden field \"challenge\" is a required field": "O campo oculto \"desafio\" é obrigatório",
|
||||||
"Hidden field \"token\" is a required field": "O campo oculto \"token\" é um campo obrigatório",
|
"Hidden field \"token\" is a required field": "O campo oculto \"token\" é um campo obrigatório",
|
||||||
"Erroneous challenge": "Desafio inválido",
|
"Erroneous challenge": "Desafio inválido",
|
||||||
"Erroneous token": "Token inválido",
|
"Erroneous token": "Token inválido",
|
||||||
"No such user": "Utilizador inválido",
|
"No such user": "Utilizador inválido",
|
||||||
"Token is expired, please try again": "Token expirou, tente novamente",
|
"Token is expired, please try again": "Token caducado, tente novamente",
|
||||||
"English": "Inglês",
|
"English": "Inglês",
|
||||||
"English (auto-generated)": "Inglês (auto-gerado)",
|
"English (auto-generated)": "Inglês (auto-gerado)",
|
||||||
"Afrikaans": "Africano",
|
"Afrikaans": "Africânder",
|
||||||
"Albanian": "Albanês",
|
"Albanian": "Albanês",
|
||||||
"Amharic": "Amárico",
|
"Amharic": "Amárico",
|
||||||
"Arabic": "Árabe",
|
"Arabic": "Árabe",
|
||||||
|
@ -209,7 +210,7 @@
|
||||||
"Cebuano": "Cebuano",
|
"Cebuano": "Cebuano",
|
||||||
"Chinese (Simplified)": "Chinês (simplificado)",
|
"Chinese (Simplified)": "Chinês (simplificado)",
|
||||||
"Chinese (Traditional)": "Chinês (tradicional)",
|
"Chinese (Traditional)": "Chinês (tradicional)",
|
||||||
"Corsican": "Corso",
|
"Corsican": "Córsego",
|
||||||
"Croatian": "Croata",
|
"Croatian": "Croata",
|
||||||
"Czech": "Checo",
|
"Czech": "Checo",
|
||||||
"Danish": "Dinamarquês",
|
"Danish": "Dinamarquês",
|
||||||
|
@ -252,7 +253,7 @@
|
||||||
"Macedonian": "Macedónio",
|
"Macedonian": "Macedónio",
|
||||||
"Malagasy": "Malgaxe",
|
"Malagasy": "Malgaxe",
|
||||||
"Malay": "Malaio",
|
"Malay": "Malaio",
|
||||||
"Malayalam": "Malaiala",
|
"Malayalam": "Malaialaio",
|
||||||
"Maltese": "Maltês",
|
"Maltese": "Maltês",
|
||||||
"Maori": "Maori",
|
"Maori": "Maori",
|
||||||
"Marathi": "Marathi",
|
"Marathi": "Marathi",
|
||||||
|
@ -297,30 +298,37 @@
|
||||||
"Yiddish": "Iídiche",
|
"Yiddish": "Iídiche",
|
||||||
"Yoruba": "Ioruba",
|
"Yoruba": "Ioruba",
|
||||||
"Zulu": "Zulu",
|
"Zulu": "Zulu",
|
||||||
"generic_count_years": "{{count}} ano",
|
"generic_count_years_0": "{{count}} ano",
|
||||||
"generic_count_years_plural": "{{count}} anos",
|
"generic_count_years_1": "{{count}} anos",
|
||||||
"generic_count_months": "{{count}} mês",
|
"generic_count_years_2": "{{count}} anos",
|
||||||
"generic_count_months_plural": "{{count}} meses",
|
"generic_count_months_0": "{{count}} mês",
|
||||||
"generic_count_weeks": "{{count}} seman",
|
"generic_count_months_1": "{{count}} meses",
|
||||||
"generic_count_weeks_plural": "{{count}} semanas",
|
"generic_count_months_2": "{{count}} meses",
|
||||||
"generic_count_days": "{{count}} dia",
|
"generic_count_weeks_0": "{{count}} semana",
|
||||||
"generic_count_days_plural": "{{count}} dias",
|
"generic_count_weeks_1": "{{count}} semanas",
|
||||||
"generic_count_hours": "{{count}} hora",
|
"generic_count_weeks_2": "{{count}} semanas",
|
||||||
"generic_count_hours_plural": "{{count}} horas",
|
"generic_count_days_0": "{{count}} dia",
|
||||||
"generic_count_minutes": "{{count}} minuto",
|
"generic_count_days_1": "{{count}} dias",
|
||||||
"generic_count_minutes_plural": "{{count}} minutos",
|
"generic_count_days_2": "{{count}} dias",
|
||||||
"generic_count_seconds": "{{count}} segundo",
|
"generic_count_hours_0": "{{count}} hora",
|
||||||
"generic_count_seconds_plural": "{{count}} segundos",
|
"generic_count_hours_1": "{{count}} horas",
|
||||||
"Fallback comments: ": "Comentários alternativos: ",
|
"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: ": "Alternativa para comentários: ",
|
||||||
"Popular": "Popular",
|
"Popular": "Popular",
|
||||||
"Search": "Pesquisar",
|
"Search": "Pesquisar",
|
||||||
"Top": "Destaques",
|
"Top": "Destaques",
|
||||||
"About": "Sobre",
|
"About": "Acerca",
|
||||||
"Rating: ": "Avaliação: ",
|
"Rating: ": "Avaliação: ",
|
||||||
"preferences_locale_label": "Idioma: ",
|
"preferences_locale_label": "Idioma: ",
|
||||||
"View as playlist": "Ver como lista de reprodução",
|
"View as playlist": "Ver como lista de reprodução",
|
||||||
"Default": "Predefinido",
|
"Default": "Padrão",
|
||||||
"Music": "Música",
|
"Music": "Músicas",
|
||||||
"Gaming": "Jogos",
|
"Gaming": "Jogos",
|
||||||
"News": "Notícias",
|
"News": "Notícias",
|
||||||
"Movies": "Filmes",
|
"Movies": "Filmes",
|
||||||
|
@ -328,9 +336,9 @@
|
||||||
"Download as: ": "Descarregar como: ",
|
"Download as: ": "Descarregar como: ",
|
||||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||||
"(edited)": "(editado)",
|
"(edited)": "(editado)",
|
||||||
"YouTube comment permalink": "Hiperligação permanente do comentário no YouTube",
|
"YouTube comment permalink": "Ligação permanente do comentário no YouTube",
|
||||||
"permalink": "hiperligação permanente",
|
"permalink": "ligação permanente",
|
||||||
"`x` marked it with a ❤": "`x` foi marcado como ❤",
|
"`x` marked it with a ❤": "`x` foi marcado com um ❤",
|
||||||
"Audio mode": "Modo de áudio",
|
"Audio mode": "Modo de áudio",
|
||||||
"Video mode": "Modo de vídeo",
|
"Video mode": "Modo de vídeo",
|
||||||
"channel_tab_videos_label": "Vídeos",
|
"channel_tab_videos_label": "Vídeos",
|
||||||
|
@ -338,7 +346,7 @@
|
||||||
"channel_tab_community_label": "Comunidade",
|
"channel_tab_community_label": "Comunidade",
|
||||||
"search_filters_sort_option_relevance": "Relevância",
|
"search_filters_sort_option_relevance": "Relevância",
|
||||||
"search_filters_sort_option_rating": "Avaliação",
|
"search_filters_sort_option_rating": "Avaliação",
|
||||||
"search_filters_sort_option_date": "Data de envio",
|
"search_filters_sort_option_date": "Data de carregamento",
|
||||||
"search_filters_sort_option_views": "Visualizações",
|
"search_filters_sort_option_views": "Visualizações",
|
||||||
"search_filters_type_label": "Tipo",
|
"search_filters_type_label": "Tipo",
|
||||||
"search_filters_duration_label": "Duração",
|
"search_filters_duration_label": "Duração",
|
||||||
|
@ -353,38 +361,44 @@
|
||||||
"search_filters_type_option_channel": "Canal",
|
"search_filters_type_option_channel": "Canal",
|
||||||
"search_filters_type_option_playlist": "Lista de reprodução",
|
"search_filters_type_option_playlist": "Lista de reprodução",
|
||||||
"search_filters_type_option_movie": "Filme",
|
"search_filters_type_option_movie": "Filme",
|
||||||
"search_filters_type_option_show": "Espetáculo",
|
"search_filters_type_option_show": "Séries",
|
||||||
"search_filters_features_option_hd": "HD",
|
"search_filters_features_option_hd": "HD",
|
||||||
"search_filters_features_option_subtitles": "Legendas",
|
"search_filters_features_option_subtitles": "Legendas",
|
||||||
"search_filters_features_option_c_commons": "Creative Commons",
|
"search_filters_features_option_c_commons": "Creative Commons",
|
||||||
"search_filters_features_option_three_d": "3D",
|
"search_filters_features_option_three_d": "3D",
|
||||||
"search_filters_features_option_live": "Em direto",
|
"search_filters_features_option_live": "Direto",
|
||||||
"search_filters_features_option_four_k": "4K",
|
"search_filters_features_option_four_k": "4K",
|
||||||
"search_filters_features_option_location": "Localização",
|
"search_filters_features_option_location": "Localização",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"Current version: ": "Versão atual: ",
|
"Current version: ": "Versão atual: ",
|
||||||
"next_steps_error_message": "Pode tentar as seguintes opções: ",
|
"next_steps_error_message": "Pode tentar as seguintes opções: ",
|
||||||
"next_steps_error_message_refresh": "Atualizar",
|
"next_steps_error_message_refresh": "Recarregar",
|
||||||
"next_steps_error_message_go_to_youtube": "Ir ao YouTube",
|
"next_steps_error_message_go_to_youtube": "Ir para o YouTube",
|
||||||
"search_filters_title": "Filtro",
|
"search_filters_title": "Filtro",
|
||||||
"generic_videos_count": "{{count}} vídeo",
|
"generic_videos_count_0": "{{count}} vídeo",
|
||||||
"generic_videos_count_plural": "{{count}} vídeos",
|
"generic_videos_count_1": "{{count}} vídeos",
|
||||||
"generic_playlists_count": "{{count}} lista de reprodução",
|
"generic_videos_count_2": "{{count}} vídeos",
|
||||||
"generic_playlists_count_plural": "{{count}} listas de reprodução",
|
"generic_playlists_count_0": "{{count}} lista de reprodução",
|
||||||
"generic_subscriptions_count": "{{count}} inscrição",
|
"generic_playlists_count_1": "{{count}} listas de reprodução",
|
||||||
"generic_subscriptions_count_plural": "{{count}} inscrições",
|
"generic_playlists_count_2": "{{count}} listas de reprodução",
|
||||||
"generic_views_count": "{{count}} visualização",
|
"generic_subscriptions_count_0": "{{count}} subscrição",
|
||||||
"generic_views_count_plural": "{{count}} visualizações",
|
"generic_subscriptions_count_1": "{{count}} subscrições",
|
||||||
"generic_subscribers_count": "{{count}} inscrito",
|
"generic_subscriptions_count_2": "{{count}} subscrições",
|
||||||
"generic_subscribers_count_plural": "{{count}} inscritos",
|
"generic_views_count_0": "{{count}} visualização",
|
||||||
|
"generic_views_count_1": "{{count}} visualizações",
|
||||||
|
"generic_views_count_2": "{{count}} visualizações",
|
||||||
|
"generic_subscribers_count_0": "{{count}} subscritor",
|
||||||
|
"generic_subscribers_count_1": "{{count}} subscritores",
|
||||||
|
"generic_subscribers_count_2": "{{count}} subscritores",
|
||||||
"preferences_quality_dash_option_4320p": "4320p",
|
"preferences_quality_dash_option_4320p": "4320p",
|
||||||
"preferences_quality_dash_label": "Qualidade de vídeo DASH preferida: ",
|
"preferences_quality_dash_label": "Qualidade de vídeo DASH preferida: ",
|
||||||
"preferences_quality_dash_option_2160p": "2160p",
|
"preferences_quality_dash_option_2160p": "2160p",
|
||||||
"subscriptions_unseen_notifs_count": "{{count}} notificação não vista",
|
"subscriptions_unseen_notifs_count_0": "{{count}} notificação não vista",
|
||||||
"subscriptions_unseen_notifs_count_plural": "{{count}} notificações não vistas",
|
"subscriptions_unseen_notifs_count_1": "{{count}} notificações não vistas",
|
||||||
|
"subscriptions_unseen_notifs_count_2": "{{count}} notificações não vistas",
|
||||||
"Popular enabled: ": "Página \"popular\" ativada: ",
|
"Popular enabled: ": "Página \"popular\" ativada: ",
|
||||||
"search_message_no_results": "Nenhum resultado encontrado.",
|
"search_message_no_results": "Nenhum resultado encontrado.",
|
||||||
"preferences_quality_dash_option_auto": "Automático",
|
"preferences_quality_dash_option_auto": "Automática",
|
||||||
"preferences_region_label": "País do conteúdo: ",
|
"preferences_region_label": "País do conteúdo: ",
|
||||||
"preferences_quality_dash_option_1440p": "1440p",
|
"preferences_quality_dash_option_1440p": "1440p",
|
||||||
"preferences_quality_dash_option_720p": "720p",
|
"preferences_quality_dash_option_720p": "720p",
|
||||||
|
@ -403,10 +417,12 @@
|
||||||
"preferences_quality_dash_option_240p": "240p",
|
"preferences_quality_dash_option_240p": "240p",
|
||||||
"Video unavailable": "Vídeo não disponível",
|
"Video unavailable": "Vídeo não disponível",
|
||||||
"Russian (auto-generated)": "Russo (gerado automaticamente)",
|
"Russian (auto-generated)": "Russo (gerado automaticamente)",
|
||||||
"comments_view_x_replies": "Ver {{count}} resposta",
|
"comments_view_x_replies_0": "Ver {{count}} resposta",
|
||||||
"comments_view_x_replies_plural": "Ver {{count}} respostas",
|
"comments_view_x_replies_1": "Ver {{count}} respostas",
|
||||||
"comments_points_count": "{{count}} ponto",
|
"comments_view_x_replies_2": "Ver {{count}} respostas",
|
||||||
"comments_points_count_plural": "{{count}} pontos",
|
"comments_points_count_0": "{{count}} ponto",
|
||||||
|
"comments_points_count_1": "{{count}} pontos",
|
||||||
|
"comments_points_count_2": "{{count}} pontos",
|
||||||
"English (United Kingdom)": "Inglês (Reino Unido)",
|
"English (United Kingdom)": "Inglês (Reino Unido)",
|
||||||
"Chinese (Hong Kong)": "Chinês (Hong Kong)",
|
"Chinese (Hong Kong)": "Chinês (Hong Kong)",
|
||||||
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
||||||
|
@ -432,13 +448,13 @@
|
||||||
"videoinfo_watch_on_youTube": "Ver no YouTube",
|
"videoinfo_watch_on_youTube": "Ver no YouTube",
|
||||||
"videoinfo_youTube_embed_link": "Incorporar",
|
"videoinfo_youTube_embed_link": "Incorporar",
|
||||||
"adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado",
|
"adminprefs_modified_source_code_url_label": "URL do repositório do código-fonte alterado",
|
||||||
"videoinfo_invidious_embed_link": "Incorporar hiperligação",
|
"videoinfo_invidious_embed_link": "Incorporar ligação",
|
||||||
"none": "nenhum",
|
"none": "nenhum",
|
||||||
"videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`",
|
"videoinfo_started_streaming_x_ago": "Iniciou a transmissão há `x`",
|
||||||
"download_subtitles": "Legendas - `x` (.vtt)",
|
"download_subtitles": "Legendas - `x` (.vtt)",
|
||||||
"user_created_playlists": "`x` listas de reprodução criadas",
|
"user_created_playlists": "`x` listas de reprodução criadas",
|
||||||
"user_saved_playlists": "`x` listas de reprodução guardadas",
|
"user_saved_playlists": "`x` listas de reprodução guardadas",
|
||||||
"preferences_save_player_pos_label": "Guardar a posição de reprodução atual do vídeo: ",
|
"preferences_save_player_pos_label": "Guardar posição de reprodução: ",
|
||||||
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
|
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
|
||||||
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
|
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
|
||||||
"Chinese (China)": "Chinês (China)",
|
"Chinese (China)": "Chinês (China)",
|
||||||
|
@ -455,21 +471,52 @@
|
||||||
"search_filters_date_option_none": "Qualquer data",
|
"search_filters_date_option_none": "Qualquer data",
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
"search_filters_features_option_three_sixty": "360°",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_message_use_another_instance": " Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
|
"search_message_use_another_instance": "Também pode <a href=\"`x`\">pesquisar noutra instância</a>.",
|
||||||
"crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!",
|
"crash_page_you_found_a_bug": "Parece que encontrou um erro no Invidious!",
|
||||||
"crash_page_before_reporting": "Antes de reportar um erro, verifique se:",
|
"crash_page_before_reporting": "Antes de reportar um erro, verifique se:",
|
||||||
"crash_page_read_the_faq": "leia as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
|
"crash_page_read_the_faq": "leu as <a href=\"`x`\">Perguntas frequentes (FAQ)</a>",
|
||||||
"crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
|
"crash_page_search_issue": "procurou se <a href=\"`x`\">o erro já foi reportado no GitHub</a>",
|
||||||
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto tal qual (NÃO o traduza):",
|
"crash_page_report_issue": "Se nenhuma opção acima ajudou, por favor <a href=\"`x`\">abra um novo problema no Github</a> (preferencialmente em inglês) e inclua o seguinte texto (NÃO o traduza):",
|
||||||
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
|
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
|
||||||
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
|
"crash_page_refresh": "tentou <a href=\"`x`\">recarregar a página</a>",
|
||||||
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
|
"crash_page_switch_instance": "tentou <a href=\"`x`\">usar outra instância</a>",
|
||||||
"error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para a página inicial da lista de reprodução.</a>",
|
"error_video_not_in_playlist": "O vídeo pedido não existe nesta lista de reprodução. <a href=\"`x`\">Clique aqui para voltar à página inicial da lista de reprodução.</a>",
|
||||||
"Artist: ": "Artista: ",
|
"Artist: ": "Artista: ",
|
||||||
"Album: ": "Álbum: ",
|
"Album: ": "Álbum: ",
|
||||||
"channel_tab_streams_label": "Diretos",
|
"channel_tab_streams_label": "Emissões em direto",
|
||||||
"channel_tab_playlists_label": "Listas de reprodução",
|
"channel_tab_playlists_label": "Listas de reprodução",
|
||||||
"channel_tab_channels_label": "Canais",
|
"channel_tab_channels_label": "Canais",
|
||||||
"Music in this video": "Música neste vídeo",
|
"Music in this video": "Música neste vídeo",
|
||||||
"channel_tab_shorts_label": "Curtos"
|
"channel_tab_shorts_label": "Curtos",
|
||||||
|
"generic_button_delete": "Eliminar",
|
||||||
|
"generic_button_edit": "Editar",
|
||||||
|
"generic_button_save": "Guardar",
|
||||||
|
"generic_button_cancel": "Cancelar",
|
||||||
|
"Import YouTube playlist (.csv)": "Importar lista de reprodução do YouTube (.csv)",
|
||||||
|
"Song: ": "Canção: ",
|
||||||
|
"Answer": "Responder",
|
||||||
|
"The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador.",
|
||||||
|
"Channel Sponsor": "Patrocinador do canal",
|
||||||
|
"Download is disabled": "A descarga está desativada",
|
||||||
|
"Add to playlist": "Adicionar à lista de reprodução",
|
||||||
|
"Add to playlist: ": "Adicionar à lista de reprodução: ",
|
||||||
|
"Search for videos": "Procurar vídeos",
|
||||||
|
"generic_channels_count_0": "{{count}} canal",
|
||||||
|
"generic_channels_count_1": "{{count}} canais",
|
||||||
|
"generic_channels_count_2": "{{count}} canais",
|
||||||
|
"generic_button_rss": "RSS",
|
||||||
|
"Import YouTube watch history (.json)": "Importar histórico de reprodução do YouTube (.json)",
|
||||||
|
"preferences_preload_label": "Pré-carregamento dos dados: ",
|
||||||
|
"playlist_button_add_items": "Adicionar vídeos",
|
||||||
|
"channel_tab_podcasts_label": "Podcasts",
|
||||||
|
"channel_tab_releases_label": "Lançamentos",
|
||||||
|
"carousel_slide": "Diapositivo {{current}} de{{total}}",
|
||||||
|
"carousel_skip": "Ignorar carrossel",
|
||||||
|
"carousel_go_to": "Ir para o diapositivo`x`",
|
||||||
|
"First page": "Primeira página",
|
||||||
|
"Standard YouTube license": "Licença padrão do YouTube",
|
||||||
|
"Filipino (auto-generated)": "Filipino (gerado automaticamente)",
|
||||||
|
"channel_tab_courses_label": "Cursos",
|
||||||
|
"channel_tab_posts_label": "Publicações",
|
||||||
|
"toggle_theme": "Trocar tema"
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,5 +513,10 @@
|
||||||
"carousel_slide": "Diapositivo {{current}} de{{total}}",
|
"carousel_slide": "Diapositivo {{current}} de{{total}}",
|
||||||
"carousel_skip": "Ignorar carrossel",
|
"carousel_skip": "Ignorar carrossel",
|
||||||
"carousel_go_to": "Ir para o diapositivo`x`",
|
"carousel_go_to": "Ir para o diapositivo`x`",
|
||||||
"The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador."
|
"The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador.",
|
||||||
|
"preferences_preload_label": "Pré-carregamento dos dados: ",
|
||||||
|
"Filipino (auto-generated)": "Filipino (gerado automaticamente)",
|
||||||
|
"First page": "Primeira página",
|
||||||
|
"channel_tab_courses_label": "Cursos",
|
||||||
|
"channel_tab_posts_label": "Publicações"
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"last": "последние",
|
"last": "последние",
|
||||||
"Next page": "Следующая страница",
|
"Next page": "Следующая страница",
|
||||||
"Previous page": "Предыдущая страница",
|
"Previous page": "Предыдущая страница",
|
||||||
|
"First page": "Первая страница",
|
||||||
"Clear watch history?": "Очистить историю просмотров?",
|
"Clear watch history?": "Очистить историю просмотров?",
|
||||||
"New password": "Новый пароль",
|
"New password": "Новый пароль",
|
||||||
"New passwords must match": "Новые пароли не совпадают",
|
"New passwords must match": "Новые пароли не совпадают",
|
||||||
|
@ -48,8 +49,8 @@
|
||||||
"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": "Скорость видео по умолчанию: ",
|
||||||
|
@ -474,7 +475,7 @@
|
||||||
"search_filters_date_option_none": "Любая дата",
|
"search_filters_date_option_none": "Любая дата",
|
||||||
"search_filters_date_label": "Дата загрузки",
|
"search_filters_date_label": "Дата загрузки",
|
||||||
"search_message_no_results": "Ничего не найдено.",
|
"search_message_no_results": "Ничего не найдено.",
|
||||||
"search_message_use_another_instance": " Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.",
|
"search_message_use_another_instance": "Дополнительно вы можете <a href=\"`x`\">поискать на других зеркалах</a>.",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос и/или изменить фильтры.",
|
"search_message_change_filters_or_query": "Попробуйте расширить поисковый запрос и/или изменить фильтры.",
|
||||||
"search_filters_duration_option_medium": "Средние (4 - 20 минут)",
|
"search_filters_duration_option_medium": "Средние (4 - 20 минут)",
|
||||||
|
@ -513,5 +514,8 @@
|
||||||
"toggle_theme": "Переключатель тем",
|
"toggle_theme": "Переключатель тем",
|
||||||
"carousel_slide": "Пролистано {{current}} из {{total}}",
|
"carousel_slide": "Пролистано {{current}} из {{total}}",
|
||||||
"carousel_skip": "Пропустить всё",
|
"carousel_skip": "Пропустить всё",
|
||||||
"carousel_go_to": "Перейти к странице `x`"
|
"carousel_go_to": "Перейти к странице `x`",
|
||||||
|
"preferences_preload_label": "Предзагрузка видеоданных: ",
|
||||||
|
"channel_tab_courses_label": "Курсы",
|
||||||
|
"channel_tab_posts_label": "Записи"
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"Import and Export Data": "Uvoz in izvoz podatkov",
|
"Import and Export Data": "Uvoz in izvoz podatkov",
|
||||||
"Import": "Uvozi",
|
"Import": "Uvozi",
|
||||||
"Import Invidious data": "Uvozi Invidious JSON podatke",
|
"Import Invidious data": "Uvozi Invidious JSON podatke",
|
||||||
"Import YouTube subscriptions": "Uvozi YouTube/OPML naročnine",
|
"Import YouTube subscriptions": "Uvozi YouTube CSV ali OPML naročnine",
|
||||||
"Import FreeTube subscriptions (.db)": "Uvozi FreeTube (.db) naročnine",
|
"Import FreeTube subscriptions (.db)": "Uvozi FreeTube (.db) naročnine",
|
||||||
"Import NewPipe data (.zip)": "Uvozi NewPipe (.zip) podatke",
|
"Import NewPipe data (.zip)": "Uvozi NewPipe (.zip) podatke",
|
||||||
"Export": "Izvozi",
|
"Export": "Izvozi",
|
||||||
|
@ -105,7 +105,7 @@
|
||||||
"Show more": "Pokaži več",
|
"Show more": "Pokaži več",
|
||||||
"Switch Invidious Instance": "Preklopi Invidious instanco",
|
"Switch Invidious Instance": "Preklopi Invidious instanco",
|
||||||
"search_message_change_filters_or_query": "Poskusi razširiti iskalno poizvedbo in/ali spremeniti filtre.",
|
"search_message_change_filters_or_query": "Poskusi razširiti iskalno poizvedbo in/ali spremeniti filtre.",
|
||||||
"search_message_use_another_instance": " Lahko tudi <a href=\"`x`\">iščeš v drugi istanci</a>.",
|
"search_message_use_another_instance": "Lahko tudi <a href=\"`x`\">iščeš v drugi istanci</a>.",
|
||||||
"Wilson score: ": "Wilsonov rezultat: ",
|
"Wilson score: ": "Wilsonov rezultat: ",
|
||||||
"Engagement: ": "Sodelovanje: ",
|
"Engagement: ": "Sodelovanje: ",
|
||||||
"Blacklisted regions: ": "Regije na seznamu nedovoljenih: ",
|
"Blacklisted regions: ": "Regije na seznamu nedovoljenih: ",
|
||||||
|
@ -462,7 +462,7 @@
|
||||||
"search_filters_features_option_four_k": "4K",
|
"search_filters_features_option_four_k": "4K",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"next_steps_error_message_refresh": "Osveži",
|
"next_steps_error_message_refresh": "Osveži",
|
||||||
"search_filters_date_option_hour": "Zadnja ura",
|
"search_filters_date_option_hour": "V zadnji uri",
|
||||||
"search_filters_features_option_purchased": "Kupljeno",
|
"search_filters_features_option_purchased": "Kupljeno",
|
||||||
"search_filters_sort_label": "Razvrsti po",
|
"search_filters_sort_label": "Razvrsti po",
|
||||||
"search_filters_sort_option_views": "številu ogledov",
|
"search_filters_sort_option_views": "številu ogledov",
|
||||||
|
@ -521,5 +521,16 @@
|
||||||
"generic_channels_count_1": "{{count}} kanala",
|
"generic_channels_count_1": "{{count}} kanala",
|
||||||
"generic_channels_count_2": "{{count}} kanali",
|
"generic_channels_count_2": "{{count}} kanali",
|
||||||
"generic_channels_count_3": "{{count}} kanalov",
|
"generic_channels_count_3": "{{count}} kanalov",
|
||||||
"Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)"
|
"Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)",
|
||||||
|
"Add to playlist": "Dodaj na seznam predvajanja",
|
||||||
|
"Add to playlist: ": "Dodaj na seznam predvajanja: ",
|
||||||
|
"Search for videos": "Iskanje videoposnetkov",
|
||||||
|
"The Popular feed has been disabled by the administrator.": "Administrator je onemogočil priljubljeni vir.",
|
||||||
|
"Answer": "Odgovor",
|
||||||
|
"Filipino (auto-generated)": "filipinščina (samodejno ustvarjeno)",
|
||||||
|
"toggle_theme": "Preklopi temo",
|
||||||
|
"carousel_slide": "Diapozitiv {{current}} od {{total}}",
|
||||||
|
"carousel_skip": "Preskoči galerijo",
|
||||||
|
"carousel_go_to": "Pojdi na diapozitiv `x`",
|
||||||
|
"preferences_preload_label": "Predhodno naloži video podatke: "
|
||||||
}
|
}
|
||||||
|
|
|
@ -492,5 +492,11 @@
|
||||||
"The Popular feed has been disabled by the administrator.": "Prurja Popullore është çaktivizuar nga përgjegjësi.",
|
"The Popular feed has been disabled by the administrator.": "Prurja Popullore është çaktivizuar nga përgjegjësi.",
|
||||||
"carousel_skip": "Anashkaloje Rrotullamen",
|
"carousel_skip": "Anashkaloje Rrotullamen",
|
||||||
"carousel_slide": "Diapozitiv {{current}} nga {{total}}",
|
"carousel_slide": "Diapozitiv {{current}} nga {{total}}",
|
||||||
"carousel_go_to": "Kalo te diapozitivi `x`"
|
"carousel_go_to": "Kalo te diapozitivi `x`",
|
||||||
|
"Filipino (auto-generated)": "Filipineze (të prodhuara automatikisht)",
|
||||||
|
"preferences_preload_label": "Parangarko të dhëna videoje: ",
|
||||||
|
"toggle_theme": "Ndërroni Temë",
|
||||||
|
"channel_tab_courses_label": "Kurse",
|
||||||
|
"channel_tab_posts_label": "Postime",
|
||||||
|
"First page": "Faqja e parë"
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,5 +513,10 @@
|
||||||
"Answer": "Odgovor",
|
"Answer": "Odgovor",
|
||||||
"Search for videos": "Pretražite video snimke",
|
"Search for videos": "Pretražite video snimke",
|
||||||
"carousel_skip": "Preskoči karusel",
|
"carousel_skip": "Preskoči karusel",
|
||||||
"toggle_theme": "Подеси тему"
|
"toggle_theme": "Podesi temu",
|
||||||
|
"preferences_preload_label": "Unapred učitaj podatke o video snimku: ",
|
||||||
|
"Filipino (auto-generated)": "Filipinski (automatski generisano)",
|
||||||
|
"channel_tab_posts_label": "Objave",
|
||||||
|
"First page": "Prva stranica",
|
||||||
|
"channel_tab_courses_label": "Kursevi"
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,5 +513,10 @@
|
||||||
"Add to playlist: ": "Додајте на плејлисту: ",
|
"Add to playlist: ": "Додајте на плејлисту: ",
|
||||||
"carousel_skip": "Прескочи карусел",
|
"carousel_skip": "Прескочи карусел",
|
||||||
"The Popular feed has been disabled by the administrator.": "Администратор је онемогућио фид „Популарно“.",
|
"The Popular feed has been disabled by the administrator.": "Администратор је онемогућио фид „Популарно“.",
|
||||||
"carousel_slide": "Слајд {{current}} од {{total}}"
|
"carousel_slide": "Слајд {{current}} од {{total}}",
|
||||||
|
"preferences_preload_label": "Унапред учитај податке о видео снимку: ",
|
||||||
|
"Filipino (auto-generated)": "Филипински (аутоматски генерисано)",
|
||||||
|
"channel_tab_courses_label": "Курсеви",
|
||||||
|
"First page": "Прва страница",
|
||||||
|
"channel_tab_posts_label": "Објаве"
|
||||||
}
|
}
|
||||||
|
|
|
@ -496,5 +496,10 @@
|
||||||
"The Popular feed has been disabled by the administrator.": "Det populära flödet har inaktiverats av administratören.",
|
"The Popular feed has been disabled by the administrator.": "Det populära flödet har inaktiverats av administratören.",
|
||||||
"carousel_slide": "Bildspel {{current}} av {{total}}",
|
"carousel_slide": "Bildspel {{current}} av {{total}}",
|
||||||
"carousel_skip": "Hoppa över karusellen",
|
"carousel_skip": "Hoppa över karusellen",
|
||||||
"carousel_go_to": "Gå till bildspel `x`"
|
"carousel_go_to": "Gå till bildspel `x`",
|
||||||
|
"preferences_preload_label": "Förladda video data: ",
|
||||||
|
"Filipino (auto-generated)": "Filippinska (auto-genererad)",
|
||||||
|
"First page": "Första sidan",
|
||||||
|
"channel_tab_courses_label": "Kurser",
|
||||||
|
"channel_tab_posts_label": "Inlägg"
|
||||||
}
|
}
|
||||||
|
|
502
locales/ta.json
Normal file
502
locales/ta.json
Normal file
|
@ -0,0 +1,502 @@
|
||||||
|
{
|
||||||
|
"Add to playlist": "பிளேலிச்ட்டில் சேர்க்கவும்",
|
||||||
|
"generic_channels_count": "{{count}} சேனல்",
|
||||||
|
"generic_channels_count_plural": "{{count}} சேனல்கள்",
|
||||||
|
"generic_views_count": "{{count}} பார்வை",
|
||||||
|
"generic_views_count_plural": "{{count}} காட்சிகள்",
|
||||||
|
"generic_videos_count": "{{count}} வீடியோ",
|
||||||
|
"generic_videos_count_plural": "{{count}} வீடியோக்கள்",
|
||||||
|
"generic_playlists_count": "{{count}} பிளேலிச்ட்",
|
||||||
|
"generic_playlists_count_plural": "{{count}} பிளேலிச்ட்கள்",
|
||||||
|
"generic_subscribers_count": "{{count}} சந்தாதாரர்",
|
||||||
|
"generic_subscribers_count_plural": "{{count}} சந்தாதாரர்கள்",
|
||||||
|
"generic_button_delete": "நீக்கு",
|
||||||
|
"generic_button_rss": "ஆர்.எச்.எச்",
|
||||||
|
"LIVE": "வாழ",
|
||||||
|
"Shared `x` ago": "`X` முன்பு பகிரப்பட்டது",
|
||||||
|
"Unsubscribe": "குழுவிலகவும்",
|
||||||
|
"View playlist on YouTube": "யூடியூப்பில் பிளேலிச்ட்டைக் காண்க",
|
||||||
|
"newest": "புதியது",
|
||||||
|
"oldest": "பழமையானது",
|
||||||
|
"popular": "மக்கள்",
|
||||||
|
"last": "கடைசி",
|
||||||
|
"Next page": "அடுத்த பக்கம்",
|
||||||
|
"Previous page": "முந்தைய பக்கம்",
|
||||||
|
"Clear watch history?": "தெளிவான கண்காணிப்பு வரலாறு?",
|
||||||
|
"New password": "புதிய கடவுச்சொல்",
|
||||||
|
"New passwords must match": "புதிய கடவுச்சொற்கள் பொருந்த வேண்டும்",
|
||||||
|
"Authorize token?": "கிள்ளாக்கை அங்கீகரிக்கவா?",
|
||||||
|
"Yes": "ஆம்",
|
||||||
|
"Import YouTube playlist (.csv)": "யூடியூப் பிளேலிச்ட்டை இறக்குமதி செய்க (.csv)",
|
||||||
|
"Import YouTube watch history (.json)": "YouTube வாட்ச் வரலாற்றை இறக்குமதி செய்க (.json)",
|
||||||
|
"Import Invidious data": "வன்கவர்வு சாதொபொகு தரவை இறக்குமதி செய்க",
|
||||||
|
"Import YouTube subscriptions": "YouTube காபிம அல்லது OPML சந்தாக்களை இறக்குமதி செய்க",
|
||||||
|
"Import FreeTube subscriptions (.db)": "ஃப்ரீட்யூப் சந்தாக்களை இறக்குமதி செய்க (.db)",
|
||||||
|
"Import NewPipe data (.zip)": "நியூபைப் தரவை இறக்குமதி செய்க (.zip)",
|
||||||
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML ஆக சந்தாக்களை ஏற்றுமதி செய்யுங்கள் (நியூபைப் & ஃப்ரீட்யூப்பிற்கு)",
|
||||||
|
"Export subscriptions as OPML": "OPML ஆக சந்தாக்களை ஏற்றுமதி செய்யுங்கள்",
|
||||||
|
"Export data as JSON": "சாதொபொகு ஆக வன்கவர்வு தரவை ஏற்றுமதி செய்யுங்கள்",
|
||||||
|
"Delete account?": "கணக்கை நீக்கவா?",
|
||||||
|
"History": "வரலாறு",
|
||||||
|
"JavaScript license information": "சாவாச்கிரிப்ட் உரிம செய்தி",
|
||||||
|
"source": "மூலம்",
|
||||||
|
"An alternative front-end to YouTube": "YouTube க்கு ஒரு மாற்று முன் இறுதியில்",
|
||||||
|
"Log in": "புகுபதிகை",
|
||||||
|
"Log in/register": "உள்நுழைக/பதிவு செய்யுங்கள்",
|
||||||
|
"User ID": "பயனர் ஐடி",
|
||||||
|
"Password": "கடவுச்சொல்",
|
||||||
|
"Time (h:mm:ss):": "நேரம் (h: மிமீ: எச்எச்):",
|
||||||
|
"Sign In": "விடுபதிகை",
|
||||||
|
"Register": "பதிவு செய்யுங்கள்",
|
||||||
|
"E-mail": "மின்னஞ்சல்",
|
||||||
|
"Preferences": "விருப்பத்தேர்வுகள்",
|
||||||
|
"preferences_preload_label": "வீடியோ தரவை முன்பே ஏற்றவும்: ",
|
||||||
|
"preferences_autoplay_label": "தன்னியக்க: ",
|
||||||
|
"preferences_continue_label": "இயல்பாக அடுத்து விளையாடுங்கள்: ",
|
||||||
|
"preferences_local_label": "பதிலாள் வீடியோக்கள்: ",
|
||||||
|
"preferences_watch_history_label": "கண்காணிப்பு வரலாற்றை இயக்கு: ",
|
||||||
|
"preferences_speed_label": "இயல்புநிலை வேகம்: ",
|
||||||
|
"preferences_quality_label": "விருப்பமான வீடியோ தரம்: ",
|
||||||
|
"preferences_quality_dash_label": "விருப்பமான கோடு வீடியோ தரம்: ",
|
||||||
|
"preferences_quality_dash_option_auto": "தானி",
|
||||||
|
"preferences_quality_dash_option_best": "சிறந்த",
|
||||||
|
"preferences_quality_dash_option_worst": "மோசமான",
|
||||||
|
"preferences_quality_dash_option_4320p": "4320 ப",
|
||||||
|
"preferences_quality_dash_option_1080p": "1080 ப",
|
||||||
|
"preferences_quality_dash_option_720p": "720 ஆ",
|
||||||
|
"preferences_quality_dash_option_480p": "480 ப",
|
||||||
|
"preferences_quality_dash_option_360p": "360 ப",
|
||||||
|
"preferences_quality_dash_option_144p": "144 ப",
|
||||||
|
"preferences_volume_label": "பிளேயர் தொகுதி: ",
|
||||||
|
"preferences_comments_label": "இயல்புநிலை கருத்துகள்: ",
|
||||||
|
"Fallback captions: ": "குறைவடையும் தலைப்புகள்: ",
|
||||||
|
"preferences_captions_label": "இயல்புநிலை தலைப்புகள்: ",
|
||||||
|
"preferences_related_videos_label": "தொடர்புடைய வீடியோக்களைக் காட்டு: ",
|
||||||
|
"preferences_annotations_label": "முன்னிருப்பாக சிறுகுறிப்புகளைக் காட்டு: ",
|
||||||
|
"preferences_vr_mode_label": "ஊடாடும் 360 டிகிரி வீடியோக்கள் (வெப்சிஎல் தேவை): ",
|
||||||
|
"preferences_category_visual": "காட்சி விருப்பத்தேர்வுகள்",
|
||||||
|
"light": "ஒளி",
|
||||||
|
"preferences_thin_mode_label": "மெல்லிய பயன்முறை: ",
|
||||||
|
"preferences_category_misc": "இதர விருப்பத்தேர்வுகள்",
|
||||||
|
"preferences_category_subscription": "சந்தா விருப்பத்தேர்வுகள்",
|
||||||
|
"preferences_annotations_subscribed_label": "சந்தா சேனல்களுக்கு முன்னிருப்பாக சிறுகுறிப்புகளைக் காட்டவா? ",
|
||||||
|
"Redirect homepage to feed: ": "உணவளிக்க முகப்புப்பக்கத்தை திருப்பி விடுங்கள்: ",
|
||||||
|
"preferences_sort_label": "வீடியோக்களை வரிசைப்படுத்துங்கள்: ",
|
||||||
|
"published": "வெளியிடப்பட்டது",
|
||||||
|
"published - reverse": "வெளியிடப்பட்டது - தலைகீழ்",
|
||||||
|
"alphabetically": "அகரவரிசை",
|
||||||
|
"preferences_unseen_only_label": "கவனக்குறைவாக மட்டுமே காட்டுங்கள்: ",
|
||||||
|
"preferences_notifications_only_label": "அறிவிப்புகளைக் காட்டுங்கள் (ஏதேனும் இருந்தால்): ",
|
||||||
|
"Enable web notifications": "வலை அறிவிப்புகளை இயக்கவும்",
|
||||||
|
"`x` is live": "`x` நேரலையில்",
|
||||||
|
"preferences_category_data": "தரவு விருப்பத்தேர்வுகள்",
|
||||||
|
"Manage subscriptions": "சந்தாக்களை நிர்வகிக்கவும்",
|
||||||
|
"Watch history": "வரலாற்றைப் பாருங்கள்",
|
||||||
|
"Delete account": "கணக்கை நீக்கு",
|
||||||
|
"preferences_category_admin": "நிர்வாகி விருப்பத்தேர்வுகள்",
|
||||||
|
"preferences_default_home_label": "இயல்புநிலை முகப்புப்பக்கம்: ",
|
||||||
|
"preferences_feed_menu_label": "ஊட்ட மெனு: ",
|
||||||
|
"preferences_show_nick_label": "மேலே புனைப்பெயரைக் காட்டு: ",
|
||||||
|
"Top enabled: ": "மேலே இயக்கப்பட்டது: ",
|
||||||
|
"CAPTCHA enabled: ": "கேப்ட்சா இயக்கப்பட்டது: ",
|
||||||
|
"Login enabled: ": "உள்நுழைவு இயக்கப்பட்டது: ",
|
||||||
|
"Registration enabled: ": "பதிவு இயக்கப்பட்டது: ",
|
||||||
|
"Report statistics: ": "அறிக்கை புள்ளிவிவரங்கள்: ",
|
||||||
|
"Save preferences": "விருப்பங்களை சேமிக்கவும்",
|
||||||
|
"Subscription manager": "சந்தா மேலாளர்",
|
||||||
|
"Token manager": "கிள்ளாக்கு மேலாளர்",
|
||||||
|
"Token": "கிள்ளாக்கு",
|
||||||
|
"search": "தேடல்",
|
||||||
|
"Released under the AGPLv3 on Github.": "கிட்அப்பில் AgPlv3 இன் கீழ் வெளியிடப்பட்டது.",
|
||||||
|
"View JavaScript license information.": "சாவாச்கிரிப்ட் உரிமத் தகவலைக் காண்க.",
|
||||||
|
"View privacy policy.": "தனியுரிமைக் கொள்கையைக் காண்க.",
|
||||||
|
"Trending": "டிரெண்டிங்",
|
||||||
|
"Public": "பொது",
|
||||||
|
"Unlisted": "பட்டியலிடப்படாதது",
|
||||||
|
"Private": "தனிப்பட்ட",
|
||||||
|
"View all playlists": "அனைத்து பிளேலிச்ட்களையும் காண்க",
|
||||||
|
"Updated `x` ago": "`X` முன்பு புதுப்பிக்கப்பட்டது",
|
||||||
|
"Delete playlist `x`?": "பிளேலிச்ட்டை நீக்கவா?",
|
||||||
|
"Playlist privacy": "பிளேலிச்ட் தனியுரிமை",
|
||||||
|
"Watch on YouTube": "YouTube இல் பாருங்கள்",
|
||||||
|
"Hide annotations": "சிறுகுறிப்புகளை மறைக்கவும்",
|
||||||
|
"Show replies": "பதில்களைக் காட்டு",
|
||||||
|
"Incorrect password": "தவறான கடவுச்சொல்",
|
||||||
|
"Wrong answer": "தவறான பதில்",
|
||||||
|
"Erroneous CAPTCHA": "தவறான கேப்ட்சா",
|
||||||
|
"CAPTCHA is a required field": "கேப்ட்சா ஒரு தேவையான புலம்",
|
||||||
|
"User ID is a required field": "பயனர் ஐடி தேவையான புலம்",
|
||||||
|
"Password is a required field": "கடவுச்சொல் தேவையான புலம்",
|
||||||
|
"Password cannot be empty": "கடவுச்சொல் காலியாக இருக்க முடியாது",
|
||||||
|
"Please log in": "தயவுசெய்து உள்நுழைக",
|
||||||
|
"This channel does not exist.": "இந்த சேனல் இல்லை.",
|
||||||
|
"Could not get channel info.": "சேனல் தகவலைப் பெற முடியவில்லை.",
|
||||||
|
"Could not fetch comments": "கருத்துகளைப் பெற முடியவில்லை",
|
||||||
|
"comments_points_count": "{{count}} புள்ளி",
|
||||||
|
"comments_points_count_plural": "{{count}} புள்ளிகள்",
|
||||||
|
"Could not create mix.": "கலவையை உருவாக்க முடியவில்லை.",
|
||||||
|
"Empty playlist": "வெற்று பிளேலிச்ட்",
|
||||||
|
"Not a playlist.": "ஒரு பிளேலிச்ட் அல்ல.",
|
||||||
|
"Playlist does not exist.": "பிளேலிச்ட் இல்லை.",
|
||||||
|
"Could not pull trending pages.": "பிரபலமான பக்கங்களை இழுக்க முடியவில்லை.",
|
||||||
|
"Erroneous challenge": "தவறான அறைகூவல்",
|
||||||
|
"Erroneous token": "தவறான கிள்ளாக்கு",
|
||||||
|
"No such user": "அத்தகைய பயனர் இல்லை",
|
||||||
|
"Token is expired, please try again": "கிள்ளாக்கு காலாவதியானது, தயவுசெய்து மீண்டும் முயற்சிக்கவும்",
|
||||||
|
"English": "ஆங்கிலம்",
|
||||||
|
"English (United States)": "ஆங்கிலம் (ஐக்கிய அமெரிக்க)",
|
||||||
|
"English (United Kingdom)": "ஆங்கிலம் (ஐக்கிய முடியரசு)",
|
||||||
|
"English (auto-generated)": "ஆங்கிலம் (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Afrikaans": "ஆப்பிரிக்கா",
|
||||||
|
"Albanian": "அல்பேனிய",
|
||||||
|
"Amharic": "அம்ஆரிக்",
|
||||||
|
"Arabic": "அரபு",
|
||||||
|
"Armenian": "ஆர்மீனியன்",
|
||||||
|
"Azerbaijani": "அசர்பைசானி",
|
||||||
|
"Bangla": "பாங்லா",
|
||||||
|
"Basque": "பாச்க்",
|
||||||
|
"Belarusian": "பெலாருசியன்",
|
||||||
|
"Bosnian": "போச்னிய",
|
||||||
|
"Bulgarian": "பல்கேரியன்",
|
||||||
|
"Burmese": "பர்மீச்",
|
||||||
|
"Cantonese (Hong Kong)": "கான்டோனீச் (ஆங்காங்)",
|
||||||
|
"Catalan": "கற்றலான்",
|
||||||
|
"Cebuano": "செபுவானோ",
|
||||||
|
"Chinese": "சீன",
|
||||||
|
"Chinese (China)": "சீன (சீனா)",
|
||||||
|
"Chinese (Hong Kong)": "சீன (ஆங்காங்)",
|
||||||
|
"Chinese (Simplified)": "சீன (எளிமைப்படுத்தப்பட்ட)",
|
||||||
|
"Chinese (Taiwan)": "சீன (தைவான்)",
|
||||||
|
"Chinese (Traditional)": "சீன (பாரம்பரிய)",
|
||||||
|
"Dutch": "டச்சு",
|
||||||
|
"Finnish": "பின்னிச்",
|
||||||
|
"French": "பிரஞ்சு",
|
||||||
|
"German (auto-generated)": "செர்மன் (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Greek": "கிரேக்கம்",
|
||||||
|
"Gujarati": "குசராத்தி",
|
||||||
|
"Haitian Creole": "ஐட்டிய கிரியோல்",
|
||||||
|
"Hungarian": "அங்கேரியன்",
|
||||||
|
"Icelandic": "ஐச்லாந்திய",
|
||||||
|
"Igbo": "இக்போ",
|
||||||
|
"Korean (auto-generated)": "கொரிய (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Macedonian": "மாசிடோனியன்",
|
||||||
|
"Malagasy": "மலகாசி",
|
||||||
|
"Maltese": "மால்டிச்",
|
||||||
|
"Maori": "மௌரி",
|
||||||
|
"Malayalam": "மலையாளம்",
|
||||||
|
"Marathi": "மராத்தி",
|
||||||
|
"Mongolian": "மங்கோலியன்",
|
||||||
|
"Nepali": "நேபாளி",
|
||||||
|
"Norwegian Bokmål": "நார்வேசியன் பொக்மால்",
|
||||||
|
"Nyanja": "நயன்சா",
|
||||||
|
"Russian": "ரச்ய",
|
||||||
|
"Russian (auto-generated)": "ரச்ய (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Samoan": "சமோவான்",
|
||||||
|
"Scottish Gaelic": "ச்கோட்டிச் கயாலிக்",
|
||||||
|
"Serbian": "செர்பிய",
|
||||||
|
"Shona": "சோனா",
|
||||||
|
"Sindhi": "சிந்தி",
|
||||||
|
"Somali": "சோமாலி",
|
||||||
|
"Southern Sotho": "தெற்கத்திய சோதோ",
|
||||||
|
"Spanish": "ச்பானிச்",
|
||||||
|
"Spanish (auto-generated)": "ச்பானிச் (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Sundanese": "சுந்தானியர்கள்",
|
||||||
|
"Swahili": "ச்வாஇலி",
|
||||||
|
"Swedish": "ச்வீடிச்",
|
||||||
|
"Tajik": "தசிக்",
|
||||||
|
"Tamil": "தமிழ்",
|
||||||
|
"Thai": "தாய்",
|
||||||
|
"Turkish": "துருக்கிய",
|
||||||
|
"Vietnamese": "வியட்நாமிய",
|
||||||
|
"Welsh": "வேல்ச்",
|
||||||
|
"Xhosa": "ஓசா",
|
||||||
|
"Yiddish": "யெட்டிச்",
|
||||||
|
"Yoruba": "யோருபா",
|
||||||
|
"Top": "மேலே",
|
||||||
|
"About": "பற்றி",
|
||||||
|
"View as playlist": "பிளேலிச்ட்டாக காண்க",
|
||||||
|
"Gaming": "கேமிங்",
|
||||||
|
"News": "செய்தி",
|
||||||
|
"Movies": "திரைப்படங்கள்",
|
||||||
|
"Download as: ": "என பதிவிறக்கவும்: ",
|
||||||
|
"Download is disabled": "பதிவிறக்கம் முடக்கப்பட்டுள்ளது",
|
||||||
|
"(edited)": "(திருத்தப்பட்டது)",
|
||||||
|
"YouTube comment permalink": "YouTube கருத்து பெர்மாலின்க்",
|
||||||
|
"`x` marked it with a ❤": "`x` அதை a உடன் குறித்தது",
|
||||||
|
"Video mode": "வீடியோ பயன்முறை",
|
||||||
|
"Playlists": "பிளேலிச்ட்கள்",
|
||||||
|
"search_filters_date_option_today": "இன்று",
|
||||||
|
"search_filters_date_option_week": "இந்த வாரம்",
|
||||||
|
"search_filters_date_option_month": "இந்த மாதம்",
|
||||||
|
"search_filters_type_option_channel": "வாய்க்கால்",
|
||||||
|
"search_filters_type_option_playlist": "பிளேலிச்ட்",
|
||||||
|
"search_filters_duration_label": "காலம்",
|
||||||
|
"search_filters_duration_option_none": "எந்த காலமும்",
|
||||||
|
"search_filters_duration_option_medium": "நடுத்தர (4 - 20 நிமிடங்கள்)",
|
||||||
|
"search_filters_duration_option_long": "நீண்ட (> 20 நிமிடங்கள்)",
|
||||||
|
"search_filters_features_label": "நற்பொருத்தங்கள்",
|
||||||
|
"search_filters_features_option_four_k": "எச்.சி.",
|
||||||
|
"search_filters_features_option_live": "நேரடி",
|
||||||
|
"search_filters_features_option_hd": "எச்டி",
|
||||||
|
"search_filters_features_option_subtitles": "வசன வரிகள்/சிசி",
|
||||||
|
"search_filters_features_option_c_commons": "கிரியேட்டிவ் காமன்ச்",
|
||||||
|
"search_filters_features_option_three_sixty": "360 °",
|
||||||
|
"search_filters_features_option_three_d": "ZD",
|
||||||
|
"search_filters_features_option_hdr": "எச்.டி.ஆர்",
|
||||||
|
"search_filters_features_option_location": "இடம்",
|
||||||
|
"search_filters_sort_option_relevance": "பொருத்தமானது",
|
||||||
|
"search_filters_sort_option_rating": "செயல்வரம்பு",
|
||||||
|
"Current version: ": "தற்போதைய பதிப்பு: ",
|
||||||
|
"next_steps_error_message": "அதன் பிறகு நீங்கள் முயற்சி செய்ய வேண்டும்: ",
|
||||||
|
"next_steps_error_message_refresh": "புதுப்பிப்பு",
|
||||||
|
"next_steps_error_message_go_to_youtube": "YouTube க்குச் செல்லுங்கள்",
|
||||||
|
"footer_donate_page": "நன்கொடை",
|
||||||
|
"footer_modfied_source_code": "மாற்றியமைக்கப்பட்ட மூலக் குறியீடு",
|
||||||
|
"adminprefs_modified_source_code_url_label": "மாற்றியமைக்கப்பட்ட மூலக் குறியீடு களஞ்சியத்திற்கு முகவரி",
|
||||||
|
"videoinfo_started_streaming_x_ago": "`X` முன்பு ச்ட்ரீமிங் செய்யத் தொடங்கியது",
|
||||||
|
"videoinfo_watch_on_youTube": "YouTube இல் பாருங்கள்",
|
||||||
|
"download_subtitles": "வசன வரிகள் - `x` (.vtt)",
|
||||||
|
"user_created_playlists": "`x` உருவாக்கியது பிளேலிச்ட்கள்",
|
||||||
|
"user_saved_playlists": "`x` சேமித்த பிளேலிச்ட்கள்",
|
||||||
|
"crash_page_before_reporting": "ஒரு பிழையைப் புகாரளிப்பதற்கு முன், உங்களிடம் இருப்பதை உறுதிப்படுத்திக் கொள்ளுங்கள்:",
|
||||||
|
"crash_page_switch_instance": "<a href = \"` x` \"> மற்றொரு நிகழ்வைப் பயன்படுத்த முயற்சித்தேன் </a>",
|
||||||
|
"crash_page_search_issue": "அறிவிலிமையத்தில் உள்ள <a href=\"`x`\"> தற்போதைய சிக்கல்களைத் தேடியது</a>",
|
||||||
|
"channel_tab_shorts_label": "குறுக்குகள்",
|
||||||
|
"channel_tab_streams_label": "லைவ்ச்ட்ரீம்கள்",
|
||||||
|
"carousel_go_to": "`X` ச்லைடு செல்லவும்",
|
||||||
|
"Popular": "புகழ்பெற்ற",
|
||||||
|
"Subscribe": "குழுசேர்",
|
||||||
|
"View channel on YouTube": "YouTube இல் சேனலைக் காண்க",
|
||||||
|
"Authorize token for `x`?": "`X` க்கு கிள்ளாக்கை அங்கீகரிக்கவா?",
|
||||||
|
"No": "இல்லை",
|
||||||
|
"Add to playlist: ": "பிளேலிச்ட்டில் சேர்க்கவும்: ",
|
||||||
|
"Answer": "பதில்",
|
||||||
|
"Search for videos": "வீடியோக்களைத் தேடுங்கள்",
|
||||||
|
"The Popular feed has been disabled by the administrator.": "பிரபலமான ஊட்டத்தை நிர்வாகியால் முடக்கப்பட்டுள்ளது.",
|
||||||
|
"generic_subscriptions_count": "{{count}} சந்தா",
|
||||||
|
"generic_subscriptions_count_plural": "{{count}} சந்தாக்கள்",
|
||||||
|
"generic_button_edit": "தொகு",
|
||||||
|
"generic_button_save": "சேமி",
|
||||||
|
"generic_button_cancel": "ரத்துசெய்",
|
||||||
|
"Import and Export Data": "தரவை இறக்குமதி செய்து ஏற்றுமதி செய்யுங்கள்",
|
||||||
|
"Import": "இறக்குமதி",
|
||||||
|
"Import NewPipe subscriptions (.json)": "நியூபிப்பிப் சந்தாக்களை இறக்குமதி செய்யுங்கள் (.json)",
|
||||||
|
"Export": "ஏற்றுமதி",
|
||||||
|
"Text CAPTCHA": "உரை கேப்ட்சா",
|
||||||
|
"Image CAPTCHA": "பட கேப்ட்சா",
|
||||||
|
"preferences_category_player": "பிளேயர் விருப்பத்தேர்வுகள்",
|
||||||
|
"preferences_video_loop_label": "எப்போதும் லூப்: ",
|
||||||
|
"preferences_continue_autoplay_label": "தன்னியக்க அடுத்த வீடியோ: ",
|
||||||
|
"preferences_listen_label": "இயல்பாக கேளுங்கள்: ",
|
||||||
|
"preferences_quality_option_dash": "கோடு (தகவமைப்பு தரம்)",
|
||||||
|
"preferences_quality_option_hd720": "HD720",
|
||||||
|
"preferences_quality_option_medium": "சராசரி",
|
||||||
|
"preferences_quality_option_small": "சிறிய",
|
||||||
|
"preferences_quality_dash_option_2160p": "2160 ப",
|
||||||
|
"preferences_quality_dash_option_1440p": "1440 ப",
|
||||||
|
"preferences_quality_dash_option_240p": "240 ப",
|
||||||
|
"youtube": "YouTube",
|
||||||
|
"reddit": "ரெடிட்",
|
||||||
|
"invidious": "வெகுவாக",
|
||||||
|
"preferences_extend_desc_label": "வீடியோ விளக்கத்தை தானாக நீட்டிக்கவும்: ",
|
||||||
|
"preferences_region_label": "உள்ளடக்க நாடு: ",
|
||||||
|
"preferences_player_style_label": "பிளேயர் ச்டைல்: ",
|
||||||
|
"Dark mode: ": "இருண்ட முறை: ",
|
||||||
|
"preferences_dark_mode_label": "தீம்: ",
|
||||||
|
"dark": "இருண்ட",
|
||||||
|
"preferences_automatic_instance_redirect_label": "தானியங்கி நிகழ்வு திசைதிருப்பல் (redirect.invidious.io க்கு குறைவடையும்): ",
|
||||||
|
"preferences_max_results_label": "ஊட்டத்தில் காட்டப்பட்டுள்ள வீடியோக்களின் எண்ணிக்கை: ",
|
||||||
|
"alphabetically - reverse": "அகரவரிசை - தலைகீழ்",
|
||||||
|
"channel name": "சேனல் பெயர்",
|
||||||
|
"channel name - reverse": "சேனல் பெயர் - தலைகீழ்",
|
||||||
|
"Only show latest video from channel: ": "சேனலில் இருந்து அண்மைக் கால வீடியோவைக் காட்டுங்கள்: ",
|
||||||
|
"Only show latest unwatched video from channel: ": "சேனலில் இருந்து அண்மைக் கால கவனிக்கப்படாத வீடியோவைக் காட்டுங்கள்: ",
|
||||||
|
"`x` uploaded a video": "`x` ஒரு வீடியோவைப் பதிவேற்றியது",
|
||||||
|
"Clear watch history": "தெளிவான கண்காணிப்பு வரலாறு",
|
||||||
|
"Log out": "விடுபதிகை",
|
||||||
|
"Source available here.": "சான்று இங்கே கிடைக்கிறது.",
|
||||||
|
"Delete playlist": "பிளேலிச்ட்டை நீக்கு",
|
||||||
|
"Create playlist": "பிளேலிச்ட்டை உருவாக்கவும்",
|
||||||
|
"Title": "தலைப்பு",
|
||||||
|
"Import/export data": "தரவு இறக்குமதி/ஏற்றுமதி",
|
||||||
|
"Change password": "கடவுச்சொல்லை மாற்றவும்",
|
||||||
|
"Manage tokens": "டோக்கன்களை நிர்வகிக்கவும்",
|
||||||
|
"Popular enabled: ": "பிரபலமான இயக்கப்பட்டது: ",
|
||||||
|
"tokens_count": "{{count}} கிள்ளாக்கு",
|
||||||
|
"tokens_count_plural": "{{count}} டோக்கன்கள்",
|
||||||
|
"Import/export": "இறக்குமதி/ஏற்றுமதி",
|
||||||
|
"unsubscribe": "குழுவிலகவும்",
|
||||||
|
"revoke": "ரத்து செய்யுங்கள்",
|
||||||
|
"Subscriptions": "சந்தாக்கள்",
|
||||||
|
"subscriptions_unseen_notifs_count": "{{count}} காணப்படாத அறிவிப்பு",
|
||||||
|
"subscriptions_unseen_notifs_count_plural": "{{count}} காணப்படாத அறிவிப்புகள்",
|
||||||
|
"Editing playlist `x`": "பிளேலிச்ட்டைத் திருத்துதல் `x`",
|
||||||
|
"playlist_button_add_items": "வீடியோக்களைச் சேர்க்கவும்",
|
||||||
|
"Show more": "மேலும் காட்டு",
|
||||||
|
"Show less": "குறைவாகக் காட்டு",
|
||||||
|
"Switch Invidious Instance": "அக்யோர்ட் உதாரணத்தை மாற்றவும்",
|
||||||
|
"search_message_no_results": "முடிவுகள் எதுவும் கிடைக்கவில்லை.",
|
||||||
|
"search_message_change_filters_or_query": "உங்கள் தேடல் வினவலை அகலப்படுத்த முயற்சிக்கவும்/அல்லது வடிப்பான்களை மாற்றவும்.",
|
||||||
|
"search_message_use_another_instance": "நீங்கள் <a href = \"` x` \"> மற்றொரு நிகழ்வில் தேடலாம் </a>.",
|
||||||
|
"Show annotations": "சிறுகுறிப்புகளைக் காட்டு",
|
||||||
|
"Genre: ": "வகை: ",
|
||||||
|
"License: ": "உரிமம்: ",
|
||||||
|
"Standard YouTube license": "நிலையான YouTube உரிமம்",
|
||||||
|
"Family friendly? ": "குடும்ப நட்பு? ",
|
||||||
|
"Wilson score: ": "வில்சன் மதிப்பெண்: ",
|
||||||
|
"Engagement: ": "நிச்சயதார்த்தம்: ",
|
||||||
|
"Whitelisted regions: ": "அனுமதிப்பட்டிய பகுதிகள்: ",
|
||||||
|
"Blacklisted regions: ": "தடுப்புப்பட்டியாக்கப்பட்ட பகுதிகள்: ",
|
||||||
|
"Music in this video": "இந்த வீடியோவில் இசை",
|
||||||
|
"Artist: ": "கலைஞர்: ",
|
||||||
|
"Song: ": "பாடல்: ",
|
||||||
|
"Album: ": "ஆல்பம்: ",
|
||||||
|
"Shared `x`": "பகிரப்பட்டது `x`",
|
||||||
|
"Premieres in `x`": "`X` இல் பிரீமியர்ச்",
|
||||||
|
"Premieres `x`": "பிரீமியர்ச் `x`",
|
||||||
|
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "ஆய்! நீங்கள் சாவாச்கிரிப்ட் முடக்கப்பட்டிருப்பது போல் தெரிகிறது. கருத்துகளைக் காண இங்கே சொடுக்கு செய்க, அவர்கள் ஏற்றுவதற்கு சிறிது நேரம் ஆகலாம் என்பதை நினைவில் கொள்ளுங்கள்.",
|
||||||
|
"View YouTube comments": "YouTube கருத்துகளைக் காண்க",
|
||||||
|
"View more comments on Reddit": "ரெடிட் குறித்த கூடுதல் கருத்துகளைக் காண்க",
|
||||||
|
"View `x` comments": {
|
||||||
|
"([^.,0-9]|^)1([^.,0-9]|$)": "`X` கருத்தைக் காண்க",
|
||||||
|
"": "`X` கருத்துகளைக் காண்க"
|
||||||
|
},
|
||||||
|
"View Reddit comments": "ரெடிட் கருத்துகளைக் காண்க",
|
||||||
|
"Hide replies": "பதில்களை மறைக்கவும்",
|
||||||
|
"Wrong username or password": "தவறான பயனர்பெயர் அல்லது கடவுச்சொல்",
|
||||||
|
"Password cannot be longer than 55 characters": "கடவுச்சொல் 55 எழுத்துகளை விட நீளமாக இருக்க முடியாது",
|
||||||
|
"Invidious Private Feed for `x`": "`X` க்கான மோசமான தனியார் ஊட்டம்",
|
||||||
|
"channel:`x`": "சேனல்: `x`",
|
||||||
|
"Deleted or invalid channel": "நீக்கப்பட்ட அல்லது தவறான சேனல்",
|
||||||
|
"comments_view_x_replies": "{{count}} பதிலைக் காண்க",
|
||||||
|
"comments_view_x_replies_plural": "{{count}} பதில்களைக் காண்க",
|
||||||
|
"`x` ago": "`x` முன்பு",
|
||||||
|
"Load more": "மேலும் ஏற்றவும்",
|
||||||
|
"Hidden field \"challenge\" is a required field": "மறைக்கப்பட்ட புலம் \"அறைகூவல்\" என்பது தேவையான புலம்",
|
||||||
|
"Hidden field \"token\" is a required field": "மறைக்கப்பட்ட புலம் \"கிள்ளாக்கு\" என்பது தேவையான புலம்",
|
||||||
|
"Corsican": "கார்சிகன்",
|
||||||
|
"Croatian": "குரோசியன்",
|
||||||
|
"Czech": "செக்",
|
||||||
|
"Danish": "டேனிச்",
|
||||||
|
"Dutch (auto-generated)": "டச்சு (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Esperanto": "எச்பெராண்டோ",
|
||||||
|
"Estonian": "எச்டோனிய",
|
||||||
|
"Filipino": "ஃபிலிபினோ",
|
||||||
|
"Filipino (auto-generated)": "பிலிப்பைன்ச் (தானாக உருவாக்கிய)",
|
||||||
|
"French (auto-generated)": "பிரஞ்சு (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Galician": "காலிசியன்",
|
||||||
|
"Georgian": "சார்சியன்",
|
||||||
|
"German": "செர்மன்",
|
||||||
|
"Hausa": "ஔசா",
|
||||||
|
"Lao": "லாவோ",
|
||||||
|
"Latin": "லத்தீன்",
|
||||||
|
"Latvian": "லாட்வியன்",
|
||||||
|
"Hawaiian": "அவாயியன்",
|
||||||
|
"Hebrew": "எபிரேய",
|
||||||
|
"Lithuanian": "லிதுவேனியன்",
|
||||||
|
"Hindi": "இந்தி",
|
||||||
|
"Hmong": "அமோங்",
|
||||||
|
"Indonesian": "இந்தோனேசிய",
|
||||||
|
"Indonesian (auto-generated)": "இந்தோனேசிய (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Interlingue": "இன்டர்லின்குய்",
|
||||||
|
"Irish": "ஐரிச்",
|
||||||
|
"Italian": "இத்தாலிய",
|
||||||
|
"Italian (auto-generated)": "இத்தாலியன் (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Japanese": "சப்பானியர்கள்",
|
||||||
|
"Japanese (auto-generated)": "சப்பானிய (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Javanese": "சாவானீச்",
|
||||||
|
"Kannada": "கன்னடா",
|
||||||
|
"Kazakh": "கசாக்",
|
||||||
|
"Khmer": "கெமர்",
|
||||||
|
"Korean": "கொரிய",
|
||||||
|
"Kurdish": "குர்திச்",
|
||||||
|
"Kyrgyz": "கிர்கிச்",
|
||||||
|
"Luxembourgish": "லக்சம்போர்கிச்",
|
||||||
|
"Malay": "மலாய்",
|
||||||
|
"Pashto": "பச்தோ",
|
||||||
|
"Persian": "பெர்சியன்",
|
||||||
|
"Polish": "போலீச்",
|
||||||
|
"Portuguese": "போர்த்துகீசியம்",
|
||||||
|
"Portuguese (auto-generated)": "போர்த்துகீசியம் (தானாக உருவாக்கிய)",
|
||||||
|
"generic_count_minutes": "{{count}} மணித்துளி",
|
||||||
|
"generic_count_minutes_plural": "{{count}} நிமிடங்கள்",
|
||||||
|
"generic_count_seconds": "{{count}} இரண்டாவது",
|
||||||
|
"generic_count_seconds_plural": "{{count}} வினாடிகள்",
|
||||||
|
"Fallback comments: ": "குறைவடையும் கருத்துரைகள்: ",
|
||||||
|
"Portuguese (Brazil)": "போர்த்துகீசியம் (பிரேசில்)",
|
||||||
|
"Punjabi": "பஞ்சாபி",
|
||||||
|
"Romanian": "ருமேனிய",
|
||||||
|
"Sinhala": "சிங்களம்",
|
||||||
|
"Slovak": "ச்லோவாக்",
|
||||||
|
"Slovenian": "ச்லோவேனியன்",
|
||||||
|
"Spanish (Latin America)": "ச்பானிச் (லத்தீன் அமெரிக்கா)",
|
||||||
|
"Spanish (Mexico)": "ச்பானிச் (மெக்சிகோ)",
|
||||||
|
"Spanish (Spain)": "ச்பானிச் (ச்பெயின்)",
|
||||||
|
"Telugu": "தெலுங்கு",
|
||||||
|
"Turkish (auto-generated)": "துருக்கிய (தானாக உருவாக்கிய)",
|
||||||
|
"Ukrainian": "உக்ரேனிய",
|
||||||
|
"Urdu": "உருது",
|
||||||
|
"Uzbek": "உச்பெக்",
|
||||||
|
"Vietnamese (auto-generated)": "வியட்நாமிய (தானாக உருவாக்கப்பட்ட)",
|
||||||
|
"Western Frisian": "மேற்கு ஃபிரிசியன்",
|
||||||
|
"Zulu": "சுலு",
|
||||||
|
"generic_count_years": "{{count}}} ஆண்டு",
|
||||||
|
"generic_count_years_plural": "{{count}} ஆண்டுகள்",
|
||||||
|
"generic_count_months": "{{count}} மாதம்",
|
||||||
|
"generic_count_months_plural": "{{count}} மாதங்கள்",
|
||||||
|
"generic_count_weeks": "{{count}}} வாரம்",
|
||||||
|
"generic_count_weeks_plural": "{{count}} வாரங்கள்",
|
||||||
|
"generic_count_days": "{{count}}} நாள்",
|
||||||
|
"generic_count_days_plural": "{{count}} நாட்கள்",
|
||||||
|
"generic_count_hours": "{{count}} மணிநேரம்",
|
||||||
|
"generic_count_hours_plural": "{{count}} மணிநேரம்",
|
||||||
|
"Search": "தேடல்",
|
||||||
|
"Rating: ": "மதிப்பீடு: ",
|
||||||
|
"preferences_locale_label": "மொழி: ",
|
||||||
|
"Default": "இயல்புநிலை",
|
||||||
|
"Music": "இசை",
|
||||||
|
"Download": "பதிவிறக்கம்",
|
||||||
|
"%A %B %-d, %Y": "%A %b %-d, %y",
|
||||||
|
"permalink": "பெர்மாலின்க்",
|
||||||
|
"Channel Sponsor": "சேனல் ஒப்புரவாளர்",
|
||||||
|
"Audio mode": "ஆடியோ பயன்முறை",
|
||||||
|
"search_filters_duration_option_short": "குறுகிய (<4 நிமிடங்கள்)",
|
||||||
|
"search_filters_title": "வடிப்பான்கள்",
|
||||||
|
"search_filters_date_label": "தேதி பதிவேற்றும் தேதி",
|
||||||
|
"search_filters_date_option_none": "எந்த தேதி",
|
||||||
|
"search_filters_date_option_hour": "கடைசி மணி",
|
||||||
|
"search_filters_date_option_year": "இந்த ஆண்டு",
|
||||||
|
"search_filters_type_label": "வகை",
|
||||||
|
"search_filters_type_option_all": "எந்த வகை",
|
||||||
|
"search_filters_type_option_video": "ஒளிதோற்றம்",
|
||||||
|
"search_filters_type_option_movie": "படம்",
|
||||||
|
"search_filters_type_option_show": "காட்டு",
|
||||||
|
"search_filters_features_option_vr180": "VR180",
|
||||||
|
"search_filters_features_option_purchased": "வாங்கப்பட்டது",
|
||||||
|
"search_filters_sort_label": "வரிசைப்படுத்தவும்",
|
||||||
|
"search_filters_sort_option_date": "பதிவேற்ற தேதி",
|
||||||
|
"search_filters_sort_option_views": "எண்ணிக்கை காண்க",
|
||||||
|
"search_filters_apply_button": "தேர்ந்தெடுக்கப்பட்ட வடிப்பான்களைப் பயன்படுத்துங்கள்",
|
||||||
|
"footer_documentation": "ஆவணப்படுத்துதல்",
|
||||||
|
"footer_source_code": "மூலக் குறியீடு",
|
||||||
|
"footer_original_source_code": "அசல் மூலக் குறியீடு",
|
||||||
|
"none": "எதுவுமில்லை",
|
||||||
|
"videoinfo_youTube_embed_link": "உட்பொதிக்கப்பட்டது",
|
||||||
|
"videoinfo_invidious_embed_link": "உட்பொதிப்பு இணைப்பு",
|
||||||
|
"Video unavailable": "வீடியோ கிடைக்கவில்லை",
|
||||||
|
"preferences_save_player_pos_label": "பிளேபேக் நிலையை சேமிக்கவும்: ",
|
||||||
|
"crash_page_you_found_a_bug": "நீங்கள் ஒரு பிழையை கண்டுபிடித்ததாகத் தெரிகிறது!",
|
||||||
|
"crash_page_refresh": "<a href = \"` x` \"> பக்கத்தை புதுப்பிக்க முயற்சித்தேன் </a>",
|
||||||
|
"crash_page_read_the_faq": "<a href = \"` x` \"> அடிக்கடி கேட்கப்படும் கேள்விகள் (கேள்விகள்) </a> ஐப் படியுங்கள்",
|
||||||
|
"crash_page_report_issue": "மேலே எதுவும் உதவவில்லை என்றால், தயவுசெய்து <a href = \"` x` \"> அறிவிலிமையம் </a> (முன்னுரிமை ஆங்கிலத்தில்) ஒரு புதிய சிக்கலைத் திறந்து உங்கள் செய்தியில் பின்வரும் உரையைச் சேர்க்கவும் (அந்த உரையை மொழிபெயர்க்க வேண்டாம்):",
|
||||||
|
"error_video_not_in_playlist": "கோரப்பட்ட வீடியோ இந்த பிளேலிச்ட்டில் இல்லை. <a href = \"` x` \"> பிளேலிச்ட் முகப்பு பக்கத்திற்கு இங்கே சொடுக்கு செய்க. </a>",
|
||||||
|
"channel_tab_videos_label": "வீடியோக்கள்",
|
||||||
|
"channel_tab_podcasts_label": "பாட்காச்ட்கள்",
|
||||||
|
"channel_tab_releases_label": "வெளியீடுகள்",
|
||||||
|
"channel_tab_playlists_label": "பிளேலிச்ட்கள்",
|
||||||
|
"channel_tab_community_label": "சமூகம்",
|
||||||
|
"channel_tab_channels_label": "சேனல்கள்",
|
||||||
|
"toggle_theme": "கருப்பொருளை மாற்றவும்",
|
||||||
|
"carousel_slide": "{{total}} இன் ச்லைடு {{current}}",
|
||||||
|
"carousel_skip": "கொணர்வி தவிர்க்கவும்"
|
||||||
|
}
|
1
locales/tok.json
Normal file
1
locales/tok.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{}
|
|
@ -496,5 +496,10 @@
|
||||||
"carousel_slide": "Sunum {{current}} / {{total}}",
|
"carousel_slide": "Sunum {{current}} / {{total}}",
|
||||||
"carousel_skip": "Kayar menüyü atla",
|
"carousel_skip": "Kayar menüyü atla",
|
||||||
"carousel_go_to": "`x` sunumuna git",
|
"carousel_go_to": "`x` sunumuna git",
|
||||||
"The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı."
|
"The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı.",
|
||||||
|
"preferences_preload_label": "Video verilerini önceden yükle: ",
|
||||||
|
"First page": "İlk sayfa",
|
||||||
|
"Filipino (auto-generated)": "Filipince (oto-oluşturuldu)",
|
||||||
|
"channel_tab_courses_label": "Kurslar",
|
||||||
|
"channel_tab_posts_label": "Yazılar"
|
||||||
}
|
}
|
||||||
|
|
|
@ -513,5 +513,10 @@
|
||||||
"The Popular feed has been disabled by the administrator.": "Стрічка Популярні вимкнена адміністратором.",
|
"The Popular feed has been disabled by the administrator.": "Стрічка Популярні вимкнена адміністратором.",
|
||||||
"carousel_slide": "Слайд {{current}} з {{total}}",
|
"carousel_slide": "Слайд {{current}} з {{total}}",
|
||||||
"carousel_skip": "Пропустити карусель",
|
"carousel_skip": "Пропустити карусель",
|
||||||
"carousel_go_to": "Перейти до слайда `x`"
|
"carousel_go_to": "Перейти до слайда `x`",
|
||||||
|
"preferences_preload_label": "Попереднє завантаження відеоданих: ",
|
||||||
|
"Filipino (auto-generated)": "Філіппінська (згенеровано автоматично)",
|
||||||
|
"First page": "Перша сторінка",
|
||||||
|
"channel_tab_courses_label": "Курси",
|
||||||
|
"channel_tab_posts_label": "Дописи"
|
||||||
}
|
}
|
||||||
|
|
|
@ -314,11 +314,11 @@
|
||||||
"search_filters_duration_label": "Thời lượng",
|
"search_filters_duration_label": "Thời lượng",
|
||||||
"search_filters_features_label": "Đặc điểm",
|
"search_filters_features_label": "Đặc điểm",
|
||||||
"search_filters_sort_label": "Sắp xếp theo",
|
"search_filters_sort_label": "Sắp xếp theo",
|
||||||
"search_filters_date_option_hour": "Một giờ qua",
|
"search_filters_date_option_hour": "Một giờ trước",
|
||||||
"search_filters_date_option_today": "Hôm nay",
|
"search_filters_date_option_today": "Hôm nay",
|
||||||
"search_filters_date_option_week": "Tuần này",
|
"search_filters_date_option_week": "Tuần này",
|
||||||
"search_filters_date_option_month": "Tháng này",
|
"search_filters_date_option_month": "Tháng này",
|
||||||
"search_filters_date_option_year": "Năm này",
|
"search_filters_date_option_year": "Năm nay",
|
||||||
"search_filters_type_option_video": "video",
|
"search_filters_type_option_video": "video",
|
||||||
"search_filters_type_option_channel": "Kênh",
|
"search_filters_type_option_channel": "Kênh",
|
||||||
"search_filters_type_option_playlist": "Danh sách phát",
|
"search_filters_type_option_playlist": "Danh sách phát",
|
||||||
|
@ -479,5 +479,8 @@
|
||||||
"carousel_skip": "Bỏ qua Carousel",
|
"carousel_skip": "Bỏ qua Carousel",
|
||||||
"carousel_go_to": "Đi tới trang `x`",
|
"carousel_go_to": "Đi tới trang `x`",
|
||||||
"Search for videos": "Tìm kiếm video",
|
"Search for videos": "Tìm kiếm video",
|
||||||
"The Popular feed has been disabled by the administrator.": "Bảng tin phổ biến đã bị tắt bởi ban quản lý."
|
"The Popular feed has been disabled by the administrator.": "Bảng tin phổ biến đã bị tắt bởi ban quản lý.",
|
||||||
|
"preferences_preload_label": "Tải trước dữ liệu video: ",
|
||||||
|
"Filipino (auto-generated)": "Tiếng Philippines (tự động tạo)",
|
||||||
|
"First page": "Trang đầu"
|
||||||
}
|
}
|
||||||
|
|
|
@ -420,7 +420,7 @@
|
||||||
"Chinese": "中文",
|
"Chinese": "中文",
|
||||||
"Chinese (China)": "中文 (中国)",
|
"Chinese (China)": "中文 (中国)",
|
||||||
"Chinese (Hong Kong)": "中文 (中国香港)",
|
"Chinese (Hong Kong)": "中文 (中国香港)",
|
||||||
"Chinese (Taiwan)": "中文 (中国台湾)",
|
"Chinese (Taiwan)": "中文 (台湾)",
|
||||||
"German (auto-generated)": "德语 (自动生成)",
|
"German (auto-generated)": "德语 (自动生成)",
|
||||||
"Indonesian (auto-generated)": "印尼语 (自动生成)",
|
"Indonesian (auto-generated)": "印尼语 (自动生成)",
|
||||||
"Interlingue": "国际语",
|
"Interlingue": "国际语",
|
||||||
|
@ -479,5 +479,10 @@
|
||||||
"The Popular feed has been disabled by the administrator.": "“流行”源已被管理员禁用。",
|
"The Popular feed has been disabled by the administrator.": "“流行”源已被管理员禁用。",
|
||||||
"carousel_slide": "当前为第 {{current}} 张图,共 {{total}} 张图",
|
"carousel_slide": "当前为第 {{current}} 张图,共 {{total}} 张图",
|
||||||
"carousel_skip": "跳过图集",
|
"carousel_skip": "跳过图集",
|
||||||
"carousel_go_to": "转到图 `x`"
|
"carousel_go_to": "转到图 `x`",
|
||||||
|
"preferences_preload_label": "预加载视频数据: ",
|
||||||
|
"Filipino (auto-generated)": "菲律宾语 (自动生成)",
|
||||||
|
"channel_tab_posts_label": "帖子",
|
||||||
|
"First page": "第一页",
|
||||||
|
"channel_tab_courses_label": "课程"
|
||||||
}
|
}
|
||||||
|
|
|
@ -479,5 +479,10 @@
|
||||||
"carousel_slide": "第 {{current}} 張投影片,共 {{total}} 張",
|
"carousel_slide": "第 {{current}} 張投影片,共 {{total}} 張",
|
||||||
"carousel_skip": "略過輪播",
|
"carousel_skip": "略過輪播",
|
||||||
"carousel_go_to": "跳到投影片 `x`",
|
"carousel_go_to": "跳到投影片 `x`",
|
||||||
"The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。"
|
"The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。",
|
||||||
|
"preferences_preload_label": "預先載入影片資訊 ",
|
||||||
|
"Filipino (auto-generated)": "菲律賓語(自動產生)",
|
||||||
|
"channel_tab_courses_label": "課程",
|
||||||
|
"First page": "第一頁",
|
||||||
|
"channel_tab_posts_label": "貼文"
|
||||||
}
|
}
|
||||||
|
|
56
scripts/generate_js_licenses.cr
Normal file
56
scripts/generate_js_licenses.cr
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
# This file automatically generates Crystal strings of rows within an HTML Javascript licenses table
|
||||||
|
#
|
||||||
|
# These strings will then be placed within a `<%= %>` statement in licenses.ecr at compile time which
|
||||||
|
# will be interpolated at run-time. This interpolation is only for the translation of the "source" string
|
||||||
|
# so maybe we can just switch to a non-translated string to simplify the logic here.
|
||||||
|
#
|
||||||
|
# The Javascript Web Labels table defined at https://www.gnu.org/software/librejs/free-your-javascript.html#step3
|
||||||
|
# for example just reiterates the name of the source file rather than use a "source" string.
|
||||||
|
all_javascript_files = Dir.glob("assets/**/*.js")
|
||||||
|
|
||||||
|
videojs_js = [] of String
|
||||||
|
invidious_js = [] of String
|
||||||
|
|
||||||
|
all_javascript_files.each do |js_path|
|
||||||
|
if js_path.starts_with?("assets/videojs/")
|
||||||
|
videojs_js << js_path[7..]
|
||||||
|
else
|
||||||
|
invidious_js << js_path[7..]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_licence_tr(path, file_name, licence_name, licence_link, source_location)
|
||||||
|
tr = <<-HTML
|
||||||
|
"<tr>
|
||||||
|
<td><a href=\\"/#{path}\\">#{file_name}</a></td>
|
||||||
|
<td><a href=\\"#{licence_link}\\">#{licence_name}</a></td>
|
||||||
|
<td><a href=\\"#{source_location}\\">\#{translate(locale, "source")}</a></td>
|
||||||
|
</tr>"
|
||||||
|
HTML
|
||||||
|
|
||||||
|
# New lines are removed as to allow for using String.join and StringLiteral.split
|
||||||
|
# to get a clean list of each table row.
|
||||||
|
tr.gsub('\n', "")
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO Use videojs-dependencies.yml to generate license info for videojs javascript
|
||||||
|
jslicence_table_rows = [] of String
|
||||||
|
|
||||||
|
invidious_js.each do |path|
|
||||||
|
file_name = path.split('/')[-1]
|
||||||
|
|
||||||
|
# A couple non Invidious JS files are also shipped alongside Invidious due to various reasons
|
||||||
|
next if {
|
||||||
|
"sse.js", "silvermine-videojs-quality-selector.min.js", "videojs-youtube-annotations.min.js",
|
||||||
|
}.includes?(file_name)
|
||||||
|
|
||||||
|
jslicence_table_rows << create_licence_tr(
|
||||||
|
path: path,
|
||||||
|
file_name: file_name,
|
||||||
|
licence_name: "AGPL-3.0",
|
||||||
|
licence_link: "https://www.gnu.org/licenses/agpl-3.0.html",
|
||||||
|
source_location: path
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
puts jslicence_table_rows.join("\n")
|
|
@ -18,7 +18,7 @@ shards:
|
||||||
|
|
||||||
exception_page:
|
exception_page:
|
||||||
git: https://github.com/crystal-loot/exception_page.git
|
git: https://github.com/crystal-loot/exception_page.git
|
||||||
version: 0.2.2
|
version: 0.4.1
|
||||||
|
|
||||||
http_proxy:
|
http_proxy:
|
||||||
git: https://github.com/mamantoha/http_proxy.git
|
git: https://github.com/mamantoha/http_proxy.git
|
||||||
|
@ -26,11 +26,7 @@ shards:
|
||||||
|
|
||||||
kemal:
|
kemal:
|
||||||
git: https://github.com/kemalcr/kemal.git
|
git: https://github.com/kemalcr/kemal.git
|
||||||
version: 1.1.2
|
version: 1.6.0
|
||||||
|
|
||||||
kilt:
|
|
||||||
git: https://github.com/jeromegn/kilt.git
|
|
||||||
version: 0.6.1
|
|
||||||
|
|
||||||
pg:
|
pg:
|
||||||
git: https://github.com/will/crystal-pg.git
|
git: https://github.com/will/crystal-pg.git
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: invidious
|
name: invidious
|
||||||
version: 2.20241110.0
|
version: 2.20250517.0-dev
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Invidious team <contact@invidious.io>
|
- Invidious team <contact@invidious.io>
|
||||||
|
@ -17,10 +17,7 @@ dependencies:
|
||||||
version: ~> 0.21.0
|
version: ~> 0.21.0
|
||||||
kemal:
|
kemal:
|
||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
version: ~> 1.1.2
|
version: ~> 1.6.0
|
||||||
kilt:
|
|
||||||
github: jeromegn/kilt
|
|
||||||
version: ~> 0.6.1
|
|
||||||
protodec:
|
protodec:
|
||||||
github: iv-org/protodec
|
github: iv-org/protodec
|
||||||
version: ~> 0.1.5
|
version: ~> 0.1.5
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
# Overrides for Kemal's `content_for` macro in order to keep using
|
|
||||||
# kilt as it was before Kemal v1.1.1 (Kemal PR #618).
|
|
||||||
|
|
||||||
require "kemal"
|
|
||||||
require "kilt"
|
|
||||||
|
|
||||||
macro content_for(key, file = __FILE__)
|
|
||||||
%proc = ->() {
|
|
||||||
__kilt_io__ = IO::Memory.new
|
|
||||||
{{ yield }}
|
|
||||||
__kilt_io__.to_s
|
|
||||||
}
|
|
||||||
|
|
||||||
CONTENT_FOR_BLOCKS[{{key}}] = Tuple.new {{file}}, %proc
|
|
||||||
nil
|
|
||||||
end
|
|
|
@ -71,7 +71,7 @@ def send_file(env : HTTP::Server::Context, file_path : String, data : Slice(UInt
|
||||||
filesize = data.bytesize
|
filesize = data.bytesize
|
||||||
attachment(env, filename, disposition)
|
attachment(env, filename, disposition)
|
||||||
|
|
||||||
Kemal.config.static_headers.try(&.call(env.response, file_path, filestat))
|
Kemal.config.static_headers.try(&.call(env, file_path, filestat))
|
||||||
|
|
||||||
file = IO::Memory.new(data)
|
file = IO::Memory.new(data)
|
||||||
if env.request.method == "GET" && env.request.headers.has_key?("Range")
|
if env.request.method == "GET" && env.request.headers.has_key?("Range")
|
||||||
|
|
|
@ -17,10 +17,8 @@
|
||||||
require "digest/md5"
|
require "digest/md5"
|
||||||
require "file_utils"
|
require "file_utils"
|
||||||
|
|
||||||
# Require kemal, kilt, then our own overrides
|
# Require kemal, then our own overrides
|
||||||
require "kemal"
|
require "kemal"
|
||||||
require "kilt"
|
|
||||||
require "./ext/kemal_content_for.cr"
|
|
||||||
require "./ext/kemal_static_file_handler.cr"
|
require "./ext/kemal_static_file_handler.cr"
|
||||||
|
|
||||||
require "http_proxy"
|
require "http_proxy"
|
||||||
|
@ -49,7 +47,8 @@ require "./invidious/channels/*"
|
||||||
require "./invidious/user/*"
|
require "./invidious/user/*"
|
||||||
require "./invidious/search/*"
|
require "./invidious/search/*"
|
||||||
require "./invidious/routes/**"
|
require "./invidious/routes/**"
|
||||||
require "./invidious/jobs/**"
|
require "./invidious/jobs/base_job"
|
||||||
|
require "./invidious/jobs/*"
|
||||||
|
|
||||||
# Declare the base namespace for invidious
|
# Declare the base namespace for invidious
|
||||||
module Invidious
|
module Invidious
|
||||||
|
@ -97,6 +96,10 @@ YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
|
||||||
|
|
||||||
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
|
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
|
||||||
|
|
||||||
|
COMPANION_POOL = CompanionConnectionPool.new(
|
||||||
|
capacity: CONFIG.pool_size
|
||||||
|
)
|
||||||
|
|
||||||
# CLI
|
# CLI
|
||||||
Kemal.config.extra_options do |parser|
|
Kemal.config.extra_options do |parser|
|
||||||
parser.banner = "Usage: invidious [arguments]"
|
parser.banner = "Usage: invidious [arguments]"
|
||||||
|
@ -192,8 +195,9 @@ if CONFIG.popular_enabled
|
||||||
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
|
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
|
||||||
end
|
end
|
||||||
|
|
||||||
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
|
NOTIFICATION_CHANNEL = ::Channel(VideoNotification).new(32)
|
||||||
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url)
|
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
|
||||||
|
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(NOTIFICATION_CHANNEL, CONNECTION_CHANNEL, CONFIG.database_url)
|
||||||
|
|
||||||
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
||||||
|
|
||||||
|
@ -221,8 +225,8 @@ error 500 do |env, ex|
|
||||||
error_template(500, ex)
|
error_template(500, ex)
|
||||||
end
|
end
|
||||||
|
|
||||||
static_headers do |response|
|
static_headers do |env|
|
||||||
response.headers.add("Cache-Control", "max-age=2629800")
|
env.response.headers.add("Cache-Control", "max-age=2629800")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Init Kemal
|
# Init Kemal
|
||||||
|
@ -239,8 +243,6 @@ add_context_storage_type(Preferences)
|
||||||
add_context_storage_type(Invidious::User)
|
add_context_storage_type(Invidious::User)
|
||||||
|
|
||||||
Kemal.config.logger = LOGGER
|
Kemal.config.logger = LOGGER
|
||||||
Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.config.host_binding : CONFIG.host_binding
|
|
||||||
Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port
|
|
||||||
Kemal.config.app_name = "Invidious"
|
Kemal.config.app_name = "Invidious"
|
||||||
|
|
||||||
# Use in kemal's production mode.
|
# Use in kemal's production mode.
|
||||||
|
@ -249,4 +251,16 @@ Kemal.config.app_name = "Invidious"
|
||||||
Kemal.config.env = "production" if !ENV.has_key?("KEMAL_ENV")
|
Kemal.config.env = "production" if !ENV.has_key?("KEMAL_ENV")
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
Kemal.run
|
Kemal.run do |config|
|
||||||
|
if socket_binding = CONFIG.socket_binding
|
||||||
|
File.delete?(socket_binding.path)
|
||||||
|
# Create a socket and set its desired permissions
|
||||||
|
server = UNIXServer.new(socket_binding.path)
|
||||||
|
perms = socket_binding.permissions.to_i(base: 8)
|
||||||
|
File.chmod(socket_binding.path, perms)
|
||||||
|
config.server.not_nil!.bind server
|
||||||
|
else
|
||||||
|
Kemal.config.host_binding = Kemal.config.host_binding != "0.0.0.0" ? Kemal.config.host_binding : CONFIG.host_binding
|
||||||
|
Kemal.config.port = Kemal.config.port != 3000 ? Kemal.config.port : CONFIG.port
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -249,11 +249,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||||
|
|
||||||
if was_insert
|
if was_insert
|
||||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
|
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
|
||||||
if CONFIG.enable_user_notifications
|
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
|
||||||
Invidious::Database::Users.add_notification(video)
|
|
||||||
else
|
|
||||||
Invidious::Database::Users.feed_needs_update(video)
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
|
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
|
||||||
end
|
end
|
||||||
|
@ -285,11 +281,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||||
if Time.utc - video.published > 1.minute
|
if Time.utc - video.published > 1.minute
|
||||||
was_insert = Invidious::Database::ChannelVideos.insert(video)
|
was_insert = Invidious::Database::ChannelVideos.insert(video)
|
||||||
if was_insert
|
if was_insert
|
||||||
if CONFIG.enable_user_notifications
|
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
|
||||||
Invidious::Database::Users.add_notification(video)
|
|
||||||
else
|
|
||||||
Invidious::Database::Users.feed_needs_update(video)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,3 +44,12 @@ def fetch_channel_releases(ucid, author, continuation)
|
||||||
end
|
end
|
||||||
return extract_items(initial_data, author, ucid)
|
return extract_items(initial_data, author, ucid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def fetch_channel_courses(ucid, author, continuation)
|
||||||
|
if continuation
|
||||||
|
initial_data = YoutubeAPI.browse(continuation)
|
||||||
|
else
|
||||||
|
initial_data = YoutubeAPI.browse(ucid, params: "Egdjb3Vyc2Vz8gYFCgPCAQA%3D")
|
||||||
|
end
|
||||||
|
return extract_items(initial_data, author, ucid)
|
||||||
|
end
|
||||||
|
|
|
@ -8,6 +8,13 @@ struct DBConfig
|
||||||
property dbname : String
|
property dbname : String
|
||||||
end
|
end
|
||||||
|
|
||||||
|
struct SocketBindingConfig
|
||||||
|
include YAML::Serializable
|
||||||
|
|
||||||
|
property path : String
|
||||||
|
property permissions : String
|
||||||
|
end
|
||||||
|
|
||||||
struct ConfigPreferences
|
struct ConfigPreferences
|
||||||
include YAML::Serializable
|
include YAML::Serializable
|
||||||
|
|
||||||
|
@ -28,7 +35,7 @@ struct ConfigPreferences
|
||||||
property max_results : Int32 = 40
|
property max_results : Int32 = 40
|
||||||
property notifications_only : Bool = false
|
property notifications_only : Bool = false
|
||||||
property player_style : String = "invidious"
|
property player_style : String = "invidious"
|
||||||
property quality : String = "hd720"
|
property quality : String = "dash"
|
||||||
property quality_dash : String = "auto"
|
property quality_dash : String = "auto"
|
||||||
property default_home : String? = "Popular"
|
property default_home : String? = "Popular"
|
||||||
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
|
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
|
||||||
|
@ -67,6 +74,16 @@ end
|
||||||
class Config
|
class Config
|
||||||
include YAML::Serializable
|
include YAML::Serializable
|
||||||
|
|
||||||
|
class CompanionConfig
|
||||||
|
include YAML::Serializable
|
||||||
|
|
||||||
|
@[YAML::Field(converter: Preferences::URIConverter)]
|
||||||
|
property private_url : URI = URI.parse("")
|
||||||
|
|
||||||
|
@[YAML::Field(converter: Preferences::URIConverter)]
|
||||||
|
property public_url : URI = URI.parse("")
|
||||||
|
end
|
||||||
|
|
||||||
# Number of threads to use for crawling videos from channels (for updating subscriptions)
|
# Number of threads to use for crawling videos from channels (for updating subscriptions)
|
||||||
property channel_threads : Int32 = 1
|
property channel_threads : Int32 = 1
|
||||||
# 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).
|
||||||
|
@ -138,6 +155,8 @@ class Config
|
||||||
property port : Int32 = 3000
|
property port : Int32 = 3000
|
||||||
# Host to bind (overridden by command line argument)
|
# Host to bind (overridden by command line argument)
|
||||||
property host_binding : String = "0.0.0.0"
|
property host_binding : String = "0.0.0.0"
|
||||||
|
# Path and permissions to make Invidious listen on a UNIX socket instead of a TCP port
|
||||||
|
property socket_binding : SocketBindingConfig? = nil
|
||||||
# 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
|
||||||
# HTTP Proxy configuration
|
# HTTP Proxy configuration
|
||||||
|
@ -151,6 +170,12 @@ class Config
|
||||||
# poToken for passing bot attestation
|
# poToken for passing bot attestation
|
||||||
property po_token : String? = nil
|
property po_token : String? = nil
|
||||||
|
|
||||||
|
# Invidious companion
|
||||||
|
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
|
||||||
|
|
||||||
|
# Invidious companion API key
|
||||||
|
property invidious_companion_key : String = ""
|
||||||
|
|
||||||
# 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
|
||||||
|
@ -184,6 +209,9 @@ class Config
|
||||||
config = Config.from_yaml(config_yaml)
|
config = Config.from_yaml(config_yaml)
|
||||||
|
|
||||||
# Update config from env vars (upcased and prefixed with "INVIDIOUS_")
|
# Update config from env vars (upcased and prefixed with "INVIDIOUS_")
|
||||||
|
#
|
||||||
|
# Also checks if any top-level config options are set to "CHANGE_ME!!"
|
||||||
|
# TODO: Support non-top-level config options such as the ones in DBConfig
|
||||||
{% for ivar in Config.instance_vars %}
|
{% for ivar in Config.instance_vars %}
|
||||||
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
|
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
|
||||||
|
|
||||||
|
@ -220,16 +248,40 @@ class Config
|
||||||
exit(1)
|
exit(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Warn when any config attribute is set to "CHANGE_ME!!"
|
||||||
|
if config.{{ivar.id}} == "CHANGE_ME!!"
|
||||||
|
puts "Config: The value of '#{ {{ivar.stringify}} }' needs to be changed!!"
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
|
if config.invidious_companion.present?
|
||||||
|
# invidious_companion and signature_server can't work together
|
||||||
|
if config.signature_server
|
||||||
|
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."
|
||||||
|
exit(1)
|
||||||
|
elsif config.invidious_companion_key.empty?
|
||||||
|
puts "Config: Please configure a key if you are using invidious companion."
|
||||||
|
exit(1)
|
||||||
|
elsif config.invidious_companion_key == "CHANGE_ME!!"
|
||||||
|
puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
|
||||||
|
exit(1)
|
||||||
|
elsif config.invidious_companion_key.size != 16
|
||||||
|
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters."
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
elsif config.signature_server
|
||||||
|
puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/")
|
||||||
|
else
|
||||||
|
puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/companion-installation/")
|
||||||
|
end
|
||||||
|
|
||||||
# HMAC_key is mandatory
|
# HMAC_key is mandatory
|
||||||
# See: https://github.com/iv-org/invidious/issues/3854
|
# See: https://github.com/iv-org/invidious/issues/3854
|
||||||
if config.hmac_key.empty?
|
if config.hmac_key.empty?
|
||||||
puts "Config: 'hmac_key' is required/can't be empty"
|
puts "Config: 'hmac_key' is required/can't be empty"
|
||||||
exit(1)
|
exit(1)
|
||||||
elsif config.hmac_key == "CHANGE_ME!!"
|
|
||||||
puts "Config: The value of 'hmac_key' needs to be changed!!"
|
|
||||||
exit(1)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Build database_url from db.* if it's not set directly
|
# Build database_url from db.* if it's not set directly
|
||||||
|
@ -249,6 +301,24 @@ class Config
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check if the socket configuration is valid
|
||||||
|
if sb = config.socket_binding
|
||||||
|
if sb.path.ends_with?("/") || File.directory?(sb.path)
|
||||||
|
puts "Config: The socket path " + sb.path + " must not be a directory!"
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
d = File.dirname(sb.path)
|
||||||
|
if !File.directory?(d)
|
||||||
|
puts "Config: Socket directory " + sb.path + " does not exist or is not a directory!"
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
p = sb.permissions.to_i?(base: 8)
|
||||||
|
if !p || p < 0 || p > 0o777
|
||||||
|
puts "Config: Socket permissions must be an octal between 0 and 777!"
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return config
|
return config
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -91,7 +91,7 @@ module Invidious::Database::Playlists
|
||||||
end
|
end
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Salect
|
# Select
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def select(*, id : String) : InvidiousPlaylist?
|
def select(*, id : String) : InvidiousPlaylist?
|
||||||
|
@ -113,7 +113,7 @@ module Invidious::Database::Playlists
|
||||||
end
|
end
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Salect (filtered)
|
# Select (filtered)
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def select_like_iv(email : String) : Array(InvidiousPlaylist)
|
def select_like_iv(email : String) : Array(InvidiousPlaylist)
|
||||||
|
@ -213,7 +213,7 @@ module Invidious::Database::PlaylistVideos
|
||||||
end
|
end
|
||||||
|
|
||||||
# -------------------
|
# -------------------
|
||||||
# Salect
|
# Select
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)
|
def select(plid : String, index : VideoIndex, offset, limit = 100) : Array(PlaylistVideo)
|
||||||
|
|
|
@ -119,15 +119,15 @@ module Invidious::Database::Users
|
||||||
# Update (notifs)
|
# Update (notifs)
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def add_notification(video : ChannelVideo)
|
def add_multiple_notifications(channel_id : String, video_ids : Array(String))
|
||||||
request = <<-SQL
|
request = <<-SQL
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET notifications = array_append(notifications, $1),
|
SET notifications = array_cat(notifications, $1),
|
||||||
feed_needs_update = true
|
feed_needs_update = true
|
||||||
WHERE $2 = ANY(subscriptions)
|
WHERE $2 = ANY(subscriptions)
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
PG_DB.exec(request, video.id, video.ucid)
|
PG_DB.exec(request, video_ids, channel_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def remove_notification(user : User, vid : String)
|
def remove_notification(user : User, vid : String)
|
||||||
|
@ -154,14 +154,14 @@ module Invidious::Database::Users
|
||||||
# Update (misc)
|
# Update (misc)
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
def feed_needs_update(video : ChannelVideo)
|
def feed_needs_update(channel_id : String)
|
||||||
request = <<-SQL
|
request = <<-SQL
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET feed_needs_update = true
|
SET feed_needs_update = true
|
||||||
WHERE $1 = ANY(subscriptions)
|
WHERE $1 = ANY(subscriptions)
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
PG_DB.exec(request, video.ucid)
|
PG_DB.exec(request, channel_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_preferences(user : User)
|
def update_preferences(user : User)
|
||||||
|
|
|
@ -7,8 +7,9 @@ module Invidious::Frontend::ChannelPage
|
||||||
Streams
|
Streams
|
||||||
Podcasts
|
Podcasts
|
||||||
Releases
|
Releases
|
||||||
|
Courses
|
||||||
Playlists
|
Playlists
|
||||||
Community
|
Posts
|
||||||
Channels
|
Channels
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,24 @@ require "uri"
|
||||||
module Invidious::Frontend::Pagination
|
module Invidious::Frontend::Pagination
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
|
private def first_page(str : String::Builder, locale : String?, url : String)
|
||||||
|
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
|
||||||
|
|
||||||
|
if locale_is_rtl?(locale)
|
||||||
|
# Inverted arrow ("first" points to the right)
|
||||||
|
str << translate(locale, "First page")
|
||||||
|
str << " "
|
||||||
|
str << %(<i class="icon ion-ios-arrow-forward"></i>)
|
||||||
|
else
|
||||||
|
# Regular arrow ("first" points to the left)
|
||||||
|
str << %(<i class="icon ion-ios-arrow-back"></i>)
|
||||||
|
str << " "
|
||||||
|
str << translate(locale, "First page")
|
||||||
|
end
|
||||||
|
|
||||||
|
str << "</a>"
|
||||||
|
end
|
||||||
|
|
||||||
private def previous_page(str : String::Builder, locale : String?, url : String)
|
private def previous_page(str : String::Builder, locale : String?, url : String)
|
||||||
# Link
|
# Link
|
||||||
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
|
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
|
||||||
|
@ -72,18 +90,24 @@ module Invidious::Frontend::Pagination
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?)
|
def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?, first_page : Bool, params : URI::Params)
|
||||||
return String.build do |str|
|
return String.build do |str|
|
||||||
str << %(<div class="h-box">\n)
|
str << %(<div class="h-box">\n)
|
||||||
str << %(<div class="page-nav-container flexible">\n)
|
str << %(<div class="page-nav-container flexible">\n)
|
||||||
|
|
||||||
str << %(<div class="page-prev-container flex-left"></div>\n)
|
str << %(<div class="page-prev-container flex-left">)
|
||||||
|
|
||||||
|
if !first_page
|
||||||
|
self.first_page(str, locale, base_url.to_s)
|
||||||
|
end
|
||||||
|
|
||||||
|
str << %(</div>\n)
|
||||||
|
|
||||||
str << %(<div class="page-next-container flex-right">)
|
str << %(<div class="page-next-container flex-right">)
|
||||||
|
|
||||||
if !ctoken.nil?
|
if !ctoken.nil?
|
||||||
params_next = URI::Params{"continuation" => ctoken}
|
params["continuation"] = ctoken
|
||||||
url_next = HttpServer::Utils.add_params_to_url(base_url, params_next)
|
url_next = HttpServer::Utils.add_params_to_url(base_url, params)
|
||||||
|
|
||||||
self.next_page(str, locale, url_next.to_s)
|
self.next_page(str, locale, url_next.to_s)
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ module Invidious::Frontend::WatchPage
|
||||||
@full_videos,
|
@full_videos,
|
||||||
@video_streams,
|
@video_streams,
|
||||||
@audio_streams,
|
@audio_streams,
|
||||||
@captions
|
@captions,
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -23,10 +23,16 @@ module Invidious::Frontend::WatchPage
|
||||||
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
|
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
url = "/download"
|
||||||
|
if (CONFIG.invidious_companion.present?)
|
||||||
|
invidious_companion = CONFIG.invidious_companion.sample
|
||||||
|
url = "#{invidious_companion.public_url}/download?check=#{invidious_companion_encrypt(video.id)}"
|
||||||
|
end
|
||||||
|
|
||||||
return String.build(4000) do |str|
|
return String.build(4000) do |str|
|
||||||
str << "<form"
|
str << "<form"
|
||||||
str << " class=\"pure-form pure-form-stacked\""
|
str << " class=\"pure-form pure-form-stacked\""
|
||||||
str << " action='/download'"
|
str << " action='#{url}'"
|
||||||
str << " method='post'"
|
str << " method='post'"
|
||||||
str << " rel='noopener'"
|
str << " rel='noopener'"
|
||||||
str << " target='_blank'>"
|
str << " target='_blank'>"
|
||||||
|
|
|
@ -18,40 +18,6 @@ end
|
||||||
class HTTP::Client
|
class HTTP::Client
|
||||||
property family : Socket::Family = Socket::Family::UNSPEC
|
property family : Socket::Family = Socket::Family::UNSPEC
|
||||||
|
|
||||||
# Override stdlib to automatically initialize proxy if configured
|
|
||||||
#
|
|
||||||
# Accurate as of crystal 1.12.1
|
|
||||||
|
|
||||||
def initialize(@host : String, port = nil, tls : TLSContext = nil)
|
|
||||||
check_host_only(@host)
|
|
||||||
|
|
||||||
{% if flag?(:without_openssl) %}
|
|
||||||
if tls
|
|
||||||
raise "HTTP::Client TLS is disabled because `-D without_openssl` was passed at compile time"
|
|
||||||
end
|
|
||||||
@tls = nil
|
|
||||||
{% else %}
|
|
||||||
@tls = case tls
|
|
||||||
when true
|
|
||||||
OpenSSL::SSL::Context::Client.new
|
|
||||||
when OpenSSL::SSL::Context::Client
|
|
||||||
tls
|
|
||||||
when false, nil
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
@port = (port || (@tls ? 443 : 80)).to_i
|
|
||||||
|
|
||||||
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(@io : IO, @host = "", @port = 80)
|
|
||||||
@reconnect = false
|
|
||||||
|
|
||||||
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
|
||||||
end
|
|
||||||
|
|
||||||
private def io
|
private def io
|
||||||
io = @io
|
io = @io
|
||||||
return io if io
|
return io if io
|
||||||
|
|
|
@ -18,16 +18,7 @@ def github_details(summary : String, content : String)
|
||||||
return HTML.escape(details)
|
return HTML.escape(details)
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
|
def get_issue_template(env : HTTP::Server::Context, exception : Exception) : Tuple(String, String)
|
||||||
if exception.is_a?(InfoException)
|
|
||||||
return error_template_helper(env, status_code, exception.message || "")
|
|
||||||
end
|
|
||||||
|
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
|
||||||
|
|
||||||
env.response.content_type = "text/html"
|
|
||||||
env.response.status_code = status_code
|
|
||||||
|
|
||||||
issue_title = "#{exception.message} (#{exception.class})"
|
issue_title = "#{exception.message} (#{exception.class})"
|
||||||
|
|
||||||
issue_template = <<-TEXT
|
issue_template = <<-TEXT
|
||||||
|
@ -40,6 +31,24 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
|
||||||
|
|
||||||
issue_template += github_details("Backtrace", exception.inspect_with_backtrace)
|
issue_template += github_details("Backtrace", exception.inspect_with_backtrace)
|
||||||
|
|
||||||
|
return issue_title, issue_template
|
||||||
|
end
|
||||||
|
|
||||||
|
def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
|
||||||
|
if exception.is_a?(InfoException)
|
||||||
|
return error_template_helper(env, status_code, exception.message || "")
|
||||||
|
end
|
||||||
|
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
env.response.content_type = "text/html"
|
||||||
|
env.response.status_code = status_code
|
||||||
|
|
||||||
|
# Unpacking into issue_title, issue_template directly causes a compiler error
|
||||||
|
# I have no idea why.
|
||||||
|
issue_template_components = get_issue_template(env, exception)
|
||||||
|
issue_title, issue_template = issue_template_components
|
||||||
|
|
||||||
# URLs for the error message below
|
# URLs for the error message below
|
||||||
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
|
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
|
||||||
url_search_issues = "https://github.com/iv-org/invidious/issues"
|
url_search_issues = "https://github.com/iv-org/invidious/issues"
|
||||||
|
@ -69,7 +78,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
|
||||||
<p>#{translate(locale, "crash_page_report_issue", url_new_issue)}</p>
|
<p>#{translate(locale, "crash_page_report_issue", url_new_issue)}</p>
|
||||||
|
|
||||||
<!-- TODO: Add a "copy to clipboard" button -->
|
<!-- TODO: Add a "copy to clipboard" button -->
|
||||||
<pre style="padding: 20px; background: rgba(0, 0, 0, 0.12345);">#{issue_template}</pre>
|
<pre class="error-issue-template">#{issue_template}</pre>
|
||||||
</div>
|
</div>
|
||||||
END_HTML
|
END_HTML
|
||||||
|
|
||||||
|
@ -130,7 +139,7 @@ def error_json_helper(
|
||||||
env : HTTP::Server::Context,
|
env : HTTP::Server::Context,
|
||||||
status_code : Int32,
|
status_code : Int32,
|
||||||
exception : Exception,
|
exception : Exception,
|
||||||
additional_fields : Hash(String, Object) | Nil = nil
|
additional_fields : Hash(String, Object) | Nil = nil,
|
||||||
)
|
)
|
||||||
if exception.is_a?(InfoException)
|
if exception.is_a?(InfoException)
|
||||||
return error_json_helper(env, status_code, exception.message || "", additional_fields)
|
return error_json_helper(env, status_code, exception.message || "", additional_fields)
|
||||||
|
@ -152,7 +161,7 @@ def error_json_helper(
|
||||||
env : HTTP::Server::Context,
|
env : HTTP::Server::Context,
|
||||||
status_code : Int32,
|
status_code : Int32,
|
||||||
message : String,
|
message : String,
|
||||||
additional_fields : Hash(String, Object) | Nil = nil
|
additional_fields : Hash(String, Object) | Nil = nil,
|
||||||
)
|
)
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
env.response.status_code = status_code
|
env.response.status_code = status_code
|
||||||
|
|
|
@ -27,6 +27,7 @@ class Kemal::RouteHandler
|
||||||
# Processes the route if it's a match. Otherwise renders 404.
|
# Processes the route if it's a match. Otherwise renders 404.
|
||||||
private def process_request(context)
|
private def process_request(context)
|
||||||
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
|
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
|
||||||
|
return if context.response.closed?
|
||||||
content = context.route.handler.call(context)
|
content = context.route.handler.call(context)
|
||||||
|
|
||||||
if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) && exclude_match?(context)
|
if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) && exclude_match?(context)
|
||||||
|
|
|
@ -54,6 +54,7 @@ LOCALES_LIST = {
|
||||||
"sr" => "Srpski (latinica)", # Serbian (Latin)
|
"sr" => "Srpski (latinica)", # Serbian (Latin)
|
||||||
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
|
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
|
||||||
"sv-SE" => "Svenska", # Swedish
|
"sv-SE" => "Svenska", # Swedish
|
||||||
|
"ta" => "தமிழ்", # Tamil
|
||||||
"tr" => "Türkçe", # Turkish
|
"tr" => "Türkçe", # Turkish
|
||||||
"uk" => "Українська", # Ukrainian
|
"uk" => "Українська", # Ukrainian
|
||||||
"vi" => "Tiếng Việt", # Vietnamese
|
"vi" => "Tiếng Việt", # Vietnamese
|
||||||
|
|
|
@ -55,12 +55,11 @@ macro templated(_filename, template = "template", navbar_search = true)
|
||||||
{{ layout = "src/invidious/views/" + template + ".ecr" }}
|
{{ layout = "src/invidious/views/" + template + ".ecr" }}
|
||||||
|
|
||||||
__content_filename__ = {{filename}}
|
__content_filename__ = {{filename}}
|
||||||
content = Kilt.render({{filename}})
|
render {{filename}}, {{layout}}
|
||||||
Kilt.render({{layout}})
|
|
||||||
end
|
end
|
||||||
|
|
||||||
macro rendered(filename)
|
macro rendered(filename)
|
||||||
Kilt.render("src/invidious/views/#{{{filename}}}.ecr")
|
render("src/invidious/views/#{{{filename}}}.ecr")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Similar to Kemals halt method but works in a
|
# Similar to Kemals halt method but works in a
|
||||||
|
|
|
@ -24,6 +24,7 @@ struct SearchVideo
|
||||||
property length_seconds : Int32
|
property length_seconds : Int32
|
||||||
property premiere_timestamp : Time?
|
property premiere_timestamp : Time?
|
||||||
property author_verified : Bool
|
property author_verified : Bool
|
||||||
|
property author_thumbnail : String?
|
||||||
property badges : VideoBadges
|
property badges : VideoBadges
|
||||||
|
|
||||||
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
||||||
|
@ -88,6 +89,24 @@ struct SearchVideo
|
||||||
json.field "authorUrl", "/channel/#{self.ucid}"
|
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||||
json.field "authorVerified", self.author_verified
|
json.field "authorVerified", self.author_verified
|
||||||
|
|
||||||
|
author_thumbnail = self.author_thumbnail
|
||||||
|
|
||||||
|
if author_thumbnail
|
||||||
|
json.field "authorThumbnails" do
|
||||||
|
json.array do
|
||||||
|
qualities = {32, 48, 76, 100, 176, 512}
|
||||||
|
|
||||||
|
qualities.each do |quality|
|
||||||
|
json.object do
|
||||||
|
json.field "url", author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
|
||||||
|
json.field "width", quality
|
||||||
|
json.field "height", quality
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
json.field "videoThumbnails" do
|
json.field "videoThumbnails" do
|
||||||
Invidious::JSONify::APIv1.thumbnails(json, self.id)
|
Invidious::JSONify::APIv1.thumbnails(json, self.id)
|
||||||
end
|
end
|
||||||
|
@ -223,7 +242,7 @@ struct SearchChannel
|
||||||
|
|
||||||
qualities.each do |quality|
|
qualities.each do |quality|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
|
json.field "url", self.author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
|
||||||
json.field "width", quality
|
json.field "width", quality
|
||||||
json.field "height", quality
|
json.field "height", quality
|
||||||
end
|
end
|
||||||
|
@ -272,6 +291,55 @@ struct SearchHashtag
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# A `ProblematicTimelineItem` is a `SearchItem` created by Invidious that
|
||||||
|
# represents an item that caused an exception during parsing.
|
||||||
|
#
|
||||||
|
# This is not a parsed object from YouTube but rather an Invidious-only type
|
||||||
|
# created to gracefully communicate parse errors without throwing away
|
||||||
|
# the rest of the (hopefully) successfully parsed item on a page.
|
||||||
|
struct ProblematicTimelineItem
|
||||||
|
property parse_exception : Exception
|
||||||
|
property id : String
|
||||||
|
|
||||||
|
def initialize(@parse_exception)
|
||||||
|
@id = Random.new.hex(8)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_json(locale : String?, json : JSON::Builder)
|
||||||
|
json.object do
|
||||||
|
json.field "type", "parse-error"
|
||||||
|
json.field "errorMessage", @parse_exception.message
|
||||||
|
json.field "errorBacktrace", @parse_exception.inspect_with_backtrace
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Provides compatibility with PlaylistVideo
|
||||||
|
def to_json(json : JSON::Builder, *args, **kwargs)
|
||||||
|
return to_json("", json)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_xml(env, locale, xml : XML::Builder)
|
||||||
|
xml.element("entry") do
|
||||||
|
xml.element("id") { xml.text "iv-err-#{@id}" }
|
||||||
|
xml.element("title") { xml.text "Parse Error: This item has failed to parse" }
|
||||||
|
xml.element("updated") { xml.text Time.utc.to_rfc3339 }
|
||||||
|
|
||||||
|
xml.element("content", type: "xhtml") do
|
||||||
|
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
||||||
|
xml.element("div") do
|
||||||
|
xml.element("h4") { translate(locale, "timeline_parse_error_placeholder_heading") }
|
||||||
|
xml.element("p") { translate(locale, "timeline_parse_error_placeholder_message") }
|
||||||
|
end
|
||||||
|
|
||||||
|
xml.element("pre") do
|
||||||
|
get_issue_template(env, @parse_exception)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class Category
|
class Category
|
||||||
include DB::Serializable
|
include DB::Serializable
|
||||||
|
|
||||||
|
@ -314,4 +382,4 @@ struct Continuation
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category
|
alias SearchItem = SearchVideo | SearchChannel | SearchPlaylist | SearchHashtag | Category | ProblematicTimelineItem
|
||||||
|
|
|
@ -262,7 +262,7 @@ def get_referer(env, fallback = "/", unroll = true)
|
||||||
end
|
end
|
||||||
|
|
||||||
referer = referer.request_target
|
referer = referer.request_target
|
||||||
referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z]/, "").lstrip("/\\")
|
referer = "/" + referer.gsub(/[^\/?@&%=\-_.:,*0-9a-zA-Z+]/, "").lstrip("/\\")
|
||||||
|
|
||||||
if referer == env.request.path
|
if referer == env.request.path
|
||||||
referer = fallback
|
referer = fallback
|
||||||
|
@ -383,3 +383,22 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
|
||||||
end
|
end
|
||||||
return text
|
return text
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def encrypt_ecb_without_salt(data, key)
|
||||||
|
cipher = OpenSSL::Cipher.new("aes-128-ecb")
|
||||||
|
cipher.encrypt
|
||||||
|
cipher.key = key
|
||||||
|
|
||||||
|
io = IO::Memory.new
|
||||||
|
io.write(cipher.update(data))
|
||||||
|
io.write(cipher.final)
|
||||||
|
io.rewind
|
||||||
|
|
||||||
|
return io
|
||||||
|
end
|
||||||
|
|
||||||
|
def invidious_companion_encrypt(data)
|
||||||
|
timestamp = Time.utc.to_unix
|
||||||
|
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
|
||||||
|
return Base64.urlsafe_encode(encrypted_data)
|
||||||
|
end
|
||||||
|
|
|
@ -1,8 +1,32 @@
|
||||||
|
struct VideoNotification
|
||||||
|
getter video_id : String
|
||||||
|
getter channel_id : String
|
||||||
|
getter published : Time
|
||||||
|
|
||||||
|
def_hash @channel_id, @video_id
|
||||||
|
|
||||||
|
def ==(other)
|
||||||
|
video_id == other.video_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.from_video(video : ChannelVideo) : self
|
||||||
|
VideoNotification.new(video.id, video.ucid, video.published)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(@video_id, @channel_id, @published)
|
||||||
|
end
|
||||||
|
|
||||||
|
def clone : VideoNotification
|
||||||
|
VideoNotification.new(video_id.clone, channel_id.clone, published.clone)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
|
class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
|
||||||
|
private getter notification_channel : ::Channel(VideoNotification)
|
||||||
private getter connection_channel : ::Channel({Bool, ::Channel(PQ::Notification)})
|
private getter connection_channel : ::Channel({Bool, ::Channel(PQ::Notification)})
|
||||||
private getter pg_url : URI
|
private getter pg_url : URI
|
||||||
|
|
||||||
def initialize(@connection_channel, @pg_url)
|
def initialize(@notification_channel, @connection_channel, @pg_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
def begin
|
def begin
|
||||||
|
@ -10,6 +34,70 @@ class Invidious::Jobs::NotificationJob < Invidious::Jobs::BaseJob
|
||||||
|
|
||||||
PG.connect_listen(pg_url, "notifications") { |event| connections.each(&.send(event)) }
|
PG.connect_listen(pg_url, "notifications") { |event| connections.each(&.send(event)) }
|
||||||
|
|
||||||
|
# hash of channels to their videos (id+published) that need notifying
|
||||||
|
to_notify = Hash(String, Set(VideoNotification)).new(
|
||||||
|
->(hash : Hash(String, Set(VideoNotification)), key : String) {
|
||||||
|
hash[key] = Set(VideoNotification).new
|
||||||
|
}
|
||||||
|
)
|
||||||
|
notify_mutex = Mutex.new
|
||||||
|
|
||||||
|
# fiber to locally cache all incoming notifications (from pubsub webhooks and refresh channels job)
|
||||||
|
spawn do
|
||||||
|
begin
|
||||||
|
loop do
|
||||||
|
notification = notification_channel.receive
|
||||||
|
notify_mutex.synchronize do
|
||||||
|
to_notify[notification.channel_id] << notification
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# fiber to regularly persist all cached notifications
|
||||||
|
spawn do
|
||||||
|
loop do
|
||||||
|
begin
|
||||||
|
LOGGER.debug("NotificationJob: waking up")
|
||||||
|
cloned = {} of String => Set(VideoNotification)
|
||||||
|
notify_mutex.synchronize do
|
||||||
|
cloned = to_notify.clone
|
||||||
|
to_notify.clear
|
||||||
|
end
|
||||||
|
|
||||||
|
cloned.each do |channel_id, notifications|
|
||||||
|
if notifications.empty?
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
LOGGER.info("NotificationJob: updating channel #{channel_id} with #{notifications.size} notifications")
|
||||||
|
if CONFIG.enable_user_notifications
|
||||||
|
video_ids = notifications.map(&.video_id)
|
||||||
|
Invidious::Database::Users.add_multiple_notifications(channel_id, video_ids)
|
||||||
|
PG_DB.using_connection do |conn|
|
||||||
|
notifications.each do |n|
|
||||||
|
# Deliver notifications to `/api/v1/auth/notifications`
|
||||||
|
payload = {
|
||||||
|
"topic" => n.channel_id,
|
||||||
|
"videoId" => n.video_id,
|
||||||
|
"published" => n.published.to_unix,
|
||||||
|
}.to_json
|
||||||
|
conn.exec("NOTIFY notifications, E'#{payload}'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
Invidious::Database::Users.feed_needs_update(channel_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
LOGGER.trace("NotificationJob: Done, sleeping")
|
||||||
|
rescue ex
|
||||||
|
LOGGER.error("NotificationJob: #{ex.message}")
|
||||||
|
end
|
||||||
|
sleep 1.minute
|
||||||
|
Fiber.yield
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
loop do
|
loop do
|
||||||
action, connection = connection_channel.receive
|
action, connection = connection_channel.receive
|
||||||
|
|
||||||
|
|
|
@ -267,6 +267,12 @@ module Invidious::JSONify::APIv1
|
||||||
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
|
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
|
||||||
json.field "viewCountText", rv["short_view_count"]?
|
json.field "viewCountText", rv["short_view_count"]?
|
||||||
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
|
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
|
||||||
|
json.field "published", rv["published"]?
|
||||||
|
if rv["published"]?.try &.presence
|
||||||
|
json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
|
||||||
|
else
|
||||||
|
json.field "publishedText", ""
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -81,7 +81,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_mix(mix)
|
def template_mix(mix, listen)
|
||||||
html = <<-END_HTML
|
html = <<-END_HTML
|
||||||
<h3>
|
<h3>
|
||||||
<a href="/mix?list=#{mix["mixId"]}">
|
<a href="/mix?list=#{mix["mixId"]}">
|
||||||
|
@ -95,7 +95,7 @@ def template_mix(mix)
|
||||||
mix["videos"].as_a.each do |video|
|
mix["videos"].as_a.each do |video|
|
||||||
html += <<-END_HTML
|
html += <<-END_HTML
|
||||||
<li class="pure-menu-item">
|
<li class="pure-menu-item">
|
||||||
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
|
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}#{listen ? "&listen=1" : ""}">
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
||||||
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||||
|
|
|
@ -432,7 +432,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
|
||||||
offset = initial_data.dig?("contents", "twoColumnWatchNextResults", "playlist", "playlist", "currentIndex").try &.as_i || offset
|
offset = initial_data.dig?("contents", "twoColumnWatchNextResults", "playlist", "playlist", "currentIndex").try &.as_i || offset
|
||||||
end
|
end
|
||||||
|
|
||||||
videos = [] of PlaylistVideo
|
videos = [] of PlaylistVideo | ProblematicTimelineItem
|
||||||
|
|
||||||
until videos.size >= 200 || videos.size == playlist.video_count || offset >= playlist.video_count
|
until videos.size >= 200 || videos.size == playlist.video_count || offset >= playlist.video_count
|
||||||
# 100 videos per request
|
# 100 videos per request
|
||||||
|
@ -448,7 +448,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
|
def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
|
||||||
videos = [] of PlaylistVideo
|
videos = [] of PlaylistVideo | ProblematicTimelineItem
|
||||||
|
|
||||||
if initial_data["contents"]?
|
if initial_data["contents"]?
|
||||||
tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]
|
tabs = initial_data["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]
|
||||||
|
@ -500,12 +500,14 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
|
||||||
index: index,
|
index: index,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
rescue ex
|
||||||
|
videos << ProblematicTimelineItem.new(parse_exception: ex)
|
||||||
end
|
end
|
||||||
|
|
||||||
return videos
|
return videos
|
||||||
end
|
end
|
||||||
|
|
||||||
def template_playlist(playlist)
|
def template_playlist(playlist, listen)
|
||||||
html = <<-END_HTML
|
html = <<-END_HTML
|
||||||
<h3>
|
<h3>
|
||||||
<a href="/playlist?list=#{playlist["playlistId"]}">
|
<a href="/playlist?list=#{playlist["playlistId"]}">
|
||||||
|
@ -519,7 +521,7 @@ def template_playlist(playlist)
|
||||||
playlist["videos"].as_a.each do |video|
|
playlist["videos"].as_a.each do |video|
|
||||||
html += <<-END_HTML
|
html += <<-END_HTML
|
||||||
<li class="pure-menu-item" id="#{video["videoId"]}">
|
<li class="pure-menu-item" id="#{video["videoId"]}">
|
||||||
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}">
|
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}#{listen ? "&listen=1" : ""}">
|
||||||
<div class="thumbnail">
|
<div class="thumbnail">
|
||||||
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
|
||||||
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>
|
||||||
|
|
|
@ -328,17 +328,9 @@ module Invidious::Routes::Account
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if env.params.query["action_revoke_token"]?
|
case action = env.params.query["action"]?
|
||||||
action = "action_revoke_token"
|
when "revoke_token"
|
||||||
else
|
session = env.params.query["session"]
|
||||||
return env.redirect referer
|
|
||||||
end
|
|
||||||
|
|
||||||
session = env.params.query["session"]?
|
|
||||||
session ||= ""
|
|
||||||
|
|
||||||
case action
|
|
||||||
when .starts_with? "action_revoke_token"
|
|
||||||
Invidious::Database::SessionIDs.delete(sid: session, email: user.email)
|
Invidious::Database::SessionIDs.delete(sid: session, email: user.email)
|
||||||
else
|
else
|
||||||
return error_json(400, "Unsupported action #{action}")
|
return error_json(400, "Unsupported action #{action}")
|
||||||
|
|
|
@ -8,6 +8,11 @@ module Invidious::Routes::API::Manifest
|
||||||
id = env.params.url["id"]
|
id = env.params.url["id"]
|
||||||
region = env.params.query["region"]?
|
region = env.params.query["region"]?
|
||||||
|
|
||||||
|
if CONFIG.invidious_companion.present?
|
||||||
|
invidious_companion = CONFIG.invidious_companion.sample
|
||||||
|
return env.redirect "#{invidious_companion.public_url}/api/manifest/dash/id/#{id}?#{env.params.query}"
|
||||||
|
end
|
||||||
|
|
||||||
# Since some implementations create playlists based on resolution regardless of different codecs,
|
# Since some implementations create playlists based on resolution regardless of different codecs,
|
||||||
# we can opt to only add a source to a representation if it has a unique height within that representation
|
# we can opt to only add a source to a representation if it has a unique height within that representation
|
||||||
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
@ -27,28 +32,21 @@ module Invidious::Routes::API::Manifest
|
||||||
haltf env, status_code: response.status_code
|
haltf env, status_code: response.status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
manifest = response.body
|
# Proxy URLs for video playback on invidious.
|
||||||
|
# Other API clients can get the original URLs by omiting `local=true`.
|
||||||
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
||||||
url = baseurl.lchop("<BaseURL>")
|
url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
|
||||||
url = url.rchop("</BaseURL>")
|
url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
|
||||||
|
|
||||||
if local
|
|
||||||
uri = URI.parse(url)
|
|
||||||
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
|
|
||||||
end
|
|
||||||
|
|
||||||
"<BaseURL>#{url}</BaseURL>"
|
"<BaseURL>#{url}</BaseURL>"
|
||||||
end
|
end
|
||||||
|
|
||||||
return manifest
|
return manifest
|
||||||
end
|
end
|
||||||
|
|
||||||
adaptive_fmts = video.adaptive_fmts
|
# Ditto, only proxify URLs if `local=true` is used
|
||||||
|
|
||||||
if local
|
if local
|
||||||
adaptive_fmts.each do |fmt|
|
video.adaptive_fmts.each do |fmt|
|
||||||
fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
|
fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -70,17 +68,23 @@ module Invidious::Routes::API::Manifest
|
||||||
# OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
|
# OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
|
||||||
next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
|
next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
|
||||||
|
|
||||||
|
audio_track = fmt["audioTrack"]?.try &.as_h? || {} of String => JSON::Any
|
||||||
|
lang = audio_track["id"]?.try &.as_s.split('.')[0] || "und"
|
||||||
|
is_default = audio_track.has_key?("audioIsDefault") ? audio_track["audioIsDefault"].as_bool : i == 0
|
||||||
|
displayname = audio_track["displayName"]?.try &.as_s || "Unknown"
|
||||||
|
bitrate = fmt["bitrate"]
|
||||||
|
|
||||||
# Different representations of the same audio should be groupped into one AdaptationSet.
|
# Different representations of the same audio should be groupped into one AdaptationSet.
|
||||||
# However, most players don't support auto quality switching, so we have to trick them
|
# However, most players don't support auto quality switching, so we have to trick them
|
||||||
# into providing a quality selector.
|
# into providing a quality selector.
|
||||||
# See https://github.com/iv-org/invidious/issues/3074 for more details.
|
# See https://github.com/iv-org/invidious/issues/3074 for more details.
|
||||||
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: fmt["bitrate"].to_s + "k") do
|
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: "#{displayname} [#{bitrate}k]", lang: lang) do
|
||||||
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
|
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
|
||||||
bandwidth = fmt["bitrate"].as_i
|
bandwidth = fmt["bitrate"].as_i
|
||||||
itag = fmt["itag"].as_i
|
itag = fmt["itag"].as_i
|
||||||
url = fmt["url"].as_s
|
url = fmt["url"].as_s
|
||||||
|
|
||||||
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: i == 0 ? "main" : "alternate")
|
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: is_default ? "main" : "alternate")
|
||||||
|
|
||||||
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
|
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
|
||||||
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
|
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
|
||||||
|
@ -177,8 +181,9 @@ module Invidious::Routes::API::Manifest
|
||||||
manifest = response.body
|
manifest = response.body
|
||||||
|
|
||||||
if local
|
if local
|
||||||
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
|
manifest = manifest.gsub(/https:\/\/[^\n"]*/m) do |match|
|
||||||
path = URI.parse(match).path
|
uri = URI.parse(match)
|
||||||
|
path = uri.path
|
||||||
|
|
||||||
path = path.lchop("/videoplayback/")
|
path = path.lchop("/videoplayback/")
|
||||||
path = path.rchop("/")
|
path = path.rchop("/")
|
||||||
|
@ -207,7 +212,7 @@ module Invidious::Routes::API::Manifest
|
||||||
raw_params["fvip"] = fvip["fvip"]
|
raw_params["fvip"] = fvip["fvip"]
|
||||||
end
|
end
|
||||||
|
|
||||||
raw_params["local"] = "true"
|
raw_params["host"] = uri.host.not_nil!
|
||||||
|
|
||||||
"#{HOST_URL}/videoplayback?#{raw_params}"
|
"#{HOST_URL}/videoplayback?#{raw_params}"
|
||||||
end
|
end
|
||||||
|
|
|
@ -368,6 +368,35 @@ module Invidious::Routes::API::V1::Channels
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.courses(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
ucid = env.params.url["ucid"]
|
||||||
|
continuation = env.params.query["continuation"]?
|
||||||
|
|
||||||
|
# Use the macro defined above
|
||||||
|
channel = nil # Make the compiler happy
|
||||||
|
get_channel()
|
||||||
|
|
||||||
|
items, next_continuation = fetch_channel_courses(channel.ucid, channel.author, continuation)
|
||||||
|
|
||||||
|
JSON.build do |json|
|
||||||
|
json.object do
|
||||||
|
json.field "playlists" do
|
||||||
|
json.array do
|
||||||
|
items.each do |item|
|
||||||
|
item.to_json(locale, json) if item.is_a?(SearchPlaylist)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
json.field "continuation", next_continuation if next_continuation
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.community(env)
|
def self.community(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,9 @@ module Invidious::Routes::API::V1::Misc
|
||||||
format = env.params.query["format"]?
|
format = env.params.query["format"]?
|
||||||
format ||= "json"
|
format ||= "json"
|
||||||
|
|
||||||
|
listen_param = env.params.query["listen"]?
|
||||||
|
listen = (listen_param == "true" || listen_param == "1")
|
||||||
|
|
||||||
if plid.starts_with? "RD"
|
if plid.starts_with? "RD"
|
||||||
return env.redirect "/api/v1/mixes/#{plid}"
|
return env.redirect "/api/v1/mixes/#{plid}"
|
||||||
end
|
end
|
||||||
|
@ -85,7 +88,7 @@ module Invidious::Routes::API::V1::Misc
|
||||||
end
|
end
|
||||||
|
|
||||||
if format == "html"
|
if format == "html"
|
||||||
playlist_html = template_playlist(json_response)
|
playlist_html = template_playlist(json_response, listen)
|
||||||
index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
|
index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
|
@ -111,6 +114,9 @@ module Invidious::Routes::API::V1::Misc
|
||||||
format = env.params.query["format"]?
|
format = env.params.query["format"]?
|
||||||
format ||= "json"
|
format ||= "json"
|
||||||
|
|
||||||
|
listen_param = env.params.query["listen"]?
|
||||||
|
listen = (listen_param == "true" || listen_param == "1")
|
||||||
|
|
||||||
begin
|
begin
|
||||||
mix = fetch_mix(rdid, continuation, locale: locale)
|
mix = fetch_mix(rdid, continuation, locale: locale)
|
||||||
|
|
||||||
|
@ -141,9 +147,7 @@ module Invidious::Routes::API::V1::Misc
|
||||||
json.field "authorUrl", "/channel/#{video.ucid}"
|
json.field "authorUrl", "/channel/#{video.ucid}"
|
||||||
|
|
||||||
json.field "videoThumbnails" do
|
json.field "videoThumbnails" do
|
||||||
json.array do
|
Invidious::JSONify::APIv1.thumbnails(json, video.id)
|
||||||
Invidious::JSONify::APIv1.thumbnails(json, video.id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
json.field "index", video.index
|
json.field "index", video.index
|
||||||
|
@ -157,7 +161,7 @@ module Invidious::Routes::API::V1::Misc
|
||||||
|
|
||||||
if format == "html"
|
if format == "html"
|
||||||
response = JSON.parse(response)
|
response = JSON.parse(response)
|
||||||
playlist_html = template_mix(response)
|
playlist_html = template_mix(response, listen)
|
||||||
next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"]
|
next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"]
|
||||||
|
|
||||||
response = {
|
response = {
|
||||||
|
|
|
@ -429,4 +429,90 @@ module Invidious::Routes::API::V1::Videos
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Fetches transcripts from YouTube
|
||||||
|
#
|
||||||
|
# Use the `lang` and `autogen` query parameter to select which transcript to fetch
|
||||||
|
# Request without any URL parameters to see all the available transcripts.
|
||||||
|
def self.transcripts(env)
|
||||||
|
env.response.content_type = "application/json"
|
||||||
|
|
||||||
|
id = env.params.url["id"]
|
||||||
|
lang = env.params.query["lang"]?
|
||||||
|
label = env.params.query["label"]?
|
||||||
|
auto_generated = env.params.query["autogen"]? ? true : false
|
||||||
|
|
||||||
|
# Return all available transcript options when none is given
|
||||||
|
if !label && !lang
|
||||||
|
begin
|
||||||
|
video = get_video(id)
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
response = JSON.build do |json|
|
||||||
|
# The amount of transcripts available to fetch is the
|
||||||
|
# same as the amount of captions available.
|
||||||
|
available_transcripts = video.captions
|
||||||
|
|
||||||
|
json.object do
|
||||||
|
json.field "transcripts" do
|
||||||
|
json.array do
|
||||||
|
available_transcripts.each do |transcript|
|
||||||
|
json.object do
|
||||||
|
json.field "label", transcript.name
|
||||||
|
json.field "languageCode", transcript.language_code
|
||||||
|
json.field "autoGenerated", transcript.auto_generated
|
||||||
|
|
||||||
|
if transcript.auto_generated
|
||||||
|
json.field "url", "/api/v1/transcripts/#{id}?lang=#{URI.encode_www_form(transcript.language_code)}&autogen"
|
||||||
|
else
|
||||||
|
json.field "url", "/api/v1/transcripts/#{id}?lang=#{URI.encode_www_form(transcript.language_code)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return response
|
||||||
|
end
|
||||||
|
|
||||||
|
# If lang is not given then we attempt to fetch
|
||||||
|
# the transcript through the given label
|
||||||
|
if lang.nil?
|
||||||
|
begin
|
||||||
|
video = get_video(id)
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
target_transcript = video.captions.select(&.name.== label)
|
||||||
|
if target_transcript.empty?
|
||||||
|
return error_json(404, NotFoundException.new("Requested transcript does not exist"))
|
||||||
|
else
|
||||||
|
target_transcript = target_transcript[0]
|
||||||
|
lang, auto_generated = target_transcript.language_code, target_transcript.auto_generated
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
params = Invidious::Videos::Transcript.generate_param(id, lang, auto_generated)
|
||||||
|
|
||||||
|
begin
|
||||||
|
transcript = Invidious::Videos::Transcript.from_raw(
|
||||||
|
YoutubeAPI.get_transcript(params), lang, auto_generated
|
||||||
|
)
|
||||||
|
rescue ex : NotFoundException
|
||||||
|
return error_json(404, ex)
|
||||||
|
rescue ex
|
||||||
|
return error_json(500, ex)
|
||||||
|
end
|
||||||
|
|
||||||
|
return transcript.to_json
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,14 +20,6 @@ module Invidious::Routes::BeforeAll
|
||||||
env.response.headers["X-XSS-Protection"] = "1; mode=block"
|
env.response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||||
env.response.headers["X-Content-Type-Options"] = "nosniff"
|
env.response.headers["X-Content-Type-Options"] = "nosniff"
|
||||||
|
|
||||||
# Allow media resources to be loaded from google servers
|
|
||||||
# TODO: check if *.youtube.com can be removed
|
|
||||||
if CONFIG.disabled?("local") || !preferences.local
|
|
||||||
extra_media_csp = " https://*.googlevideo.com:443 https://*.youtube.com:443"
|
|
||||||
else
|
|
||||||
extra_media_csp = ""
|
|
||||||
end
|
|
||||||
|
|
||||||
# Only allow the pages at /embed/* to be embedded
|
# Only allow the pages at /embed/* to be embedded
|
||||||
if env.request.resource.starts_with?("/embed")
|
if env.request.resource.starts_with?("/embed")
|
||||||
frame_ancestors = "'self' file: http: https:"
|
frame_ancestors = "'self' file: http: https:"
|
||||||
|
@ -45,7 +37,7 @@ module Invidious::Routes::BeforeAll
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
"connect-src 'self'",
|
"connect-src 'self'",
|
||||||
"manifest-src 'self'",
|
"manifest-src 'self'",
|
||||||
"media-src 'self' blob:" + extra_media_csp,
|
"media-src 'self' blob:",
|
||||||
"child-src 'self' blob:",
|
"child-src 'self' blob:",
|
||||||
"frame-src 'self'",
|
"frame-src 'self'",
|
||||||
"frame-ancestors " + frame_ancestors,
|
"frame-ancestors " + frame_ancestors,
|
||||||
|
@ -110,6 +102,21 @@ module Invidious::Routes::BeforeAll
|
||||||
preferences.locale = locale
|
preferences.locale = locale
|
||||||
env.set "preferences", preferences
|
env.set "preferences", preferences
|
||||||
|
|
||||||
|
# Allow media resources to be loaded from google servers
|
||||||
|
# TODO: check if *.youtube.com can be removed
|
||||||
|
#
|
||||||
|
# `!preferences.local` has to be checked after setting and
|
||||||
|
# reading `preferences` from the "PREFS" cookie and
|
||||||
|
# saved user preferences from the database, otherwise
|
||||||
|
# `https://*.googlevideo.com:443 https://*.youtube.com:443`
|
||||||
|
# will not be set in the CSP header if
|
||||||
|
# `default_user_preferences.local` is set to true on the
|
||||||
|
# configuration file, causing preference “Proxy Videos”
|
||||||
|
# not to work while having it disabled and using medium quality.
|
||||||
|
if CONFIG.disabled?("local") || !preferences.local
|
||||||
|
env.response.headers["Content-Security-Policy"] = env.response.headers["Content-Security-Policy"].gsub("media-src", "media-src https://*.googlevideo.com:443 https://*.youtube.com:443")
|
||||||
|
end
|
||||||
|
|
||||||
current_page = env.request.path
|
current_page = env.request.path
|
||||||
if env.request.query
|
if env.request.query
|
||||||
query = HTTP::Params.parse(env.request.query.not_nil!)
|
query = HTTP::Params.parse(env.request.query.not_nil!)
|
||||||
|
|
|
@ -197,7 +197,29 @@ module Invidious::Routes::Channels
|
||||||
templated "channel"
|
templated "channel"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.courses(env)
|
||||||
|
data = self.fetch_basic_information(env)
|
||||||
|
return data if !data.is_a?(Tuple)
|
||||||
|
|
||||||
|
locale, user, subscriptions, continuation, ucid, channel = data
|
||||||
|
|
||||||
|
sort_by = ""
|
||||||
|
sort_options = [] of String
|
||||||
|
|
||||||
|
items, next_continuation = fetch_channel_courses(
|
||||||
|
channel.ucid, channel.author, continuation
|
||||||
|
)
|
||||||
|
|
||||||
|
items = items.select(SearchPlaylist)
|
||||||
|
items.each(&.author = "")
|
||||||
|
|
||||||
|
selected_tab = Frontend::ChannelPage::TabsAvailable::Courses
|
||||||
|
templated "channel"
|
||||||
|
end
|
||||||
|
|
||||||
def self.community(env)
|
def self.community(env)
|
||||||
|
return env.redirect env.request.path.sub("posts", "community") if env.request.path.split("/").last == "posts"
|
||||||
|
|
||||||
data = self.fetch_basic_information(env)
|
data = self.fetch_basic_information(env)
|
||||||
if !data.is_a?(Tuple)
|
if !data.is_a?(Tuple)
|
||||||
return data
|
return data
|
||||||
|
@ -214,7 +236,7 @@ module Invidious::Routes::Channels
|
||||||
|
|
||||||
continuation = env.params.query["continuation"]?
|
continuation = env.params.query["continuation"]?
|
||||||
|
|
||||||
if !channel.tabs.includes? "community"
|
if !channel.tabs.includes? "community" && "posts"
|
||||||
return env.redirect "/channel/#{channel.ucid}"
|
return env.redirect "/channel/#{channel.ucid}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -307,7 +329,8 @@ module Invidious::Routes::Channels
|
||||||
|
|
||||||
private KNOWN_TABS = {
|
private KNOWN_TABS = {
|
||||||
"home", "videos", "shorts", "streams", "podcasts",
|
"home", "videos", "shorts", "streams", "podcasts",
|
||||||
"releases", "playlists", "community", "channels", "about",
|
"releases", "courses", "playlists", "community", "channels", "about",
|
||||||
|
"posts",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Redirects brand url channels to a normal /channel/:ucid route
|
# Redirects brand url channels to a normal /channel/:ucid route
|
||||||
|
|
|
@ -12,13 +12,15 @@ module Invidious::Routes::Embed
|
||||||
url = "/playlist?list=#{plid}"
|
url = "/playlist?list=#{plid}"
|
||||||
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
|
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
first_playlist_video = videos[0].as(PlaylistVideo)
|
||||||
rescue ex : NotFoundException
|
rescue ex : NotFoundException
|
||||||
return error_template(404, ex)
|
return error_template(404, ex)
|
||||||
rescue ex
|
rescue ex
|
||||||
return error_template(500, ex)
|
return error_template(500, ex)
|
||||||
end
|
end
|
||||||
|
|
||||||
url = "/embed/#{videos[0].id}?#{env.params.query}"
|
url = "/embed/#{first_playlist_video}?#{env.params.query}"
|
||||||
|
|
||||||
if env.params.query.size > 0
|
if env.params.query.size > 0
|
||||||
url += "?#{env.params.query}"
|
url += "?#{env.params.query}"
|
||||||
|
@ -72,13 +74,15 @@ module Invidious::Routes::Embed
|
||||||
url = "/playlist?list=#{plid}"
|
url = "/playlist?list=#{plid}"
|
||||||
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
|
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
first_playlist_video = videos[0].as(PlaylistVideo)
|
||||||
rescue ex : NotFoundException
|
rescue ex : NotFoundException
|
||||||
return error_template(404, ex)
|
return error_template(404, ex)
|
||||||
rescue ex
|
rescue ex
|
||||||
return error_template(500, ex)
|
return error_template(500, ex)
|
||||||
end
|
end
|
||||||
|
|
||||||
url = "/embed/#{videos[0].id}"
|
url = "/embed/#{first_playlist_video.id}"
|
||||||
elsif video_series
|
elsif video_series
|
||||||
url = "/embed/#{video_series.shift}"
|
url = "/embed/#{video_series.shift}"
|
||||||
env.params.query["playlist"] = video_series.join(",")
|
env.params.query["playlist"] = video_series.join(",")
|
||||||
|
@ -157,10 +161,12 @@ module Invidious::Routes::Embed
|
||||||
adaptive_fmts = video.adaptive_fmts
|
adaptive_fmts = video.adaptive_fmts
|
||||||
|
|
||||||
if params.local
|
if params.local
|
||||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
||||||
|
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||||
|
|
||||||
video_streams = video.video_streams
|
video_streams = video.video_streams
|
||||||
audio_streams = video.audio_streams
|
audio_streams = video.audio_streams
|
||||||
|
|
||||||
|
@ -201,6 +207,14 @@ module Invidious::Routes::Embed
|
||||||
return env.redirect url
|
return env.redirect url
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if CONFIG.invidious_companion.present?
|
||||||
|
invidious_companion = CONFIG.invidious_companion.sample
|
||||||
|
env.response.headers["Content-Security-Policy"] =
|
||||||
|
env.response.headers["Content-Security-Policy"]
|
||||||
|
.gsub("media-src", "media-src #{invidious_companion.public_url}")
|
||||||
|
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
|
||||||
|
end
|
||||||
|
|
||||||
rendered "embed"
|
rendered "embed"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -143,32 +143,25 @@ module Invidious::Routes::Feeds
|
||||||
# RSS feeds
|
# RSS feeds
|
||||||
|
|
||||||
def self.rss_channel(env)
|
def self.rss_channel(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
|
||||||
|
|
||||||
env.response.headers["Content-Type"] = "application/atom+xml"
|
env.response.headers["Content-Type"] = "application/atom+xml"
|
||||||
env.response.content_type = "application/atom+xml"
|
env.response.content_type = "application/atom+xml"
|
||||||
|
|
||||||
ucid = env.params.url["ucid"]
|
if env.params.url["ucid"].matches?(/^[\w-]+$/)
|
||||||
|
ucid = env.params.url["ucid"]
|
||||||
|
else
|
||||||
|
return error_atom(400, InfoException.new("Invalid channel ucid provided."))
|
||||||
|
end
|
||||||
|
|
||||||
params = HTTP::Params.parse(env.params.query["params"]? || "")
|
params = HTTP::Params.parse(env.params.query["params"]? || "")
|
||||||
|
|
||||||
begin
|
|
||||||
channel = get_about_info(ucid, locale)
|
|
||||||
rescue ex : ChannelRedirect
|
|
||||||
return env.redirect env.request.resource.gsub(ucid, ex.channel_id)
|
|
||||||
rescue ex : NotFoundException
|
|
||||||
return error_atom(404, ex)
|
|
||||||
rescue ex
|
|
||||||
return error_atom(500, ex)
|
|
||||||
end
|
|
||||||
|
|
||||||
namespaces = {
|
namespaces = {
|
||||||
"yt" => "http://www.youtube.com/xml/schemas/2015",
|
"yt" => "http://www.youtube.com/xml/schemas/2015",
|
||||||
"media" => "http://search.yahoo.com/mrss/",
|
"media" => "http://search.yahoo.com/mrss/",
|
||||||
"default" => "http://www.w3.org/2005/Atom",
|
"default" => "http://www.w3.org/2005/Atom",
|
||||||
}
|
}
|
||||||
|
|
||||||
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{channel.ucid}")
|
response = YT_POOL.client &.get("/feeds/videos.xml?channel_id=#{ucid}")
|
||||||
|
return error_atom(404, NotFoundException.new("Channel does not exist.")) if response.status_code == 404
|
||||||
rss = XML.parse(response.body)
|
rss = XML.parse(response.body)
|
||||||
|
|
||||||
videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry|
|
videos = rss.xpath_nodes("//default:feed/default:entry", namespaces).map do |entry|
|
||||||
|
@ -179,7 +172,7 @@ module Invidious::Routes::Feeds
|
||||||
updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content)
|
updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content)
|
||||||
|
|
||||||
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
|
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
|
||||||
ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
|
video_ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
|
||||||
description_html = entry.xpath_node("media:group/media:description", namespaces).not_nil!.to_s
|
description_html = entry.xpath_node("media:group/media:description", namespaces).not_nil!.to_s
|
||||||
views = entry.xpath_node("media:group/media:community/media:statistics", namespaces).not_nil!.["views"].to_i64
|
views = entry.xpath_node("media:group/media:community/media:statistics", namespaces).not_nil!.["views"].to_i64
|
||||||
|
|
||||||
|
@ -187,41 +180,44 @@ module Invidious::Routes::Feeds
|
||||||
title: title,
|
title: title,
|
||||||
id: video_id,
|
id: video_id,
|
||||||
author: author,
|
author: author,
|
||||||
ucid: ucid,
|
ucid: video_ucid,
|
||||||
published: published,
|
published: published,
|
||||||
views: views,
|
views: views,
|
||||||
description_html: description_html,
|
description_html: description_html,
|
||||||
length_seconds: 0,
|
length_seconds: 0,
|
||||||
premiere_timestamp: nil,
|
premiere_timestamp: nil,
|
||||||
author_verified: false,
|
author_verified: false,
|
||||||
|
author_thumbnail: nil,
|
||||||
badges: VideoBadges::None,
|
badges: VideoBadges::None,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
author = ""
|
||||||
|
author = videos[0].author if videos.size > 0
|
||||||
|
|
||||||
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||||
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
|
xml.element("feed", "xmlns:yt": "http://www.youtube.com/xml/schemas/2015",
|
||||||
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
|
"xmlns:media": "http://search.yahoo.com/mrss/", xmlns: "http://www.w3.org/2005/Atom",
|
||||||
"xml:lang": "en-US") do
|
"xml:lang": "en-US") do
|
||||||
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
|
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
|
||||||
xml.element("id") { xml.text "yt:channel:#{channel.ucid}" }
|
xml.element("id") { xml.text "yt:channel:#{ucid}" }
|
||||||
xml.element("yt:channelId") { xml.text channel.ucid }
|
xml.element("yt:channelId") { xml.text ucid }
|
||||||
xml.element("icon") { xml.text channel.author_thumbnail }
|
xml.element("title") { xml.text author }
|
||||||
xml.element("title") { xml.text channel.author }
|
xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{ucid}")
|
||||||
xml.element("link", rel: "alternate", href: "#{HOST_URL}/channel/#{channel.ucid}")
|
|
||||||
|
|
||||||
xml.element("author") do
|
xml.element("author") do
|
||||||
xml.element("name") { xml.text channel.author }
|
xml.element("name") { xml.text author }
|
||||||
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{channel.ucid}" }
|
xml.element("uri") { xml.text "#{HOST_URL}/channel/#{ucid}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
xml.element("image") do
|
xml.element("image") do
|
||||||
xml.element("url") { xml.text channel.author_thumbnail }
|
xml.element("url") { xml.text "" }
|
||||||
xml.element("title") { xml.text channel.author }
|
xml.element("title") { xml.text author }
|
||||||
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
|
xml.element("link", rel: "self", href: "#{HOST_URL}#{env.request.resource}")
|
||||||
end
|
end
|
||||||
|
|
||||||
videos.each do |video|
|
videos.each do |video|
|
||||||
video.to_xml(channel.auto_generated, params, xml)
|
video.to_xml(false, params, xml)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -300,7 +296,13 @@ module Invidious::Routes::Feeds
|
||||||
xml.element("name") { xml.text playlist.author }
|
xml.element("name") { xml.text playlist.author }
|
||||||
end
|
end
|
||||||
|
|
||||||
videos.each &.to_xml(xml)
|
videos.each do |video|
|
||||||
|
if video.is_a? PlaylistVideo
|
||||||
|
video.to_xml(xml)
|
||||||
|
else
|
||||||
|
video.to_xml(env, locale, xml)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
@ -309,8 +311,9 @@ module Invidious::Routes::Feeds
|
||||||
end
|
end
|
||||||
|
|
||||||
response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
|
response = YT_POOL.client &.get("/feeds/videos.xml?playlist_id=#{plid}")
|
||||||
document = XML.parse(response.body)
|
return error_atom(404, NotFoundException.new("Playlist does not exist.")) if response.status_code == 404
|
||||||
|
|
||||||
|
document = XML.parse(response.body)
|
||||||
document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|
|
document.xpath_nodes(%q(//*[@href]|//*[@url])).each do |node|
|
||||||
node.attributes.each do |attribute|
|
node.attributes.each do |attribute|
|
||||||
case attribute.name
|
case attribute.name
|
||||||
|
@ -423,16 +426,6 @@ module Invidious::Routes::Feeds
|
||||||
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
|
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.enable_user_notifications
|
|
||||||
# Deliver notifications to `/api/v1/auth/notifications`
|
|
||||||
payload = {
|
|
||||||
"topic" => video.ucid,
|
|
||||||
"videoId" => video.id,
|
|
||||||
"published" => published.to_unix,
|
|
||||||
}.to_json
|
|
||||||
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
|
|
||||||
end
|
|
||||||
|
|
||||||
video = ChannelVideo.new({
|
video = ChannelVideo.new({
|
||||||
id: id,
|
id: id,
|
||||||
title: video.title,
|
title: video.title,
|
||||||
|
@ -448,11 +441,7 @@ module Invidious::Routes::Feeds
|
||||||
|
|
||||||
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
|
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
|
||||||
if was_insert
|
if was_insert
|
||||||
if CONFIG.enable_user_notifications
|
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
|
||||||
Invidious::Database::Users.add_notification(video)
|
|
||||||
else
|
|
||||||
Invidious::Database::Users.feed_needs_update(video)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -111,7 +111,7 @@ module Invidious::Routes::Images
|
||||||
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"
|
||||||
if get_ytimg_pool("i9").client &.head(thumbnail_resource_path, headers).status_code == 200
|
if get_ytimg_pool("i").client &.head(thumbnail_resource_path, headers).status_code == 200
|
||||||
name = thumb[:url] + ".jpg"
|
name = thumb[:url] + ".jpg"
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,9 +21,6 @@ module Invidious::Routes::Login
|
||||||
account_type = env.params.query["type"]?
|
account_type = env.params.query["type"]?
|
||||||
account_type ||= "invidious"
|
account_type ||= "invidious"
|
||||||
|
|
||||||
captcha_type = env.params.query["captcha"]?
|
|
||||||
captcha_type ||= "image"
|
|
||||||
|
|
||||||
templated "user/login"
|
templated "user/login"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -88,34 +85,14 @@ module Invidious::Routes::Login
|
||||||
password = password.byte_slice(0, 55)
|
password = password.byte_slice(0, 55)
|
||||||
|
|
||||||
if CONFIG.captcha_enabled
|
if CONFIG.captcha_enabled
|
||||||
captcha_type = env.params.body["captcha_type"]?
|
|
||||||
answer = env.params.body["answer"]?
|
answer = env.params.body["answer"]?
|
||||||
change_type = env.params.body["change_type"]?
|
|
||||||
|
|
||||||
if !captcha_type || change_type
|
account_type = "invidious"
|
||||||
if change_type
|
captcha = Invidious::User::Captcha.generate_image(HMAC_KEY)
|
||||||
captcha_type = change_type
|
|
||||||
end
|
|
||||||
captcha_type ||= "image"
|
|
||||||
|
|
||||||
account_type = "invidious"
|
|
||||||
|
|
||||||
if captcha_type == "image"
|
|
||||||
captcha = Invidious::User::Captcha.generate_image(HMAC_KEY)
|
|
||||||
else
|
|
||||||
captcha = Invidious::User::Captcha.generate_text(HMAC_KEY)
|
|
||||||
end
|
|
||||||
|
|
||||||
return templated "user/login"
|
|
||||||
end
|
|
||||||
|
|
||||||
tokens = env.params.body.select { |k, _| k.match(/^token\[\d+\]$/) }.map { |_, v| v }
|
tokens = env.params.body.select { |k, _| k.match(/^token\[\d+\]$/) }.map { |_, v| v }
|
||||||
|
|
||||||
answer ||= ""
|
if answer
|
||||||
captcha_type ||= "image"
|
|
||||||
|
|
||||||
case captcha_type
|
|
||||||
when "image"
|
|
||||||
answer = answer.lstrip('0')
|
answer = answer.lstrip('0')
|
||||||
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
answer = OpenSSL::HMAC.hexdigest(:sha256, HMAC_KEY, answer)
|
||||||
|
|
||||||
|
@ -124,27 +101,8 @@ module Invidious::Routes::Login
|
||||||
rescue ex
|
rescue ex
|
||||||
return error_template(400, ex)
|
return error_template(400, ex)
|
||||||
end
|
end
|
||||||
else # "text"
|
else
|
||||||
answer = Digest::MD5.hexdigest(answer.downcase.strip)
|
return templated "user/login"
|
||||||
|
|
||||||
if tokens.empty?
|
|
||||||
return error_template(500, "Erroneous CAPTCHA")
|
|
||||||
end
|
|
||||||
|
|
||||||
found_valid_captcha = false
|
|
||||||
error_exception = Exception.new
|
|
||||||
tokens.each do |tok|
|
|
||||||
begin
|
|
||||||
validate_request(tok, answer, env.request, HMAC_KEY, locale)
|
|
||||||
found_valid_captcha = true
|
|
||||||
rescue ex
|
|
||||||
error_exception = ex
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if !found_valid_captcha
|
|
||||||
return error_template(500, error_exception)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -42,12 +42,17 @@ module Invidious::Routes::Misc
|
||||||
referer = get_referer(env)
|
referer = get_referer(env)
|
||||||
|
|
||||||
instance_list = Invidious::Jobs::InstanceListRefreshJob::INSTANCES["INSTANCES"]
|
instance_list = Invidious::Jobs::InstanceListRefreshJob::INSTANCES["INSTANCES"]
|
||||||
if instance_list.empty?
|
# Filter out the current instance
|
||||||
|
other_available_instances = instance_list.reject { |_, domain| domain == CONFIG.domain }
|
||||||
|
|
||||||
|
if other_available_instances.empty?
|
||||||
|
# If the current instance is the only one, use the redirect URL as fallback
|
||||||
instance_url = "redirect.invidious.io"
|
instance_url = "redirect.invidious.io"
|
||||||
else
|
else
|
||||||
|
# Select other random instance
|
||||||
# Sample returns an array
|
# Sample returns an array
|
||||||
# Instances are packaged as {region, domain} in the instance list
|
# Instances are packaged as {region, domain} in the instance list
|
||||||
instance_url = instance_list.sample(1)[0][1]
|
instance_url = other_available_instances.sample(1)[0][1]
|
||||||
end
|
end
|
||||||
|
|
||||||
env.redirect "https://#{instance_url}#{referer}"
|
env.redirect "https://#{instance_url}#{referer}"
|
||||||
|
|
|
@ -304,23 +304,6 @@ module Invidious::Routes::Playlists
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if env.params.query["action_create_playlist"]?
|
|
||||||
action = "action_create_playlist"
|
|
||||||
elsif env.params.query["action_delete_playlist"]?
|
|
||||||
action = "action_delete_playlist"
|
|
||||||
elsif env.params.query["action_edit_playlist"]?
|
|
||||||
action = "action_edit_playlist"
|
|
||||||
elsif env.params.query["action_add_video"]?
|
|
||||||
action = "action_add_video"
|
|
||||||
video_id = env.params.query["video_id"]
|
|
||||||
elsif env.params.query["action_remove_video"]?
|
|
||||||
action = "action_remove_video"
|
|
||||||
elsif env.params.query["action_move_video_before"]?
|
|
||||||
action = "action_move_video_before"
|
|
||||||
else
|
|
||||||
return env.redirect referer
|
|
||||||
end
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
playlist_id = env.params.query["playlist_id"]
|
playlist_id = env.params.query["playlist_id"]
|
||||||
playlist = get_playlist(playlist_id).as(InvidiousPlaylist)
|
playlist = get_playlist(playlist_id).as(InvidiousPlaylist)
|
||||||
|
@ -335,12 +318,8 @@ module Invidious::Routes::Playlists
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
email = user.email
|
case action = env.params.query["action"]?
|
||||||
|
when "add_video"
|
||||||
case action
|
|
||||||
when "action_edit_playlist"
|
|
||||||
# TODO: Playlist stub
|
|
||||||
when "action_add_video"
|
|
||||||
if playlist.index.size >= CONFIG.playlist_length_limit
|
if playlist.index.size >= CONFIG.playlist_length_limit
|
||||||
if redirect
|
if redirect
|
||||||
return error_template(400, "Playlist cannot have more than #{CONFIG.playlist_length_limit} videos")
|
return error_template(400, "Playlist cannot have more than #{CONFIG.playlist_length_limit} videos")
|
||||||
|
@ -377,12 +356,14 @@ module Invidious::Routes::Playlists
|
||||||
|
|
||||||
Invidious::Database::PlaylistVideos.insert(playlist_video)
|
Invidious::Database::PlaylistVideos.insert(playlist_video)
|
||||||
Invidious::Database::Playlists.update_video_added(playlist_id, playlist_video.index)
|
Invidious::Database::Playlists.update_video_added(playlist_id, playlist_video.index)
|
||||||
when "action_remove_video"
|
when "remove_video"
|
||||||
index = env.params.query["set_video_id"]
|
index = env.params.query["set_video_id"]
|
||||||
Invidious::Database::PlaylistVideos.delete(index)
|
Invidious::Database::PlaylistVideos.delete(index)
|
||||||
Invidious::Database::Playlists.update_video_removed(playlist_id, index)
|
Invidious::Database::Playlists.update_video_removed(playlist_id, index)
|
||||||
when "action_move_video_before"
|
when "move_video_before"
|
||||||
# TODO: Playlist stub
|
# TODO: Playlist stub
|
||||||
|
when nil
|
||||||
|
return error_json(400, "Missing action")
|
||||||
else
|
else
|
||||||
return error_json(400, "Unsupported action #{action}")
|
return error_json(400, "Unsupported action #{action}")
|
||||||
end
|
end
|
||||||
|
|
|
@ -58,7 +58,11 @@ module Invidious::Routes::Search
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
items = query.process
|
if user
|
||||||
|
items = query.process(user.as(User))
|
||||||
|
else
|
||||||
|
items = query.process
|
||||||
|
end
|
||||||
rescue ex : ChannelSearchException
|
rescue ex : ChannelSearchException
|
||||||
return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.")
|
return error_template(404, "Unable to find channel with id of '#{HTML.escape(ex.channel)}'. Are you sure that's an actual channel id? It should look like 'UC4QobU6STFB0P71PMvOGN5A'.")
|
||||||
rescue ex
|
rescue ex
|
||||||
|
|
|
@ -32,24 +32,16 @@ module Invidious::Routes::Subscriptions
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if env.params.query["action_create_subscription_to_channel"]?.try &.to_i?.try &.== 1
|
|
||||||
action = "action_create_subscription_to_channel"
|
|
||||||
elsif env.params.query["action_remove_subscriptions"]?.try &.to_i?.try &.== 1
|
|
||||||
action = "action_remove_subscriptions"
|
|
||||||
else
|
|
||||||
return env.redirect referer
|
|
||||||
end
|
|
||||||
|
|
||||||
channel_id = env.params.query["c"]?
|
channel_id = env.params.query["c"]?
|
||||||
channel_id ||= ""
|
channel_id ||= ""
|
||||||
|
|
||||||
case action
|
case action = env.params.query["action"]?
|
||||||
when "action_create_subscription_to_channel"
|
when "create_subscription_to_channel"
|
||||||
if !user.subscriptions.includes? channel_id
|
if !user.subscriptions.includes? channel_id
|
||||||
get_channel(channel_id)
|
get_channel(channel_id)
|
||||||
Invidious::Database::Users.subscribe_channel(user, channel_id)
|
Invidious::Database::Users.subscribe_channel(user, channel_id)
|
||||||
end
|
end
|
||||||
when "action_remove_subscriptions"
|
when "remove_subscriptions"
|
||||||
Invidious::Database::Users.unsubscribe_channel(user, channel_id)
|
Invidious::Database::Users.unsubscribe_channel(user, channel_id)
|
||||||
else
|
else
|
||||||
return error_json(400, "Unsupported action #{action}")
|
return error_json(400, "Unsupported action #{action}")
|
||||||
|
|
|
@ -21,7 +21,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sanity check, to avoid being used as an open proxy
|
# Sanity check, to avoid being used as an open proxy
|
||||||
if !host.matches?(/[\w-]+.googlevideo.com/)
|
if !host.matches?(/[\w-]+\.(?:googlevideo|c\.youtube)\.com/)
|
||||||
return error_template(400, "Invalid \"host\" parameter.")
|
return error_template(400, "Invalid \"host\" parameter.")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,7 +37,8 @@ module Invidious::Routes::VideoPlayback
|
||||||
|
|
||||||
# See: https://github.com/iv-org/invidious/issues/3302
|
# See: https://github.com/iv-org/invidious/issues/3302
|
||||||
range_header = env.request.headers["Range"]?
|
range_header = env.request.headers["Range"]?
|
||||||
if range_header.nil?
|
sq = query_params["sq"]?
|
||||||
|
if range_header.nil? && sq.nil?
|
||||||
range_for_head = query_params["range"]? || "0-640"
|
range_for_head = query_params["range"]? || "0-640"
|
||||||
headers["Range"] = "bytes=#{range_for_head}"
|
headers["Range"] = "bytes=#{range_for_head}"
|
||||||
end
|
end
|
||||||
|
@ -164,10 +165,13 @@ module Invidious::Routes::VideoPlayback
|
||||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
|
||||||
if location = resp.headers["Location"]?
|
if location = resp.headers["Location"]?
|
||||||
location = URI.parse(location)
|
url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region)
|
||||||
location = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
|
||||||
|
|
||||||
env.redirect location
|
if title = query_params["title"]?
|
||||||
|
url = "#{url}&title=#{URI.encode_www_form(title)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
env.redirect url
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -253,6 +257,11 @@ module Invidious::Routes::VideoPlayback
|
||||||
# YouTube /videoplayback links expire after 6 hours,
|
# YouTube /videoplayback links expire after 6 hours,
|
||||||
# so we have a mechanism here to redirect to the latest version
|
# so we have a mechanism here to redirect to the latest version
|
||||||
def self.latest_version(env)
|
def self.latest_version(env)
|
||||||
|
if CONFIG.invidious_companion.present?
|
||||||
|
invidious_companion = CONFIG.invidious_companion.sample
|
||||||
|
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
|
||||||
|
end
|
||||||
|
|
||||||
id = env.params.query["id"]?
|
id = env.params.query["id"]?
|
||||||
itag = env.params.query["itag"]?.try &.to_i?
|
itag = env.params.query["itag"]?.try &.to_i?
|
||||||
|
|
||||||
|
|
|
@ -121,10 +121,12 @@ module Invidious::Routes::Watch
|
||||||
adaptive_fmts = video.adaptive_fmts
|
adaptive_fmts = video.adaptive_fmts
|
||||||
|
|
||||||
if params.local
|
if params.local
|
||||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
||||||
|
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||||
|
|
||||||
video_streams = video.video_streams
|
video_streams = video.video_streams
|
||||||
audio_streams = video.audio_streams
|
audio_streams = video.audio_streams
|
||||||
|
|
||||||
|
@ -190,6 +192,14 @@ module Invidious::Routes::Watch
|
||||||
captions: video.captions
|
captions: video.captions
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if CONFIG.invidious_companion.present?
|
||||||
|
invidious_companion = CONFIG.invidious_companion.sample
|
||||||
|
env.response.headers["Content-Security-Policy"] =
|
||||||
|
env.response.headers["Content-Security-Policy"]
|
||||||
|
.gsub("media-src", "media-src #{invidious_companion.public_url}")
|
||||||
|
.gsub("connect-src", "connect-src #{invidious_companion.public_url}")
|
||||||
|
end
|
||||||
|
|
||||||
templated "watch"
|
templated "watch"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -241,18 +251,10 @@ module Invidious::Routes::Watch
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if env.params.query["action_mark_watched"]?
|
case action = env.params.query["action"]?
|
||||||
action = "action_mark_watched"
|
when "mark_watched"
|
||||||
elsif env.params.query["action_mark_unwatched"]?
|
|
||||||
action = "action_mark_unwatched"
|
|
||||||
else
|
|
||||||
return env.redirect referer
|
|
||||||
end
|
|
||||||
|
|
||||||
case action
|
|
||||||
when "action_mark_watched"
|
|
||||||
Invidious::Database::Users.mark_watched(user, id)
|
Invidious::Database::Users.mark_watched(user, id)
|
||||||
when "action_mark_unwatched"
|
when "mark_unwatched"
|
||||||
Invidious::Database::Users.mark_unwatched(user, id)
|
Invidious::Database::Users.mark_unwatched(user, id)
|
||||||
else
|
else
|
||||||
return error_json(400, "Unsupported action #{action}")
|
return error_json(400, "Unsupported action #{action}")
|
||||||
|
@ -291,6 +293,9 @@ module Invidious::Routes::Watch
|
||||||
if CONFIG.disabled?("downloads")
|
if CONFIG.disabled?("downloads")
|
||||||
return error_template(403, "Administrator has disabled this endpoint.")
|
return error_template(403, "Administrator has disabled this endpoint.")
|
||||||
end
|
end
|
||||||
|
if CONFIG.invidious_companion.present?
|
||||||
|
return error_template(403, "Downloads should be routed through Companion when present")
|
||||||
|
end
|
||||||
|
|
||||||
title = env.params.body["title"]? || ""
|
title = env.params.body["title"]? || ""
|
||||||
video_id = env.params.body["id"]? || ""
|
video_id = env.params.body["id"]? || ""
|
||||||
|
@ -320,10 +325,9 @@ module Invidious::Routes::Watch
|
||||||
env.params.query["label"] = URI.decode_www_form(label.as_s)
|
env.params.query["label"] = URI.decode_www_form(label.as_s)
|
||||||
|
|
||||||
return Invidious::Routes::API::V1::Videos.captions(env)
|
return Invidious::Routes::API::V1::Videos.captions(env)
|
||||||
elsif itag = download_widget["itag"]?.try &.as_i
|
elsif itag = download_widget["itag"]?.try &.as_i.to_s
|
||||||
# URL params specific to /latest_version
|
# URL params specific to /latest_version
|
||||||
env.params.query["id"] = video_id
|
env.params.query["id"] = video_id
|
||||||
env.params.query["itag"] = itag.to_s
|
|
||||||
env.params.query["title"] = filename
|
env.params.query["title"] = filename
|
||||||
env.params.query["local"] = "true"
|
env.params.query["local"] = "true"
|
||||||
|
|
||||||
|
|
|
@ -120,8 +120,10 @@ module Invidious::Routing
|
||||||
get "/channel/:ucid/streams", Routes::Channels, :streams
|
get "/channel/:ucid/streams", Routes::Channels, :streams
|
||||||
get "/channel/:ucid/podcasts", Routes::Channels, :podcasts
|
get "/channel/:ucid/podcasts", Routes::Channels, :podcasts
|
||||||
get "/channel/:ucid/releases", Routes::Channels, :releases
|
get "/channel/:ucid/releases", Routes::Channels, :releases
|
||||||
|
get "/channel/:ucid/courses", Routes::Channels, :courses
|
||||||
get "/channel/:ucid/playlists", Routes::Channels, :playlists
|
get "/channel/:ucid/playlists", Routes::Channels, :playlists
|
||||||
get "/channel/:ucid/community", Routes::Channels, :community
|
get "/channel/:ucid/community", Routes::Channels, :community
|
||||||
|
get "/channel/:ucid/posts", 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
|
||||||
|
|
||||||
|
@ -236,6 +238,7 @@ module Invidious::Routing
|
||||||
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
|
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
|
||||||
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments
|
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments
|
||||||
get "/api/v1/clips/:id", {{namespace}}::Videos, :clips
|
get "/api/v1/clips/:id", {{namespace}}::Videos, :clips
|
||||||
|
get "/api/v1/transcripts/:id", {{namespace}}::Videos, :transcripts
|
||||||
|
|
||||||
# Feeds
|
# Feeds
|
||||||
get "/api/v1/trending", {{namespace}}::Feeds, :trending
|
get "/api/v1/trending", {{namespace}}::Feeds, :trending
|
||||||
|
@ -249,8 +252,10 @@ module Invidious::Routing
|
||||||
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
|
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
|
||||||
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
|
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
|
||||||
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
|
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
|
||||||
|
get "/api/v1/channels/:ucid/courses", {{namespace}}::Channels, :courses
|
||||||
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists
|
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists
|
||||||
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community
|
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community
|
||||||
|
get "/api/v1/channels/:ucid/posts", {{namespace}}::Channels, :community
|
||||||
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels
|
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels
|
||||||
get "/api/v1/channels/:ucid/search", {{namespace}}::Channels, :search
|
get "/api/v1/channels/:ucid/search", {{namespace}}::Channels, :search
|
||||||
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue