mirror of
https://github.com/iv-org/invidious.git
synced 2025-04-23 08:49:24 -04:00
Compare commits
No commits in common. "master" and "v2.20240427" have entirely different histories.
master
...
v2.2024042
78
.ameba.yml
78
.ameba.yml
@ -20,13 +20,6 @@ Lint/ShadowingOuterLocalVar:
|
||||
Excluded:
|
||||
- src/invidious/helpers/tokens.cr
|
||||
|
||||
Lint/NotNil:
|
||||
Enabled: false
|
||||
|
||||
Lint/SpecFilename:
|
||||
Excluded:
|
||||
- spec/parsers_helper.cr
|
||||
|
||||
|
||||
#
|
||||
# Style
|
||||
@ -38,29 +31,6 @@ Style/RedundantBegin:
|
||||
Style/RedundantReturn:
|
||||
Enabled: false
|
||||
|
||||
Style/RedundantNext:
|
||||
Enabled: false
|
||||
|
||||
Style/ParenthesesAroundCondition:
|
||||
Enabled: false
|
||||
|
||||
# This requires a rewrite of most data structs (and their usage) in Invidious.
|
||||
Naming/QueryBoolMethods:
|
||||
Enabled: false
|
||||
|
||||
Naming/AccessorMethodName:
|
||||
Enabled: false
|
||||
|
||||
Naming/BlockParameterName:
|
||||
Enabled: false
|
||||
|
||||
# Hides TODO comment warnings.
|
||||
#
|
||||
# Call `bin/ameba --only Documentation/DocumentationAdmonition` to
|
||||
# list them
|
||||
Documentation/DocumentationAdmonition:
|
||||
Enabled: false
|
||||
|
||||
|
||||
#
|
||||
# Metrics
|
||||
@ -69,4 +39,50 @@ Documentation/DocumentationAdmonition:
|
||||
# Ignore function complexity (number of if/else & case/when branches)
|
||||
# For some functions that can hardly be simplified for now
|
||||
Metrics/CyclomaticComplexity:
|
||||
Enabled: false
|
||||
Excluded:
|
||||
# get_about_info(ucid, locale) => [17/10]
|
||||
- src/invidious/channels/about.cr
|
||||
|
||||
# fetch_channel_community(ucid, continuation, ...) => [34/10]
|
||||
- src/invidious/channels/community.cr
|
||||
|
||||
# create_notification_stream(env, topics, connection_channel) => [14/10]
|
||||
- src/invidious/helpers/helpers.cr:84:5
|
||||
|
||||
# get_index(plural_form, count) => [25/10]
|
||||
- src/invidious/helpers/i18next.cr
|
||||
|
||||
# call(context) => [18/10]
|
||||
- src/invidious/helpers/static_file_handler.cr
|
||||
|
||||
# show(env) => [38/10]
|
||||
- src/invidious/routes/embed.cr
|
||||
|
||||
# get_video_playback(env) => [45/10]
|
||||
- src/invidious/routes/video_playback.cr
|
||||
|
||||
# handle(env) => [40/10]
|
||||
- src/invidious/routes/watch.cr
|
||||
|
||||
# playlist_ajax(env) => [24/10]
|
||||
- src/invidious/routes/playlists.cr
|
||||
|
||||
# fetch_youtube_comments(id, cursor, ....) => [40/10]
|
||||
# template_youtube_comments(comments, locale, ...) => [16/10]
|
||||
# content_to_comment_html(content) => [14/10]
|
||||
- src/invidious/comments.cr
|
||||
|
||||
# to_json(locale, json) => [21/10]
|
||||
# extract_video_info(video_id, ...) => [44/10]
|
||||
# process_video_params(query, preferences) => [20/10]
|
||||
- src/invidious/videos.cr
|
||||
|
||||
|
||||
|
||||
#src/invidious/playlists.cr:327:5
|
||||
#[C] Metrics/CyclomaticComplexity: Cyclomatic complexity too high [19/10]
|
||||
# fetch_playlist(plid : String)
|
||||
|
||||
#src/invidious/playlists.cr:436:5
|
||||
#[C] Metrics/CyclomaticComplexity: Cyclomatic complexity too high [11/10]
|
||||
# extract_playlist_videos(initial_data : Hash(String, JSON::Any))
|
||||
|
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -6,7 +6,7 @@ docker/ @unixfox
|
||||
kubernetes/ @unixfox
|
||||
|
||||
README.md @thefrenchghosty
|
||||
config/config.example.yml @SamantazFox @unixfox
|
||||
config/config.example.yml @thefrenchghosty @SamantazFox @unixfox
|
||||
|
||||
scripts/ @syeopite
|
||||
shards.lock @syeopite
|
||||
|
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -10,10 +10,8 @@ assignees: ''
|
||||
<!--
|
||||
BEFORE TRYING TO REPORT A BUG:
|
||||
|
||||
* Read the FAQ: https://docs.invidious.io/faq/!
|
||||
* 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.
|
||||
* Read the FAQ!
|
||||
* Use the search function to check if there is already an issue open for your problem!
|
||||
|
||||
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
|
||||
|
81
.github/workflows/build-stable-container.yml
vendored
81
.github/workflows/build-stable-container.yml
vendored
@ -1,81 +0,0 @@
|
||||
name: Build and release container
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_PASSWORD }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: quay.io/invidious/invidious
|
||||
flavor: |
|
||||
latest=false
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
quay.expires-after=12w
|
||||
|
||||
- name: Build and push Docker AMD64 image for Push Event
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
platforms: linux/amd64
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
"release=1"
|
||||
|
||||
- name: Docker meta
|
||||
id: meta-arm64
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: quay.io/invidious/invidious
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-arm64
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=raw,value=latest
|
||||
labels: |
|
||||
quay.expires-after=12w
|
||||
|
||||
- name: Build and push Docker ARM64 image for Push Event
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile.arm64
|
||||
platforms: linux/arm64/v8
|
||||
labels: ${{ steps.meta-arm64.outputs.labels }}
|
||||
push: true
|
||||
tags: ${{ steps.meta-arm64.outputs.tags }}
|
||||
build-args: |
|
||||
"release=1"
|
69
.github/workflows/ci.yml
vendored
69
.github/workflows/ci.yml
vendored
@ -38,10 +38,10 @@ jobs:
|
||||
matrix:
|
||||
stable: [true]
|
||||
crystal:
|
||||
- 1.12.1
|
||||
- 1.13.2
|
||||
- 1.14.0
|
||||
- 1.15.0
|
||||
- 1.7.3
|
||||
- 1.8.2
|
||||
- 1.9.2
|
||||
- 1.10.1
|
||||
include:
|
||||
- crystal: nightly
|
||||
stable: false
|
||||
@ -51,11 +51,6 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install required APT packages
|
||||
run: |
|
||||
sudo apt install -y libsqlite3-dev
|
||||
shell: bash
|
||||
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1.8.0
|
||||
with:
|
||||
@ -64,9 +59,7 @@ jobs:
|
||||
- name: Cache Shards
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./lib
|
||||
./bin
|
||||
path: ./lib
|
||||
key: shards-${{ hashFiles('shard.lock') }}
|
||||
|
||||
- name: Install Shards
|
||||
@ -78,6 +71,14 @@ jobs:
|
||||
- name: Run tests
|
||||
run: crystal spec
|
||||
|
||||
- name: Run lint
|
||||
run: |
|
||||
if ! crystal tool format --check; then
|
||||
crystal tool format
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
|
||||
|
||||
@ -89,10 +90,10 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Build Docker
|
||||
run: docker compose build --build-arg release=0
|
||||
run: docker-compose build --build-arg release=0
|
||||
|
||||
- name: Run Docker
|
||||
run: docker compose up -d
|
||||
run: docker-compose up -d
|
||||
|
||||
- name: Test Docker
|
||||
run: while curl -Isf http://localhost:3000; do sleep 1; done
|
||||
@ -123,44 +124,4 @@ jobs:
|
||||
- name: Test Docker
|
||||
run: while curl -Isf http://localhost:3000; do sleep 1; done
|
||||
|
||||
lint:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Crystal
|
||||
id: lint_step_install_crystal
|
||||
uses: crystal-lang/install-crystal@v1.8.0
|
||||
with:
|
||||
crystal: latest
|
||||
|
||||
- name: Cache Shards
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
./lib
|
||||
./bin
|
||||
key: shards-${{ hashFiles('shard.lock') }}-${{ steps.lint_step_install_crystal.outputs.crystal }}
|
||||
|
||||
- name: Install Shards
|
||||
run: |
|
||||
if ! shards check; then
|
||||
shards install
|
||||
fi
|
||||
|
||||
- name: Check Crystal formatter compliance
|
||||
run: |
|
||||
if ! crystal tool format --check; then
|
||||
crystal tool format
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run Ameba linter
|
||||
run: bin/ameba
|
||||
|
@ -1,4 +1,4 @@
|
||||
name: Build and release container directly from master
|
||||
name: Build and release container
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -23,6 +23,19 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1.8.0
|
||||
with:
|
||||
crystal: 1.9.2
|
||||
|
||||
- name: Run lint
|
||||
run: |
|
||||
if ! crystal tool format --check; then
|
||||
crystal tool format
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
@ -45,7 +58,7 @@ jobs:
|
||||
images: quay.io/invidious/invidious
|
||||
tags: |
|
||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
labels: |
|
||||
quay.expires-after=12w
|
||||
|
||||
@ -70,7 +83,7 @@ jobs:
|
||||
suffix=-arm64
|
||||
tags: |
|
||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
labels: |
|
||||
quay.expires-after=12w
|
||||
|
13
.github/workflows/stale.yml
vendored
13
.github/workflows/stale.yml
vendored
@ -13,11 +13,14 @@ jobs:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 730
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 60
|
||||
days-before-stale: 365
|
||||
days-before-pr-stale: 90
|
||||
days-before-close: 30
|
||||
exempt-pr-labels: blocked,exempt-stale
|
||||
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
||||
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
||||
stale-issue-label: "stale"
|
||||
stale-pr-label: "stale"
|
||||
ascending: true
|
||||
# Exempt the following types of issues from being staled
|
||||
exempt-issue-labels: "feature-request,enhancement,discussion,exempt-stale"
|
||||
# Never mark feature requests/enhancements as stale
|
||||
exempt-issue-labels: "feature-request,enhancement,exempt-stale"
|
||||
|
424
CHANGELOG.md
424
CHANGELOG.md
@ -1,428 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
## vX.Y.0 (future)
|
||||
|
||||
## 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
|
||||
|
||||
### Wrap-up
|
||||
|
||||
This release is most importantly here to fix to the annoying "Youtube API returned error 400"
|
||||
error that prevented all channel pages from loading.
|
||||
|
||||
If you're updating from the previous release, it provides no improvements on the ability to play
|
||||
videos. If updating from a commit in-between release, it removes the "Please sign in" error caused
|
||||
by a previous attempt at restoring video playback on large instances.
|
||||
|
||||
In the preferences, a new option allows for control of video preload. When enabled, this option
|
||||
tells the browser to load the video as soon as the page is loaded (this used to be the default).
|
||||
When disabled, the video starts loading only when the "play" button is pressed.
|
||||
|
||||
New interface languages available: Bulgarian, Welsh and Lombard
|
||||
|
||||
New dependency required: `tzdata`.
|
||||
|
||||
An HTTP proxy can be configured directly in Invidious, if needed. \
|
||||
**NOTE:** In that case, it is recommended to comment out `force_resolve`.
|
||||
|
||||
|
||||
### New features & important changes
|
||||
|
||||
#### For users
|
||||
|
||||
* Channels: Fix "Youtube API returned error 400" error preventing channel pages from loading
|
||||
* Channels: Shorts can now be sorted by "newest", "oldest" and "popular"
|
||||
* Preferences: Addition of the new "preload" option
|
||||
* New interface languages available: Bulgarian, Welsh and Lombard
|
||||
* Added "Filipino (auto-generated)" to the list of caption languages available
|
||||
* Lots of new translations from Weblate
|
||||
|
||||
#### For instance owners
|
||||
|
||||
* Allow the configuration of an HTTP proxy to talk to Youtube
|
||||
* Invidious tries to reconnect to `inv_sig_helper` if the socket is closed
|
||||
* The instance list is downloaded in the background to improve redirection speed
|
||||
* New `colorize_logs` option makes each log level a different color
|
||||
|
||||
#### For developpers
|
||||
|
||||
* `/api/v1/channels/{id}/shorts` now supports the `sort-by` parameter with the following values:
|
||||
`newest`, `oldest` and `popular`
|
||||
* Older `/api/v1/channels/xyz/{id}` (tab name before UCID) were removed
|
||||
* API/Search: New video metadata available: `isNew`, `is4k`, `is8k`, `isVr180`, `isVr360`,
|
||||
`is3d` and `hasCaptions`
|
||||
|
||||
### Bugs fixed
|
||||
|
||||
#### User-side
|
||||
|
||||
* Channels: The second page of shorts now loads as expected
|
||||
* Channels: Fixed intermittent empty "playlists" tab
|
||||
* Search: Fixed `youtu.be` URLs not being properly redirected to the watch page
|
||||
* Fixed `DB::MappingException` error on the subscriptions feed (due to missing `tzdata` in docker)
|
||||
* Switching to another instance is much faster
|
||||
* Fixed an "invalid byte sequence" error when subscribing to a playlist
|
||||
* Videos: Playback URLs were sometimes broken when cached and `inv_sig_helper` was used
|
||||
|
||||
#### For instance owners
|
||||
|
||||
* Fix `force_resolve` being ignored in some cases
|
||||
|
||||
#### API
|
||||
|
||||
* API/Videos: Fixed `live_now` and `premiere_timestamp` sometimes not having the right values
|
||||
|
||||
|
||||
### Full list of pull requests merged since the last release (newest first)
|
||||
|
||||
* API: Add "sort_by" parameter to channels/shorts endpoint ([#5071], thanks @iBicha)
|
||||
* Docker: Install tzdata in Dockerfile ([#5070], by @SamantazFox)
|
||||
* Videos: Stop using TVHTML5_SIMPLY_EMBEDDED_PLAYER ([#5063], thanks @unixfox)
|
||||
* Routing: Deprecate old channel API routes ([#5045], by @SamantazFox)
|
||||
* Videos: use WEB client instead of WEB CREATOR ([#4984], thanks @unixfox)
|
||||
* Parsers: Fix parsing live_now and premiere_timestamp ([#4934], thanks @absidue)
|
||||
* Stale bot updates ([#5060], thanks @syeopite)
|
||||
* Channels: Fix "Youtube API returned error 400" ([#5059], by @SamantazFox)
|
||||
* Channels: Fix for live videos ([#5027], thanks @iBicha)
|
||||
* Locales: Add Bulgarian, Welsh and Lombard to the list ([#5046], by @SamantazFox)
|
||||
* Shards: Update database dependencies ([#5034], by @SamantazFox)
|
||||
* Logger: Add color support for different log levels ([#4931], thanks @Fijxu)
|
||||
* Fix named arg syntax when passing force_resolve ([#4754], thanks @syeopite)
|
||||
* Use make_client instead of calling HTTP::Client ([#4709], thanks @syeopite)
|
||||
* Add "Filipino (auto-generated)" to the list of caption languages ([#4995], by @SamantazFox)
|
||||
* Makefile: Add MT option to enable the 'preview_mt' flag ([#4993], by @SamantazFox)
|
||||
* SigHelper: Reconnect to signature helper ([#4991], thanks @Fijxu)
|
||||
* Fix player menus hiding onHover ready ([#4750], thanks @giacomocerquone)
|
||||
* Use connection pools when requesting images from YouTube ([#4326], thanks @syeopite)
|
||||
* Add support for using Invidious through a HTTP Proxy ([#4270], thanks @syeopite)
|
||||
* Search: Fix 'youtu.be' URLs in sanitizer ([#4894], by @SamantazFox)
|
||||
* Ameba: Disable Style/RedundantNext rule ([#4888], thanks @syeopite)
|
||||
* Playlists: Fix 'invalid byte sequence' error when subscribing ([#4887], thanks @DmitrySandalov)
|
||||
* Parse more metadata badges for SearchVideos ([#4863], thanks @ChunkyProgrammer)
|
||||
* Translations update from Hosted Weblate ([#4862], thanks to our many translators)
|
||||
* Videos: Convert URL before putting result into cache ([#4850], by @SamantazFox)
|
||||
* HTML: Add error message to "search issues on GitHub" link ([#4652], thanks @tracedgod)
|
||||
* Preferences: Add option to control preloading of video data ([#4122], thanks @Nerdmind)
|
||||
* Performance: Improve speed of automatic instance redirection ([#4193], thanks @syeopite)
|
||||
* Remove myself from CODEOWNERS on the config file ([#4942], by @TheFrenchGhosty)
|
||||
* Update latest version WEB_CREATOR + fix comment web embed ([#4930], thanks @unixfox)
|
||||
* use WEB_CREATOR when po_token with WEB_EMBED as a fallback ([#4928], thanks @unixfox)
|
||||
* Revert "use web screen embed for fixing potoken functionality"
|
||||
* use web screen embed for fixing potoken functionality ([#4923], thanks @unixfox)
|
||||
|
||||
[#4122]: https://github.com/iv-org/invidious/pull/4122
|
||||
[#4193]: https://github.com/iv-org/invidious/pull/4193
|
||||
[#4270]: https://github.com/iv-org/invidious/pull/4270
|
||||
[#4326]: https://github.com/iv-org/invidious/pull/4326
|
||||
[#4652]: https://github.com/iv-org/invidious/pull/4652
|
||||
[#4709]: https://github.com/iv-org/invidious/pull/4709
|
||||
[#4750]: https://github.com/iv-org/invidious/pull/4750
|
||||
[#4754]: https://github.com/iv-org/invidious/pull/4754
|
||||
[#4850]: https://github.com/iv-org/invidious/pull/4850
|
||||
[#4862]: https://github.com/iv-org/invidious/pull/4862
|
||||
[#4863]: https://github.com/iv-org/invidious/pull/4863
|
||||
[#4887]: https://github.com/iv-org/invidious/pull/4887
|
||||
[#4888]: https://github.com/iv-org/invidious/pull/4888
|
||||
[#4894]: https://github.com/iv-org/invidious/pull/4894
|
||||
[#4923]: https://github.com/iv-org/invidious/pull/4923
|
||||
[#4928]: https://github.com/iv-org/invidious/pull/4928
|
||||
[#4930]: https://github.com/iv-org/invidious/pull/4930
|
||||
[#4931]: https://github.com/iv-org/invidious/pull/4931
|
||||
[#4934]: https://github.com/iv-org/invidious/pull/4934
|
||||
[#4942]: https://github.com/iv-org/invidious/pull/4942
|
||||
[#4984]: https://github.com/iv-org/invidious/pull/4984
|
||||
[#4991]: https://github.com/iv-org/invidious/pull/4991
|
||||
[#4993]: https://github.com/iv-org/invidious/pull/4993
|
||||
[#4995]: https://github.com/iv-org/invidious/pull/4995
|
||||
[#5027]: https://github.com/iv-org/invidious/pull/5027
|
||||
[#5034]: https://github.com/iv-org/invidious/pull/5034
|
||||
[#5045]: https://github.com/iv-org/invidious/pull/5045
|
||||
[#5046]: https://github.com/iv-org/invidious/pull/5046
|
||||
[#5059]: https://github.com/iv-org/invidious/pull/5059
|
||||
[#5060]: https://github.com/iv-org/invidious/pull/5060
|
||||
[#5063]: https://github.com/iv-org/invidious/pull/5063
|
||||
[#5070]: https://github.com/iv-org/invidious/pull/5070
|
||||
[#5071]: https://github.com/iv-org/invidious/pull/5071
|
||||
|
||||
|
||||
## v2.20240825.2 (2024-08-26)
|
||||
|
||||
This releases fixes the container tags pushed on quay.io.
|
||||
Previously, the ARM64 build was released under the `latest` tag, instead of `latest-arm64`.
|
||||
|
||||
### Full list of pull requests merged since the last release (newest first)
|
||||
|
||||
CI: Fix docker container tags ([#4883], by @SamantazFox)
|
||||
|
||||
[#4877]: https://github.com/iv-org/invidious/pull/4877
|
||||
|
||||
|
||||
## v2.20240825.1 (2024-08-25)
|
||||
|
||||
Add patch component to be [semver] compliant and make github actions happy.
|
||||
|
||||
[semver]: https://semver.org/
|
||||
|
||||
### Full list of pull requests merged since the last release (newest first)
|
||||
|
||||
Allow manual trigger of release-container build ([#4877], thanks @syeopite)
|
||||
|
||||
[#4877]: https://github.com/iv-org/invidious/pull/4877
|
||||
|
||||
|
||||
## v2.20240825.0 (2024-08-25)
|
||||
|
||||
### New features & important changes
|
||||
|
||||
#### For users
|
||||
|
||||
* The search bar now has a button that you can click!
|
||||
* Youtube URLs can be pasted directly in the search bar. Prepend search query with a
|
||||
backslash (`\`) to disable that feature (useful if you need to search for a video whose
|
||||
title contains some youtube URL).
|
||||
* On the channel page the "streams" tab can be sorted by either: "newest", "oldest" or "popular"
|
||||
* Lots of translations have been updated (thanks to our contributors on Weblate!)
|
||||
* Videos embedded in local HTML files (e.g: a webpage saved from a blog) can now be played
|
||||
|
||||
#### For instance owners
|
||||
|
||||
* Invidious now has the ability to provide a `po_token` and `visitordata` to Youtube in order to
|
||||
circumvent current Youtube restrictions.
|
||||
* Invidious can use an (optional) external signature server like [inv_sig_helper]. Please note that
|
||||
some videos can't be played without that signature server.
|
||||
* The Helm charts were moved to a separate repo: https://github.com/iv-org/invidious-helm-chart
|
||||
* We have changed how containers are released: the `latest` tag now tracks tagged releases, whereas
|
||||
the `master` tag tracks the most recent commits of the `master` branch ("nightly" builds).
|
||||
|
||||
[inv_sig_helper]: https://github.com/iv-org/inv_sig_helper
|
||||
|
||||
#### For developpers
|
||||
|
||||
* The versions of Crystal that we test in CI/CD are now: `1.9.2`, `1.10.1`, `1.11.2`, `1.12.1`.
|
||||
Please note that due to a bug in the `libxml` bindings (See [#4256]), versions prior to `1.10.0`
|
||||
are not recommended to use.
|
||||
* Thanks to @syeopite, the code is now [ameba] compliant.
|
||||
* Ameba is part of our CI/CD pipeline, and its rules will be enforced in future PRs.
|
||||
* The transcript code has been rewritten to permit transcripts as a feature rather than being
|
||||
only a workaround for captions. Trancripts feature is coming soon!
|
||||
* Various fixes regarding the logic interacting with Youtube
|
||||
* The `sort_by` parameter can be used on the `/api/v1/channels/{id}/streams` endpoint. Accepted
|
||||
values are: "newest", "oldest" and "popular"
|
||||
|
||||
[ameba]: https://github.com/crystal-ameba/ameba
|
||||
[#4256]: https://github.com/iv-org/invidious/issues/4256
|
||||
|
||||
|
||||
### Bugs fixed
|
||||
|
||||
#### User-side
|
||||
|
||||
* Channels: fixed broken "subscribers" and "views" counters
|
||||
* Watch page: playback position is reset at the end of a video, so that the next time this video
|
||||
is watched, it will start from the beginning rather than 15 seconds before the end
|
||||
* Watch page: the items in the "add to playlist" drop down are now sorted alphabetically
|
||||
* Videos: the "genre" URL is now always pointing to a valid webpage
|
||||
* Playlists: Fixed `Could not parse N episodes` error on podcast playlists
|
||||
* All external links should now have the [`rel`] attibute set to `noreferrer noopener` for
|
||||
increased privacy.
|
||||
* Preferences: Fixed the admin-only "modified source code" input being ignored
|
||||
* Watch/channel pages: use the full image URL in `og:image` and `twitter:image` meta tags
|
||||
|
||||
[`rel`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel
|
||||
|
||||
#### API
|
||||
|
||||
* fixed the `local` parameter not applying to `formatStreams` on `/api/v1/videos/{id}`
|
||||
* fixed an `Index out of bounds` error hapenning when a playlist had no videos
|
||||
* fixed duplicated query parameters in proxied video URLs
|
||||
* Return actual video height/width/fps rather than hard coded values
|
||||
* Fixed the `/api/v1/popular` endpoint not returning a proper error code/message when the
|
||||
popular page/endpoint are disabled.
|
||||
|
||||
|
||||
### Full list of pull requests merged since the last release (newest first)
|
||||
|
||||
* HTML: Sort playlists alphabetically in watch page drop down ([#4853], by @SamantazFox)
|
||||
* Videos: Fix XSS vulnerability in description/comments ([#4852], thanks _anonymous_)
|
||||
* YtAPI: Bump client versions ([#4849], by @SamantazFox)
|
||||
* SigHelper: Fix inverted time comparison in 'check_update' ([#4845], by @SamantazFox)
|
||||
* Storyboards: Various fixes and code cleaning ([#4153], by SamantazFox)
|
||||
* Fix lint errors introduced in #4146 and #4295 ([#4876], thanks @syeopite)
|
||||
* Search: Add support for Youtube URLs ([#4146], by @SamantazFox)
|
||||
* Channel: Render age restricted channels ([#4295], thanks @ChunkyProgrammer)
|
||||
* Ameba: Miscellaneous fixes ([#4807], thanks @syeopite)
|
||||
* API: Proxy formatStreams URLs too ([#4859], thanks @colinleroy)
|
||||
* UI: Add search button to search bar ([#4706], thanks @thansk)
|
||||
* Add ability to set po_token and visitordata ID ([#4789], thanks @unixfox)
|
||||
* Add support for an external signature server ([#4772], by @SamantazFox)
|
||||
* Ameba: Fix Naming/VariableNames ([#4790], thanks @syeopite)
|
||||
* Translations update from Hosted Weblate ([#4659])
|
||||
* Ameba: Fix Lint/UselessAssign ([#4795], thanks @syeopite)
|
||||
* HTML: Add rel="noreferrer noopener" to external links ([#4667], thanks @ulmemxpoc)
|
||||
* Remove unused methods in Invidious::LogHandler ([#4812], thanks @syeopite)
|
||||
* Ameba: Fix Lint/NotNilAfterNoBang ([#4796], thanks @syeopite)
|
||||
* Ameba: Fix unused argument Lint warnings ([#4805], thanks @syeopite)
|
||||
* Ameba: i18next.cr fixes ([#4806], thanks @syeopite)
|
||||
* Ameba: Disable rules ([#4792], thanks @syeopite)
|
||||
* Channel: parse subscriber count and channel banner ([#4785], thanks @ChunkyProgrammer)
|
||||
* Player: Fix playback position of already watched videos ([#4731], thanks @Fijxu)
|
||||
* Videos: Fix genre url being unusable ([#4717], thanks @meatball133)
|
||||
* API: Fix out of bound error on empty playlists ([#4696], thanks @Fijxu)
|
||||
* Handle playlists cataloged as Podcast ([#4695], thanks @Fijxu)
|
||||
* API: Fix duplicated query parameters in proxied video URLs ([#4587], thanks @absidue)
|
||||
* API: Return actual stream height, width and fps ([#4586], thanks @absidue)
|
||||
* Preferences: Fix handling of modified source code URL ([#4437], thanks @nooptek)
|
||||
* API: Fix URL for vtt subtitles ([#4221], thanks @karelrooted)
|
||||
* Channels: Add sort options to streams ([#4224], thanks @src-tinkerer)
|
||||
* API: Fix error code for disabled popular endpoint ([#4296], thanks @iBicha)
|
||||
* Allow embedding videos in local HTML files ([#4450], thanks @tomasz1986)
|
||||
* CI: Bump Crystal version matrix ([#4654], by @SamantazFox)
|
||||
* YtAPI: Remove API keys like official clients ([#4655], by @SamantazFox)
|
||||
* HTML: Use full URL in the og:image property ([#4675], thanks @Fijxu)
|
||||
* Rewrite transcript logic to be more generic ([#4747], thanks @syeopite)
|
||||
* CI: Run Ameba ([#4753], thanks @syeopite)
|
||||
* CI: Add release based containers ([#4763], thanks @syeopite)
|
||||
* move helm chart to a dedicated github repository ([#4711], thanks @unixfox)
|
||||
|
||||
[#4146]: https://github.com/iv-org/invidious/pull/4146
|
||||
[#4153]: https://github.com/iv-org/invidious/pull/4153
|
||||
[#4221]: https://github.com/iv-org/invidious/pull/4221
|
||||
[#4224]: https://github.com/iv-org/invidious/pull/4224
|
||||
[#4295]: https://github.com/iv-org/invidious/pull/4295
|
||||
[#4296]: https://github.com/iv-org/invidious/pull/4296
|
||||
[#4437]: https://github.com/iv-org/invidious/pull/4437
|
||||
[#4450]: https://github.com/iv-org/invidious/pull/4450
|
||||
[#4586]: https://github.com/iv-org/invidious/pull/4586
|
||||
[#4587]: https://github.com/iv-org/invidious/pull/4587
|
||||
[#4654]: https://github.com/iv-org/invidious/pull/4654
|
||||
[#4655]: https://github.com/iv-org/invidious/pull/4655
|
||||
[#4659]: https://github.com/iv-org/invidious/pull/4659
|
||||
[#4667]: https://github.com/iv-org/invidious/pull/4667
|
||||
[#4675]: https://github.com/iv-org/invidious/pull/4675
|
||||
[#4695]: https://github.com/iv-org/invidious/pull/4695
|
||||
[#4696]: https://github.com/iv-org/invidious/pull/4696
|
||||
[#4706]: https://github.com/iv-org/invidious/pull/4706
|
||||
[#4711]: https://github.com/iv-org/invidious/pull/4711
|
||||
[#4717]: https://github.com/iv-org/invidious/pull/4717
|
||||
[#4731]: https://github.com/iv-org/invidious/pull/4731
|
||||
[#4747]: https://github.com/iv-org/invidious/pull/4747
|
||||
[#4753]: https://github.com/iv-org/invidious/pull/4753
|
||||
[#4763]: https://github.com/iv-org/invidious/pull/4763
|
||||
[#4772]: https://github.com/iv-org/invidious/pull/4772
|
||||
[#4785]: https://github.com/iv-org/invidious/pull/4785
|
||||
[#4789]: https://github.com/iv-org/invidious/pull/4789
|
||||
[#4790]: https://github.com/iv-org/invidious/pull/4790
|
||||
[#4792]: https://github.com/iv-org/invidious/pull/4792
|
||||
[#4795]: https://github.com/iv-org/invidious/pull/4795
|
||||
[#4796]: https://github.com/iv-org/invidious/pull/4796
|
||||
[#4805]: https://github.com/iv-org/invidious/pull/4805
|
||||
[#4806]: https://github.com/iv-org/invidious/pull/4806
|
||||
[#4807]: https://github.com/iv-org/invidious/pull/4807
|
||||
[#4812]: https://github.com/iv-org/invidious/pull/4812
|
||||
[#4845]: https://github.com/iv-org/invidious/pull/4845
|
||||
[#4849]: https://github.com/iv-org/invidious/pull/4849
|
||||
[#4852]: https://github.com/iv-org/invidious/pull/4852
|
||||
[#4853]: https://github.com/iv-org/invidious/pull/4853
|
||||
[#4859]: https://github.com/iv-org/invidious/pull/4859
|
||||
[#4876]: https://github.com/iv-org/invidious/pull/4876
|
||||
|
||||
|
||||
## v2.20240427 (2024-04-27)
|
||||
## 2024-04-26
|
||||
|
||||
Major bug fixes:
|
||||
* Videos: Use android test suite client (#4650, thanks @SamantazFox)
|
||||
|
9
Makefile
9
Makefile
@ -7,11 +7,6 @@ STATIC := 0
|
||||
|
||||
NO_DBG_SYMBOLS := 0
|
||||
|
||||
# Enable multi-threading.
|
||||
# Warning: Experimental feature!!
|
||||
# invidious is not stable when MT is enabled.
|
||||
MT := 0
|
||||
|
||||
|
||||
FLAGS ?=
|
||||
|
||||
@ -24,10 +19,6 @@ ifeq ($(STATIC), 1)
|
||||
FLAGS += --static
|
||||
endif
|
||||
|
||||
ifeq ($(MT), 1)
|
||||
FLAGS += -Dpreview_mt
|
||||
endif
|
||||
|
||||
|
||||
ifeq ($(NO_DBG_SYMBOLS), 1)
|
||||
FLAGS += --no-debug
|
||||
|
@ -278,14 +278,7 @@ div.thumbnail > .bottom-right-overlay {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.searchbar .pure-form {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.searchbar .pure-form fieldset {
|
||||
padding: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.searchbar .pure-form fieldset { padding: 0; }
|
||||
|
||||
.searchbar input[type="search"] {
|
||||
width: 100%;
|
||||
@ -317,16 +310,6 @@ input[type="search"]::-webkit-search-cancel-button {
|
||||
background-size: 14px;
|
||||
}
|
||||
|
||||
.searchbar #searchbutton {
|
||||
border: none;
|
||||
background: none;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.searchbar #searchbutton:hover {
|
||||
color: rgb(0, 182, 240);
|
||||
}
|
||||
|
||||
.user-field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -68,7 +68,6 @@
|
||||
|
||||
.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu {
|
||||
margin-bottom: 2em;
|
||||
padding-top: 2em
|
||||
}
|
||||
|
||||
.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control {height: 5px;
|
||||
|
@ -91,7 +91,7 @@
|
||||
var count = document.getElementById('count');
|
||||
count.textContent--;
|
||||
|
||||
var url = '/token_ajax?action=revoke_token&redirect=false' +
|
||||
var url = '/token_ajax?action_revoke_token=1&redirect=false' +
|
||||
'&referer=' + encodeURIComponent(location.href) +
|
||||
'&session=' + target.getAttribute('data-session');
|
||||
|
||||
@ -111,7 +111,7 @@
|
||||
var count = document.getElementById('count');
|
||||
count.textContent--;
|
||||
|
||||
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
|
||||
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
|
||||
'&referer=' + encodeURIComponent(location.href) +
|
||||
'&c=' + target.getAttribute('data-ucid');
|
||||
|
||||
|
@ -1,93 +0,0 @@
|
||||
'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);
|
||||
}
|
||||
}
|
||||
});
|
@ -3,6 +3,7 @@ var player_data = JSON.parse(document.getElementById('player_data').textContent)
|
||||
var video_data = JSON.parse(document.getElementById('video_data').textContent);
|
||||
|
||||
var options = {
|
||||
preload: 'auto',
|
||||
liveui: true,
|
||||
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
|
||||
controlBar: {
|
||||
@ -134,32 +135,26 @@ player.on('timeupdate', function () {
|
||||
// YouTube links
|
||||
|
||||
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');
|
||||
if (elem_yt_embed) {
|
||||
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
|
||||
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
|
||||
}
|
||||
|
||||
let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url');
|
||||
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
|
||||
|
||||
elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch);
|
||||
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
|
||||
|
||||
// Invidious links
|
||||
|
||||
let domain = window.location.origin;
|
||||
|
||||
let elem_iv_embed = document.getElementById('link-iv-embed');
|
||||
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');
|
||||
if (elem_iv_other) {
|
||||
let base_url_iv_other = elem_iv_other.getAttribute('data-base-url');
|
||||
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain);
|
||||
}
|
||||
|
||||
let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url');
|
||||
let base_url_iv_other = elem_iv_other.getAttribute('data-base-url');
|
||||
|
||||
elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain);
|
||||
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain);
|
||||
});
|
||||
|
||||
|
||||
@ -356,12 +351,7 @@ if (video_data.params.save_player_pos) {
|
||||
const rememberedTime = get_video_time();
|
||||
let lastUpdated = 0;
|
||||
|
||||
if(!hasTimeParam) {
|
||||
if (rememberedTime >= video_data.length_seconds - 20)
|
||||
set_seconds_after_start(0);
|
||||
else
|
||||
set_seconds_after_start(rememberedTime);
|
||||
}
|
||||
if(!hasTimeParam) set_seconds_after_start(rememberedTime);
|
||||
|
||||
player.on('timeupdate', function () {
|
||||
const raw = player.currentTime();
|
||||
|
@ -6,7 +6,7 @@ function add_playlist_video(target) {
|
||||
var select = target.parentNode.children[0].children[1];
|
||||
var option = select.children[select.selectedIndex];
|
||||
|
||||
var url = '/playlist_ajax?action=add_video&redirect=false' +
|
||||
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
||||
'&video_id=' + target.getAttribute('data-id') +
|
||||
'&playlist_id=' + option.getAttribute('data-plid');
|
||||
|
||||
@ -21,7 +21,7 @@ function add_playlist_item(target) {
|
||||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||
tile.style.display = 'none';
|
||||
|
||||
var url = '/playlist_ajax?action=add_video&redirect=false' +
|
||||
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
|
||||
'&video_id=' + target.getAttribute('data-id') +
|
||||
'&playlist_id=' + target.getAttribute('data-plid');
|
||||
|
||||
@ -36,7 +36,7 @@ function remove_playlist_item(target) {
|
||||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||
tile.style.display = 'none';
|
||||
|
||||
var url = '/playlist_ajax?action=remove_video&redirect=false' +
|
||||
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
|
||||
'&set_video_id=' + target.getAttribute('data-index') +
|
||||
'&playlist_id=' + target.getAttribute('data-plid');
|
||||
|
||||
|
@ -16,7 +16,7 @@ function subscribe() {
|
||||
subscribe_button.onclick = unsubscribe;
|
||||
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
||||
|
||||
var url = '/subscription_ajax?action=create_subscription_to_channel&redirect=false' +
|
||||
var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
|
||||
'&c=' + subscribe_data.ucid;
|
||||
|
||||
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
|
||||
@ -32,7 +32,7 @@ function unsubscribe() {
|
||||
subscribe_button.onclick = subscribe;
|
||||
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
|
||||
|
||||
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
|
||||
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
|
||||
'&c=' + subscribe_data.ucid;
|
||||
|
||||
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {
|
||||
|
@ -67,10 +67,6 @@ function get_playlist(plid) {
|
||||
'&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'}, {
|
||||
on200: function (response) {
|
||||
playlist.innerHTML = response.playlistHtml;
|
||||
|
@ -6,7 +6,7 @@ function mark_watched(target) {
|
||||
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
|
||||
tile.style.display = 'none';
|
||||
|
||||
var url = '/watch_ajax?action=mark_watched&redirect=false' +
|
||||
var url = '/watch_ajax?action_mark_watched=1&redirect=false' +
|
||||
'&id=' + target.getAttribute('data-id');
|
||||
|
||||
helpers.xhr('POST', url, {payload: payload}, {
|
||||
@ -22,7 +22,7 @@ function mark_unwatched(target) {
|
||||
var count = document.getElementById('count');
|
||||
count.textContent--;
|
||||
|
||||
var url = '/watch_ajax?action=mark_unwatched&redirect=false' +
|
||||
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
|
||||
'&id=' + target.getAttribute('data-id');
|
||||
|
||||
helpers.xhr('POST', url, {payload: payload}, {
|
||||
|
@ -1,6 +1,6 @@
|
||||
#########################################
|
||||
#
|
||||
# Database and other external servers
|
||||
# Database configuration
|
||||
#
|
||||
#########################################
|
||||
|
||||
@ -41,66 +41,6 @@ db:
|
||||
#check_tables: false
|
||||
|
||||
|
||||
##
|
||||
## Path to an external signature resolver, used to emulate
|
||||
## the Youtube client's Javascript. If no such server is
|
||||
## available, some videos will not be playable.
|
||||
##
|
||||
## When this setting is commented out, no external
|
||||
## resolver will be used.
|
||||
##
|
||||
## Accepted values: a path to a UNIX socket or "<IP>:<Port>"
|
||||
## Default: <none>
|
||||
##
|
||||
#signature_server:
|
||||
|
||||
##
|
||||
## 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 size of the key needs to be more or equal to 16.
|
||||
##
|
||||
## 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
|
||||
## Default: <none>
|
||||
##
|
||||
#invidious_companion_key: "CHANGE_ME!!"
|
||||
|
||||
#########################################
|
||||
#
|
||||
@ -177,20 +117,6 @@ https_only: false
|
||||
##
|
||||
#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)
|
||||
@ -234,17 +160,6 @@ https_only: false
|
||||
##
|
||||
#force_resolve:
|
||||
|
||||
##
|
||||
## Configuration for using a HTTP proxy
|
||||
##
|
||||
## If unset, then no HTTP proxy will be used.
|
||||
##
|
||||
#http_proxy:
|
||||
# user:
|
||||
# password:
|
||||
# host:
|
||||
# port:
|
||||
|
||||
|
||||
##
|
||||
## Use Innertube's transcripts API instead of timedtext for closed captions
|
||||
@ -258,18 +173,6 @@ https_only: false
|
||||
##
|
||||
# use_innertube_for_captions: false
|
||||
|
||||
##
|
||||
## Send Google session informations. This is useful when Invidious is blocked
|
||||
## by the message "This helps protect our community."
|
||||
## See https://github.com/iv-org/invidious/issues/4734.
|
||||
##
|
||||
## Warning: These strings gives much more identifiable information to Google!
|
||||
##
|
||||
## Accepted values: String
|
||||
## Default: <none>
|
||||
##
|
||||
# po_token: ""
|
||||
# visitor_data: ""
|
||||
|
||||
# -----------------------------
|
||||
# Logging
|
||||
@ -294,17 +197,6 @@ https_only: false
|
||||
##
|
||||
#log_level: Info
|
||||
|
||||
##
|
||||
## Enables colors in logs. Useful for debugging purposes
|
||||
## This is overridden if "-k" or "--colorize"
|
||||
## are passed on the command line.
|
||||
## Colors are also disabled if the environment variable
|
||||
## NO_COLOR is present and has any value
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: true
|
||||
##
|
||||
#colorize_logs: false
|
||||
|
||||
# -----------------------------
|
||||
# Features
|
||||
@ -451,6 +343,21 @@ full_refresh: false
|
||||
##
|
||||
feed_threads: 1
|
||||
|
||||
##
|
||||
## Enable/Disable the polling job that keeps the decryption
|
||||
## function (for "secured" videos) up to date.
|
||||
##
|
||||
## Note: This part of the code generate a small amount of data every minute.
|
||||
## This may not be desired if you have bandwidth limits set by your ISP.
|
||||
##
|
||||
## Note 2: This part of the code is currently broken, so changing
|
||||
## this setting has no impact.
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: false
|
||||
##
|
||||
#decrypt_polling: false
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
@ -790,22 +697,6 @@ default_user_preferences:
|
||||
# Video player behavior
|
||||
# -----------------------------
|
||||
|
||||
##
|
||||
## This option controls the value of the HTML5 <video> element's
|
||||
## "preload" attribute.
|
||||
##
|
||||
## If set to 'false', no video data will be loaded until the user
|
||||
## explicitly starts the video by clicking the "Play" button.
|
||||
## If set to 'true', the web browser will buffer some video data
|
||||
## while the page is loading.
|
||||
##
|
||||
## See: https://www.w3schools.com/tags/att_video_preload.asp
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: true
|
||||
##
|
||||
#preload: true
|
||||
|
||||
##
|
||||
## Automatically play videos on page load.
|
||||
##
|
||||
@ -858,9 +749,9 @@ default_user_preferences:
|
||||
## Default video quality.
|
||||
##
|
||||
## Accepted values: dash, hd720, medium, small
|
||||
## Default: dash
|
||||
## Default: hd720
|
||||
##
|
||||
#quality: dash
|
||||
#quality: hd720
|
||||
|
||||
##
|
||||
## Default dash video quality.
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM crystallang/crystal:1.12.2-alpine AS builder
|
||||
FROM crystallang/crystal:1.8.2-alpine AS builder
|
||||
|
||||
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 \
|
||||
--link-flags "-lxml2 -llzma"
|
||||
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
|
||||
RUN if [[ "${release}" == 1 ]] ; then \
|
||||
crystal build ./src/invidious.cr \
|
||||
--release \
|
||||
--static --warnings all \
|
||||
@ -32,8 +32,8 @@ RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ;
|
||||
--link-flags "-lxml2 -llzma"; \
|
||||
fi
|
||||
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
||||
FROM alpine:3.18
|
||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
||||
WORKDIR /invidious
|
||||
RUN addgroup -g 1000 -S invidious && \
|
||||
adduser -u 1000 -S invidious -G invidious
|
||||
|
@ -1,6 +1,5 @@
|
||||
FROM alpine:3.20 AS builder
|
||||
RUN apk add --no-cache 'crystal=1.12.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \
|
||||
zlib-static openssl-libs-static openssl-dev musl-dev xz-static
|
||||
FROM alpine:3.18 AS builder
|
||||
RUN apk add --no-cache 'crystal=1.8.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static zlib-static openssl-libs-static openssl-dev musl-dev xz-static
|
||||
|
||||
ARG release
|
||||
|
||||
@ -22,7 +21,7 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
||||
RUN crystal spec --warnings all \
|
||||
--link-flags "-lxml2 -llzma"
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
|
||||
RUN if [[ "${release}" == 1 ]] ; then \
|
||||
crystal build ./src/invidious.cr \
|
||||
--release \
|
||||
--static --warnings all \
|
||||
@ -33,8 +32,8 @@ RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ;
|
||||
--link-flags "-lxml2 -llzma"; \
|
||||
fi
|
||||
|
||||
FROM alpine:3.20
|
||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
||||
FROM alpine:3.18
|
||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
||||
WORKDIR /invidious
|
||||
RUN addgroup -g 1000 -S invidious && \
|
||||
adduser -u 1000 -S invidious -G invidious
|
||||
|
1
kubernetes/.gitignore
vendored
Normal file
1
kubernetes/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/charts/*.tgz
|
6
kubernetes/Chart.lock
Normal file
6
kubernetes/Chart.lock
Normal file
@ -0,0 +1,6 @@
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
repository: https://charts.bitnami.com/bitnami/
|
||||
version: 12.11.1
|
||||
digest: sha256:3c10008175c4f5c1cec38782f5a7316154b89074c77ebbd9bcc4be4f5ff21122
|
||||
generated: "2023-09-14T22:40:43.171275362Z"
|
22
kubernetes/Chart.yaml
Normal file
22
kubernetes/Chart.yaml
Normal file
@ -0,0 +1,22 @@
|
||||
apiVersion: v2
|
||||
name: invidious
|
||||
description: Invidious is an alternative front-end to YouTube
|
||||
version: 1.1.1
|
||||
appVersion: 0.20.1
|
||||
keywords:
|
||||
- youtube
|
||||
- proxy
|
||||
- video
|
||||
- privacy
|
||||
home: https://invidio.us/
|
||||
icon: https://raw.githubusercontent.com/iv-org/invidious/05988c1c49851b7d0094fca16aeaf6382a7f64ab/assets/favicon-32x32.png
|
||||
sources:
|
||||
- https://github.com/iv-org/invidious
|
||||
maintainers:
|
||||
- name: Leon Klingele
|
||||
email: mail@leonklingele.de
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: ~12.11.0
|
||||
repository: "https://charts.bitnami.com/bitnami/"
|
||||
engine: gotpl
|
@ -1 +1,41 @@
|
||||
The Helm chart has moved to a dedicated GitHub repository: https://github.com/iv-org/invidious-helm-chart/tree/master/invidious
|
||||
# Invidious Helm chart
|
||||
|
||||
Easily deploy Invidious to Kubernetes.
|
||||
|
||||
## Installing Helm chart
|
||||
|
||||
```sh
|
||||
# Build Helm dependencies
|
||||
$ helm dep build
|
||||
|
||||
# Add PostgreSQL init scripts
|
||||
$ kubectl create configmap invidious-postgresql-init \
|
||||
--from-file=../config/sql/channels.sql \
|
||||
--from-file=../config/sql/videos.sql \
|
||||
--from-file=../config/sql/channel_videos.sql \
|
||||
--from-file=../config/sql/users.sql \
|
||||
--from-file=../config/sql/session_ids.sql \
|
||||
--from-file=../config/sql/nonces.sql \
|
||||
--from-file=../config/sql/annotations.sql \
|
||||
--from-file=../config/sql/playlists.sql \
|
||||
--from-file=../config/sql/playlist_videos.sql
|
||||
|
||||
# Install Helm app to your Kubernetes cluster
|
||||
$ helm install invidious ./
|
||||
```
|
||||
|
||||
## Upgrading
|
||||
|
||||
```sh
|
||||
# Upgrading is easy, too!
|
||||
$ helm upgrade invidious ./
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```sh
|
||||
# Get rid of everything (except database)
|
||||
$ helm delete invidious
|
||||
|
||||
# To also delete the database, remove all invidious-postgresql PVCs
|
||||
```
|
||||
|
16
kubernetes/templates/_helpers.tpl
Normal file
16
kubernetes/templates/_helpers.tpl
Normal file
@ -0,0 +1,16 @@
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "invidious.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
*/}}
|
||||
{{- define "invidious.fullname" -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
11
kubernetes/templates/configmap.yaml
Normal file
11
kubernetes/templates/configmap.yaml
Normal file
@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: {{ template "invidious.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "invidious.name" . }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
release: {{ .Release.Name }}
|
||||
data:
|
||||
INVIDIOUS_CONFIG: |
|
||||
{{ toYaml .Values.config | indent 4 }}
|
61
kubernetes/templates/deployment.yaml
Normal file
61
kubernetes/templates/deployment.yaml
Normal file
@ -0,0 +1,61 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ template "invidious.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "invidious.name" . }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
replicas: {{ .Values.replicaCount }}
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ template "invidious.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ template "invidious.name" . }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
securityContext:
|
||||
runAsUser: {{ .Values.securityContext.runAsUser }}
|
||||
runAsGroup: {{ .Values.securityContext.runAsGroup }}
|
||||
fsGroup: {{ .Values.securityContext.fsGroup }}
|
||||
initContainers:
|
||||
- name: wait-for-postgresql
|
||||
image: postgres
|
||||
args:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- until pg_isready -h {{ .Values.config.db.host }} -p {{ .Values.config.db.port }} -U {{ .Values.config.db.user }}; do echo waiting for database; sleep 2; done;
|
||||
containers:
|
||||
- name: {{ .Chart.Name }}
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
ports:
|
||||
- containerPort: 3000
|
||||
env:
|
||||
- name: INVIDIOUS_CONFIG
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
key: INVIDIOUS_CONFIG
|
||||
name: {{ template "invidious.fullname" . }}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: {{ .Values.securityContext.allowPrivilegeEscalation }}
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 10 }}
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
port: 3000
|
||||
path: /
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
port: 3000
|
||||
path: /
|
||||
initialDelaySeconds: 15
|
||||
restartPolicy: Always
|
18
kubernetes/templates/hpa.yaml
Normal file
18
kubernetes/templates/hpa.yaml
Normal file
@ -0,0 +1,18 @@
|
||||
{{- if .Values.autoscaling.enabled }}
|
||||
apiVersion: autoscaling/v1
|
||||
kind: HorizontalPodAutoscaler
|
||||
metadata:
|
||||
name: {{ template "invidious.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "invidious.name" . }}
|
||||
chart: "{{ .Chart.Name }}-{{ .Chart.Version }}"
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
scaleTargetRef:
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
name: {{ template "invidious.fullname" . }}
|
||||
minReplicas: {{ .Values.autoscaling.minReplicas }}
|
||||
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
|
||||
targetCPUUtilizationPercentage: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
|
||||
{{- end }}
|
20
kubernetes/templates/service.yaml
Normal file
20
kubernetes/templates/service.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ template "invidious.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "invidious.name" . }}
|
||||
chart: {{ .Chart.Name }}
|
||||
release: {{ .Release.Name }}
|
||||
spec:
|
||||
type: {{ .Values.service.type }}
|
||||
ports:
|
||||
- name: http
|
||||
port: {{ .Values.service.port }}
|
||||
targetPort: 3000
|
||||
selector:
|
||||
app: {{ template "invidious.name" . }}
|
||||
release: {{ .Release.Name }}
|
||||
{{- if .Values.service.loadBalancerIP }}
|
||||
loadBalancerIP: {{ .Values.service.loadBalancerIP }}
|
||||
{{- end }}
|
61
kubernetes/values.yaml
Normal file
61
kubernetes/values.yaml
Normal file
@ -0,0 +1,61 @@
|
||||
name: invidious
|
||||
|
||||
image:
|
||||
repository: quay.io/invidious/invidious
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 16
|
||||
targetCPUUtilizationPercentage: 50
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3000
|
||||
#loadBalancerIP:
|
||||
|
||||
resources: {}
|
||||
#requests:
|
||||
# cpu: 100m
|
||||
# memory: 64Mi
|
||||
#limits:
|
||||
# cpu: 800m
|
||||
# memory: 512Mi
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql
|
||||
postgresql:
|
||||
image:
|
||||
tag: 13
|
||||
auth:
|
||||
username: kemal
|
||||
password: kemal
|
||||
database: invidious
|
||||
primary:
|
||||
initdb:
|
||||
username: kemal
|
||||
password: kemal
|
||||
scriptsConfigMap: invidious-postgresql-init
|
||||
|
||||
# Adapted from ../config/config.yml
|
||||
config:
|
||||
channel_threads: 1
|
||||
feed_threads: 1
|
||||
db:
|
||||
user: kemal
|
||||
password: kemal
|
||||
host: invidious-postgresql
|
||||
port: 5432
|
||||
dbname: invidious
|
||||
full_refresh: false
|
||||
https_only: false
|
||||
domain:
|
@ -483,7 +483,7 @@
|
||||
"comments_view_x_replies_3": "عرض رد {{count}}",
|
||||
"comments_view_x_replies_4": "عرض الردود {{count}}",
|
||||
"comments_view_x_replies_5": "عرض رد {{count}}",
|
||||
"search_message_use_another_instance": "يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
|
||||
"search_message_use_another_instance": " يمكنك أيضًا البحث عن <a href=\"`x`\"> في مثيل آخر </a>.",
|
||||
"comments_points_count_0": "{{count}} نقطة",
|
||||
"comments_points_count_1": "نقطة واحدة",
|
||||
"comments_points_count_2": "نقطتان",
|
||||
@ -559,12 +559,10 @@
|
||||
"toggle_theme": "تبديل الموضوع",
|
||||
"Add to playlist": "أضف إلى قائمة التشغيل",
|
||||
"Add to playlist: ": "أضف إلى قائمة التشغيل: ",
|
||||
"Answer": "اجابة",
|
||||
"Answer": "الرد",
|
||||
"Search for videos": "ابحث عن مقاطع الفيديو",
|
||||
"The Popular feed has been disabled by the administrator.": "تم تعطيل الخلاصة الشائعة من قبل المسؤول.",
|
||||
"carousel_slide": "الشريحة {{current}} من {{total}}",
|
||||
"carousel_skip": "تخطي الكاروسيل",
|
||||
"carousel_go_to": "انتقل إلى الشريحة `x`",
|
||||
"preferences_preload_label": "التحميل المسبق لبيانات الفيديو: ",
|
||||
"Filipino (auto-generated)": "الفلبينية (المولدة تلقائيًا)"
|
||||
"carousel_go_to": "انتقل إلى الشريحة `x`"
|
||||
}
|
||||
|
@ -487,11 +487,5 @@
|
||||
"generic_views_count": "{{count}} гледане",
|
||||
"generic_views_count_plural": "{{count}} гледания",
|
||||
"Next page": "Следваща страница",
|
||||
"Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)",
|
||||
"toggle_theme": "Смени темата",
|
||||
"Add to playlist": "Добави към плейлист",
|
||||
"Add to playlist: ": "Добави към плейлист: ",
|
||||
"Answer": "Отговор",
|
||||
"Search for videos": "Търсене на видеа",
|
||||
"The Popular feed has been disabled by the administrator.": "Популярната страница е деактивирана от администратора."
|
||||
"Import YouTube watch history (.json)": "Импортиране на историята на гледане от YouTube (.json)"
|
||||
}
|
||||
|
@ -487,7 +487,5 @@
|
||||
"generic_button_edit": "Edita",
|
||||
"generic_button_rss": "RSS",
|
||||
"generic_button_delete": "Suprimeix",
|
||||
"Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)",
|
||||
"Answer": "Resposta",
|
||||
"toggle_theme": "Commuta el tema"
|
||||
"Import YouTube watch history (.json)": "Importa l'historial de visualitzacions de YouTube (.json)"
|
||||
}
|
||||
|
@ -137,7 +137,7 @@
|
||||
"Family friendly? ": "Vhodné pro rodiny? ",
|
||||
"Engagement: ": "Zapojení: ",
|
||||
"English": "Angličtina",
|
||||
"English (auto-generated)": "Angličtina (vytvořeno automaticky)",
|
||||
"English (auto-generated)": "Angličtina (automaticky generováno)",
|
||||
"Afrikaans": "Afrikánština",
|
||||
"Albanian": "Albánština",
|
||||
"Amharic": "Amharština",
|
||||
@ -294,8 +294,8 @@
|
||||
"Chinese (China)": "Čínština (Čína)",
|
||||
"Chinese (Hong Kong)": "Čínština (Hong Kong)",
|
||||
"Chinese (Taiwan)": "Čínština (Taiwan)",
|
||||
"Portuguese (auto-generated)": "Portugalština (vytvořeno automaticky)",
|
||||
"Spanish (auto-generated)": "Španělština (vytvořeno automaticky)",
|
||||
"Portuguese (auto-generated)": "Portugalština (automaticky generováno)",
|
||||
"Spanish (auto-generated)": "Španělština (automaticky generováno)",
|
||||
"Spanish (Mexico)": "Španělština (Mexiko)",
|
||||
"Spanish (Spain)": "Španělština (Španělsko)",
|
||||
"generic_count_years_0": "{{count}} rokem",
|
||||
@ -352,13 +352,13 @@
|
||||
"comments_points_count_0": "{{count}} bod",
|
||||
"comments_points_count_1": "{{count}} body",
|
||||
"comments_points_count_2": "{{count}} bodů",
|
||||
"German (auto-generated)": "Němčina (vytvořeno automaticky)",
|
||||
"Indonesian (auto-generated)": "Indonéština (vytvořeno automaticky)",
|
||||
"German (auto-generated)": "Němčina (automaticky generováno)",
|
||||
"Indonesian (auto-generated)": "Indonéština (automaticky generováno)",
|
||||
"Interlingue": "Interlingue",
|
||||
"Italian (auto-generated)": "Italština (vytvořeno automaticky)",
|
||||
"Japanese (auto-generated)": "Japonština (vytvořeno automaticky)",
|
||||
"Korean (auto-generated)": "Korejština (vytvořeno automaticky)",
|
||||
"Russian (auto-generated)": "Ruština (vytvořeno automaticky)",
|
||||
"Italian (auto-generated)": "Italština (automaticky generováno)",
|
||||
"Japanese (auto-generated)": "Japonština (automaticky generováno)",
|
||||
"Korean (auto-generated)": "Korejština (automaticky generováno)",
|
||||
"Russian (auto-generated)": "Ruština (automaticky generováno)",
|
||||
"generic_count_months_0": "{{count}} měsícem",
|
||||
"generic_count_months_1": "{{count}} měsíci",
|
||||
"generic_count_months_2": "{{count}} měsíci",
|
||||
@ -371,7 +371,7 @@
|
||||
"footer_documentation": "Dokumentace",
|
||||
"next_steps_error_message_refresh": "Obnovit stránku",
|
||||
"Chinese": "Čínština",
|
||||
"Dutch (auto-generated)": "Nizozemština (vytvořeno automaticky)",
|
||||
"Dutch (auto-generated)": "Nizozemština (automaticky generováno)",
|
||||
"Erroneous token": "Chybný token",
|
||||
"tokens_count_0": "{{count}} token",
|
||||
"tokens_count_1": "{{count}} tokeny",
|
||||
@ -380,9 +380,9 @@
|
||||
"Token is expired, please try again": "Token vypršel, zkuste to prosím znovu",
|
||||
"English (United States)": "Angličtina (Spojené státy)",
|
||||
"Cantonese (Hong Kong)": "Kantonština (Hong Kong)",
|
||||
"French (auto-generated)": "Francouzština (vytvořeno automaticky)",
|
||||
"Turkish (auto-generated)": "Turečtina (vytvořeno automaticky)",
|
||||
"Vietnamese (auto-generated)": "Vietnamština (vytvořeno automaticky)",
|
||||
"French (auto-generated)": "Francouzština (automaticky generováno)",
|
||||
"Turkish (auto-generated)": "Turečtina (automaticky generováno)",
|
||||
"Vietnamese (auto-generated)": "Vietnamština (automaticky generováno)",
|
||||
"Current version: ": "Aktuální verze: ",
|
||||
"next_steps_error_message": "Měli byste zkusit: ",
|
||||
"footer_donate_page": "Přispět",
|
||||
@ -471,7 +471,7 @@
|
||||
"search_filters_title": "Filtry",
|
||||
"search_filters_duration_option_medium": "Střední (4 - 20 minut)",
|
||||
"search_filters_duration_option_long": "Dlouhá (> 20 minut)",
|
||||
"search_message_use_another_instance": "Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.",
|
||||
"search_message_use_another_instance": " Můžete také <a href=\"`x`\">hledat na jiné instanci</a>.",
|
||||
"search_filters_features_label": "Vlastnosti",
|
||||
"search_filters_features_option_three_sixty": "360°",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
@ -513,7 +513,5 @@
|
||||
"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_skip": "Přeskočit galerii",
|
||||
"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)"
|
||||
"carousel_go_to": "Přejít na snímek `x`"
|
||||
}
|
||||
|
385
locales/cy.json
385
locales/cy.json
@ -1,385 +0,0 @@
|
||||
{
|
||||
"Time (h:mm:ss):": "Amser (h:mm:ss):",
|
||||
"Password": "Cyfrinair",
|
||||
"preferences_quality_dash_option_auto": "Awtomatig",
|
||||
"preferences_quality_dash_option_best": "Gorau",
|
||||
"preferences_quality_dash_option_worst": "Gwaethaf",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"published": "dyddiad cyhoeddi",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"preferences_quality_dash_option_144p": "144p",
|
||||
"preferences_comments_label": "Ffynhonnell sylwadau: ",
|
||||
"preferences_captions_label": "Isdeitlau rhagosodedig: ",
|
||||
"youtube": "YouTube",
|
||||
"reddit": "Reddit",
|
||||
"Fallback captions: ": "Isdeitlau amgen: ",
|
||||
"preferences_related_videos_label": "Dangos fideos perthnasol: ",
|
||||
"dark": "tywyll",
|
||||
"preferences_dark_mode_label": "Thema: ",
|
||||
"light": "golau",
|
||||
"preferences_sort_label": "Trefnu fideo yn ôl: ",
|
||||
"Import/export data": "Mewnforio/allforio data",
|
||||
"Delete account": "Dileu eich cyfrif",
|
||||
"preferences_category_admin": "Hoffterau gweinyddu",
|
||||
"playlist_button_add_items": "Ychwanegu fideos",
|
||||
"Delete playlist": "Dileu'r rhestr chwarae",
|
||||
"Create playlist": "Creu rhestr chwarae",
|
||||
"Show less": "Dangos llai",
|
||||
"Show more": "Dangos rhagor",
|
||||
"Watch on YouTube": "Gwylio ar YouTube",
|
||||
"search_message_no_results": "Dim canlyniadau.",
|
||||
"search_message_change_filters_or_query": "Ceisiwch ehangu eich chwiliad ac/neu newid yr hidlyddion.",
|
||||
"License: ": "Trwydded: ",
|
||||
"Standard YouTube license": "Trwydded safonol YouTube",
|
||||
"Family friendly? ": "Addas i bawb? ",
|
||||
"Wilson score: ": "Sgôr Wilson: ",
|
||||
"Show replies": "Dangos ymatebion",
|
||||
"Music in this video": "Cerddoriaeth yn y fideo hwn",
|
||||
"Artist: ": "Artist: ",
|
||||
"Erroneous CAPTCHA": "CAPTCHA anghywir",
|
||||
"This channel does not exist.": "Dyw'r sianel hon ddim yn bodoli.",
|
||||
"Not a playlist.": "Ddim yn rhestr chwarae.",
|
||||
"Could not fetch comments": "Wedi methu llwytho sylwadau",
|
||||
"Playlist does not exist.": "Dyw'r rhestr chwarae ddim yn bodoli.",
|
||||
"Erroneous challenge": "Her annilys",
|
||||
"channel_tab_podcasts_label": "Podlediadau",
|
||||
"channel_tab_playlists_label": "Rhestrau chwarae",
|
||||
"channel_tab_streams_label": "Fideos byw",
|
||||
"crash_page_read_the_faq": "darllen y <a href=\"`x`\">cwestiynau cyffredin</a>",
|
||||
"crash_page_switch_instance": "ceisio <a href=\"`x`\">defnyddio gweinydd arall</a>",
|
||||
"crash_page_refresh": "ceisio <a href=\"`x`\">ail-lwytho'r dudalen</a>",
|
||||
"search_filters_features_option_four_k": "4K",
|
||||
"search_filters_features_label": "Nodweddion",
|
||||
"search_filters_duration_option_medium": "Canolig (4 - 20 munud)",
|
||||
"search_filters_features_option_live": "Yn fyw",
|
||||
"search_filters_duration_option_long": "Hir (> 20 munud)",
|
||||
"search_filters_date_option_year": "Eleni",
|
||||
"search_filters_type_label": "Math",
|
||||
"search_filters_date_option_month": "Y mis hwn",
|
||||
"generic_views_count_0": "{{count}} o wyliadau",
|
||||
"generic_views_count_1": "{{count}} gwyliad",
|
||||
"generic_views_count_2": "{{count}} wyliad",
|
||||
"generic_views_count_3": "{{count}} o wyliadau",
|
||||
"generic_views_count_4": "{{count}} o wyliadau",
|
||||
"generic_views_count_5": "{{count}} o wyliadau",
|
||||
"Answer": "Ateb",
|
||||
"Add to playlist: ": "Ychwanegu at y rhestr chwarae: ",
|
||||
"Add to playlist": "Ychwanegu at y rhestr chwarae",
|
||||
"generic_button_cancel": "Diddymu",
|
||||
"generic_button_rss": "RSS",
|
||||
"LIVE": "YN FYW",
|
||||
"Import YouTube watch history (.json)": "Mewnforio hanes gwylio YouTube (.json)",
|
||||
"generic_videos_count_0": "{{count}} fideo",
|
||||
"generic_videos_count_1": "{{count}} fideo",
|
||||
"generic_videos_count_2": "{{count}} fideo",
|
||||
"generic_videos_count_3": "{{count}} fideo",
|
||||
"generic_videos_count_4": "{{count}} fideo",
|
||||
"generic_videos_count_5": "{{count}} fideo",
|
||||
"generic_subscribers_count_0": "{{count}} tanysgrifiwr",
|
||||
"generic_subscribers_count_1": "{{count}} tanysgrifiwr",
|
||||
"generic_subscribers_count_2": "{{count}} danysgrifiwr",
|
||||
"generic_subscribers_count_3": "{{count}} thanysgrifiwr",
|
||||
"generic_subscribers_count_4": "{{count}} o danysgrifwyr",
|
||||
"generic_subscribers_count_5": "{{count}} o danysgrifwyr",
|
||||
"Authorize token?": "Awdurdodi'r tocyn?",
|
||||
"Authorize token for `x`?": "Awdurdodi'r tocyn ar gyfer `x`?",
|
||||
"English": "Saesneg",
|
||||
"English (United Kingdom)": "Saesneg (Y Deyrnas Unedig)",
|
||||
"English (United States)": "Saesneg (Yr Unol Daleithiau)",
|
||||
"Afrikaans": "Affricaneg",
|
||||
"English (auto-generated)": "Saesneg (awtomatig)",
|
||||
"Amharic": "Amhareg",
|
||||
"Albanian": "Albaneg",
|
||||
"Arabic": "Arabeg",
|
||||
"crash_page_report_issue": "Os nad yw'r awgrymiadau uchod wedi helpu, <a href=\"`x`\">codwch 'issue' newydd ar Github </a> (yn Saesneg, gorau oll) a chynnwys y testun canlynol yn eich neges (peidiwch â chyfieithu'r testun hwn):",
|
||||
"Search for videos": "Chwilio am fideos",
|
||||
"The Popular feed has been disabled by the administrator.": "Mae'r ffrwd fideos poblogaidd wedi ei hanalluogi gan y gweinyddwr.",
|
||||
"generic_channels_count_0": "{{count}} sianel",
|
||||
"generic_channels_count_1": "{{count}} sianel",
|
||||
"generic_channels_count_2": "{{count}} sianel",
|
||||
"generic_channels_count_3": "{{count}} sianel",
|
||||
"generic_channels_count_4": "{{count}} sianel",
|
||||
"generic_channels_count_5": "{{count}} sianel",
|
||||
"generic_button_delete": "Dileu",
|
||||
"generic_button_edit": "Golygu",
|
||||
"generic_button_save": "Cadw",
|
||||
"Shared `x` ago": "Rhannwyd `x` yn ôl",
|
||||
"Unsubscribe": "Dad-danysgrifio",
|
||||
"Subscribe": "Tanysgrifio",
|
||||
"View channel on YouTube": "Gweld y sianel ar YouTube",
|
||||
"View playlist on YouTube": "Gweld y rhestr chwarae ar YouTube",
|
||||
"newest": "diweddaraf",
|
||||
"oldest": "hynaf",
|
||||
"popular": "poblogaidd",
|
||||
"Next page": "Tudalen nesaf",
|
||||
"Previous page": "Tudalen flaenorol",
|
||||
"Clear watch history?": "Clirio'ch hanes gwylio?",
|
||||
"New password": "Cyfrinair newydd",
|
||||
"Import and Export Data": "Mewnforio ac allforio data",
|
||||
"Import": "Mewnforio",
|
||||
"Import Invidious data": "Mewnforio data JSON Invidious",
|
||||
"Import YouTube subscriptions": "Mewnforio tanysgrifiadau YouTube ar fformat CSV neu OPML",
|
||||
"Import YouTube playlist (.csv)": "Mewnforio rhestr chwarae YouTube (.csv)",
|
||||
"Export": "Allforio",
|
||||
"Export data as JSON": "Allforio data Invidious ar fformat JSON",
|
||||
"Delete account?": "Ydych chi'n siŵr yr hoffech chi ddileu eich cyfrif?",
|
||||
"History": "Hanes",
|
||||
"JavaScript license information": "Gwybodaeth am y drwydded JavaScript",
|
||||
"generic_subscriptions_count_0": "{{count}} tanysgrifiad",
|
||||
"generic_subscriptions_count_1": "{{count}} tanysgrifiad",
|
||||
"generic_subscriptions_count_2": "{{count}} danysgrifiad",
|
||||
"generic_subscriptions_count_3": "{{count}} thanysgrifiad",
|
||||
"generic_subscriptions_count_4": "{{count}} o danysgrifiadau",
|
||||
"generic_subscriptions_count_5": "{{count}} o danysgrifiadau",
|
||||
"Yes": "Iawn",
|
||||
"No": "Na",
|
||||
"Import FreeTube subscriptions (.db)": "Mewnforio tanysgrifiadau FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Mewnforio tanysgrifiadau NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Mewnforio data NewPipe (.zip)",
|
||||
"An alternative front-end to YouTube": "Pen blaen amgen i YouTube",
|
||||
"source": "ffynhonnell",
|
||||
"Log in": "Mewngofnodi",
|
||||
"Log in/register": "Mewngofnodi/Cofrestru",
|
||||
"User ID": "Enw defnyddiwr",
|
||||
"preferences_quality_option_dash": "DASH (ansawdd addasol)",
|
||||
"Sign In": "Mewngofnodi",
|
||||
"Register": "Cofrestru",
|
||||
"E-mail": "Ebost",
|
||||
"Preferences": "Hoffterau",
|
||||
"preferences_category_player": "Hoffterau'r chwaraeydd",
|
||||
"preferences_autoplay_label": "Chwarae'n awtomatig: ",
|
||||
"preferences_local_label": "Llwytho fideos drwy ddirprwy weinydd: ",
|
||||
"preferences_watch_history_label": "Galluogi hanes gwylio: ",
|
||||
"preferences_speed_label": "Cyflymder rhagosodedig: ",
|
||||
"preferences_quality_label": "Ansawdd fideos: ",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
"preferences_quality_option_medium": "Canolig",
|
||||
"preferences_quality_option_small": "Bach",
|
||||
"preferences_quality_dash_option_2160p": "2160p",
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"preferences_quality_dash_option_720p": "720p",
|
||||
"invidious": "Invidious",
|
||||
"Text CAPTCHA": "CAPTCHA testun",
|
||||
"Image CAPTCHA": "CAPTCHA delwedd",
|
||||
"preferences_continue_label": "Chwarae'r fideo nesaf fel rhagosodiad: ",
|
||||
"preferences_continue_autoplay_label": "Chwarae'r fideo nesaf yn awtomatig: ",
|
||||
"preferences_listen_label": "Sain yn unig: ",
|
||||
"preferences_quality_dash_label": "Ansawdd fideos DASH a ffefrir: ",
|
||||
"preferences_volume_label": "Uchder sain y chwaraeydd: ",
|
||||
"preferences_category_visual": "Hoffterau'r wefan",
|
||||
"preferences_region_label": "Gwlad y cynnwys: ",
|
||||
"preferences_player_style_label": "Arddull y chwaraeydd: ",
|
||||
"Dark mode: ": "Modd tywyll: ",
|
||||
"preferences_thin_mode_label": "Modd tenau: ",
|
||||
"preferences_category_misc": "Hoffterau amrywiol",
|
||||
"preferences_category_subscription": "Hoffterau tanysgrifio",
|
||||
"preferences_max_results_label": "Nifer o fideos a ddangosir yn eich ffrwd: ",
|
||||
"alphabetically": "yr wyddor",
|
||||
"alphabetically - reverse": "yr wyddor - am yn ôl",
|
||||
"published - reverse": "dyddiad cyhoeddi - am yn ôl",
|
||||
"channel name": "enw'r sianel",
|
||||
"channel name - reverse": "enw'r sianel - am yn ôl",
|
||||
"Only show latest video from channel: ": "Dangos fideo diweddaraf y sianeli rydych chi'n tanysgrifio iddynt: ",
|
||||
"Only show latest unwatched video from channel: ": "Dangos fideo heb ei wylio diweddaraf y sianeli rydych chi'n tanysgrifio iddynt: ",
|
||||
"Enable web notifications": "Galluogi hysbysiadau gwe",
|
||||
"`x` uploaded a video": "uwchlwythodd `x` fideo",
|
||||
"`x` is live": "mae `x` yn darlledu'n fyw",
|
||||
"preferences_category_data": "Hoffterau data",
|
||||
"Clear watch history": "Clirio'ch hanes gwylio",
|
||||
"Change password": "Newid eich cyfrinair",
|
||||
"Manage subscriptions": "Rheoli tanysgrifiadau",
|
||||
"Manage tokens": "Rheoli tocynnau",
|
||||
"Watch history": "Hanes gwylio",
|
||||
"preferences_default_home_label": "Hafan ragosodedig: ",
|
||||
"preferences_show_nick_label": "Dangos eich enw defnyddiwr ar frig y dudalen: ",
|
||||
"preferences_annotations_label": "Dangos nodiadau fel rhagosodiad: ",
|
||||
"preferences_unseen_only_label": "Dangos fideos heb eu gwylio yn unig: ",
|
||||
"preferences_notifications_only_label": "Dangos hysbysiadau yn unig (os oes unrhyw rai): ",
|
||||
"Token manager": "Rheolydd tocynnau",
|
||||
"Token": "Tocyn",
|
||||
"unsubscribe": "dad-danysgrifio",
|
||||
"Subscriptions": "Tanysgrifiadau",
|
||||
"Import/export": "Mewngofnodi/allgofnodi",
|
||||
"search": "chwilio",
|
||||
"Log out": "Allgofnodi",
|
||||
"View privacy policy.": "Polisi preifatrwydd",
|
||||
"Trending": "Pynciau llosg",
|
||||
"Public": "Cyhoeddus",
|
||||
"Private": "Preifat",
|
||||
"Updated `x` ago": "Diweddarwyd `x` yn ôl",
|
||||
"Delete playlist `x`?": "Ydych chi'n siŵr yr hoffech chi ddileu'r rhestr chwarae `x`?",
|
||||
"Title": "Teitl",
|
||||
"Playlist privacy": "Preifatrwydd y rhestr chwarae",
|
||||
"search_message_use_another_instance": " Gallwch hefyd <a href=\"`x`\">chwilio ar weinydd arall</a>.",
|
||||
"Popular enabled: ": "Tudalen fideos poblogaidd wedi'i galluogi: ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA wedi'i alluogi: ",
|
||||
"Registration enabled: ": "Cofrestru wedi'i alluogi: ",
|
||||
"Save preferences": "Cadw'r hoffterau",
|
||||
"Subscription manager": "Rheolydd tanysgrifio",
|
||||
"revoke": "tynnu",
|
||||
"subscriptions_unseen_notifs_count_0": "{{count}} hysbysiad heb ei weld",
|
||||
"subscriptions_unseen_notifs_count_1": "{{count}} hysbysiad heb ei weld",
|
||||
"subscriptions_unseen_notifs_count_2": "{{count}} hysbysiad heb eu gweld",
|
||||
"subscriptions_unseen_notifs_count_3": "{{count}} hysbysiad heb eu gweld",
|
||||
"subscriptions_unseen_notifs_count_4": "{{count}} hysbysiad heb eu gweld",
|
||||
"subscriptions_unseen_notifs_count_5": "{{count}} hysbysiad heb eu gweld",
|
||||
"Released under the AGPLv3 on Github.": "Cyhoeddwyd dan drwydded AGPLv3 ar GitHub",
|
||||
"Unlisted": "Heb ei restru",
|
||||
"Switch Invidious Instance": "Newid gweinydd Invidious",
|
||||
"Report statistics: ": "Galluogi ystadegau'r gweinydd: ",
|
||||
"View all playlists": "Gweld pob rhestr chwarae",
|
||||
"Editing playlist `x`": "Yn golygu'r rhestr chwarae `x`",
|
||||
"Whitelisted regions: ": "Rhanbarthau a ganiateir: ",
|
||||
"Blacklisted regions: ": "Rhanbarthau a rwystrir: ",
|
||||
"Song: ": "Cân: ",
|
||||
"Album: ": "Albwm: ",
|
||||
"Shared `x`": "Rhannwyd `x`",
|
||||
"View YouTube comments": "Dangos sylwadau YouTube",
|
||||
"View more comments on Reddit": "Dangos rhagor o sylwadau ar Reddit",
|
||||
"View Reddit comments": "Dangos sylwadau Reddit",
|
||||
"Hide replies": "Cuddio ymatebion",
|
||||
"Incorrect password": "Cyfrinair anghywir",
|
||||
"Wrong answer": "Ateb anghywir",
|
||||
"CAPTCHA is a required field": "Rhaid rhoi'r CAPTCHA",
|
||||
"User ID is a required field": "Rhaid rhoi enw defnyddiwr",
|
||||
"Password is a required field": "Rhaid rhoi cyfrinair",
|
||||
"Wrong username or password": "Enw defnyddiwr neu gyfrinair anghywir",
|
||||
"Password cannot be empty": "All y cyfrinair ddim bod yn wag",
|
||||
"Password cannot be longer than 55 characters": "All y cyfrinair ddim bod yn hirach na 55 nod",
|
||||
"Please log in": "Mewngofnodwch",
|
||||
"channel:`x`": "sianel: `x`",
|
||||
"Deleted or invalid channel": "Sianel wedi'i dileu neu'n annilys",
|
||||
"Could not get channel info.": "Wedi methu llwytho gwybodaeth y sianel.",
|
||||
"`x` ago": "`x` yn ôl",
|
||||
"Load more": "Llwytho rhagor",
|
||||
"Empty playlist": "Rhestr chwarae wag",
|
||||
"Hide annotations": "Cuddio nodiadau",
|
||||
"Show annotations": "Dangos nodiadau",
|
||||
"Premieres in `x`": "Yn dechrau mewn `x`",
|
||||
"Premieres `x`": "Yn dechrau `x`",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Helo! Mae'n ymddangos eich bod wedi diffodd JavaScript. Cliciwch yma i weld sylwadau, ond cofiwch y gall gymryd mwy o amser i'w llwytho.",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Gweld `x` sylw",
|
||||
"": "Gweld `x` sylw"
|
||||
},
|
||||
"Could not create mix.": "Wedi methu creu'r cymysgiad hwn.",
|
||||
"Erroneous token": "Tocyn annilys",
|
||||
"No such user": "Dyw'r defnyddiwr hwn ddim yn bodoli",
|
||||
"Token is expired, please try again": "Mae'r tocyn hwn wedi dod i ben, ceisiwch eto",
|
||||
"Bangla": "Bangleg",
|
||||
"Basque": "Basgeg",
|
||||
"Bulgarian": "Bwlgareg",
|
||||
"Catalan": "Catalaneg",
|
||||
"Chinese": "Tsieineeg",
|
||||
"Chinese (China)": "Tsieineeg (Tsieina)",
|
||||
"Chinese (Hong Kong)": "Tsieineeg (Hong Kong)",
|
||||
"Chinese (Taiwan)": "Tsieineeg (Taiwan)",
|
||||
"Danish": "Daneg",
|
||||
"Dutch": "Iseldireg",
|
||||
"Esperanto": "Esperanteg",
|
||||
"Finnish": "Ffinneg",
|
||||
"French": "Ffrangeg",
|
||||
"German": "Almaeneg",
|
||||
"Greek": "Groeg",
|
||||
"Could not pull trending pages.": "Wedi methu llwytho tudalennau pynciau llosg.",
|
||||
"Hidden field \"challenge\" is a required field": "Mae'r maes cudd \"her\" yn ofynnol",
|
||||
"Hidden field \"token\" is a required field": "Mae'r maes cudd \"tocyn\" yn ofynnol",
|
||||
"Hebrew": "Hebraeg",
|
||||
"Hungarian": "Hwngareg",
|
||||
"Irish": "Gwyddeleg",
|
||||
"Italian": "Eidaleg",
|
||||
"Welsh": "Cymraeg",
|
||||
"generic_count_hours_0": "{{count}} awr",
|
||||
"generic_count_hours_1": "{{count}} awr",
|
||||
"generic_count_hours_2": "{{count}} awr",
|
||||
"generic_count_hours_3": "{{count}} awr",
|
||||
"generic_count_hours_4": "{{count}} awr",
|
||||
"generic_count_hours_5": "{{count}} awr",
|
||||
"generic_count_minutes_0": "{{count}} munud",
|
||||
"generic_count_minutes_1": "{{count}} munud",
|
||||
"generic_count_minutes_2": "{{count}} funud",
|
||||
"generic_count_minutes_3": "{{count}} munud",
|
||||
"generic_count_minutes_4": "{{count}} o funudau",
|
||||
"generic_count_minutes_5": "{{count}} o funudau",
|
||||
"generic_count_weeks_0": "{{count}} wythnos",
|
||||
"generic_count_weeks_1": "{{count}} wythnos",
|
||||
"generic_count_weeks_2": "{{count}} wythnos",
|
||||
"generic_count_weeks_3": "{{count}} wythnos",
|
||||
"generic_count_weeks_4": "{{count}} wythnos",
|
||||
"generic_count_weeks_5": "{{count}} wythnos",
|
||||
"generic_count_seconds_0": "{{count}} eiliad",
|
||||
"generic_count_seconds_1": "{{count}} eiliad",
|
||||
"generic_count_seconds_2": "{{count}} eiliad",
|
||||
"generic_count_seconds_3": "{{count}} eiliad",
|
||||
"generic_count_seconds_4": "{{count}} o eiliadau",
|
||||
"generic_count_seconds_5": "{{count}} o eiliadau",
|
||||
"Fallback comments: ": "Sylwadau amgen: ",
|
||||
"Popular": "Poblogaidd",
|
||||
"preferences_locale_label": "Iaith: ",
|
||||
"About": "Ynghylch",
|
||||
"Search": "Chwilio",
|
||||
"search_filters_features_option_c_commons": "Comin Creu",
|
||||
"search_filters_features_option_subtitles": "Isdeitlau (CC)",
|
||||
"search_filters_features_option_hd": "HD",
|
||||
"permalink": "dolen barhaol",
|
||||
"search_filters_duration_option_short": "Byr (< 4 munud)",
|
||||
"search_filters_duration_option_none": "Unrhyw hyd",
|
||||
"search_filters_duration_label": "Hyd",
|
||||
"search_filters_type_option_show": "Rhaglen",
|
||||
"search_filters_type_option_movie": "Ffilm",
|
||||
"search_filters_type_option_playlist": "Rhestr chwarae",
|
||||
"search_filters_type_option_channel": "Sianel",
|
||||
"search_filters_type_option_video": "Fideo",
|
||||
"search_filters_type_option_all": "Unrhyw fath",
|
||||
"search_filters_date_option_week": "Yr wythnos hon",
|
||||
"search_filters_date_option_today": "Heddiw",
|
||||
"search_filters_date_option_hour": "Yr awr ddiwethaf",
|
||||
"search_filters_date_option_none": "Unrhyw ddyddiad",
|
||||
"search_filters_date_label": "Dyddiad uwchlwytho",
|
||||
"search_filters_title": "Hidlyddion",
|
||||
"Playlists": "Rhestrau chwarae",
|
||||
"Video mode": "Modd fideo",
|
||||
"Audio mode": "Modd sain",
|
||||
"Channel Sponsor": "Noddwr y sianel",
|
||||
"(edited)": "(golygwyd)",
|
||||
"Download": "Islwytho",
|
||||
"Movies": "Ffilmiau",
|
||||
"News": "Newyddion",
|
||||
"Gaming": "Gemau",
|
||||
"Music": "Cerddoriaeth",
|
||||
"Download is disabled": "Mae islwytho wedi'i analluogi",
|
||||
"Download as: ": "Islwytho fel: ",
|
||||
"View as playlist": "Gweld fel rhestr chwarae",
|
||||
"Default": "Rhagosodiad",
|
||||
"YouTube comment permalink": "Dolen barhaol i'r sylw ar YouTube",
|
||||
"crash_page_before_reporting": "Cyn adrodd nam, sicrhewch eich bod wedi:",
|
||||
"crash_page_search_issue": "<a href=\"`x`\">chwilio am y nam ar GitHub</a>",
|
||||
"videoinfo_watch_on_youTube": "Gwylio ar YouTube",
|
||||
"videoinfo_started_streaming_x_ago": "Yn ffrydio'n fyw ers `x` o funudau",
|
||||
"videoinfo_invidious_embed_link": "Dolen mewnblannu",
|
||||
"footer_documentation": "Dogfennaeth",
|
||||
"footer_donate_page": "Rhoddi",
|
||||
"Current version: ": "Fersiwn gyfredol: ",
|
||||
"search_filters_apply_button": "Rhoi'r hidlyddion ar waith",
|
||||
"search_filters_sort_option_date": "Dyddiad uwchlwytho",
|
||||
"search_filters_sort_option_relevance": "Perthnasedd",
|
||||
"search_filters_sort_label": "Trefnu yn ôl",
|
||||
"search_filters_features_option_location": "Lleoliad",
|
||||
"search_filters_features_option_hdr": "HDR",
|
||||
"search_filters_features_option_three_d": "3D",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_features_option_three_sixty": "360°",
|
||||
"videoinfo_youTube_embed_link": "Mewnblannu",
|
||||
"download_subtitles": "Isdeitlau - `x` (.vtt)",
|
||||
"user_created_playlists": "`x` rhestr chwarae wedi'u creu",
|
||||
"user_saved_playlists": "`x` rhestr chwarae wedi'u cadw",
|
||||
"Video unavailable": "Fideo ddim ar gael",
|
||||
"crash_page_you_found_a_bug": "Mae'n debyg eich bod wedi dod o hyd i nam yn Invidious!",
|
||||
"channel_tab_channels_label": "Sianeli",
|
||||
"channel_tab_community_label": "Cymuned",
|
||||
"channel_tab_shorts_label": "Fideos byrion",
|
||||
"channel_tab_videos_label": "Fideos"
|
||||
}
|
@ -11,7 +11,6 @@
|
||||
"last": "neueste",
|
||||
"Next page": "Nächste Seite",
|
||||
"Previous page": "Vorherige Seite",
|
||||
"First page": "Erste Seite",
|
||||
"Clear watch history?": "Verlauf löschen?",
|
||||
"New password": "Neues Passwort",
|
||||
"New passwords must match": "Neue Passwörter müssen übereinstimmen",
|
||||
@ -22,7 +21,7 @@
|
||||
"Import and Export Data": "Daten importieren und exportieren",
|
||||
"Import": "Importieren",
|
||||
"Import Invidious data": "Invidious-JSON-Daten importieren",
|
||||
"Import YouTube subscriptions": "YouTube-CSV/OPML-Abonnements importieren",
|
||||
"Import YouTube subscriptions": "YouTube-/OPML-Abonnements importieren",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTube Abonnements importieren (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipe Abonnements importieren (.json)",
|
||||
"Import NewPipe data (.zip)": "NewPipe Daten importieren (.zip)",
|
||||
@ -48,7 +47,6 @@
|
||||
"Preferences": "Einstellungen",
|
||||
"preferences_category_player": "Wiedergabeeinstellungen",
|
||||
"preferences_video_loop_label": "Immer wiederholen: ",
|
||||
"preferences_preload_label": "Videodaten vorladen: ",
|
||||
"preferences_autoplay_label": "Automatisch abspielen: ",
|
||||
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
|
||||
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
|
||||
@ -324,7 +322,7 @@
|
||||
"channel_tab_community_label": "Gemeinschaft",
|
||||
"search_filters_sort_option_relevance": "Relevanz",
|
||||
"search_filters_sort_option_rating": "Bewertung",
|
||||
"search_filters_sort_option_date": "Hochladedatum",
|
||||
"search_filters_sort_option_date": "Datum",
|
||||
"search_filters_sort_option_views": "Aufrufe",
|
||||
"search_filters_type_label": "Inhaltstyp",
|
||||
"search_filters_duration_label": "Dauer",
|
||||
@ -456,7 +454,7 @@
|
||||
"Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)",
|
||||
"search_filters_title": "Filtern",
|
||||
"search_message_change_filters_or_query": "Versuchen Sie, Ihre Suchanfrage zu erweitern und/oder die Filter zu ändern.",
|
||||
"search_message_use_another_instance": "Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.",
|
||||
"search_message_use_another_instance": " Sie können auch <a href=\"`x`\">auf einer anderen Instanz suchen</a>.",
|
||||
"Popular enabled: ": "„Beliebt“-Seite aktiviert: ",
|
||||
"search_message_no_results": "Keine Ergebnisse gefunden.",
|
||||
"search_filters_duration_option_medium": "Mittel (4 - 20 Minuten)",
|
||||
@ -491,13 +489,9 @@
|
||||
"generic_channels_count_plural": "{{count}} Kanäle",
|
||||
"Import YouTube watch history (.json)": "YouTube Wiedergabeverlauf importieren (.json)",
|
||||
"Answer": "Antwort",
|
||||
"The Popular feed has been disabled by the administrator.": "Der Feed für beliebte Inhalte wurde vom Administrator deaktiviert.",
|
||||
"The Popular feed has been disabled by the administrator.": "Der Angesagt-Feed wurde vom Administrator deaktiviert.",
|
||||
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
|
||||
"Search for videos": "Nach Videos suchen",
|
||||
"toggle_theme": "Thema wechseln",
|
||||
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
|
||||
"carousel_go_to": "Zu Element `x` springen",
|
||||
"carousel_slide": "Seite {{current}} von {{total}}",
|
||||
"carousel_skip": "Galerie überspringen",
|
||||
"Filipino (auto-generated)": "Philippinisch (automatisch generiert)"
|
||||
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: "
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"Import and Export Data": "Εισαγωγή και Εξαγωγή Δεδομένων",
|
||||
"Import": "Εισαγωγή",
|
||||
"Import Invidious data": "Εsαγωγή δεδομένων Invidious JSON",
|
||||
"Import YouTube subscriptions": "Εισαγωγή συνδρομών YouTube απο CVS/OPML",
|
||||
"Import YouTube subscriptions": "Εισαγωγή συνδρομών YouTube/OPML",
|
||||
"Import FreeTube subscriptions (.db)": "Εισαγωγή συνδρομών FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Εισαγωγή συνδρομών NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Εισαγωγή δεδομένων NewPipe (.zip)",
|
||||
@ -455,7 +455,7 @@
|
||||
"channel_tab_streams_label": "Ζωντανή μετάδοση",
|
||||
"playlist_button_add_items": "Προσθήκη βίντεο",
|
||||
"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_cancel": "Ακύρωση",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} μη αναγνωσμένη ειδοποίηση",
|
||||
@ -486,17 +486,5 @@
|
||||
"Switch Invidious Instance": "Αλλαγή Instance Invidious",
|
||||
"Standard YouTube license": "Τυπική άδεια YouTube",
|
||||
"search_filters_duration_option_medium": "Μεσαία (4 - 20 λεπτά)",
|
||||
"search_filters_date_label": "Ημερομηνία αναφόρτωσης",
|
||||
"Search for videos": "Αναζήτηση βίντεο",
|
||||
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
|
||||
"Answer": "Απάντηση",
|
||||
"Add to playlist": "Προσθήκη στην λίιστα αναπαραγωγής",
|
||||
"Add to playlist: ": "Προσθήκη στην λίστα αναπαραγωγής : ",
|
||||
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
|
||||
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
|
||||
"toggle_theme": "Αλλαγή θέματος",
|
||||
"Import YouTube watch history (.json)": "Εισαγωγή ιστορικού προβολής YouTube (.json)",
|
||||
"Filipino (auto-generated)": "Φιλιππινέζικα (αυτόματη παραγωγή)",
|
||||
"preferences_preload_label": "Προφόρτιση δεδομένων βίντεο: ",
|
||||
"carousel_skip": "Αποφυγή εμφάνισης εικόνων"
|
||||
"search_filters_date_label": "Ημερομηνία αναφόρτωσης"
|
||||
}
|
||||
|
@ -33,7 +33,6 @@
|
||||
"last": "last",
|
||||
"Next page": "Next page",
|
||||
"Previous page": "Previous page",
|
||||
"First page": "First page",
|
||||
"Clear watch history?": "Clear watch history?",
|
||||
"New password": "New password",
|
||||
"New passwords must match": "New passwords must match",
|
||||
@ -72,7 +71,6 @@
|
||||
"Preferences": "Preferences",
|
||||
"preferences_category_player": "Player preferences",
|
||||
"preferences_video_loop_label": "Always loop: ",
|
||||
"preferences_preload_label": "Preload video data: ",
|
||||
"preferences_autoplay_label": "Autoplay: ",
|
||||
"preferences_continue_label": "Play next by default: ",
|
||||
"preferences_continue_autoplay_label": "Autoplay next video: ",
|
||||
@ -192,7 +190,7 @@
|
||||
"Switch Invidious Instance": "Switch Invidious Instance",
|
||||
"search_message_no_results": "No results found.",
|
||||
"search_message_change_filters_or_query": "Try widening your search query and/or changing the filters.",
|
||||
"search_message_use_another_instance": "You can also <a href=\"`x`\">search on another instance</a>.",
|
||||
"search_message_use_another_instance": " You can also <a href=\"`x`\">search on another instance</a>.",
|
||||
"Hide annotations": "Hide annotations",
|
||||
"Show annotations": "Show annotations",
|
||||
"Genre: ": "Genre: ",
|
||||
@ -287,7 +285,6 @@
|
||||
"Esperanto": "Esperanto",
|
||||
"Estonian": "Estonian",
|
||||
"Filipino": "Filipino",
|
||||
"Filipino (auto-generated)": "Filipino (auto-generated)",
|
||||
"Finnish": "Finnish",
|
||||
"French": "French",
|
||||
"French (auto-generated)": "French (auto-generated)",
|
||||
@ -425,7 +422,7 @@
|
||||
"search_filters_title": "Filters",
|
||||
"search_filters_date_label": "Upload date",
|
||||
"search_filters_date_option_none": "Any date",
|
||||
"search_filters_date_option_hour": "Last hour",
|
||||
"search_filters_date_option_hour": "Last Hour",
|
||||
"search_filters_date_option_today": "Today",
|
||||
"search_filters_date_option_week": "This week",
|
||||
"search_filters_date_option_month": "This month",
|
||||
@ -457,7 +454,7 @@
|
||||
"search_filters_sort_label": "Sort By",
|
||||
"search_filters_sort_option_relevance": "Relevance",
|
||||
"search_filters_sort_option_rating": "Rating",
|
||||
"search_filters_sort_option_date": "Upload date",
|
||||
"search_filters_sort_option_date": "Upload Date",
|
||||
"search_filters_sort_option_views": "View count",
|
||||
"search_filters_apply_button": "Apply selected filters",
|
||||
"Current version: ": "Current version: ",
|
||||
@ -493,10 +490,8 @@
|
||||
"channel_tab_streams_label": "Livestreams",
|
||||
"channel_tab_podcasts_label": "Podcasts",
|
||||
"channel_tab_releases_label": "Releases",
|
||||
"channel_tab_courses_label": "Courses",
|
||||
"channel_tab_playlists_label": "Playlists",
|
||||
"channel_tab_community_label": "Community",
|
||||
"channel_tab_posts_label": "Posts",
|
||||
"channel_tab_channels_label": "Channels",
|
||||
"toggle_theme": "Toggle Theme",
|
||||
"carousel_slide": "Slide {{current}} of {{total}}",
|
||||
|
@ -478,7 +478,7 @@
|
||||
"tokens_count_0": "{{count}} token",
|
||||
"tokens_count_1": "{{count}} tokens",
|
||||
"tokens_count_2": "{{count}} tokens",
|
||||
"search_message_use_another_instance": "También puedes <a href=\"`x`\">buscar en otra instancia</a>.",
|
||||
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
|
||||
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
||||
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
|
||||
"channel_tab_streams_label": "Directos",
|
||||
@ -513,7 +513,5 @@
|
||||
"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_skip": "Saltar el carrusel",
|
||||
"carousel_go_to": "Ir a la diapositiva `x`",
|
||||
"preferences_preload_label": "Precargar datos del vídeo: ",
|
||||
"Filipino (auto-generated)": "Filipino (generado automáticamente)"
|
||||
"carousel_go_to": "Ir a la diapositiva `x`"
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
"View playlist on YouTube": "دیدن فهرست پخش در یوتیوب",
|
||||
"newest": "تازهترین",
|
||||
"oldest": "کهنهترین",
|
||||
"popular": "پرطرفدار",
|
||||
"popular": "محبوب",
|
||||
"last": "آخرین",
|
||||
"Next page": "صفحه بعد",
|
||||
"Previous page": "صفحه قبل",
|
||||
@ -31,7 +31,7 @@
|
||||
"Import and Export Data": "درونبرد و برونبرد داده",
|
||||
"Import": "درونبرد",
|
||||
"Import Invidious data": "وارد کردن داده JSON اینویدیوس",
|
||||
"Import YouTube subscriptions": "وارد کردن فایل CSV یا OPML سابسکرایب های یوتیوب",
|
||||
"Import YouTube subscriptions": "وارد کردن اشتراک OPML/ یوتیوب",
|
||||
"Import FreeTube subscriptions (.db)": "درونبرد اشتراکهای فریتیوب (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "درونبرد اشتراکهای نیوپایپ (.json)",
|
||||
"Import NewPipe data (.zip)": "درونبرد داده نیوپایپ (.zip)",
|
||||
@ -328,7 +328,7 @@
|
||||
"generic_count_seconds": "{{count}} ثانیه",
|
||||
"generic_count_seconds_plural": "{{count}} ثانیه",
|
||||
"Fallback comments: ": "نظرات عقب گرد: ",
|
||||
"Popular": "پربیننده",
|
||||
"Popular": "محبوب",
|
||||
"Search": "جست و جو",
|
||||
"Top": "بالا",
|
||||
"About": "درباره",
|
||||
@ -360,7 +360,7 @@
|
||||
"search_filters_duration_label": "مدت",
|
||||
"search_filters_features_label": "ویژگیها",
|
||||
"search_filters_sort_label": "به ترتیب",
|
||||
"search_filters_date_option_hour": "ساعت گذشته",
|
||||
"search_filters_date_option_hour": "یک ساعت گذشته",
|
||||
"search_filters_date_option_today": "امروز",
|
||||
"search_filters_date_option_week": "این هفته",
|
||||
"search_filters_date_option_month": "این ماه",
|
||||
@ -461,7 +461,7 @@
|
||||
"Song: ": "آهنگ: ",
|
||||
"Channel Sponsor": "اسپانسر کانال",
|
||||
"Standard YouTube license": "پروانه استاندارد YouTube",
|
||||
"search_message_use_another_instance": "همچنین میتوانید <a href=\"`x`\">در نمونهای دیگر هم جستوجو کنید</a>.",
|
||||
"search_message_use_another_instance": " شما همچنین میتوانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>.",
|
||||
"Download is disabled": "دریافت غیرفعال است",
|
||||
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
|
||||
"playlist_button_add_items": "افزودن ویدیو",
|
||||
@ -484,18 +484,5 @@
|
||||
"channel_tab_shorts_label": "Shortها",
|
||||
"channel_tab_playlists_label": "فهرستهای پخش",
|
||||
"channel_tab_channels_label": "کانالها",
|
||||
"error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. <a href=\"`x`\">کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید.</a>",
|
||||
"Add to playlist": "به لیست پخش افزوده شود",
|
||||
"Answer": "پاسخ",
|
||||
"Search for videos": "جست و جو برای ویدیوها",
|
||||
"Add to playlist: ": "افزودن به لیست پخش ",
|
||||
"The Popular feed has been disabled by the administrator.": "بخش ویدیوهای پرطرفدار توسط مدیر غیرفعال شده است.",
|
||||
"carousel_slide": "اسلاید {{current}} از {{total}}",
|
||||
"carousel_skip": "رد شدن از گرداننده",
|
||||
"carousel_go_to": "به اسلاید `x` برو",
|
||||
"crash_page_search_issue": "دنبال <a href=\"`x`\"> گشتیم بین مشکلات در گیت هاب </a>",
|
||||
"crash_page_report_issue": "اگر هیچ یک از روش های بالا کمکی نکردند لطفا <a href=\"`x`\"> (ترجیحا به انگلیسی) یک سوال جدید در گیت هاب بپرسید و </a> طوری که سوالتون شامل متن زیر باشه:",
|
||||
"channel_tab_releases_label": "آثار",
|
||||
"toggle_theme": "تغییر وضعیت تم",
|
||||
"preferences_preload_label": "پیش بار کردن دادههای ویدیو: "
|
||||
"error_video_not_in_playlist": "ویدیوی درخواستی معلق به این فهرست پخش نیست. <a href=\"`x`\">کلیک کنید تا به صفحهٔ اصلی فهرست پخش بروید.</a>"
|
||||
}
|
||||
|
123
locales/fi.json
123
locales/fi.json
@ -28,7 +28,7 @@
|
||||
"Export": "Vie",
|
||||
"Export subscriptions as OPML": "Vie tilaukset OPML-muodossa",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Vie tilaukset OPML-muodossa (NewPipe & FreeTube)",
|
||||
"Export data as JSON": "Vie Invidiousin tiedot JSON-muodossa",
|
||||
"Export data as JSON": "Vie Invidious-data JSON-muodossa",
|
||||
"Delete account?": "Poista tili?",
|
||||
"History": "Historia",
|
||||
"An alternative front-end to YouTube": "Vaihtoehtoinen front-end YouTubelle",
|
||||
@ -46,12 +46,12 @@
|
||||
"E-mail": "Sähköposti",
|
||||
"Preferences": "Asetukset",
|
||||
"preferences_category_player": "Soittimen asetukset",
|
||||
"preferences_video_loop_label": "Toista aina uudelleen: ",
|
||||
"preferences_autoplay_label": "Automaattinen toiston aloitus: ",
|
||||
"preferences_video_loop_label": "Toista jatkuvasti aina: ",
|
||||
"preferences_autoplay_label": "Automaattinen toisto: ",
|
||||
"preferences_continue_label": "Toista seuraava oletuksena: ",
|
||||
"preferences_continue_autoplay_label": "Aloita seuraava video automaattisesti: ",
|
||||
"preferences_continue_autoplay_label": "Toista seuraava video automaattisesti: ",
|
||||
"preferences_listen_label": "Kuuntele oletuksena: ",
|
||||
"preferences_local_label": "Videot välityspalvelimen kautta: ",
|
||||
"preferences_local_label": "Proxytä videot: ",
|
||||
"preferences_speed_label": "Oletusnopeus: ",
|
||||
"preferences_quality_label": "Ensisijainen videon laatu: ",
|
||||
"preferences_volume_label": "Soittimen äänenvoimakkuus: ",
|
||||
@ -63,7 +63,7 @@
|
||||
"preferences_related_videos_label": "Näytä aiheeseen liittyviä videoita: ",
|
||||
"preferences_annotations_label": "Näytä huomautukset oletuksena: ",
|
||||
"preferences_extend_desc_label": "Laajenna automaattisesti videon kuvausta: ",
|
||||
"preferences_vr_mode_label": "Interaktiiviset 360-videot (vaatii WebGL:n): ",
|
||||
"preferences_vr_mode_label": "Interaktiiviset 360-asteiset videot (vaatii WebGL:n): ",
|
||||
"preferences_category_visual": "Visuaaliset asetukset",
|
||||
"preferences_player_style_label": "Soittimen tyyli: ",
|
||||
"Dark mode: ": "Tumma tila: ",
|
||||
@ -137,9 +137,9 @@
|
||||
"Show less": "Näytä vähemmän",
|
||||
"Watch on YouTube": "Katso YouTubessa",
|
||||
"Switch Invidious Instance": "Vaihda Invidious-instanssia",
|
||||
"Hide annotations": "Piilota huomautukset",
|
||||
"Show annotations": "Näytä huomautukset",
|
||||
"Genre: ": "Tyylilaji: ",
|
||||
"Hide annotations": "Piilota merkkaukset",
|
||||
"Show annotations": "Näytä merkkaukset",
|
||||
"Genre: ": "Genre: ",
|
||||
"License: ": "Lisenssi: ",
|
||||
"Family friendly? ": "Kaiken ikäisille sopiva? ",
|
||||
"Wilson score: ": "Wilson-pistemäärä: ",
|
||||
@ -168,7 +168,7 @@
|
||||
"Wrong username or password": "Väärä käyttäjänimi tai salasana",
|
||||
"Password cannot be empty": "Salasana ei voi olla tyhjä",
|
||||
"Password cannot be longer than 55 characters": "Salasana ei voi olla yli 55 merkkiä pitkä",
|
||||
"Please log in": "Kirjaudu sisään",
|
||||
"Please log in": "Kirjaudu sisään, ole hyvä",
|
||||
"Invidious Private Feed for `x`": "Invidiousin yksityinen syöte `x`:lle",
|
||||
"channel:`x`": "kanava:`x`",
|
||||
"Deleted or invalid channel": "Poistettu tai virheellinen kanava",
|
||||
@ -178,7 +178,7 @@
|
||||
"`x` ago": "`x` sitten",
|
||||
"Load more": "Lataa lisää",
|
||||
"Could not create mix.": "Sekoituksen luominen epäonnistui.",
|
||||
"Empty playlist": "Tyhjä soittolista",
|
||||
"Empty playlist": "Tyhjennä soittolista",
|
||||
"Not a playlist.": "Ei ole soittolista.",
|
||||
"Playlist does not exist.": "Soittolistaa ei ole olemassa.",
|
||||
"Could not pull trending pages.": "Nousussa olevien sivujen lataus epäonnistui.",
|
||||
@ -216,11 +216,11 @@
|
||||
"Filipino": "filipino",
|
||||
"Finnish": "suomi",
|
||||
"French": "ranska",
|
||||
"Galician": "galicia",
|
||||
"Galician": "galego",
|
||||
"Georgian": "georgia",
|
||||
"German": "saksa",
|
||||
"Greek": "kreikka",
|
||||
"Gujarati": "gudžarati",
|
||||
"Gujarati": "gujarati",
|
||||
"Haitian Creole": "haitinkreoli",
|
||||
"Hausa": "hausa",
|
||||
"Hawaiian": "havaiji",
|
||||
@ -327,11 +327,11 @@
|
||||
"search_filters_duration_label": "Kesto",
|
||||
"search_filters_features_label": "Ominaisuudet",
|
||||
"search_filters_sort_label": "Luokittele",
|
||||
"search_filters_date_option_hour": "Tunnin sisään",
|
||||
"search_filters_date_option_hour": "Viimeisin tunti",
|
||||
"search_filters_date_option_today": "Tänään",
|
||||
"search_filters_date_option_week": "Tällä viikolla",
|
||||
"search_filters_date_option_month": "Tässä kuussa",
|
||||
"search_filters_date_option_year": "Tänä vuonna",
|
||||
"search_filters_date_option_week": "Tämä viikko",
|
||||
"search_filters_date_option_month": "Tämä kuukausi",
|
||||
"search_filters_date_option_year": "Tämä vuosi",
|
||||
"search_filters_type_option_video": "Video",
|
||||
"search_filters_type_option_channel": "Kanava",
|
||||
"search_filters_type_option_playlist": "Soittolista",
|
||||
@ -346,7 +346,7 @@
|
||||
"search_filters_features_option_location": "Sijainti",
|
||||
"search_filters_features_option_hdr": "HDR",
|
||||
"Current version: ": "Tämänhetkinen versio: ",
|
||||
"next_steps_error_message": "Kokeile seuraavia: ",
|
||||
"next_steps_error_message": "Sinun tulisi kokeilla seuraavia: ",
|
||||
"next_steps_error_message_refresh": "Päivitä",
|
||||
"next_steps_error_message_go_to_youtube": "Siirry YouTubeen",
|
||||
"generic_count_hours": "{{count}} tunti",
|
||||
@ -391,7 +391,7 @@
|
||||
"subscriptions_unseen_notifs_count": "{{count}} näkemätön ilmoitus",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} näkemätöntä ilmoitusta",
|
||||
"crash_page_switch_instance": "yrittänyt <a href=\"`x`\">käyttää toista instassia</a>",
|
||||
"videoinfo_invidious_embed_link": "Upotettava linkki",
|
||||
"videoinfo_invidious_embed_link": "Upotuslinkki",
|
||||
"user_saved_playlists": "`x` tallennetua soittolistaa",
|
||||
"crash_page_report_issue": "Jos mikään näistä ei auttanut, <a href=\"`x`\">avaathan uuden issuen GitHubissa</a> (mieluiten englanniksi) ja sisällytät seuraavan tekstin viestissäsi (ÄLÄ käännä tätä tekstiä):",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
@ -410,7 +410,7 @@
|
||||
"preferences_quality_dash_option_auto": "Auto",
|
||||
"preferences_quality_dash_option_best": "Paras",
|
||||
"preferences_quality_option_dash": "DASH (mukautuva laatu)",
|
||||
"preferences_quality_dash_label": "Ensisijainen DASH-videolaatu: ",
|
||||
"preferences_quality_dash_label": "Haluttava DASH-videolaatu: ",
|
||||
"generic_count_years": "{{count}} vuosi",
|
||||
"generic_count_years_plural": "{{count}} vuotta",
|
||||
"search_filters_features_option_purchased": "Ostettu",
|
||||
@ -421,81 +421,48 @@
|
||||
"preferences_save_player_pos_label": "Tallenna toistokohta: ",
|
||||
"footer_donate_page": "Lahjoita",
|
||||
"footer_source_code": "Lähdekoodi",
|
||||
"adminprefs_modified_source_code_url_label": "URL muokatun lähdekoodin repositorioon",
|
||||
"Released under the AGPLv3 on Github.": "Julkaistu AGPLv3-lisenssillä GitHubissa.",
|
||||
"adminprefs_modified_source_code_url_label": "URL muokattuun lähdekoodirepositoryyn",
|
||||
"Released under the AGPLv3 on Github.": "Julkaistu AGPLv3-lisenssin alla GitHubissa.",
|
||||
"search_filters_duration_option_short": "Lyhyt (< 4 minuuttia)",
|
||||
"search_filters_duration_option_long": "Pitkä (> 20 minuuttia)",
|
||||
"footer_documentation": "Dokumentaatio",
|
||||
"footer_original_source_code": "Alkuperäinen lähdekoodi",
|
||||
"footer_modfied_source_code": "Muokattu lähdekoodi",
|
||||
"Japanese (auto-generated)": "japani (automaattisesti luotu)",
|
||||
"German (auto-generated)": "saksa (automaattisesti luotu)",
|
||||
"Japanese (auto-generated)": "Japani (automaattisesti luotu)",
|
||||
"German (auto-generated)": "Saksa (automaattisesti luotu)",
|
||||
"Portuguese (auto-generated)": "portugali (automaattisesti luotu)",
|
||||
"Russian (auto-generated)": "Venäjä (automaattisesti luotu)",
|
||||
"preferences_watch_history_label": "Ota katseluhistoria käyttöön: ",
|
||||
"English (United Kingdom)": "englanti (Iso-Britannia)",
|
||||
"English (United States)": "englanti (Yhdysvallat)",
|
||||
"Cantonese (Hong Kong)": "kantoninkiina (Hongkong)",
|
||||
"Chinese": "kiina",
|
||||
"Chinese (China)": "kiina (Kiina)",
|
||||
"Chinese (Hong Kong)": "kiina (Hongkong)",
|
||||
"Chinese (Taiwan)": "kiina (Taiwan)",
|
||||
"Dutch (auto-generated)": "hollanti (automaattisesti luotu)",
|
||||
"French (auto-generated)": "ranska (automaattisesti luotu)",
|
||||
"Indonesian (auto-generated)": "indonesia (automaattisesti luotu)",
|
||||
"Interlingue": "interlingue",
|
||||
"English (United Kingdom)": "Englanti (Iso-Britannia)",
|
||||
"English (United States)": "Englanti (Yhdysvallat)",
|
||||
"Cantonese (Hong Kong)": "Kantoninkiina (Hong Kong)",
|
||||
"Chinese": "Kiina",
|
||||
"Chinese (China)": "Kiina (Kiina)",
|
||||
"Chinese (Hong Kong)": "Kiina (Hong Kong)",
|
||||
"Chinese (Taiwan)": "Kiina (Taiwan)",
|
||||
"Dutch (auto-generated)": "Hollanti (automaattisesti luotu)",
|
||||
"French (auto-generated)": "Ranska (automaattisesti luotu)",
|
||||
"Indonesian (auto-generated)": "Indonesia (automaattisesti luotu)",
|
||||
"Interlingue": "Interlingue",
|
||||
"Italian (auto-generated)": "Italia (automaattisesti luotu)",
|
||||
"Korean (auto-generated)": "korea (automaattisesti luotu)",
|
||||
"Korean (auto-generated)": "Korea (automaattisesti luotu)",
|
||||
"Portuguese (Brazil)": "portugali (Brasilia)",
|
||||
"Spanish (auto-generated)": "espanja (automaattisesti luotu)",
|
||||
"Spanish (Mexico)": "espanja (Meksiko)",
|
||||
"Spanish (Spain)": "espanja (Espanja)",
|
||||
"Turkish (auto-generated)": "turkki (automaattisesti luotu)",
|
||||
"Vietnamese (auto-generated)": "vietnam (automaattisesti luotu)",
|
||||
"search_filters_title": "Suodattimet",
|
||||
"search_message_no_results": "Tuloksia ei löytynyt.",
|
||||
"Spanish (auto-generated)": "Espanja (automaattisesti luotu)",
|
||||
"Spanish (Mexico)": "Espanja (Meksiko)",
|
||||
"Spanish (Spain)": "Espanja (Espanja)",
|
||||
"Turkish (auto-generated)": "Turkki (automaattisesti luotu)",
|
||||
"Vietnamese (auto-generated)": "Vietnam (automaattisesti luotu)",
|
||||
"search_filters_title": "Suodatin",
|
||||
"search_message_no_results": "Ei tuloksia löydetty.",
|
||||
"search_message_change_filters_or_query": "Yritä hakukyselysi laajentamista ja/tai suodattimien muuttamista.",
|
||||
"search_filters_duration_option_none": "Mikä tahansa kesto",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_apply_button": "Ota valitut suodattimet käyttöön",
|
||||
"search_filters_date_label": "Latausaika",
|
||||
"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_type_option_all": "Mikä tahansa tyyppi",
|
||||
"Popular enabled: ": "Suosittu käytössä: ",
|
||||
"error_video_not_in_playlist": "Pyydettyä videota ei ole tässä soittolistassa. <a href=\"`x`\">Klikkaa tästä päästäksesi soittolistan kotisivulle.</a>",
|
||||
"Import YouTube playlist (.csv)": "Tuo YouTube-soittolista (.csv)",
|
||||
"Music in this video": "Musiikki tässä videossa",
|
||||
"Add to playlist": "Lisää soittolistaan",
|
||||
"Add to playlist: ": "Lisää soittolistaan: ",
|
||||
"Search for videos": "Etsi videoita",
|
||||
"generic_button_rss": "RSS",
|
||||
"Answer": "Vastaus",
|
||||
"Standard YouTube license": "Vakio YouTube-lisenssi",
|
||||
"Song: ": "Kappale: ",
|
||||
"Album: ": "Albumi: ",
|
||||
"Download is disabled": "Lataus on poistettu käytöstä",
|
||||
"Channel Sponsor": "Kanavan sponsori",
|
||||
"channel_tab_podcasts_label": "Podcastit",
|
||||
"channel_tab_releases_label": "Julkaisut",
|
||||
"channel_tab_shorts_label": "Shorts-videot",
|
||||
"carousel_slide": "Dia {{current}}/{{total}}",
|
||||
"carousel_skip": "Ohita karuselli",
|
||||
"carousel_go_to": "Siirry diaan `x`",
|
||||
"channel_tab_playlists_label": "Soittolistat",
|
||||
"channel_tab_channels_label": "Kanavat",
|
||||
"generic_button_delete": "Poista",
|
||||
"generic_button_edit": "Muokkaa",
|
||||
"generic_button_save": "Tallenna",
|
||||
"generic_button_cancel": "Peru",
|
||||
"playlist_button_add_items": "Lisää videoita",
|
||||
"Artist: ": "Esittäjä: ",
|
||||
"channel_tab_streams_label": "Suoratoistot",
|
||||
"generic_channels_count": "{{count}} kanava",
|
||||
"generic_channels_count_plural": "{{count}} kanavaa",
|
||||
"The Popular feed has been disabled by the administrator.": "Järjestelmänvalvoja on poistanut Suositut-syötteen.",
|
||||
"Import YouTube watch history (.json)": "Tuo Youtube-katseluhistoria (.json)",
|
||||
"toggle_theme": "Vaihda teemaa",
|
||||
"preferences_preload_label": "Esilataa video data. "
|
||||
"error_video_not_in_playlist": "Pyydettyä videota ei löydy tästä soittolistasta. <a href=\"`x`\">Klikkaa tähän päästäksesi soittolistan etusivulle.</a>"
|
||||
}
|
||||
|
@ -18,7 +18,7 @@
|
||||
"generic_subscriptions_count_1": "{{count}} d'abonnements",
|
||||
"generic_subscriptions_count_2": "{{count}} abonnements",
|
||||
"generic_button_delete": "Supprimer",
|
||||
"generic_button_edit": "Modifier",
|
||||
"generic_button_edit": "Editer",
|
||||
"generic_button_save": "Enregistrer",
|
||||
"generic_button_cancel": "Annuler",
|
||||
"generic_button_rss": "RSS",
|
||||
@ -44,7 +44,7 @@
|
||||
"Import and Export Data": "Importer et exporter des données",
|
||||
"Import": "Importer",
|
||||
"Import Invidious data": "Importer des données Invidious au format JSON",
|
||||
"Import YouTube subscriptions": "Importer des abonnements YouTube aux formats OPML/CSV",
|
||||
"Import YouTube subscriptions": "Importer des abonnements YouTube/OPML",
|
||||
"Import FreeTube subscriptions (.db)": "Importer des abonnements FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)",
|
||||
@ -484,7 +484,7 @@
|
||||
"search_filters_duration_option_medium": "Moyenne (de 4 à 20 minutes)",
|
||||
"search_filters_apply_button": "Appliquer les filtres",
|
||||
"search_message_no_results": "Aucun résultat.",
|
||||
"search_message_use_another_instance": "Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.",
|
||||
"search_message_use_another_instance": " Vous pouvez également <a href=\"`x`\">effectuer votre recherche sur une autre instance</a>.",
|
||||
"search_filters_type_option_all": "Tous les types",
|
||||
"search_filters_date_label": "Date d'ajout",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
@ -504,16 +504,5 @@
|
||||
"Import YouTube playlist (.csv)": "Importer des listes de lecture de Youtube (.csv)",
|
||||
"channel_tab_releases_label": "Parutions",
|
||||
"channel_tab_podcasts_label": "Émissions audio",
|
||||
"Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)",
|
||||
"Add to playlist: ": "Ajouter à la playlist : ",
|
||||
"Add to playlist": "Ajouter à la playlist",
|
||||
"Answer": "Répondre",
|
||||
"Search for videos": "Rechercher des vidéos",
|
||||
"The Popular feed has been disabled by the administrator.": "Le flux populaire a été désactivé par l'administrateur.",
|
||||
"carousel_skip": "Passez le carrousel",
|
||||
"carousel_slide": "Diapositive {{current}} sur {{total}}",
|
||||
"carousel_go_to": "Aller à la diapositive `x`",
|
||||
"toggle_theme": "Changer le Thème",
|
||||
"Filipino (auto-generated)": "Philippines (automatiquement générer)",
|
||||
"preferences_preload_label": "Précharger les données de la vidéo : "
|
||||
"Import YouTube watch history (.json)": "Importer l'historique de visionnement YouTube (.json)"
|
||||
}
|
||||
|
@ -449,30 +449,30 @@
|
||||
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
|
||||
"Chinese": "Kineski",
|
||||
"Chinese (Taiwan)": "Kineski (Tajvan)",
|
||||
"Dutch (auto-generated)": "Nizozemski (automatski generirano)",
|
||||
"French (auto-generated)": "Francuski (automatski generirano)",
|
||||
"Indonesian (auto-generated)": "Indonezijski (automatski generirano)",
|
||||
"Dutch (auto-generated)": "Nizozemski (automatski generiran)",
|
||||
"French (auto-generated)": "Francuski (automatski generiran)",
|
||||
"Indonesian (auto-generated)": "Indonezijski (automatski generiran)",
|
||||
"Interlingue": "Interlingua",
|
||||
"Japanese (auto-generated)": "Japanski (automatski generirano)",
|
||||
"Russian (auto-generated)": "Ruski (automatski generirano)",
|
||||
"Turkish (auto-generated)": "Turski (automatski generirano)",
|
||||
"Vietnamese (auto-generated)": "Vijetnamski (automatski generirano)",
|
||||
"Japanese (auto-generated)": "Japanski (automatski generiran)",
|
||||
"Russian (auto-generated)": "Ruski (automatski generiran)",
|
||||
"Turkish (auto-generated)": "Turski (automatski generiran)",
|
||||
"Vietnamese (auto-generated)": "Vijetnamski (automatski generiran)",
|
||||
"Spanish (Spain)": "Španjolski (Španjolska)",
|
||||
"Italian (auto-generated)": "Talijanski (automatski generirano)",
|
||||
"Italian (auto-generated)": "Talijanski (automatski generiran)",
|
||||
"Portuguese (Brazil)": "Portugalski (Brazil)",
|
||||
"Spanish (Mexico)": "Španjolski (Meksiko)",
|
||||
"German (auto-generated)": "Njemački (automatski generirano)",
|
||||
"German (auto-generated)": "Njemački (automatski generiran)",
|
||||
"Chinese (China)": "Kineski (Kina)",
|
||||
"Chinese (Hong Kong)": "Kineski (Hong Kong)",
|
||||
"Korean (auto-generated)": "Korejski (automatski generirano)",
|
||||
"Portuguese (auto-generated)": "Portugalski (automatski generirano)",
|
||||
"Spanish (auto-generated)": "Španjolski (automatski generirano)",
|
||||
"Korean (auto-generated)": "Korejski (automatski generiran)",
|
||||
"Portuguese (auto-generated)": "Portugalski (automatski generiran)",
|
||||
"Spanish (auto-generated)": "Španjolski (automatski generiran)",
|
||||
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
|
||||
"search_filters_title": "Filtri",
|
||||
"search_filters_date_option_none": "Bilo koji datum",
|
||||
"search_filters_date_label": "Datum prijenosa",
|
||||
"search_message_no_results": "Nema rezultata.",
|
||||
"search_message_use_another_instance": "Također možeš <a href=\"`x`\">tražiti na jednoj drugoj instanci</a>.",
|
||||
"search_message_use_another_instance": " Također možeš <a href=\"`x`\">tražiti na jednoj drugoj instanci</a>.",
|
||||
"search_message_change_filters_or_query": "Pokušaj proširiti upit za pretragu i/ili promijeni filtre.",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_duration_option_none": "Bilo koje duljine",
|
||||
@ -513,7 +513,5 @@
|
||||
"toggle_theme": "Uklj./Isklj. temu",
|
||||
"carousel_slide": "Kadar {{current}} od {{total}}",
|
||||
"carousel_go_to": "Idi na kadar `x`",
|
||||
"carousel_skip": "Preskoči vrtuljak",
|
||||
"Filipino (auto-generated)": "Filipinski (automatski generirano)",
|
||||
"preferences_preload_label": "Unaprijed učitaj podatke videa: "
|
||||
"carousel_skip": "Preskoči vrtuljak"
|
||||
}
|
||||
|
@ -464,23 +464,5 @@
|
||||
"search_filters_features_option_vr180": "180°-os virtuális valóság",
|
||||
"search_filters_apply_button": "Keresés a megadott szűrőkkel",
|
||||
"Popular enabled: ": "Népszerű engedélyezve ",
|
||||
"error_video_not_in_playlist": "A lejátszási listában keresett videó nem létezik. <a href=\"`x`\">Kattintson ide a lejátszási listához jutáshoz.</a>",
|
||||
"generic_button_delete": "Törlés",
|
||||
"generic_button_rss": "RSS",
|
||||
"Import YouTube playlist (.csv)": "Youtube lejátszási lista (.csv) importálása",
|
||||
"Standard YouTube license": "Alap YouTube-licensz",
|
||||
"Add to playlist": "Hozzáadás lejátszási listához",
|
||||
"Add to playlist: ": "Hozzáadás a lejátszási listához: ",
|
||||
"Answer": "Válasz",
|
||||
"Search for videos": "Keresés videókhoz",
|
||||
"generic_channels_count": "{{count}} csatorna",
|
||||
"generic_channels_count_plural": "{{count}} csatornák",
|
||||
"generic_button_edit": "Szerkesztés",
|
||||
"generic_button_save": "Mentés",
|
||||
"generic_button_cancel": "Mégsem",
|
||||
"playlist_button_add_items": "Videók hozzáadása",
|
||||
"Music in this video": "Zene ezen videóban",
|
||||
"Song: ": "Dal: ",
|
||||
"Album: ": "Album: ",
|
||||
"Import YouTube watch history (.json)": "Youtube megtekintési előzmények (.json) importálása"
|
||||
"error_video_not_in_playlist": "A lejátszási listában keresett videó nem létezik. <a href=\"`x`\">Kattintson ide a lejátszási listához jutáshoz.</a>"
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
"invidious": "Invidious",
|
||||
"Image CAPTCHA": "Imagine CAPTCHA",
|
||||
"newest": "plus nove",
|
||||
"generic_button_save": "Salveguardar",
|
||||
"generic_button_save": "Salvar",
|
||||
"Dark mode: ": "Modo obscur: ",
|
||||
"preferences_dark_mode_label": "Thema: ",
|
||||
"preferences_category_subscription": "Preferentias de subscription",
|
||||
@ -23,7 +23,7 @@
|
||||
"light": "clar",
|
||||
"No": "Non",
|
||||
"youtube": "YouTube",
|
||||
"LIVE": "IN DIRECTO",
|
||||
"LIVE": "IN DIRECTE",
|
||||
"reddit": "Reddit",
|
||||
"preferences_category_player": "Preferentias de reproductor",
|
||||
"Preferences": "Preferentias",
|
||||
|
295
locales/is.json
295
locales/is.json
@ -1,39 +1,39 @@
|
||||
{
|
||||
"LIVE": "BEINT",
|
||||
"Shared `x` ago": "Deilt fyrir `x` síðan",
|
||||
"Shared `x` ago": "Deilt `x` síðan",
|
||||
"Unsubscribe": "Afskrá",
|
||||
"Subscribe": "Áskrifa",
|
||||
"View channel on YouTube": "Skoða rás á YouTube",
|
||||
"View playlist on YouTube": "Skoða spilunarlista á YouTube",
|
||||
"View playlist on YouTube": "Skoða spilunarlisti á YouTube",
|
||||
"newest": "nýjasta",
|
||||
"oldest": "elsta",
|
||||
"popular": "vinsælt",
|
||||
"last": "síðast",
|
||||
"Next page": "Næsta síða",
|
||||
"Previous page": "Fyrri síða",
|
||||
"Clear watch history?": "Hreinsa áhorfsferil?",
|
||||
"Clear watch history?": "Hreinsa áhorfssögu?",
|
||||
"New password": "Nýtt lykilorð",
|
||||
"New passwords must match": "Nýtt lykilorð verður að passa",
|
||||
"Authorize token?": "Leyfa teikn?",
|
||||
"Authorize token for `x`?": "Leyfa teikn fyrir `x`?",
|
||||
"Authorize token?": "Leyfa tákn?",
|
||||
"Authorize token for `x`?": "Leyfa tákn fyrir `x`?",
|
||||
"Yes": "Já",
|
||||
"No": "Nei",
|
||||
"Import and Export Data": "Inn- og útflutningur gagna",
|
||||
"Import and Export Data": "Innflutningur og Útflutningur Gagna",
|
||||
"Import": "Flytja inn",
|
||||
"Import Invidious data": "Flytja inn Invidious JSON-gögn",
|
||||
"Import YouTube subscriptions": "Flytja inn YouTube CSV eða OPML-áskriftir",
|
||||
"Import Invidious data": "Flytja inn Invidious gögn",
|
||||
"Import YouTube subscriptions": "Flytja inn YouTube áskriftir",
|
||||
"Import FreeTube subscriptions (.db)": "Flytja inn FreeTube áskriftir (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Flytja inn NewPipe áskriftir (.json)",
|
||||
"Import NewPipe data (.zip)": "Flytja inn NewPipe gögn (.zip)",
|
||||
"Export": "Flytja út",
|
||||
"Export subscriptions as OPML": "Flytja út áskriftir sem OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Flytja út áskriftir sem OPML (fyrir NewPipe & FreeTube)",
|
||||
"Export data as JSON": "Flytja út Invidious-gögn sem JSON",
|
||||
"Export data as JSON": "Flytja út gögn sem JSON",
|
||||
"Delete account?": "Eyða reikningi?",
|
||||
"History": "Ferill",
|
||||
"An alternative front-end to YouTube": "Annað viðmót fyrir YouTube",
|
||||
"JavaScript license information": "Upplýsingar um notkunarleyfi JavaScript",
|
||||
"source": "uppruni",
|
||||
"History": "Saga",
|
||||
"An alternative front-end to YouTube": "Önnur framhlið fyrir YouTube",
|
||||
"JavaScript license information": "JavaScript leyfi upplýsingar",
|
||||
"source": "uppspretta",
|
||||
"Log in": "Skrá inn",
|
||||
"Log in/register": "Innskráning/nýskráning",
|
||||
"User ID": "Notandakenni",
|
||||
@ -47,33 +47,33 @@
|
||||
"Preferences": "Kjörstillingar",
|
||||
"preferences_category_player": "Kjörstillingar spilara",
|
||||
"preferences_video_loop_label": "Alltaf lykkja: ",
|
||||
"preferences_autoplay_label": "Sjálfvirk spilun: ",
|
||||
"preferences_autoplay_label": "Spila sjálfkrafa: ",
|
||||
"preferences_continue_label": "Spila næst sjálfgefið: ",
|
||||
"preferences_continue_autoplay_label": "Spila næsta myndskeið sjálfkrafa: ",
|
||||
"preferences_continue_autoplay_label": "Spila næst sjálfkrafa: ",
|
||||
"preferences_listen_label": "Hlusta sjálfgefið: ",
|
||||
"preferences_local_label": "Milliþjónn fyrir myndskeið: ",
|
||||
"preferences_local_label": "Proxy myndbönd? ",
|
||||
"preferences_speed_label": "Sjálfgefinn hraði: ",
|
||||
"preferences_quality_label": "Æskileg gæði myndmerkis: ",
|
||||
"preferences_quality_label": "Æskilegt myndbands gæði: ",
|
||||
"preferences_volume_label": "Spilara hljóðstyrkur: ",
|
||||
"preferences_comments_label": "Sjálfgefin ummæli: ",
|
||||
"youtube": "YouTube",
|
||||
"reddit": "Reddit",
|
||||
"reddit": "reddit",
|
||||
"preferences_captions_label": "Sjálfgefin texti: ",
|
||||
"Fallback captions: ": "Varatextar: ",
|
||||
"preferences_related_videos_label": "Sýna tengd myndskeið? ",
|
||||
"preferences_related_videos_label": "Sýna tengd myndbönd? ",
|
||||
"preferences_annotations_label": "Á að sýna glósur sjálfgefið? ",
|
||||
"preferences_category_visual": "Sjónrænar stillingar",
|
||||
"preferences_player_style_label": "Stíll spilara: ",
|
||||
"Dark mode: ": "Dökkur hamur: ",
|
||||
"preferences_player_style_label": "Spilara stíl: ",
|
||||
"Dark mode: ": "Myrkur ham: ",
|
||||
"preferences_dark_mode_label": "Þema: ",
|
||||
"dark": "dökkt",
|
||||
"dark": "dimmt",
|
||||
"light": "ljóst",
|
||||
"preferences_thin_mode_label": "Grannur hamur: ",
|
||||
"preferences_thin_mode_label": "Þunnt ham: ",
|
||||
"preferences_category_subscription": "Áskriftarstillingar",
|
||||
"preferences_annotations_subscribed_label": "Á að sýna glósur sjálfgefið fyrir áskriftarrásir? ",
|
||||
"Redirect homepage to feed: ": "Endurbeina heimasíðu að streymi: ",
|
||||
"preferences_max_results_label": "Fjöldi myndskeiða sem sýnd eru í streymi: ",
|
||||
"preferences_sort_label": "Raða myndskeiðum eftir: ",
|
||||
"Redirect homepage to feed: ": "Endurbeina heimasíðu að straumi: ",
|
||||
"preferences_max_results_label": "Fjöldi myndbanda sem sýndir eru í straumi: ",
|
||||
"preferences_sort_label": "Raða myndbönd eftir: ",
|
||||
"published": "birt",
|
||||
"published - reverse": "birt - afturábak",
|
||||
"alphabetically": "í stafrófsröð",
|
||||
@ -88,31 +88,31 @@
|
||||
"`x` uploaded a video": "`x` hlóð upp myndband",
|
||||
"`x` is live": "`x` er í beinni",
|
||||
"preferences_category_data": "Gagnastillingar",
|
||||
"Clear watch history": "Hreinsa áhorfsferil",
|
||||
"Clear watch history": "Hreinsa áhorfssögu",
|
||||
"Import/export data": "Flytja inn/út gögn",
|
||||
"Change password": "Breyta lykilorði",
|
||||
"Manage subscriptions": "Sýsla með áskriftir",
|
||||
"Manage tokens": "Sýsla með teikn",
|
||||
"Watch history": "Áhorfsferill",
|
||||
"Manage subscriptions": "Stjórna áskriftum",
|
||||
"Manage tokens": "Stjórna tákn",
|
||||
"Watch history": "Áhorfssögu",
|
||||
"Delete account": "Eyða reikningi",
|
||||
"preferences_category_admin": "Kjörstillingar stjórnanda",
|
||||
"preferences_default_home_label": "Sjálfgefin heimasíða: ",
|
||||
"preferences_feed_menu_label": "Streymisvalmynd: ",
|
||||
"Top enabled: ": "Vinsælast virkt? ",
|
||||
"preferences_feed_menu_label": "Straum valmynd: ",
|
||||
"Top enabled: ": "Toppur virkur? ",
|
||||
"CAPTCHA enabled: ": "CAPTCHA virk? ",
|
||||
"Login enabled: ": "Innskráning virk? ",
|
||||
"Registration enabled: ": "Nýskráning virkjuð? ",
|
||||
"Report statistics: ": "Skrá tölfræði? ",
|
||||
"Report statistics: ": "Skrá talnagögn? ",
|
||||
"Save preferences": "Vista stillingar",
|
||||
"Subscription manager": "Áskriftarstjóri",
|
||||
"Token manager": "Teiknastjórnun",
|
||||
"Token": "Teikn",
|
||||
"Token manager": "Táknstjóri",
|
||||
"Token": "Tákn",
|
||||
"Import/export": "Flytja inn/út",
|
||||
"unsubscribe": "afskrá",
|
||||
"revoke": "afturkalla",
|
||||
"Subscriptions": "Áskriftir",
|
||||
"search": "leita",
|
||||
"Log out": "Skrá út",
|
||||
"Log out": "Útskrá",
|
||||
"Source available here.": "Frumkóði aðgengilegur hér.",
|
||||
"View JavaScript license information.": "Skoða JavaScript leyfisupplýsingar.",
|
||||
"View privacy policy.": "Skoða meðferð persónuupplýsinga.",
|
||||
@ -122,13 +122,13 @@
|
||||
"Private": "Einka",
|
||||
"View all playlists": "Skoða alla spilunarlista",
|
||||
"Updated `x` ago": "Uppfært `x` síðann",
|
||||
"Delete playlist `x`?": "Eyða spilunarlista `x`?",
|
||||
"Delete playlist": "Eyða spilunarlista",
|
||||
"Delete playlist `x`?": "Eiða spilunarlista `x`?",
|
||||
"Delete playlist": "Eiða spilunarlista",
|
||||
"Create playlist": "Búa til spilunarlista",
|
||||
"Title": "Titill",
|
||||
"Playlist privacy": "Friðhelgi spilunarlista",
|
||||
"Editing playlist `x`": "Breyti spilunarlista `x`",
|
||||
"Watch on YouTube": "Skoða á YouTube",
|
||||
"Playlist privacy": "Spilunarlista opinberri",
|
||||
"Editing playlist `x`": "Að breyta spilunarlista `x`",
|
||||
"Watch on YouTube": "Horfa á YouTube",
|
||||
"Hide annotations": "Fela glósur",
|
||||
"Show annotations": "Sýna glósur",
|
||||
"Genre: ": "Tegund: ",
|
||||
@ -160,26 +160,26 @@
|
||||
"Wrong username or password": "Rangt notandanafn eða lykilorð",
|
||||
"Password cannot be empty": "Lykilorð má ekki vera autt",
|
||||
"Password cannot be longer than 55 characters": "Lykilorð má ekki vera lengra en 55 stafir",
|
||||
"Please log in": "Skráðu þig inn",
|
||||
"Invidious Private Feed for `x`": "Persónulegt Invidious-streymi fyrir `x`",
|
||||
"Please log in": "Vinsamlegast skráðu þig inn",
|
||||
"Invidious Private Feed for `x`": "Invidious Persónulegur Straumur fyrir `x`",
|
||||
"channel:`x`": "rás:`x`",
|
||||
"Deleted or invalid channel": "Eytt eða ógild rás",
|
||||
"This channel does not exist.": "Þessi rás er ekki til.",
|
||||
"Could not get channel info.": "Ekki tókst að fá upplýsingar um rásina.",
|
||||
"Could not get channel info.": "Ekki tókst að fá rásarupplýsingar.",
|
||||
"Could not fetch comments": "Ekki tókst að sækja ummæli",
|
||||
"`x` ago": "`x` síðan",
|
||||
"Load more": "Hlaða meira",
|
||||
"Could not create mix.": "Ekki tókst að búa til blöndu.",
|
||||
"Empty playlist": "Tómur spilunarlisti",
|
||||
"Not a playlist.": "Er ekki spilunarlisti.",
|
||||
"Not a playlist.": "Ekki spilunarlisti.",
|
||||
"Playlist does not exist.": "Spilunarlisti er ekki til.",
|
||||
"Could not pull trending pages.": "Ekki tókst að draga vinsælar síður.",
|
||||
"Hidden field \"challenge\" is a required field": "Falinn reitur \"áskorun\" er nauðsynlegur reitur",
|
||||
"Hidden field \"token\" is a required field": "Falinn reitur \"teikn\" er nauðsynlegur reitur",
|
||||
"Hidden field \"token\" is a required field": "Falinn reitur \"tákn\" er nauðsynlegur reitur",
|
||||
"Erroneous challenge": "Röng áskorun",
|
||||
"Erroneous token": "Rangt teikn",
|
||||
"Erroneous token": "Rangt tákn",
|
||||
"No such user": "Enginn slíkur notandi",
|
||||
"Token is expired, please try again": "Teiknið er útrunnið, reyndu aftur",
|
||||
"Token is expired, please try again": "Tákn er útrunnið, vinsamlegast reyndu aftur",
|
||||
"English": "Enska",
|
||||
"English (auto-generated)": "Enska (sjálfkrafa)",
|
||||
"Afrikaans": "Afríkanska",
|
||||
@ -267,14 +267,14 @@
|
||||
"Somali": "Sómalska",
|
||||
"Southern Sotho": "Suður Sótó",
|
||||
"Spanish": "Spænska",
|
||||
"Spanish (Latin America)": "Spænska (Rómanska Ameríka)",
|
||||
"Spanish (Latin America)": "Spænska (Rómönsku Ameríka)",
|
||||
"Sundanese": "Sundaneska",
|
||||
"Swahili": "Svahílí",
|
||||
"Swedish": "Sænska",
|
||||
"Tajik": "Tadsikíska",
|
||||
"Tamil": "Tamílska",
|
||||
"Telugu": "Telúgú",
|
||||
"Thai": "Tælenska",
|
||||
"Thai": "Taílenska",
|
||||
"Turkish": "Tyrkneska",
|
||||
"Ukrainian": "Úkraníska",
|
||||
"Urdu": "Úrdú",
|
||||
@ -286,9 +286,9 @@
|
||||
"Yiddish": "Jiddíska",
|
||||
"Yoruba": "Jórúba",
|
||||
"Zulu": "Zúlú",
|
||||
"Fallback comments: ": "Ummæli til vara: ",
|
||||
"Fallback comments: ": "Vara ummæli: ",
|
||||
"Popular": "Vinsælt",
|
||||
"Top": "Vinsælast",
|
||||
"Top": "Topp",
|
||||
"About": "Um",
|
||||
"Rating: ": "Einkunn: ",
|
||||
"preferences_locale_label": "Tungumál: ",
|
||||
@ -307,196 +307,9 @@
|
||||
"`x` marked it with a ❤": "`x` merkti það með ❤",
|
||||
"Audio mode": "Hljóð ham",
|
||||
"Video mode": "Myndband ham",
|
||||
"channel_tab_videos_label": "Myndskeið",
|
||||
"channel_tab_videos_label": "Myndbönd",
|
||||
"Playlists": "Spilunarlistar",
|
||||
"channel_tab_community_label": "Samfélag",
|
||||
"Current version: ": "Núverandi útgáfa: ",
|
||||
"preferences_watch_history_label": "Virkja áhorfsferil: ",
|
||||
"Chinese (China)": "Kínverska (Kína)",
|
||||
"Turkish (auto-generated)": "Tyrkneska (sjálfvirkt útbúið)",
|
||||
"Search": "Leita",
|
||||
"preferences_save_player_pos_label": "Vista staðsetningu í afspilun: ",
|
||||
"Popular enabled: ": "Vinsælt virkjað: ",
|
||||
"search_filters_features_option_purchased": "Keypt",
|
||||
"Standard YouTube license": "Staðlað YouTube-notkunarleyfi",
|
||||
"French (auto-generated)": "Franska (sjálfvirkt útbúið)",
|
||||
"Spanish (Spain)": "Spænska (Spánn)",
|
||||
"search_filters_title": "Síur",
|
||||
"search_filters_date_label": "Dags. innsendingar",
|
||||
"search_filters_features_option_four_k": "4K",
|
||||
"search_filters_features_option_hd": "HD",
|
||||
"crash_page_read_the_faq": "lesið <a href=\"`x`\">Algengar spurningar (FAQ)</a>",
|
||||
"Add to playlist": "Bæta á spilunarlista",
|
||||
"Add to playlist: ": "Bæta á spilunarlista: ",
|
||||
"Answer": "Svar",
|
||||
"Search for videos": "Leita að myndskeiðum",
|
||||
"generic_channels_count": "{{count}} rás",
|
||||
"generic_channels_count_plural": "{{count}} rásir",
|
||||
"generic_videos_count": "{{count}} myndskeið",
|
||||
"generic_videos_count_plural": "{{count}} myndskeið",
|
||||
"The Popular feed has been disabled by the administrator.": "Kerfisstjórinn hefur gert Vinsælt-streymið óvirkt.",
|
||||
"generic_playlists_count": "{{count}} spilunarlisti",
|
||||
"generic_playlists_count_plural": "{{count}} spilunarlistar",
|
||||
"generic_subscribers_count": "{{count}} áskrifandi",
|
||||
"generic_subscribers_count_plural": "{{count}} áskrifendur",
|
||||
"generic_subscriptions_count": "{{count}} áskrift",
|
||||
"generic_subscriptions_count_plural": "{{count}} áskriftir",
|
||||
"generic_button_delete": "Eyða",
|
||||
"Import YouTube watch history (.json)": "Flytja inn YouTube áhorfsferil (.json)",
|
||||
"preferences_vr_mode_label": "Gagnvirk 360 gráðu myndskeið (krefst WebGL): ",
|
||||
"preferences_quality_dash_option_auto": "Sjálfvirkt",
|
||||
"preferences_quality_dash_option_best": "Best",
|
||||
"preferences_quality_dash_option_worst": "Verst",
|
||||
"preferences_quality_dash_label": "Æskileg DASH-gæði myndmerkis: ",
|
||||
"preferences_extend_desc_label": "Sjálfvirkt útvíkka lýsingu á myndskeiði: ",
|
||||
"preferences_region_label": "Land efnis: ",
|
||||
"preferences_show_nick_label": "Birta gælunafn efst: ",
|
||||
"tokens_count": "{{count}} teikn",
|
||||
"tokens_count_plural": "{{count}} teikn",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} óskoðuð tilkynning",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} óskoðaðar tilkynningar",
|
||||
"Released under the AGPLv3 on Github.": "Gefið út með AGPLv3-notkunarleyfi á GitHub.",
|
||||
"Music in this video": "Tónlist í þessu myndskeiði",
|
||||
"Artist: ": "Flytjandi: ",
|
||||
"Album: ": "Hljómplata: ",
|
||||
"comments_view_x_replies": "Skoða {{count}} svar",
|
||||
"comments_view_x_replies_plural": "Skoða {{count}} svör",
|
||||
"comments_points_count": "{{count}} punktur",
|
||||
"comments_points_count_plural": "{{count}} punktar",
|
||||
"Cantonese (Hong Kong)": "Kantónska (Hong Kong)",
|
||||
"Chinese": "Kínverska",
|
||||
"Chinese (Hong Kong)": "Kínverska (Hong Kong)",
|
||||
"Chinese (Taiwan)": "Kínverska (Taívan)",
|
||||
"Japanese (auto-generated)": "Japanska (sjálfvirkt útbúið)",
|
||||
"generic_count_minutes": "{{count}} mínúta",
|
||||
"generic_count_minutes_plural": "{{count}} mínútur",
|
||||
"generic_count_seconds": "{{count}} sekúnda",
|
||||
"generic_count_seconds_plural": "{{count}} sekúndur",
|
||||
"search_filters_date_option_hour": "Síðustu klukkustund",
|
||||
"search_filters_apply_button": "Virkja valdar síur",
|
||||
"next_steps_error_message_go_to_youtube": "Fara á YouTube",
|
||||
"footer_original_source_code": "Upprunalegur grunnkóði",
|
||||
"videoinfo_started_streaming_x_ago": "Byrjaði streymi fyrir `x` síðan",
|
||||
"next_steps_error_message": "Á eftir þessu ættirðu að prófa: ",
|
||||
"videoinfo_invidious_embed_link": "Ívefja tengil",
|
||||
"download_subtitles": "Skjátextar - `x` (.vtt)",
|
||||
"user_created_playlists": "`x` útbjó spilunarlista",
|
||||
"user_saved_playlists": "`x` vistaði spilunarlista",
|
||||
"Video unavailable": "Myndskeið ekki tiltækt",
|
||||
"videoinfo_watch_on_youTube": "Skoða á YouTube",
|
||||
"crash_page_you_found_a_bug": "Það lítur út eins og þú hafir fundið galla í Invidious!",
|
||||
"crash_page_before_reporting": "Áður en þú tilkynnir villu, gakktu úr skugga um að þú hafir:",
|
||||
"crash_page_switch_instance": "reynt að <a href=\"`x`\">nota annað tilvik</a>",
|
||||
"crash_page_report_issue": "Ef ekkert af ofantöldu hjálpaði, ættirðu að <a href=\"`x`\">opna nýja verkbeiðni (issue) á GitHub</a> (helst á ensku) og láta fylgja eftirfarandi texta í skilaboðunum þínum (alls EKKI þýða þennan texta):",
|
||||
"channel_tab_shorts_label": "Stuttmyndir",
|
||||
"carousel_slide": "Skyggna {{current}} af {{total}}",
|
||||
"carousel_go_to": "Fara á skyggnu `x`",
|
||||
"channel_tab_streams_label": "Bein streymi",
|
||||
"channel_tab_playlists_label": "Spilunarlistar",
|
||||
"toggle_theme": "Víxla þema",
|
||||
"carousel_skip": "Sleppa hringekjunni",
|
||||
"preferences_quality_option_medium": "Miðlungs",
|
||||
"search_message_use_another_instance": "Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.",
|
||||
"footer_source_code": "Grunnkóði",
|
||||
"English (United Kingdom)": "Enska (Bretland)",
|
||||
"English (United States)": "Enska (Bandarísk)",
|
||||
"Vietnamese (auto-generated)": "Víetnamska (sjálfvirkt útbúið)",
|
||||
"generic_count_months": "{{count}} mánuður",
|
||||
"generic_count_months_plural": "{{count}} mánuðir",
|
||||
"search_filters_sort_option_rating": "Einkunn",
|
||||
"videoinfo_youTube_embed_link": "Ívefja",
|
||||
"error_video_not_in_playlist": "Umbeðið myndskeið fyrirfinnst ekki í þessum spilunarlista. <a href=\"`x`\">Smelltu hér til að fara á heimasíðu spilunarlistans.</a>",
|
||||
"generic_views_count": "{{count}} áhorf",
|
||||
"generic_views_count_plural": "{{count}} áhorf",
|
||||
"playlist_button_add_items": "Bæta við myndskeiðum",
|
||||
"Show more": "Sýna meira",
|
||||
"Show less": "Sýna minna",
|
||||
"Song: ": "Lag: ",
|
||||
"channel_tab_podcasts_label": "Hlaðvörp (podcasts)",
|
||||
"channel_tab_releases_label": "Útgáfur",
|
||||
"Download is disabled": "Niðurhal er óvirkt",
|
||||
"search_filters_features_option_location": "Staðsetning",
|
||||
"preferences_quality_dash_option_720p": "720p",
|
||||
"Switch Invidious Instance": "Skipta um Invidious-tilvik",
|
||||
"search_message_no_results": "Engar niðurstöður fundust.",
|
||||
"search_message_change_filters_or_query": "Reyndu að víkka leitarsviðið og/eða breyta síunum.",
|
||||
"Dutch (auto-generated)": "Hollenska (sjálfvirkt útbúið)",
|
||||
"German (auto-generated)": "Þýska (sjálfvirkt útbúið)",
|
||||
"Indonesian (auto-generated)": "Indónesíska (sjálfvirkt útbúið)",
|
||||
"Interlingue": "Interlingue",
|
||||
"Italian (auto-generated)": "Ítalska (sjálfvirkt útbúið)",
|
||||
"Russian (auto-generated)": "Rússneska (sjálfvirkt útbúið)",
|
||||
"Spanish (auto-generated)": "Spænska (sjálfvirkt útbúið)",
|
||||
"Spanish (Mexico)": "Spænska (Mexíkó)",
|
||||
"generic_count_hours": "{{count}} klukkustund",
|
||||
"generic_count_hours_plural": "{{count}} klukkustundir",
|
||||
"generic_count_years": "{{count}} ár",
|
||||
"generic_count_years_plural": "{{count}} ár",
|
||||
"generic_count_weeks": "{{count}} vika",
|
||||
"generic_count_weeks_plural": "{{count}} vikur",
|
||||
"search_filters_date_option_none": "Hvaða dagsetning sem er",
|
||||
"Channel Sponsor": "Styrktaraðili rásar",
|
||||
"search_filters_date_option_week": "Í þessari viku",
|
||||
"search_filters_date_option_month": "Í þessum mánuði",
|
||||
"search_filters_date_option_year": "Á þessu ári",
|
||||
"search_filters_type_option_playlist": "Spilunarlisti",
|
||||
"search_filters_type_option_show": "Þáttur",
|
||||
"search_filters_duration_label": "Tímalengd",
|
||||
"search_filters_duration_option_long": "Langt (> 20 mínútur)",
|
||||
"search_filters_features_option_live": "Beint",
|
||||
"search_filters_features_option_three_sixty": "360°",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_features_option_three_d": "3D",
|
||||
"search_filters_features_option_hdr": "HDR",
|
||||
"search_filters_sort_label": "Raða eftir",
|
||||
"search_filters_sort_option_relevance": "Samsvörun",
|
||||
"footer_donate_page": "Styrkja",
|
||||
"footer_modfied_source_code": "Breyttur grunnkóði",
|
||||
"crash_page_refresh": "reynt að <a href=\"`x`\">endurlesa síðuna</a>",
|
||||
"crash_page_search_issue": "leitað að <a href=\"`x`\">fyrirliggjandi villum á GitHub</a>",
|
||||
"none": "ekkert",
|
||||
"adminprefs_modified_source_code_url_label": "Slóð á gagnasafn með breyttum grunnkóða",
|
||||
"preferences_quality_option_hd720": "HD720",
|
||||
"preferences_quality_option_small": "Lítið",
|
||||
"preferences_category_misc": "Ýmsar kjörstillingar",
|
||||
"preferences_automatic_instance_redirect_label": "Sjálfvirk endurbeining tilvika (farið til vara á redirect.invidious.io): ",
|
||||
"Portuguese (auto-generated)": "Portúgalska (sjálfvirkt útbúið)",
|
||||
"Portuguese (Brazil)": "Portúgalska (Brasilía)",
|
||||
"generic_button_edit": "Breyta",
|
||||
"generic_button_save": "Vista",
|
||||
"generic_button_cancel": "Hætta við",
|
||||
"generic_button_rss": "RSS",
|
||||
"preferences_quality_dash_option_4320p": "4320p",
|
||||
"preferences_quality_dash_option_2160p": "2160p",
|
||||
"preferences_quality_dash_option_1440p": "1440p",
|
||||
"preferences_quality_dash_option_1080p": "1080p",
|
||||
"preferences_quality_dash_option_480p": "480p",
|
||||
"preferences_quality_dash_option_360p": "360p",
|
||||
"preferences_quality_dash_option_240p": "240p",
|
||||
"preferences_quality_dash_option_144p": "144p",
|
||||
"invidious": "Invidious",
|
||||
"Korean (auto-generated)": "Kóreska (sjálfvirkt útbúið)",
|
||||
"generic_count_days": "{{count}} dagur",
|
||||
"generic_count_days_plural": "{{count}} dagar",
|
||||
"search_filters_date_option_today": "Í dag",
|
||||
"search_filters_type_label": "Tegund",
|
||||
"search_filters_type_option_all": "Hvaða tegund sem er",
|
||||
"search_filters_type_option_video": "Myndskeið",
|
||||
"search_filters_type_option_channel": "Rás",
|
||||
"search_filters_type_option_movie": "Kvikmynd",
|
||||
"search_filters_duration_option_none": "Hvaða lengd sem er",
|
||||
"search_filters_duration_option_short": "Stutt (< 4 mínútur)",
|
||||
"search_filters_duration_option_medium": "Miðlungs (4 - 20 mínútur)",
|
||||
"search_filters_features_label": "Eiginleikar",
|
||||
"search_filters_features_option_subtitles": "Skjátextar/CC",
|
||||
"search_filters_features_option_c_commons": "Creative Commons",
|
||||
"search_filters_sort_option_date": "Dags. innsendingar",
|
||||
"search_filters_sort_option_views": "Fjöldi áhorfa",
|
||||
"next_steps_error_message_refresh": "Endurlesa",
|
||||
"footer_documentation": "Leiðbeiningar",
|
||||
"channel_tab_channels_label": "Rásir",
|
||||
"Import YouTube playlist (.csv)": "Flytja inn YouTube spilunarlista (.csv)",
|
||||
"preferences_quality_option_dash": "DASH (aðlaganleg gæði)",
|
||||
"preferences_preload_label": "Forhlaða gögnum myndskeiðs: ",
|
||||
"Filipino (auto-generated)": "Filippínska (sjálfvirkt útbúin)"
|
||||
"preferences_watch_history_label": "Virkja áhorfssögu: "
|
||||
}
|
||||
|
@ -30,7 +30,7 @@
|
||||
"Import and Export Data": "Importazione ed esportazione dati",
|
||||
"Import": "Importa",
|
||||
"Import Invidious data": "Importa dati Invidious in formato JSON",
|
||||
"Import YouTube subscriptions": "Importa iscrizioni in CSV o OPML di YouTube",
|
||||
"Import YouTube subscriptions": "Importa le iscrizioni da YouTube/OPML",
|
||||
"Import FreeTube subscriptions (.db)": "Importa le iscrizioni da FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importa le iscrizioni da NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Importa i dati di NewPipe (.zip)",
|
||||
@ -449,7 +449,7 @@
|
||||
"Portuguese (Brazil)": "Portoghese (Brasile)",
|
||||
"preferences_watch_history_label": "Attiva cronologia di riproduzione: ",
|
||||
"French (auto-generated)": "Francese (generati automaticamente)",
|
||||
"search_message_use_another_instance": "Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.",
|
||||
"search_message_use_another_instance": " Puoi anche <a href=\"`x`\">cercare in un'altra istanza</a>.",
|
||||
"search_message_no_results": "Nessun risultato trovato.",
|
||||
"search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.",
|
||||
"English (United States)": "Inglese (Stati Uniti)",
|
||||
@ -469,8 +469,8 @@
|
||||
"Spanish (auto-generated)": "Spagnolo (generati automaticamente)",
|
||||
"Spanish (Mexico)": "Spagnolo (Messico)",
|
||||
"Spanish (Spain)": "Spagnolo (Spagna)",
|
||||
"Turkish (auto-generated)": "Turco (generati automaticamente)",
|
||||
"Vietnamese (auto-generated)": "Vietnamita (generati automaticamente)",
|
||||
"Turkish (auto-generated)": "Turco (auto-generato)",
|
||||
"Vietnamese (auto-generated)": "Vietnamita (auto-generato)",
|
||||
"search_filters_date_label": "Data caricamento",
|
||||
"search_filters_date_option_none": "Qualunque data",
|
||||
"search_filters_type_option_all": "Qualunque tipo",
|
||||
@ -513,7 +513,5 @@
|
||||
"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_skip": "Salta la galleria",
|
||||
"carousel_go_to": "Vai al fotogramma `x`",
|
||||
"preferences_preload_label": "Precarica dati video: ",
|
||||
"Filipino (auto-generated)": "Filippino (generati automaticamente)"
|
||||
"carousel_go_to": "Vai al fotogramma `x`"
|
||||
}
|
||||
|
@ -363,7 +363,7 @@
|
||||
"search_filters_features_option_location": "場所",
|
||||
"search_filters_features_option_hdr": "HDR",
|
||||
"Current version: ": "現在のバージョン: ",
|
||||
"next_steps_error_message": "以下をお試しください: ",
|
||||
"next_steps_error_message": "以下をお試してください: ",
|
||||
"next_steps_error_message_refresh": "再読み込み",
|
||||
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
|
||||
"search_filters_duration_option_short": "4分未満",
|
||||
@ -396,7 +396,7 @@
|
||||
"download_subtitles": "字幕 - `x` (.vtt)",
|
||||
"search_filters_features_option_purchased": "購入済み",
|
||||
"preferences_quality_option_dash": "DASH (適応的画質)",
|
||||
"preferences_quality_dash_option_worst": "最低",
|
||||
"preferences_quality_dash_option_worst": "最悪",
|
||||
"preferences_quality_dash_option_best": "最高",
|
||||
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
|
||||
"videoinfo_watch_on_youTube": "YouTubeで視聴",
|
||||
@ -434,7 +434,7 @@
|
||||
"crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>を試す",
|
||||
"crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む",
|
||||
"Popular enabled: ": "人気動画を有効化 ",
|
||||
"search_message_use_another_instance": "<a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
|
||||
"search_message_use_another_instance": " <a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
|
||||
"search_filters_apply_button": "選択したフィルターを適用",
|
||||
"user_saved_playlists": "`x`個の保存済みの再生リスト",
|
||||
"crash_page_you_found_a_bug": "Invidious のバグのようです!",
|
||||
@ -479,7 +479,5 @@
|
||||
"carousel_go_to": "スライド`x`を表示",
|
||||
"carousel_slide": "スライド{{current}} / 全{{total}}個中",
|
||||
"carousel_skip": "画像のスライド表示をスキップ",
|
||||
"toggle_theme": "テーマの切り替え",
|
||||
"preferences_preload_label": "動画データを事前に読み込む: ",
|
||||
"Filipino (auto-generated)": "フィリピノ語 (自動生成)"
|
||||
"toggle_theme": "テーマの切り替え"
|
||||
}
|
||||
|
@ -12,8 +12,8 @@
|
||||
"Dark mode: ": "다크 모드: ",
|
||||
"preferences_player_style_label": "플레이어 스타일: ",
|
||||
"preferences_category_visual": "환경 설정",
|
||||
"preferences_vr_mode_label": "360도 영상 활성화 (WebGL 필요): ",
|
||||
"preferences_extend_desc_label": "자동으로 비디오 설명 펼치기: ",
|
||||
"preferences_vr_mode_label": "VR 영상 활성화(WebGL 필요): ",
|
||||
"preferences_extend_desc_label": "자동으로 비디오 설명을 확장: ",
|
||||
"preferences_annotations_label": "기본으로 주석 표시: ",
|
||||
"preferences_related_videos_label": "관련 동영상 보기: ",
|
||||
"Fallback captions: ": "대체 자막: ",
|
||||
@ -48,7 +48,7 @@
|
||||
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
||||
"History": "시청 기록",
|
||||
"Delete account?": "계정을 삭제 하시겠습니까?",
|
||||
"Export data as JSON": "인비디어스 데이터 내보내기 (.json)",
|
||||
"Export data as JSON": "JSON으로 데이터 내보내기",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
|
||||
"Export subscriptions as OPML": "OPML로 구독 내보내기",
|
||||
"Export": "내보내기",
|
||||
@ -65,13 +65,13 @@
|
||||
"Authorize token?": "토큰을 승인하시겠습니까?",
|
||||
"New passwords must match": "새 비밀번호는 일치해야 합니다",
|
||||
"New password": "새 비밀번호",
|
||||
"Clear watch history?": "시청 기록을 지우시겠습니까?",
|
||||
"Clear watch history?": "재생 기록을 삭제 하시겠습니까?",
|
||||
"Previous page": "이전 페이지",
|
||||
"Next page": "다음 페이지",
|
||||
"last": "마지막",
|
||||
"Shared `x` ago": "`x` 전",
|
||||
"popular": "인기순",
|
||||
"oldest": "과거순",
|
||||
"popular": "인기",
|
||||
"oldest": "오래된순",
|
||||
"newest": "최신순",
|
||||
"View playlist on YouTube": "유튜브에서 재생목록 보기",
|
||||
"View channel on YouTube": "유튜브에서 채널 보기",
|
||||
@ -123,7 +123,7 @@
|
||||
"Create playlist": "재생목록 생성",
|
||||
"Trending": "급상승",
|
||||
"Delete playlist": "재생목록 삭제",
|
||||
"Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?",
|
||||
"Delete playlist `x`?": "재생목록 `x` 를 삭제 하시겠습니까?",
|
||||
"Updated `x` ago": "`x` 전에 업데이트됨",
|
||||
"Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.",
|
||||
"View all playlists": "모든 재생목록 보기",
|
||||
@ -267,7 +267,7 @@
|
||||
"Bulgarian": "불가리아어",
|
||||
"Bosnian": "보스니아어",
|
||||
"Belarusian": "벨라루스어",
|
||||
"View more comments on Reddit": "레딧에서 댓글 더 보기",
|
||||
"View more comments on Reddit": "레딧에서 더 많은 댓글 보기",
|
||||
"View YouTube comments": "유튜브 댓글 보기",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
|
||||
"Shared `x`": "`x` 업로드",
|
||||
@ -419,7 +419,7 @@
|
||||
"Portuguese (Brazil)": "포르투갈어 (브라질)",
|
||||
"search_message_no_results": "결과가 없습니다.",
|
||||
"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)": "영어 (미국)",
|
||||
"Chinese": "중국어",
|
||||
"Chinese (China)": "중국어 (중국)",
|
||||
@ -479,6 +479,5 @@
|
||||
"carousel_go_to": "`x` 슬라이드로 이동",
|
||||
"Search for videos": "비디오 검색",
|
||||
"toggle_theme": "테마 전환",
|
||||
"carousel_slide": "{{total}}의 슬라이드 {{current}}",
|
||||
"preferences_preload_label": "비디오 데이터 사전 로드: "
|
||||
"carousel_slide": "{{total}}의 슬라이드 {{current}}"
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"Import and Export Data": "Importer- og eksporter data",
|
||||
"Import": "Importer",
|
||||
"Import Invidious data": "Importer Invidious-JSON-data",
|
||||
"Import YouTube subscriptions": "Importer YouTube CSV eller OPML-abonnementer",
|
||||
"Import YouTube subscriptions": "Importer YouTube/OPML-abonnementer",
|
||||
"Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnementer (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnementer (.json)",
|
||||
"Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)",
|
||||
@ -322,13 +322,13 @@
|
||||
"channel_tab_community_label": "Gemenskap",
|
||||
"search_filters_sort_option_relevance": "relevans",
|
||||
"search_filters_sort_option_rating": "vurdering",
|
||||
"search_filters_sort_option_date": "Opplastingsdato",
|
||||
"search_filters_sort_option_date": "dato",
|
||||
"search_filters_sort_option_views": "visninger",
|
||||
"search_filters_type_label": "innholdstype",
|
||||
"search_filters_duration_label": "varighet",
|
||||
"search_filters_features_label": "funksjoner",
|
||||
"search_filters_sort_label": "sorter",
|
||||
"search_filters_date_option_hour": "Siste time",
|
||||
"search_filters_date_option_hour": "time",
|
||||
"search_filters_date_option_today": "i dag",
|
||||
"search_filters_date_option_week": "uke",
|
||||
"search_filters_date_option_month": "måned",
|
||||
@ -459,7 +459,7 @@
|
||||
"search_message_no_results": "Resultatløst.",
|
||||
"search_filters_type_option_all": "Alle typer",
|
||||
"search_filters_duration_option_none": "Enhver varighet",
|
||||
"search_message_use_another_instance": "Du kan også <a href=\"`x`\">søke på en annen instans</a>.",
|
||||
"search_message_use_another_instance": " Du kan også <a href=\"`x`\">søke på en annen instans</a>.",
|
||||
"search_filters_date_label": "Opplastningsdato",
|
||||
"search_filters_apply_button": "Bruk valgte filtre",
|
||||
"search_filters_date_option_none": "Siden begynnelsen",
|
||||
@ -487,15 +487,5 @@
|
||||
"playlist_button_add_items": "Legg til videoer",
|
||||
"generic_channels_count": "{{count}} kanal",
|
||||
"generic_channels_count_plural": "{{count}} kanaler",
|
||||
"Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)",
|
||||
"carousel_go_to": "Gå til lysark `x`",
|
||||
"Search for videos": "Søk i videoer",
|
||||
"Answer": "Svar",
|
||||
"carousel_slide": "Lysark {{current}} av {{total}}",
|
||||
"carousel_skip": "Hopp over karusellen",
|
||||
"Add to playlist": "Legg til i spilleliste",
|
||||
"Add to playlist: ": "Legg til i spilleliste: ",
|
||||
"The Popular feed has been disabled by the administrator.": "Populært-kilden er koblet ut av administratoren.",
|
||||
"toggle_theme": "Endre utseende",
|
||||
"preferences_preload_label": "Last videodata på forhånd: "
|
||||
"Import YouTube watch history (.json)": "Importere YouTube visningshistorikk (.json)"
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"Import and Export Data": "Gegevens im- en exporteren",
|
||||
"Import": "Importeren",
|
||||
"Import Invidious data": "JSON-gegevens Invidious importeren",
|
||||
"Import YouTube subscriptions": "YouTube CVS of OPML-abonnementen importeren",
|
||||
"Import YouTube subscriptions": "YouTube-/OPML-abonnementen importeren",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTube-abonnementen importeren (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipe-abonnementen importeren (.json)",
|
||||
"Import NewPipe data (.zip)": "NewPipe-gegevens importeren (.zip)",
|
||||
@ -86,7 +86,7 @@
|
||||
"Only show latest unwatched video from channel: ": "Alleen nieuwste niet-bekeken video van kanaal tonen: ",
|
||||
"preferences_unseen_only_label": "Alleen niet-bekeken videos tonen: ",
|
||||
"preferences_notifications_only_label": "Alleen meldingen tonen (als die er zijn): ",
|
||||
"Enable web notifications": "Systeemmeldingen inschakelen",
|
||||
"Enable web notifications": "Systemmeldingen inschakelen",
|
||||
"`x` uploaded a video": "`x` heeft een video geüpload",
|
||||
"`x` is live": "`x` zendt nu live uit",
|
||||
"preferences_category_data": "Gegevensinstellingen",
|
||||
@ -192,15 +192,15 @@
|
||||
"Arabic": "Arabisch",
|
||||
"Armenian": "Armeens",
|
||||
"Azerbaijani": "Azerbeidzjaans",
|
||||
"Bangla": "Bengaals",
|
||||
"Bangla": "Bangla",
|
||||
"Basque": "Baskisch",
|
||||
"Belarusian": "Wit-Russisch",
|
||||
"Belarusian": "Wit-Rrussisch",
|
||||
"Bosnian": "Bosnisch",
|
||||
"Bulgarian": "Bulgaars",
|
||||
"Burmese": "Birmaans",
|
||||
"Catalan": "Catalaans",
|
||||
"Cebuano": "Cebuaans",
|
||||
"Chinese (Simplified)": "Chinees (Vereenvoudigd)",
|
||||
"Cebuano": "Cebuano",
|
||||
"Chinese (Simplified)": "Chinees (Veereenvoudigd)",
|
||||
"Chinese (Traditional)": "Chinees (Traditioneel)",
|
||||
"Corsican": "Corsicaans",
|
||||
"Croatian": "Kroatisch",
|
||||
@ -217,23 +217,23 @@
|
||||
"German": "Duits",
|
||||
"Greek": "Grieks",
|
||||
"Gujarati": "Gujarati",
|
||||
"Haitian Creole": "Haïtiaans Creools",
|
||||
"Haitian Creole": "Creools",
|
||||
"Hausa": "Hausa",
|
||||
"Hawaiian": "Hawaïaans",
|
||||
"Hebrew": "Hebreeuws",
|
||||
"Hebrew": "Heebreeuws",
|
||||
"Hindi": "Hindi",
|
||||
"Hmong": "Hmong",
|
||||
"Hungarian": "Hongaars",
|
||||
"Icelandic": "IJslands",
|
||||
"Igbo": "Ikbo",
|
||||
"Igbo": "Igbo",
|
||||
"Indonesian": "Indonesisch",
|
||||
"Irish": "Iers",
|
||||
"Italian": "Italiaans",
|
||||
"Japanese": "Japans",
|
||||
"Javanese": "Javaans",
|
||||
"Kannada": "Kannada-taal",
|
||||
"Kannada": "Kannada",
|
||||
"Kazakh": "Kazachs",
|
||||
"Khmer": "Khmer-taal",
|
||||
"Khmer": "Khmer",
|
||||
"Korean": "Koreaans",
|
||||
"Kurdish": "Koerdisch",
|
||||
"Kyrgyz": "Kirgizisch",
|
||||
@ -245,10 +245,10 @@
|
||||
"Macedonian": "Macedonisch",
|
||||
"Malagasy": "Malagassisch",
|
||||
"Malay": "Maleisisch",
|
||||
"Malayalam": "Malayalam-taal",
|
||||
"Malayalam": "Malayalam",
|
||||
"Maltese": "Maltees",
|
||||
"Maori": "Maorisch",
|
||||
"Marathi": "Marathi-taal",
|
||||
"Marathi": "Marathi",
|
||||
"Mongolian": "Mongools",
|
||||
"Nepali": "Nepalees",
|
||||
"Norwegian Bokmål": "Noors (Bokmål)",
|
||||
@ -309,7 +309,7 @@
|
||||
"(edited)": "(bewerkt)",
|
||||
"YouTube comment permalink": "Link naar YouTube-reactie",
|
||||
"permalink": "permalink",
|
||||
"`x` marked it with a ❤": "`x` heeft dit gemarkeerd met een ❤",
|
||||
"`x` marked it with a ❤": "`x` heeft dit gemarkeerd met ❤",
|
||||
"Audio mode": "Audiomodus",
|
||||
"Video mode": "Videomodus",
|
||||
"channel_tab_videos_label": "Video's",
|
||||
@ -317,13 +317,13 @@
|
||||
"channel_tab_community_label": "Gemeenschap",
|
||||
"search_filters_sort_option_relevance": "relevantie",
|
||||
"search_filters_sort_option_rating": "beoordeling",
|
||||
"search_filters_sort_option_date": "Upload datum",
|
||||
"search_filters_sort_option_date": "datum",
|
||||
"search_filters_sort_option_views": "keren bekeken",
|
||||
"search_filters_type_label": "Type inhoud",
|
||||
"search_filters_duration_label": "duur",
|
||||
"search_filters_features_label": "eigenschappen",
|
||||
"search_filters_sort_label": "sorteren",
|
||||
"search_filters_date_option_hour": "Laatste uur",
|
||||
"search_filters_date_option_hour": "uur",
|
||||
"search_filters_date_option_today": "vandaag",
|
||||
"search_filters_date_option_week": "week",
|
||||
"search_filters_date_option_month": "maand",
|
||||
@ -357,7 +357,7 @@
|
||||
"footer_original_source_code": "Originele bron-code",
|
||||
"footer_modfied_source_code": "Gewijzigde bron-code",
|
||||
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
|
||||
"next_steps_error_message": "Waarna u zou kunnen proberen om: ",
|
||||
"next_steps_error_message": "Daarna moet u proberen om: ",
|
||||
"footer_source_code": "Bron-code",
|
||||
"search_filters_duration_option_long": "Lang (> 20 minuten)",
|
||||
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
|
||||
@ -396,7 +396,7 @@
|
||||
"Dutch (auto-generated)": "Nederlands (automatisch gegenereerd)",
|
||||
"tokens_count": "{{count}} token",
|
||||
"tokens_count_plural": "{{count}} tokens",
|
||||
"generic_count_seconds": "{{count}} seconde",
|
||||
"generic_count_seconds": "{{count}} second",
|
||||
"generic_count_seconds_plural": "{{count}} seconden",
|
||||
"generic_count_weeks": "{{count}} week",
|
||||
"generic_count_weeks_plural": "{{count}} weken",
|
||||
@ -449,8 +449,8 @@
|
||||
"generic_playlists_count_plural": "{{count}} afspeellijsten",
|
||||
"Chinese (Hong Kong)": "Chinees (Hongkong)",
|
||||
"Korean (auto-generated)": "Koreaans (automatisch gegenereerd)",
|
||||
"search_filters_apply_button": "Geselecteerde filters toepassen",
|
||||
"search_message_use_another_instance": "Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.",
|
||||
"search_filters_apply_button": "Geselecteerd filters toepassen",
|
||||
"search_message_use_another_instance": " Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.",
|
||||
"Cantonese (Hong Kong)": "Kantonees (Hongkong)",
|
||||
"Chinese (China)": "Chinees (China)",
|
||||
"crash_page_read_the_faq": "de <a href=\"`x`\">veelgestelde vragen (FAQ)</a> gelezen hebt",
|
||||
@ -477,7 +477,7 @@
|
||||
"Song: ": "Lied: ",
|
||||
"generic_channels_count": "{{count}} kanaal",
|
||||
"generic_channels_count_plural": "{{count}} kanalen",
|
||||
"Popular enabled: ": "Populair ingeschakeld: ",
|
||||
"Popular enabled: ": "Populair geactiveerd: ",
|
||||
"channel_tab_playlists_label": "Afspeellijsten",
|
||||
"generic_button_edit": "Bewerken",
|
||||
"Music in this video": "Muziek in deze video",
|
||||
@ -496,7 +496,5 @@
|
||||
"Answer": "Antwoorden",
|
||||
"Search for videos": "Naar video's zoeken",
|
||||
"carousel_skip": "Carousel overslaan",
|
||||
"toggle_theme": "Thema omschakelen",
|
||||
"preferences_preload_label": "Videogegevens vooraf laden: ",
|
||||
"Filipino (auto-generated)": "Filipijns (automatisch gegenereerd)"
|
||||
"toggle_theme": "Thema omschakelen"
|
||||
}
|
||||
|
@ -478,7 +478,7 @@
|
||||
"search_filters_date_label": "Data przesłania",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_date_option_none": "Dowolna data",
|
||||
"search_message_use_another_instance": "Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.",
|
||||
"search_message_use_another_instance": " Możesz także <a href=\"`x`\">wyszukać w innej instancji</a>.",
|
||||
"search_filters_type_option_all": "Dowolny typ",
|
||||
"search_filters_duration_option_none": "Dowolna długość",
|
||||
"search_filters_duration_option_medium": "Średnia (4-20 minut)",
|
||||
@ -513,7 +513,5 @@
|
||||
"Add to playlist: ": "Dodaj do playlisty: ",
|
||||
"carousel_slide": "Slajd {{current}} z {{total}}",
|
||||
"carousel_skip": "Pomiń karuzelę",
|
||||
"carousel_go_to": "Przejdź do slajdu `x`",
|
||||
"preferences_preload_label": "Wstępne ładowanie danych wideo: ",
|
||||
"Filipino (auto-generated)": "filipiński (wygenerowany automatycznie)"
|
||||
"carousel_go_to": "Przejdź do slajdu `x`"
|
||||
}
|
||||
|
@ -41,7 +41,7 @@
|
||||
"Time (h:mm:ss):": "Hora (h:mm:ss):",
|
||||
"Text CAPTCHA": "Mudar para um desafio de texto",
|
||||
"Image CAPTCHA": "Mudar para um desafio visual",
|
||||
"Sign In": "Fazer login",
|
||||
"Sign In": "Entrar",
|
||||
"Register": "Criar conta",
|
||||
"E-mail": "E-mail",
|
||||
"Preferences": "Preferências",
|
||||
@ -474,7 +474,7 @@
|
||||
"Spanish (auto-generated)": "Espanhol (gerado automaticamente)",
|
||||
"Spanish (Mexico)": "Espanhol (México)",
|
||||
"search_filters_duration_option_none": "Qualquer duração",
|
||||
"search_message_use_another_instance": "Você também pode <a href=\"`x`\">pesquisar em outra instância</a>.",
|
||||
"search_message_use_another_instance": " Você também pode <a href=\"`x`\">pesquisar em outra instância</a>.",
|
||||
"Spanish (Spain)": "Espanhol (Espanha)",
|
||||
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
|
||||
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)",
|
||||
@ -513,7 +513,5 @@
|
||||
"Answer": "Resposta",
|
||||
"carousel_slide": "Slide {{current}} de {{total}}",
|
||||
"carousel_skip": "Ignorar carrossel",
|
||||
"carousel_go_to": "Ir ao slide `x`",
|
||||
"preferences_preload_label": "Pré-carregar dados do vídeo: ",
|
||||
"Filipino (auto-generated)": "Filipino (gerado automaticamente)"
|
||||
"carousel_go_to": "Ir ao slide `x`"
|
||||
}
|
||||
|
@ -253,7 +253,7 @@
|
||||
"Import NewPipe data (.zip)": "Importar dados do NewPipe (.zip)",
|
||||
"Import NewPipe subscriptions (.json)": "Importar subscrições do NewPipe (.json)",
|
||||
"Import FreeTube subscriptions (.db)": "Importar subscrições do FreeTube (.db)",
|
||||
"Import YouTube subscriptions": "Importar via YouTube csv ou subscrição OPML",
|
||||
"Import YouTube subscriptions": "Importar subscrições via YouTube/OPML",
|
||||
"Import Invidious data": "Importar dados JSON do Invidious",
|
||||
"Import": "Importar",
|
||||
"No": "Não",
|
||||
@ -448,7 +448,7 @@
|
||||
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
||||
"search_message_no_results": "Nenhum resultado encontrado.",
|
||||
"search_message_change_filters_or_query": "Tente alargar os termos genéricos da pesquisa e/ou alterar os filtros.",
|
||||
"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>.",
|
||||
"English (United Kingdom)": "Inglês (Reino Unido)",
|
||||
"English (United States)": "Inglês (Estados Unidos)",
|
||||
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
|
||||
@ -508,12 +508,10 @@
|
||||
"toggle_theme": "Trocar tema",
|
||||
"Add to playlist": "Adicionar à lista de reprodução",
|
||||
"Add to playlist: ": "Adicionar à lista de reprodução: ",
|
||||
"Answer": "Responder",
|
||||
"Answer": "Resposta",
|
||||
"Search for videos": "Procurar vídeos",
|
||||
"carousel_slide": "Diapositivo {{current}} de{{total}}",
|
||||
"carousel_skip": "Ignorar carrossel",
|
||||
"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.",
|
||||
"preferences_preload_label": "Pré-carregamento dos dados: ",
|
||||
"Filipino (auto-generated)": "Filipino (gerado automaticamente)"
|
||||
"The Popular feed has been disabled by the administrator.": "O feed Popular foi desativado por um administrador."
|
||||
}
|
||||
|
@ -11,7 +11,6 @@
|
||||
"last": "последние",
|
||||
"Next page": "Следующая страница",
|
||||
"Previous page": "Предыдущая страница",
|
||||
"First page": "Первая страница",
|
||||
"Clear watch history?": "Очистить историю просмотров?",
|
||||
"New password": "Новый пароль",
|
||||
"New passwords must match": "Новые пароли не совпадают",
|
||||
@ -22,7 +21,7 @@
|
||||
"Import and Export Data": "Импорт и экспорт данных",
|
||||
"Import": "Импорт",
|
||||
"Import Invidious data": "Импортировать JSON с данными Invidious",
|
||||
"Import YouTube subscriptions": "Импортировать подписки из CSV или OPML",
|
||||
"Import YouTube subscriptions": "Импортировать подписки из YouTube/OPML",
|
||||
"Import FreeTube subscriptions (.db)": "Импортировать подписки из FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Импортировать подписки из NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Импортировать данные из NewPipe (.zip)",
|
||||
@ -49,8 +48,8 @@
|
||||
"preferences_category_player": "Настройки проигрывателя",
|
||||
"preferences_video_loop_label": "Всегда повторять: ",
|
||||
"preferences_autoplay_label": "Автовоспроизведение: ",
|
||||
"preferences_continue_label": "Воспроизводить следующее видео: ",
|
||||
"preferences_continue_autoplay_label": "Автовоспроизведение следующего видео: ",
|
||||
"preferences_continue_label": "Переходить к следующему видео? ",
|
||||
"preferences_continue_autoplay_label": "Автопроигрывание следующего видео: ",
|
||||
"preferences_listen_label": "Режим «только аудио» по умолчанию: ",
|
||||
"preferences_local_label": "Проигрывать видео через прокси? ",
|
||||
"preferences_speed_label": "Скорость видео по умолчанию: ",
|
||||
@ -505,15 +504,5 @@
|
||||
"generic_channels_count_0": "{{count}} канал",
|
||||
"generic_channels_count_1": "{{count}} канала",
|
||||
"generic_channels_count_2": "{{count}} каналов",
|
||||
"Import YouTube watch history (.json)": "Импортировать историю просмотра из YouTube (.json)",
|
||||
"Add to playlist": "Добавить в плейлист",
|
||||
"Add to playlist: ": "Добавить в плейлист: ",
|
||||
"Answer": "Ответить",
|
||||
"Search for videos": "Поиск видео",
|
||||
"The Popular feed has been disabled by the administrator.": "Лента популярного была отключена администратором.",
|
||||
"toggle_theme": "Переключатель тем",
|
||||
"carousel_slide": "Пролистано {{current}} из {{total}}",
|
||||
"carousel_skip": "Пропустить всё",
|
||||
"carousel_go_to": "Перейти к странице `x`",
|
||||
"preferences_preload_label": "Предзагрузка видеоданных: "
|
||||
"Import YouTube watch history (.json)": "Импортировать историю просмотра из YouTube (.json)"
|
||||
}
|
||||
|
@ -13,7 +13,7 @@
|
||||
"Import and Export Data": "Uvoz in izvoz podatkov",
|
||||
"Import": "Uvozi",
|
||||
"Import Invidious data": "Uvozi Invidious JSON podatke",
|
||||
"Import YouTube subscriptions": "Uvozi YouTube CSV ali OPML naročnine",
|
||||
"Import YouTube subscriptions": "Uvozi YouTube/OPML naročnine",
|
||||
"Import FreeTube subscriptions (.db)": "Uvozi FreeTube (.db) naročnine",
|
||||
"Import NewPipe data (.zip)": "Uvozi NewPipe (.zip) podatke",
|
||||
"Export": "Izvozi",
|
||||
@ -105,7 +105,7 @@
|
||||
"Show more": "Pokaži več",
|
||||
"Switch Invidious Instance": "Preklopi Invidious instanco",
|
||||
"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: ",
|
||||
"Engagement: ": "Sodelovanje: ",
|
||||
"Blacklisted regions: ": "Regije na seznamu nedovoljenih: ",
|
||||
@ -462,7 +462,7 @@
|
||||
"search_filters_features_option_four_k": "4K",
|
||||
"search_filters_features_option_hdr": "HDR",
|
||||
"next_steps_error_message_refresh": "Osveži",
|
||||
"search_filters_date_option_hour": "V zadnji uri",
|
||||
"search_filters_date_option_hour": "Zadnja ura",
|
||||
"search_filters_features_option_purchased": "Kupljeno",
|
||||
"search_filters_sort_label": "Razvrsti po",
|
||||
"search_filters_sort_option_views": "številu ogledov",
|
||||
@ -521,16 +521,5 @@
|
||||
"generic_channels_count_1": "{{count}} kanala",
|
||||
"generic_channels_count_2": "{{count}} kanali",
|
||||
"generic_channels_count_3": "{{count}} kanalov",
|
||||
"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: "
|
||||
"Import YouTube watch history (.json)": "Uvozi zgodovino gledanja YouTube (.json)"
|
||||
}
|
||||
|
@ -257,13 +257,13 @@
|
||||
"Video mode": "Mënyrë video",
|
||||
"channel_tab_videos_label": "Video",
|
||||
"search_filters_sort_option_rating": "Vlerësim",
|
||||
"search_filters_sort_option_date": "Datë ngarkimi",
|
||||
"search_filters_sort_option_date": "Datë Ngarkimi",
|
||||
"search_filters_sort_option_views": "Numër parjesh",
|
||||
"search_filters_type_label": "Lloj",
|
||||
"search_filters_duration_label": "Kohëzgjatje",
|
||||
"search_filters_features_label": "Veçori",
|
||||
"search_filters_sort_label": "Renditi Sipas",
|
||||
"search_filters_date_option_hour": "Orën e fundit",
|
||||
"search_filters_date_option_hour": "Orën e Fundit",
|
||||
"search_filters_date_option_today": "Sot",
|
||||
"search_filters_duration_option_long": "E gjatë (> 20 minuta)",
|
||||
"search_filters_features_option_hd": "HD",
|
||||
@ -435,14 +435,14 @@
|
||||
"tokens_count_plural": "{{count}} tokenë",
|
||||
"preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ",
|
||||
"Import Invidious data": "Importoni të dhëna JSON Invidious",
|
||||
"Import YouTube subscriptions": "Importoni pajtime YouTube CSV ose OPML",
|
||||
"Import YouTube subscriptions": "Importoni pajtime YouTube/OPML",
|
||||
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
|
||||
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
|
||||
"Shared `x`": "Ndarë me të tjerë më `x`",
|
||||
"search_filters_title": "Filtra",
|
||||
"Popular enabled: ": "Me populloret të aktivizuara: ",
|
||||
"error_video_not_in_playlist": "Videoja e kërkuar s’ekziston në këtë luajlistë. <a href=\"`x`\">Klikoni këtu për faqen hyrëse të luajlistës.</a>",
|
||||
"search_message_use_another_instance": "Mundeni edhe të <a href=\"`x`\">kërkoni në një instancë tjetër</a>.",
|
||||
"search_message_use_another_instance": " Mundeni edhe të <a href=\"`x`\">kërkoni në një instancë tjetër</a>.",
|
||||
"search_filters_date_label": "Datë ngarkimi",
|
||||
"preferences_watch_history_label": "Aktivizo historik parjesh: ",
|
||||
"Top enabled: ": "Me kryesueset të aktivizuara: ",
|
||||
@ -484,15 +484,5 @@
|
||||
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
|
||||
"preferences_local_label": "Video përmes ndërmjetësi: ",
|
||||
"Fallback captions: ": "Titra nga halli: ",
|
||||
"Erroneous challenge": "Zgjidhje e gabuar",
|
||||
"Add to playlist: ": "Shtoje te luajlistë: ",
|
||||
"Add to playlist": "Shtoje te luajlistë",
|
||||
"Answer": "Përgjigje",
|
||||
"Search for videos": "Kërko për video",
|
||||
"The Popular feed has been disabled by the administrator.": "Prurja Popullore është çaktivizuar nga përgjegjësi.",
|
||||
"carousel_skip": "Anashkaloje Rrotullamen",
|
||||
"carousel_slide": "Diapozitiv {{current}} nga {{total}}",
|
||||
"carousel_go_to": "Kalo te diapozitivi `x`",
|
||||
"Filipino (auto-generated)": "Filipineze (të prodhuara automatikisht)",
|
||||
"preferences_preload_label": "Parangarko të dhëna videoje: "
|
||||
"Erroneous challenge": "Zgjidhje e gabuar"
|
||||
}
|
||||
|
@ -174,7 +174,7 @@
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "Hej! Izgleda da ste isključili JavaScript. Kliknite ovde da biste videli komentare, imajte na umu da će možda potrajati malo duže da se učitaju.",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Pogledaj `x` komentar",
|
||||
"": "Pogledaj`x` komentara"
|
||||
"": "Pogledaj`x` komentare"
|
||||
},
|
||||
"View Reddit comments": "Pogledaj Reddit komentare",
|
||||
"CAPTCHA is a required field": "CAPTCHA je obavezno polje",
|
||||
@ -211,7 +211,7 @@
|
||||
"About": "O sajtu",
|
||||
"footer_source_code": "Izvorni kôd",
|
||||
"footer_original_source_code": "Originalni izvorni kôd",
|
||||
"preferences_related_videos_label": "Prikaži srodne video snimke: ",
|
||||
"preferences_related_videos_label": "Prikaži povezane video snimke: ",
|
||||
"preferences_annotations_label": "Podrazumevano prikaži napomene: ",
|
||||
"preferences_extend_desc_label": "Automatski proširi opis video snimka: ",
|
||||
"preferences_vr_mode_label": "Interaktivni video snimci od 360 stepeni (zahteva WebGl): ",
|
||||
@ -404,7 +404,7 @@
|
||||
"generic_count_months_0": "{{count}} mesec",
|
||||
"generic_count_months_1": "{{count}} meseca",
|
||||
"generic_count_months_2": "{{count}} meseci",
|
||||
"search_message_use_another_instance": "Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.",
|
||||
"search_message_use_another_instance": " Takođe, možete <a href=\"`x`\">pretraživati na drugoj instanci</a>.",
|
||||
"generic_subscribers_count_0": "{{count}} pratilac",
|
||||
"generic_subscribers_count_1": "{{count}} pratioca",
|
||||
"generic_subscribers_count_2": "{{count}} pratilaca",
|
||||
@ -513,7 +513,5 @@
|
||||
"Answer": "Odgovor",
|
||||
"Search for videos": "Pretražite video snimke",
|
||||
"carousel_skip": "Preskoči karusel",
|
||||
"toggle_theme": "Подеси тему",
|
||||
"preferences_preload_label": "Unapred učitaj podatke o video snimku: ",
|
||||
"Filipino (auto-generated)": "Filipinski (automatski generisano)"
|
||||
"toggle_theme": "Подеси тему"
|
||||
}
|
||||
|
@ -60,7 +60,7 @@
|
||||
"reddit": "Reddit",
|
||||
"preferences_captions_label": "Подразумевани титлови: ",
|
||||
"Fallback captions: ": "Резервни титлови: ",
|
||||
"preferences_related_videos_label": "Прикажи сродне видео снимке: ",
|
||||
"preferences_related_videos_label": "Прикажи повезане видео снимке: ",
|
||||
"preferences_annotations_label": "Подразумевано прикажи напомене: ",
|
||||
"preferences_category_visual": "Визуелна подешавања",
|
||||
"preferences_player_style_label": "Стил плејера: ",
|
||||
@ -246,7 +246,7 @@
|
||||
"preferences_locale_label": "Језик: ",
|
||||
"Persian": "Персијски",
|
||||
"View `x` comments": {
|
||||
"": "Погледај `x` коментара",
|
||||
"": "Погледај `x` коментаре",
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "Погледај `x` коментар"
|
||||
},
|
||||
"search_filters_type_option_channel": "Канал",
|
||||
@ -404,7 +404,7 @@
|
||||
"generic_count_months_0": "{{count}} месец",
|
||||
"generic_count_months_1": "{{count}} месеца",
|
||||
"generic_count_months_2": "{{count}} месеци",
|
||||
"search_message_use_another_instance": "Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.",
|
||||
"search_message_use_another_instance": " Такође, можете <a href=\"`x`\">претраживати на другој инстанци</a>.",
|
||||
"generic_subscribers_count_0": "{{count}} пратилац",
|
||||
"generic_subscribers_count_1": "{{count}} пратиоца",
|
||||
"generic_subscribers_count_2": "{{count}} пратилаца",
|
||||
@ -513,7 +513,5 @@
|
||||
"Add to playlist: ": "Додајте на плејлисту: ",
|
||||
"carousel_skip": "Прескочи карусел",
|
||||
"The Popular feed has been disabled by the administrator.": "Администратор је онемогућио фид „Популарно“.",
|
||||
"carousel_slide": "Слајд {{current}} од {{total}}",
|
||||
"preferences_preload_label": "Унапред учитај податке о видео снимку: ",
|
||||
"Filipino (auto-generated)": "Филипински (аутоматски генерисано)"
|
||||
"carousel_slide": "Слајд {{current}} од {{total}}"
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"Import and Export Data": "Importera och exportera data",
|
||||
"Import": "Importera",
|
||||
"Import Invidious data": "Importera Invidious JSON data",
|
||||
"Import YouTube subscriptions": "Importera YouTube CSV eller OPML prenumerationer",
|
||||
"Import YouTube subscriptions": "Importera YouTube/OPML prenumerationer",
|
||||
"Import FreeTube subscriptions (.db)": "Importera FreeTube-prenumerationer (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importera NewPipe-prenumerationer (.json)",
|
||||
"Import NewPipe data (.zip)": "Importera NewPipe-data (.zip)",
|
||||
@ -320,13 +320,13 @@
|
||||
"channel_tab_community_label": "Gemenskap",
|
||||
"search_filters_sort_option_relevance": "Relevans",
|
||||
"search_filters_sort_option_rating": "Rankning",
|
||||
"search_filters_sort_option_date": "Uppladdnings datum",
|
||||
"search_filters_sort_option_date": "Uppladdnings Datum",
|
||||
"search_filters_sort_option_views": "Visningar",
|
||||
"search_filters_type_label": "Typ",
|
||||
"search_filters_duration_label": "Varaktighet",
|
||||
"search_filters_features_label": "Funktioner",
|
||||
"search_filters_sort_label": "Sortera efter",
|
||||
"search_filters_date_option_hour": "Senaste timmen",
|
||||
"search_filters_date_option_hour": "Senaste Timmen",
|
||||
"search_filters_date_option_today": "Idag",
|
||||
"search_filters_date_option_week": "Denna vecka",
|
||||
"search_filters_date_option_month": "Denna månad",
|
||||
@ -393,7 +393,7 @@
|
||||
"Artist: ": "Artist: ",
|
||||
"generic_count_months": "{{count}}månad",
|
||||
"generic_count_months_plural": "{{count}}månader",
|
||||
"search_message_use_another_instance": "Du kan också <a href=\"`x`\">söka på en annan instans</a>.",
|
||||
"search_message_use_another_instance": " Du kan också <a href=\"`x`\">söka på en annan instans</a>.",
|
||||
"generic_subscribers_count": "{{count}} prenumerant",
|
||||
"generic_subscribers_count_plural": "{{count}} prenumeranter",
|
||||
"download_subtitles": "Undertexter - `x` (.vtt)",
|
||||
@ -496,7 +496,5 @@
|
||||
"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_skip": "Hoppa över karusellen",
|
||||
"carousel_go_to": "Gå till bildspel `x`",
|
||||
"preferences_preload_label": "Förladda video data: ",
|
||||
"Filipino (auto-generated)": "Filippinska (auto-genererad)"
|
||||
"carousel_go_to": "Gå till bildspel `x`"
|
||||
}
|
||||
|
502
locales/ta.json
502
locales/ta.json
@ -1,502 +0,0 @@
|
||||
{
|
||||
"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 +0,0 @@
|
||||
{}
|
@ -322,13 +322,13 @@
|
||||
"channel_tab_community_label": "Topluluk",
|
||||
"search_filters_sort_option_relevance": "İlgi",
|
||||
"search_filters_sort_option_rating": "Değerlendirme",
|
||||
"search_filters_sort_option_date": "Yükleme tarihi",
|
||||
"search_filters_sort_option_date": "Yükleme Tarihi",
|
||||
"search_filters_sort_option_views": "Görüntüleme Sayısı",
|
||||
"search_filters_type_label": "Tür",
|
||||
"search_filters_duration_label": "Süre",
|
||||
"search_filters_features_label": "Özellikler",
|
||||
"search_filters_sort_label": "Sıralama Ölçütü",
|
||||
"search_filters_date_option_hour": "Son saat",
|
||||
"search_filters_date_option_hour": "Son Saat",
|
||||
"search_filters_date_option_today": "Bugün",
|
||||
"search_filters_date_option_week": "Bu Hafta",
|
||||
"search_filters_date_option_month": "Bu Ay",
|
||||
@ -452,7 +452,7 @@
|
||||
"Spanish (Spain)": "İspanyolca (İspanya)",
|
||||
"Vietnamese (auto-generated)": "Vietnamca (Otomatik Oluşturuldu)",
|
||||
"preferences_watch_history_label": "İzleme Geçmişini Etkinleştir: ",
|
||||
"search_message_use_another_instance": "Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.",
|
||||
"search_message_use_another_instance": " Ayrıca <a href=\"`x`\">başka bir örnekte arayabilirsiniz</a>.",
|
||||
"search_filters_type_option_all": "Herhangi Bir Tür",
|
||||
"search_filters_duration_option_none": "Herhangi Bir Süre",
|
||||
"search_message_no_results": "Sonuç bulunamadı.",
|
||||
@ -496,6 +496,5 @@
|
||||
"carousel_slide": "Sunum {{current}} / {{total}}",
|
||||
"carousel_skip": "Kayar menüyü atla",
|
||||
"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ı.",
|
||||
"preferences_preload_label": "Video verilerini önceden yükle: "
|
||||
"The Popular feed has been disabled by the administrator.": "Popüler akışı yönetici tarafından devre dışı bırakıldı."
|
||||
}
|
||||
|
@ -21,7 +21,7 @@
|
||||
"Import and Export Data": "Імпорт і експорт даних",
|
||||
"Import": "Імпорт",
|
||||
"Import Invidious data": "Імпортувати JSON-дані Invidious",
|
||||
"Import YouTube subscriptions": "Імпортувати підписки YouTube з CSV чи OPML",
|
||||
"Import YouTube subscriptions": "Імпортувати підписки з YouTube чи OPML",
|
||||
"Import FreeTube subscriptions (.db)": "Імпортувати підписки з FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Імпортувати підписки з NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Імпортувати дані з NewPipe (.zip)",
|
||||
@ -455,7 +455,7 @@
|
||||
"search_filters_date_option_week": "Цей тиждень",
|
||||
"search_filters_type_label": "Тип",
|
||||
"search_filters_type_option_channel": "Канал",
|
||||
"search_message_use_another_instance": "Можете також <a href=\"`x`\">пошукати на іншому сервері</a>.",
|
||||
"search_message_use_another_instance": " Можете також <a href=\"`x`\">пошукати іншим сервером</a>.",
|
||||
"search_filters_title": "Фільтри",
|
||||
"search_filters_date_option_hour": "Остання година",
|
||||
"search_filters_date_option_month": "Цей місяць",
|
||||
@ -472,7 +472,7 @@
|
||||
"search_filters_features_option_three_sixty": "360°",
|
||||
"search_filters_features_option_hdr": "HDR",
|
||||
"search_filters_sort_label": "Спершу",
|
||||
"search_filters_sort_option_date": "Дата вивантаження",
|
||||
"search_filters_sort_option_date": "Нещодавні",
|
||||
"search_filters_apply_button": "Застосувати фільтри",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_filters_features_option_purchased": "Придбано",
|
||||
@ -513,7 +513,5 @@
|
||||
"The Popular feed has been disabled by the administrator.": "Стрічка Популярні вимкнена адміністратором.",
|
||||
"carousel_slide": "Слайд {{current}} з {{total}}",
|
||||
"carousel_skip": "Пропустити карусель",
|
||||
"carousel_go_to": "Перейти до слайда `x`",
|
||||
"preferences_preload_label": "Попереднє завантаження відеоданих: ",
|
||||
"Filipino (auto-generated)": "Філіппінська (згенеровано автоматично)"
|
||||
"carousel_go_to": "Перейти до слайда `x`"
|
||||
}
|
||||
|
@ -436,7 +436,7 @@
|
||||
"Turkish (auto-generated)": "土耳其语 (自动生成)",
|
||||
"Spanish (Spain)": "西班牙语 (西班牙)",
|
||||
"preferences_watch_history_label": "启用观看历史: ",
|
||||
"search_message_use_another_instance": "你也可以 <a href=\"`x`\">在另一实例上搜索</a>。",
|
||||
"search_message_use_another_instance": " 你也可以 <a href=\"`x`\">在另一实例上搜索</a>。",
|
||||
"search_filters_title": "过滤器",
|
||||
"search_filters_date_label": "上传日期",
|
||||
"search_filters_apply_button": "应用所选过滤器",
|
||||
@ -479,7 +479,5 @@
|
||||
"The Popular feed has been disabled by the administrator.": "“流行”源已被管理员禁用。",
|
||||
"carousel_slide": "当前为第 {{current}} 张图,共 {{total}} 张图",
|
||||
"carousel_skip": "跳过图集",
|
||||
"carousel_go_to": "转到图 `x`",
|
||||
"preferences_preload_label": "预加载视频数据: ",
|
||||
"Filipino (auto-generated)": "菲律宾语 (自动生成)"
|
||||
"carousel_go_to": "转到图 `x`"
|
||||
}
|
||||
|
@ -338,13 +338,13 @@
|
||||
"channel_tab_community_label": "社群",
|
||||
"search_filters_sort_option_relevance": "關聯",
|
||||
"search_filters_sort_option_rating": "評分",
|
||||
"search_filters_sort_option_date": "上傳日期",
|
||||
"search_filters_sort_option_date": "日期",
|
||||
"search_filters_sort_option_views": "檢視",
|
||||
"search_filters_type_label": "內容類型",
|
||||
"search_filters_duration_label": "時長",
|
||||
"search_filters_features_label": "特色",
|
||||
"search_filters_sort_label": "排序",
|
||||
"search_filters_date_option_hour": "最後一小時",
|
||||
"search_filters_date_option_hour": "小時",
|
||||
"search_filters_date_option_today": "今天",
|
||||
"search_filters_date_option_week": "週",
|
||||
"search_filters_date_option_month": "月",
|
||||
@ -442,7 +442,7 @@
|
||||
"search_filters_duration_option_none": "任何時長",
|
||||
"search_filters_duration_option_medium": "中等(4到20分鐘)",
|
||||
"search_filters_features_option_vr180": "VR180",
|
||||
"search_message_use_another_instance": "您也可以<a href=\"`x`\">在其他站台上搜尋</a>。",
|
||||
"search_message_use_another_instance": " 您也可以<a href=\"`x`\">在其他站台上搜尋</a>。",
|
||||
"search_filters_title": "過濾條件",
|
||||
"search_filters_date_label": "上傳日期",
|
||||
"search_filters_type_option_all": "任何類型",
|
||||
@ -479,7 +479,5 @@
|
||||
"carousel_slide": "第 {{current}} 張投影片,共 {{total}} 張",
|
||||
"carousel_skip": "略過輪播",
|
||||
"carousel_go_to": "跳到投影片 `x`",
|
||||
"The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。",
|
||||
"preferences_preload_label": "預先載入影片資訊 ",
|
||||
"Filipino (auto-generated)": "菲律賓語(自動產生)"
|
||||
"The Popular feed has been disabled by the administrator.": "熱門 feed 已被管理員停用。"
|
||||
}
|
||||
|
2
mocks
2
mocks
@ -1 +1 @@
|
||||
Subproject commit b55d58dea94f7144ff0205857dfa70ec14eaa872
|
||||
Subproject commit 11ec372f72747c09d48ffef04843f72be67d5b54
|
16
shard.lock
16
shard.lock
@ -2,7 +2,7 @@ version: 2.0
|
||||
shards:
|
||||
ameba:
|
||||
git: https://github.com/crystal-ameba/ameba.git
|
||||
version: 1.6.1
|
||||
version: 1.5.0
|
||||
|
||||
athena-negotiation:
|
||||
git: https://github.com/athena-framework/negotiation.git
|
||||
@ -10,20 +10,16 @@ shards:
|
||||
|
||||
backtracer:
|
||||
git: https://github.com/sija/backtracer.cr.git
|
||||
version: 1.2.2
|
||||
version: 1.2.1
|
||||
|
||||
db:
|
||||
git: https://github.com/crystal-lang/crystal-db.git
|
||||
version: 0.13.1
|
||||
version: 0.10.1
|
||||
|
||||
exception_page:
|
||||
git: https://github.com/crystal-loot/exception_page.git
|
||||
version: 0.2.2
|
||||
|
||||
http_proxy:
|
||||
git: https://github.com/mamantoha/http_proxy.git
|
||||
version: 0.10.3
|
||||
|
||||
kemal:
|
||||
git: https://github.com/kemalcr/kemal.git
|
||||
version: 1.1.2
|
||||
@ -34,7 +30,7 @@ shards:
|
||||
|
||||
pg:
|
||||
git: https://github.com/will/crystal-pg.git
|
||||
version: 0.28.0
|
||||
version: 0.24.0
|
||||
|
||||
protodec:
|
||||
git: https://github.com/iv-org/protodec.git
|
||||
@ -46,9 +42,9 @@ shards:
|
||||
|
||||
spectator:
|
||||
git: https://github.com/icy-arctic-fox/spectator.git
|
||||
version: 0.10.6
|
||||
version: 0.10.4
|
||||
|
||||
sqlite3:
|
||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||
version: 0.21.0
|
||||
version: 0.18.0
|
||||
|
||||
|
26
shard.yml
26
shard.yml
@ -1,20 +1,21 @@
|
||||
name: invidious
|
||||
version: 2.20250314.0-dev
|
||||
version: 0.20.1
|
||||
|
||||
authors:
|
||||
- Invidious team <contact@invidious.io>
|
||||
- Contributors!
|
||||
- Omar Roth <omarroth@protonmail.com>
|
||||
- Invidious team
|
||||
|
||||
description: |
|
||||
Invidious is an alternative front-end to YouTube
|
||||
targets:
|
||||
invidious:
|
||||
main: src/invidious.cr
|
||||
|
||||
dependencies:
|
||||
pg:
|
||||
github: will/crystal-pg
|
||||
version: ~> 0.28.0
|
||||
version: ~> 0.24.0
|
||||
sqlite3:
|
||||
github: crystal-lang/crystal-sqlite3
|
||||
version: ~> 0.21.0
|
||||
version: ~> 0.18.0
|
||||
kemal:
|
||||
github: kemalcr/kemal
|
||||
version: ~> 1.1.2
|
||||
@ -27,9 +28,6 @@ dependencies:
|
||||
athena-negotiation:
|
||||
github: athena-framework/negotiation
|
||||
version: ~> 0.1.1
|
||||
http_proxy:
|
||||
github: mamantoha/http_proxy
|
||||
version: ~> 0.10.3
|
||||
|
||||
development_dependencies:
|
||||
spectator:
|
||||
@ -37,12 +35,8 @@ development_dependencies:
|
||||
version: ~> 0.10.4
|
||||
ameba:
|
||||
github: crystal-ameba/ameba
|
||||
version: ~> 1.6.1
|
||||
version: ~> 1.5.0
|
||||
|
||||
crystal: ">= 1.10.0, < 2.0.0"
|
||||
crystal: ">= 1.0.0, < 2.0.0"
|
||||
|
||||
license: AGPLv3
|
||||
|
||||
repository: https://github.com/iv-org/invidious
|
||||
homepage: https://invidious.io
|
||||
documentation: https://docs.invidious.io
|
||||
|
@ -27,8 +27,8 @@ Spectator.describe Invidious::Hashtag do
|
||||
expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
|
||||
expect(video_11.views).to eq(40_504_893)
|
||||
|
||||
expect(video_11.badges.live_now?).to be_false
|
||||
expect(video_11.badges.premium?).to be_false
|
||||
expect(video_11.live_now).to be_false
|
||||
expect(video_11.premium).to be_false
|
||||
expect(video_11.premiere_timestamp).to be_nil
|
||||
|
||||
#
|
||||
@ -49,8 +49,8 @@ Spectator.describe Invidious::Hashtag do
|
||||
expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
|
||||
expect(video_35.views).to eq(30_790_049)
|
||||
|
||||
expect(video_35.badges.live_now?).to be_false
|
||||
expect(video_35.badges.premium?).to be_false
|
||||
expect(video_35.live_now).to be_false
|
||||
expect(video_35.premium).to be_false
|
||||
expect(video_35.premiere_timestamp).to be_nil
|
||||
end
|
||||
|
||||
@ -80,8 +80,8 @@ Spectator.describe Invidious::Hashtag do
|
||||
expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32)
|
||||
expect(video_41.views).to eq(63_240)
|
||||
|
||||
expect(video_41.badges.live_now?).to be_false
|
||||
expect(video_41.badges.premium?).to be_false
|
||||
expect(video_41.live_now).to be_false
|
||||
expect(video_41.premium).to be_false
|
||||
expect(video_41.premiere_timestamp).to be_nil
|
||||
|
||||
#
|
||||
@ -102,8 +102,8 @@ Spectator.describe Invidious::Hashtag do
|
||||
expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
|
||||
expect(video_48.views).to eq(68_704)
|
||||
|
||||
expect(video_48.badges.live_now?).to be_false
|
||||
expect(video_48.badges.premium?).to be_false
|
||||
expect(video_48.live_now).to be_false
|
||||
expect(video_48.premium).to be_false
|
||||
expect(video_48.premiere_timestamp).to be_nil
|
||||
end
|
||||
end
|
||||
|
@ -301,6 +301,7 @@ Spectator.describe Invidious::Search::Filters do
|
||||
|
||||
it "Encodes features filter (single)" do
|
||||
Invidious::Search::Filters::Features.each do |value|
|
||||
string = described_class.format_features(value)
|
||||
filters = described_class.new(features: value)
|
||||
|
||||
expect("#{filters.to_iv_params}")
|
||||
|
@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do
|
||||
# Basic video infos
|
||||
|
||||
expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island")
|
||||
expect(info["views"].as_i).to eq(220_226_287)
|
||||
expect(info["likes"].as_i).to eq(6_870_691)
|
||||
expect(info["views"].as_i).to eq(126_573_823)
|
||||
expect(info["likes"].as_i).to eq(5_157_654)
|
||||
|
||||
# For some reason the video length from VideoDetails and the
|
||||
# one from microformat differs by 1s...
|
||||
@ -48,12 +48,12 @@ Spectator.describe "parse_video_info" do
|
||||
|
||||
expect(info["relatedVideos"].as_a.size).to eq(20)
|
||||
|
||||
expect(info["relatedVideos"][0]["id"]).to eq("krsBRQbOPQ4")
|
||||
expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $250,000,000 Private Island!")
|
||||
expect(info["relatedVideos"][0]["id"]).to eq("Hwybp38GnZw")
|
||||
expect(info["relatedVideos"][0]["title"]).to eq("I Built Willy Wonka's Chocolate Factory!")
|
||||
expect(info["relatedVideos"][0]["author"]).to eq("MrBeast")
|
||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
||||
expect(info["relatedVideos"][0]["view_count"]).to eq("230617484")
|
||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M")
|
||||
expect(info["relatedVideos"][0]["view_count"]).to eq("179877630")
|
||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("179M")
|
||||
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
|
||||
|
||||
# Description
|
||||
@ -67,7 +67,7 @@ Spectator.describe "parse_video_info" do
|
||||
# Video metadata
|
||||
|
||||
expect(info["genre"].as_s).to eq("Entertainment")
|
||||
expect(info["genreUcid"].as_s?).to be_nil
|
||||
expect(info["genreUcid"].as_s).to be_empty
|
||||
expect(info["license"].as_s).to be_empty
|
||||
|
||||
# Author infos
|
||||
@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do
|
||||
expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
||||
|
||||
expect(info["authorThumbnail"].as_s).to eq(
|
||||
"https://yt3.ggpht.com/fxGKYucJAVme-Yz4fsdCroCFCrANWqw0ql4GYuvx8Uq4l_euNJHgE-w9MTkLQA805vWCi-kE0g=s48-c-k-c0x00ffffff-no-rj"
|
||||
"https://yt3.ggpht.com/ytc/AL5GRJVuqw82ERvHzsmBxL7avr1dpBtsVIXcEzBPZaloFg=s48-c-k-c0x00ffffff-no-rj"
|
||||
)
|
||||
|
||||
expect(info["authorVerified"].as_bool).to be_true
|
||||
expect(info["subCountText"].as_s).to eq("320M")
|
||||
expect(info["subCountText"].as_s).to eq("143M")
|
||||
end
|
||||
|
||||
it "parses a regular video with no descrition/comments" do
|
||||
@ -99,8 +99,8 @@ Spectator.describe "parse_video_info" do
|
||||
# Basic video infos
|
||||
|
||||
expect(info["title"].as_s).to eq("Chris Rea - Auberge")
|
||||
expect(info["views"].as_i).to eq(14_324_584)
|
||||
expect(info["likes"].as_i).to eq(35_870)
|
||||
expect(info["views"].as_i).to eq(10_943_126)
|
||||
expect(info["likes"].as_i).to eq(0)
|
||||
expect(info["lengthSeconds"].as_i).to eq(283_i64)
|
||||
expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
|
||||
|
||||
@ -132,14 +132,14 @@ Spectator.describe "parse_video_info" do
|
||||
|
||||
# Related videos
|
||||
|
||||
expect(info["relatedVideos"].as_a.size).to eq(20)
|
||||
expect(info["relatedVideos"].as_a.size).to eq(19)
|
||||
|
||||
expect(info["relatedVideos"][0]["id"]).to eq("gUUdQfnshJ4")
|
||||
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version")
|
||||
expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH")
|
||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ")
|
||||
expect(info["relatedVideos"][0]["view_count"]).to eq("53298661")
|
||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M")
|
||||
expect(info["relatedVideos"][0]["id"]).to eq("Ww3KeZ2_Yv4")
|
||||
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea")
|
||||
expect(info["relatedVideos"][0]["author"]).to eq("PanMusic")
|
||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCsKAPSuh1iNbLWUga_igPyA")
|
||||
expect(info["relatedVideos"][0]["view_count"]).to eq("31581")
|
||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("31K")
|
||||
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
|
||||
|
||||
# Description
|
||||
@ -151,18 +151,16 @@ Spectator.describe "parse_video_info" do
|
||||
# Video metadata
|
||||
|
||||
expect(info["genre"].as_s).to eq("Music")
|
||||
expect(info["genreUcid"].as_s?).to be_nil
|
||||
expect(info["genreUcid"].as_s).to be_empty
|
||||
expect(info["license"].as_s).to be_empty
|
||||
|
||||
# Author infos
|
||||
|
||||
expect(info["author"].as_s).to eq("ChrisReaVideos")
|
||||
expect(info["author"].as_s).to eq("ChrisReaOfficial")
|
||||
expect(info["ucid"].as_s).to eq("UC_5q6nWPbD30-y6oiWF_oNA")
|
||||
|
||||
expect(info["authorThumbnail"].as_s).to eq(
|
||||
"https://yt3.ggpht.com/ytc/AIdro_n71nsegpKfjeRKwn1JJmK5IVMh_7j5m_h3_1KnUUg=s48-c-k-c0x00ffffff-no-rj"
|
||||
)
|
||||
expect(info["authorThumbnail"].as_s).to be_empty
|
||||
expect(info["authorVerified"].as_bool).to be_false
|
||||
expect(info["subCountText"].as_s).to eq("3.11K")
|
||||
expect(info["subCountText"].as_s).to eq("-")
|
||||
end
|
||||
end
|
||||
|
@ -94,7 +94,7 @@ Spectator.describe "parse_video_info" do
|
||||
# Video metadata
|
||||
|
||||
expect(info["genre"].as_s).to eq("Entertainment")
|
||||
expect(info["genreUcid"].as_s?).to be_nil
|
||||
expect(info["genreUcid"].as_s).to be_empty
|
||||
expect(info["license"].as_s).to be_empty
|
||||
|
||||
# Author infos
|
||||
|
@ -23,7 +23,6 @@ require "kilt"
|
||||
require "./ext/kemal_content_for.cr"
|
||||
require "./ext/kemal_static_file_handler.cr"
|
||||
|
||||
require "http_proxy"
|
||||
require "athena-negotiation"
|
||||
require "openssl/hmac"
|
||||
require "option_parser"
|
||||
@ -93,14 +92,6 @@ SOFTWARE = {
|
||||
|
||||
YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
|
||||
|
||||
# Image request pool
|
||||
|
||||
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
|
||||
|
||||
COMPANION_POOL = CompanionConnectionPool.new(
|
||||
capacity: CONFIG.pool_size
|
||||
)
|
||||
|
||||
# CLI
|
||||
Kemal.config.extra_options do |parser|
|
||||
parser.banner = "Usage: invidious [arguments]"
|
||||
@ -126,9 +117,6 @@ Kemal.config.extra_options do |parser|
|
||||
parser.on("-l LEVEL", "--log-level=LEVEL", "Log level, one of #{LogLevel.values} (default: #{CONFIG.log_level})") do |log_level|
|
||||
CONFIG.log_level = LogLevel.parse(log_level)
|
||||
end
|
||||
parser.on("-k", "--colorize", "Colorize logs") do
|
||||
CONFIG.colorize_logs = true
|
||||
end
|
||||
parser.on("-v", "--version", "Print version") do
|
||||
puts SOFTWARE.to_pretty_json
|
||||
exit
|
||||
@ -145,7 +133,7 @@ if CONFIG.output.upcase != "STDOUT"
|
||||
FileUtils.mkdir_p(File.dirname(CONFIG.output))
|
||||
end
|
||||
OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mode: "a")
|
||||
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level, CONFIG.colorize_logs)
|
||||
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level)
|
||||
|
||||
# Check table integrity
|
||||
Invidious::Database.check_integrity(CONFIG)
|
||||
@ -165,15 +153,6 @@ Invidious::Database.check_integrity(CONFIG)
|
||||
{% puts "\nDone checking player dependencies, now compiling Invidious...\n" %}
|
||||
{% end %}
|
||||
|
||||
# Misc
|
||||
|
||||
DECRYPT_FUNCTION =
|
||||
if sig_helper_address = CONFIG.signature_server.presence
|
||||
IV::DecryptFunction.new(sig_helper_address)
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
# Start jobs
|
||||
|
||||
if CONFIG.channel_threads > 0
|
||||
@ -184,6 +163,11 @@ if CONFIG.feed_threads > 0
|
||||
Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB)
|
||||
end
|
||||
|
||||
DECRYPT_FUNCTION = DecryptFunction.new(CONFIG.decrypt_polling)
|
||||
if CONFIG.decrypt_polling
|
||||
Invidious::Jobs.register Invidious::Jobs::UpdateDecryptFunctionJob.new
|
||||
end
|
||||
|
||||
if CONFIG.statistics_enabled
|
||||
Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE)
|
||||
end
|
||||
@ -196,14 +180,11 @@ if CONFIG.popular_enabled
|
||||
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
|
||||
end
|
||||
|
||||
NOTIFICATION_CHANNEL = ::Channel(VideoNotification).new(32)
|
||||
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
|
||||
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(NOTIFICATION_CHANNEL, CONNECTION_CHANNEL, CONFIG.database_url)
|
||||
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
|
||||
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(CONNECTION_CHANNEL, CONFIG.database_url)
|
||||
|
||||
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
||||
|
||||
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
|
||||
|
||||
Invidious::Jobs.start_all
|
||||
|
||||
def popular_videos
|
||||
@ -244,6 +225,8 @@ add_context_storage_type(Preferences)
|
||||
add_context_storage_type(Invidious::User)
|
||||
|
||||
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"
|
||||
|
||||
# Use in kemal's production mode.
|
||||
@ -252,16 +235,4 @@ Kemal.config.app_name = "Invidious"
|
||||
Kemal.config.env = "production" if !ENV.has_key?("KEMAL_ENV")
|
||||
{% end %}
|
||||
|
||||
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
|
||||
Kemal.run
|
||||
|
@ -15,8 +15,7 @@ record AboutChannel,
|
||||
allowed_regions : Array(String),
|
||||
tabs : Array(String),
|
||||
tags : Array(String),
|
||||
verified : Bool,
|
||||
is_age_gated : Bool
|
||||
verified : Bool
|
||||
|
||||
def get_about_info(ucid, locale) : AboutChannel
|
||||
begin
|
||||
@ -46,102 +45,45 @@ def get_about_info(ucid, locale) : AboutChannel
|
||||
end
|
||||
|
||||
tags = [] of String
|
||||
tab_names = [] of String
|
||||
total_views = 0_i64
|
||||
joined = Time.unix(0)
|
||||
|
||||
if age_gate_renderer = initdata.dig?("contents", "twoColumnBrowseResultsRenderer", "tabs", 0, "tabRenderer", "content", "sectionListRenderer", "contents", 0, "channelAgeGateRenderer")
|
||||
description_node = nil
|
||||
author = age_gate_renderer["channelTitle"].as_s
|
||||
ucid = initdata.dig("responseContext", "serviceTrackingParams", 0, "params", 0, "value").as_s
|
||||
author_url = "https://www.youtube.com/channel/#{ucid}"
|
||||
author_thumbnail = age_gate_renderer.dig("avatar", "thumbnails", 0, "url").as_s
|
||||
banner = nil
|
||||
is_family_friendly = false
|
||||
is_age_gated = true
|
||||
tab_names = ["videos", "shorts", "streams"]
|
||||
auto_generated = false
|
||||
if auto_generated
|
||||
author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s
|
||||
author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s
|
||||
author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s
|
||||
|
||||
# Raises a KeyError on failure.
|
||||
banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
|
||||
banner = banners.try &.[-1]?.try &.["url"].as_s?
|
||||
|
||||
description_base_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]
|
||||
# some channels have the description in a simpleText
|
||||
# ex: https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/
|
||||
description_node = description_base_node.dig?("simpleText") || description_base_node
|
||||
|
||||
tags = initdata.dig?("header", "interactiveTabbedHeaderRenderer", "badges")
|
||||
.try &.as_a.map(&.["metadataBadgeRenderer"]["label"].as_s) || [] of String
|
||||
else
|
||||
if auto_generated
|
||||
author = initdata["header"]["interactiveTabbedHeaderRenderer"]["title"]["simpleText"].as_s
|
||||
author_url = initdata["microformat"]["microformatDataRenderer"]["urlCanonical"].as_s
|
||||
author_thumbnail = initdata["header"]["interactiveTabbedHeaderRenderer"]["boxArt"]["thumbnails"][0]["url"].as_s
|
||||
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
|
||||
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
|
||||
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
|
||||
author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges"))
|
||||
|
||||
# Raises a KeyError on failure.
|
||||
banners = initdata["header"]["interactiveTabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
|
||||
banner = banners.try &.[-1]?.try &.["url"].as_s?
|
||||
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
|
||||
|
||||
description_base_node = initdata["header"]["interactiveTabbedHeaderRenderer"]["description"]
|
||||
# some channels have the description in a simpleText
|
||||
# ex: https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/
|
||||
description_node = description_base_node.dig?("simpleText") || description_base_node
|
||||
# Raises a KeyError on failure.
|
||||
banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
|
||||
banner = banners.try &.[-1]?.try &.["url"].as_s?
|
||||
|
||||
tags = initdata.dig?("header", "interactiveTabbedHeaderRenderer", "badges")
|
||||
.try &.as_a.map(&.["metadataBadgeRenderer"]["label"].as_s) || [] of String
|
||||
else
|
||||
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
|
||||
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
|
||||
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
|
||||
author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges"))
|
||||
# if banner.includes? "channels/c4/default_banner"
|
||||
# banner = nil
|
||||
# end
|
||||
|
||||
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
|
||||
|
||||
# Raises a KeyError on failure.
|
||||
banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
|
||||
banners ||= initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "banner", "imageBannerViewModel", "image", "sources")
|
||||
banner = banners.try &.[-1]?.try &.["url"].as_s?
|
||||
|
||||
# if banner.includes? "channels/c4/default_banner"
|
||||
# banner = nil
|
||||
# end
|
||||
|
||||
description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?
|
||||
tags = initdata.dig?("microformat", "microformatDataRenderer", "tags").try &.as_a.map(&.as_s) || [] of String
|
||||
end
|
||||
|
||||
is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
|
||||
if tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?
|
||||
# Get the name of the tabs available on this channel
|
||||
tab_names = tabs_json.as_a.compact_map do |entry|
|
||||
name = entry.dig?("tabRenderer", "title").try &.as_s.downcase
|
||||
|
||||
# This is a small fix to not add extra code on the HTML side
|
||||
# I.e, the URL for the "live" tab is .../streams, so use "streams"
|
||||
# everywhere for the sake of simplicity
|
||||
(name == "live") ? "streams" : name
|
||||
end
|
||||
|
||||
# Get the currently active tab ("About")
|
||||
about_tab = extract_selected_tab(tabs_json)
|
||||
|
||||
# Try to find the about metadata section
|
||||
channel_about_meta = about_tab.dig?(
|
||||
"content",
|
||||
"sectionListRenderer", "contents", 0,
|
||||
"itemSectionRenderer", "contents", 0,
|
||||
"channelAboutFullMetadataRenderer"
|
||||
)
|
||||
|
||||
if !channel_about_meta.nil?
|
||||
total_views = channel_about_meta.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D/, "").to_i64? || 0_i64
|
||||
|
||||
# The joined text is split to several sub strings. The reduce joins those strings before parsing the date.
|
||||
joined = extract_text(channel_about_meta["joinedDateText"]?)
|
||||
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
|
||||
|
||||
# Normal Auto-generated channels
|
||||
# https://support.google.com/youtube/answer/2579942
|
||||
# For auto-generated channels, channel_about_meta only has
|
||||
# ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
|
||||
auto_generated = (
|
||||
(channel_about_meta["primaryLinks"]?.try &.size) == 1 && \
|
||||
extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube" ||
|
||||
channel_about_meta.dig?("links", 0, "channelExternalLinkViewModel", "title", "content").try &.as_s == "Auto-generated by YouTube"
|
||||
)
|
||||
end
|
||||
end
|
||||
description_node = initdata["metadata"]["channelMetadataRenderer"]?.try &.["description"]?
|
||||
tags = initdata.dig?("microformat", "microformatDataRenderer", "tags").try &.as_a.map(&.as_s) || [] of String
|
||||
end
|
||||
|
||||
is_family_friendly = initdata["microformat"]["microformatDataRenderer"]["familySafe"].as_bool
|
||||
|
||||
allowed_regions = initdata
|
||||
.dig?("microformat", "microformatDataRenderer", "availableCountries")
|
||||
.try &.as_a.map(&.as_s) || [] of String
|
||||
@ -159,18 +101,56 @@ def get_about_info(ucid, locale) : AboutChannel
|
||||
end
|
||||
end
|
||||
|
||||
sub_count = 0
|
||||
total_views = 0_i64
|
||||
joined = Time.unix(0)
|
||||
|
||||
if (metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a)
|
||||
metadata_rows.each do |row|
|
||||
metadata_part = row.dig?("metadataParts").try &.as_a.find { |i| i.dig?("text", "content").try &.as_s.includes?("subscribers") }
|
||||
if !metadata_part.nil?
|
||||
sub_count = short_text_to_number(metadata_part.dig("text", "content").as_s.split(" ")[0]).to_i32
|
||||
end
|
||||
break if sub_count != 0
|
||||
tab_names = [] of String
|
||||
|
||||
if tabs_json = initdata["contents"]["twoColumnBrowseResultsRenderer"]["tabs"]?
|
||||
# Get the name of the tabs available on this channel
|
||||
tab_names = tabs_json.as_a.compact_map do |entry|
|
||||
name = entry.dig?("tabRenderer", "title").try &.as_s.downcase
|
||||
|
||||
# This is a small fix to not add extra code on the HTML side
|
||||
# I.e, the URL for the "live" tab is .../streams, so use "streams"
|
||||
# everywhere for the sake of simplicity
|
||||
(name == "live") ? "streams" : name
|
||||
end
|
||||
|
||||
# Get the currently active tab ("About")
|
||||
about_tab = extract_selected_tab(tabs_json)
|
||||
|
||||
# Try to find the about metadata section
|
||||
channel_about_meta = about_tab.dig?(
|
||||
"content",
|
||||
"sectionListRenderer", "contents", 0,
|
||||
"itemSectionRenderer", "contents", 0,
|
||||
"channelAboutFullMetadataRenderer"
|
||||
)
|
||||
|
||||
if !channel_about_meta.nil?
|
||||
total_views = channel_about_meta.dig?("viewCountText", "simpleText").try &.as_s.gsub(/\D/, "").to_i64? || 0_i64
|
||||
|
||||
# The joined text is split to several sub strings. The reduce joins those strings before parsing the date.
|
||||
joined = extract_text(channel_about_meta["joinedDateText"]?)
|
||||
.try { |text| Time.parse(text, "Joined %b %-d, %Y", Time::Location.local) } || Time.unix(0)
|
||||
|
||||
# Normal Auto-generated channels
|
||||
# https://support.google.com/youtube/answer/2579942
|
||||
# For auto-generated channels, channel_about_meta only has
|
||||
# ["description"]["simpleText"] and ["primaryLinks"][0]["title"]["simpleText"]
|
||||
auto_generated = (
|
||||
(channel_about_meta["primaryLinks"]?.try &.size) == 1 && \
|
||||
extract_text(channel_about_meta.dig?("primaryLinks", 0, "title")) == "Auto-generated by YouTube" ||
|
||||
channel_about_meta.dig?("links", 0, "channelExternalLinkViewModel", "title", "content").try &.as_s == "Auto-generated by YouTube"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
sub_count = initdata
|
||||
.dig?("header", "c4TabbedHeaderRenderer", "subscriberCountText", "simpleText").try &.as_s?
|
||||
.try { |text| short_text_to_number(text.split(" ")[0]).to_i32 } || 0
|
||||
|
||||
AboutChannel.new(
|
||||
ucid: ucid,
|
||||
author: author,
|
||||
@ -188,7 +168,6 @@ def get_about_info(ucid, locale) : AboutChannel
|
||||
tabs: tab_names,
|
||||
tags: tags,
|
||||
verified: author_verified || false,
|
||||
is_age_gated: is_age_gated || false,
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -223,7 +223,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||
length_seconds = channel_video.try &.length_seconds
|
||||
length_seconds ||= 0
|
||||
|
||||
live_now = channel_video.try &.badges.live_now?
|
||||
live_now = channel_video.try &.live_now
|
||||
live_now ||= false
|
||||
|
||||
premiere_timestamp = channel_video.try &.premiere_timestamp
|
||||
@ -232,7 +232,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||
id: video_id,
|
||||
title: title,
|
||||
published: published,
|
||||
updated: updated,
|
||||
updated: Time.utc,
|
||||
ucid: ucid,
|
||||
author: author,
|
||||
length_seconds: length_seconds,
|
||||
@ -249,7 +249,11 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||
|
||||
if was_insert
|
||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
|
||||
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
|
||||
if CONFIG.enable_user_notifications
|
||||
Invidious::Database::Users.add_notification(video)
|
||||
else
|
||||
Invidious::Database::Users.feed_needs_update(video)
|
||||
end
|
||||
else
|
||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
|
||||
end
|
||||
@ -271,7 +275,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||
ucid: video.ucid,
|
||||
author: video.author,
|
||||
length_seconds: video.length_seconds,
|
||||
live_now: video.badges.live_now?,
|
||||
live_now: video.live_now,
|
||||
premiere_timestamp: video.premiere_timestamp,
|
||||
views: video.views,
|
||||
})
|
||||
@ -281,7 +285,11 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||
if Time.utc - video.published > 1.minute
|
||||
was_insert = Invidious::Database::ChannelVideos.insert(video)
|
||||
if was_insert
|
||||
NOTIFICATION_CHANNEL.send(VideoNotification.from_video(video))
|
||||
if CONFIG.enable_user_notifications
|
||||
Invidious::Database::Users.add_notification(video)
|
||||
else
|
||||
Invidious::Database::Users.feed_needs_update(video)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -44,12 +44,3 @@ def fetch_channel_releases(ucid, author, continuation)
|
||||
end
|
||||
return extract_items(initial_data, author, ucid)
|
||||
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
|
||||
|
@ -1,3 +1,67 @@
|
||||
def produce_channel_videos_continuation(ucid, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
|
||||
object_inner_2 = {
|
||||
"2:0:embedded" => {
|
||||
"1:0:varint" => 0_i64,
|
||||
},
|
||||
"5:varint" => 50_i64,
|
||||
"6:varint" => 1_i64,
|
||||
"7:varint" => (page * 30).to_i64,
|
||||
"9:varint" => 1_i64,
|
||||
"10:varint" => 0_i64,
|
||||
}
|
||||
|
||||
object_inner_2_encoded = object_inner_2
|
||||
.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
sort_by_numerical =
|
||||
case sort_by
|
||||
when "newest" then 1_i64
|
||||
when "popular" then 2_i64
|
||||
when "oldest" then 4_i64
|
||||
else 1_i64 # Fallback to "newest"
|
||||
end
|
||||
|
||||
object_inner_1 = {
|
||||
"110:embedded" => {
|
||||
"3:embedded" => {
|
||||
"15:embedded" => {
|
||||
"1:embedded" => {
|
||||
"1:string" => object_inner_2_encoded,
|
||||
},
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"3:varint" => sort_by_numerical,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
object_inner_1_encoded = object_inner_1
|
||||
.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
object = {
|
||||
"80226972:embedded" => {
|
||||
"2:string" => ucid,
|
||||
"3:string" => object_inner_1_encoded,
|
||||
"35:string" => "browse-feed#{ucid}videos102",
|
||||
},
|
||||
}
|
||||
|
||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
return continuation
|
||||
end
|
||||
|
||||
module Invidious::Channel::Tabs
|
||||
extend self
|
||||
|
||||
@ -5,6 +69,10 @@ module Invidious::Channel::Tabs
|
||||
# Regular videos
|
||||
# -------------------
|
||||
|
||||
def make_initial_video_ctoken(ucid, sort_by) : String
|
||||
return produce_channel_videos_continuation(ucid, sort_by: sort_by)
|
||||
end
|
||||
|
||||
# Wrapper for AboutChannel, as we still need to call get_videos with
|
||||
# an author name and ucid directly (e.g in RSS feeds).
|
||||
# TODO: figure out how to get rid of that
|
||||
@ -26,7 +94,7 @@ module Invidious::Channel::Tabs
|
||||
end
|
||||
|
||||
def get_videos(author : String, ucid : String, *, continuation : String? = nil, sort_by = "newest")
|
||||
continuation ||= make_initial_videos_ctoken(ucid, sort_by)
|
||||
continuation ||= make_initial_video_ctoken(ucid, sort_by)
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
|
||||
return extract_items(initial_data, author, ucid)
|
||||
@ -55,10 +123,14 @@ module Invidious::Channel::Tabs
|
||||
# Shorts
|
||||
# -------------------
|
||||
|
||||
def get_shorts(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
|
||||
continuation ||= make_initial_shorts_ctoken(channel.ucid, sort_by)
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
|
||||
def get_shorts(channel : AboutChannel, continuation : String? = nil)
|
||||
if continuation.nil?
|
||||
# EgZzaG9ydHPyBgUKA5oBAA%3D%3D is the protobuf object to load "shorts"
|
||||
# TODO: try to extract the continuation tokens that allows other sorting options
|
||||
initial_data = YoutubeAPI.browse(channel.ucid, params: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D")
|
||||
else
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
end
|
||||
return extract_items(initial_data, channel.author, channel.ucid)
|
||||
end
|
||||
|
||||
@ -66,17 +138,21 @@ module Invidious::Channel::Tabs
|
||||
# Livestreams
|
||||
# -------------------
|
||||
|
||||
def get_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
|
||||
continuation ||= make_initial_livestreams_ctoken(channel.ucid, sort_by)
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
def get_livestreams(channel : AboutChannel, continuation : String? = nil)
|
||||
if continuation.nil?
|
||||
# EgdzdHJlYW1z8gYECgJ6AA%3D%3D is the protobuf object to load "streams"
|
||||
initial_data = YoutubeAPI.browse(channel.ucid, params: "EgdzdHJlYW1z8gYECgJ6AA%3D%3D")
|
||||
else
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
end
|
||||
|
||||
return extract_items(initial_data, channel.author, channel.ucid)
|
||||
end
|
||||
|
||||
def get_60_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
|
||||
def get_60_livestreams(channel : AboutChannel, continuation : String? = nil)
|
||||
if continuation.nil?
|
||||
# Fetch the first "page" of stream
|
||||
items, next_continuation = get_livestreams(channel, sort_by: sort_by)
|
||||
# Fetch the first "page" of streams
|
||||
items, next_continuation = get_livestreams(channel)
|
||||
else
|
||||
# Fetch a "page" of streams using the given continuation token
|
||||
items, next_continuation = get_livestreams(channel, continuation: continuation)
|
||||
@ -91,102 +167,4 @@ module Invidious::Channel::Tabs
|
||||
|
||||
return items, next_continuation
|
||||
end
|
||||
|
||||
# -------------------
|
||||
# C-tokens
|
||||
# -------------------
|
||||
|
||||
private def sort_options_videos_short(sort_by : String)
|
||||
case sort_by
|
||||
when "newest" then return 4_i64
|
||||
when "popular" then return 2_i64
|
||||
when "oldest" then return 5_i64
|
||||
else return 4_i64 # Fallback to "newest"
|
||||
end
|
||||
end
|
||||
|
||||
# Generate the initial "continuation token" to get the first page of the
|
||||
# "videos" tab. The following page requires the ctoken provided in that
|
||||
# first page, and so on.
|
||||
private def make_initial_videos_ctoken(ucid : String, sort_by = "newest")
|
||||
object = {
|
||||
"15:embedded" => {
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"4:varint" => sort_options_videos_short(sort_by),
|
||||
},
|
||||
}
|
||||
|
||||
return channel_ctoken_wrap(ucid, object)
|
||||
end
|
||||
|
||||
# Generate the initial "continuation token" to get the first page of the
|
||||
# "shorts" tab. The following page requires the ctoken provided in that
|
||||
# first page, and so on.
|
||||
private def make_initial_shorts_ctoken(ucid : String, sort_by = "newest")
|
||||
object = {
|
||||
"10:embedded" => {
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"4:varint" => sort_options_videos_short(sort_by),
|
||||
},
|
||||
}
|
||||
|
||||
return channel_ctoken_wrap(ucid, object)
|
||||
end
|
||||
|
||||
# Generate the initial "continuation token" to get the first page of the
|
||||
# "livestreams" tab. The following page requires the ctoken provided in that
|
||||
# first page, and so on.
|
||||
private def make_initial_livestreams_ctoken(ucid : String, sort_by = "newest")
|
||||
sort_by_numerical =
|
||||
case sort_by
|
||||
when "newest" then 12_i64
|
||||
when "popular" then 14_i64
|
||||
when "oldest" then 13_i64
|
||||
else 12_i64 # Fallback to "newest"
|
||||
end
|
||||
|
||||
object = {
|
||||
"14:embedded" => {
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"5:varint" => sort_by_numerical,
|
||||
},
|
||||
}
|
||||
|
||||
return channel_ctoken_wrap(ucid, object)
|
||||
end
|
||||
|
||||
# The protobuf structure common between videos/shorts/livestreams
|
||||
private def channel_ctoken_wrap(ucid : String, object)
|
||||
object_inner = {
|
||||
"110:embedded" => {
|
||||
"3:embedded" => object,
|
||||
},
|
||||
}
|
||||
|
||||
object_inner_encoded = object_inner
|
||||
.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
object = {
|
||||
"80226972:embedded" => {
|
||||
"2:string" => ucid,
|
||||
"3:string" => object_inner_encoded,
|
||||
},
|
||||
}
|
||||
|
||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
return continuation
|
||||
end
|
||||
end
|
||||
|
@ -5,35 +5,35 @@ def text_to_parsed_content(text : String) : JSON::Any
|
||||
# In first case line is just a simple node before
|
||||
# check patterns inside line
|
||||
# { 'text': line }
|
||||
current_nodes = [] of JSON::Any
|
||||
initial_node = {"text" => line}
|
||||
current_nodes << (JSON.parse(initial_node.to_json))
|
||||
currentNodes = [] of JSON::Any
|
||||
initialNode = {"text" => line}
|
||||
currentNodes << (JSON.parse(initialNode.to_json))
|
||||
|
||||
# For each match with url pattern, get last node and preserve
|
||||
# last node before create new node with url information
|
||||
# { 'text': match, 'navigationEndpoint': { 'urlEndpoint' : 'url': match } }
|
||||
line.scan(/https?:\/\/[^ ]*/).each do |url_match|
|
||||
line.scan(/https?:\/\/[^ ]*/).each do |urlMatch|
|
||||
# Retrieve last node and update node without match
|
||||
last_node = current_nodes[-1].as_h
|
||||
splitted_last_node = last_node["text"].as_s.split(url_match[0])
|
||||
last_node["text"] = JSON.parse(splitted_last_node[0].to_json)
|
||||
current_nodes[-1] = JSON.parse(last_node.to_json)
|
||||
lastNode = currentNodes[currentNodes.size - 1].as_h
|
||||
splittedLastNode = lastNode["text"].as_s.split(urlMatch[0])
|
||||
lastNode["text"] = JSON.parse(splittedLastNode[0].to_json)
|
||||
currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json)
|
||||
# Create new node with match and navigation infos
|
||||
current_node = {"text" => url_match[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => url_match[0]}}}
|
||||
current_nodes << (JSON.parse(current_node.to_json))
|
||||
currentNode = {"text" => urlMatch[0], "navigationEndpoint" => {"urlEndpoint" => {"url" => urlMatch[0]}}}
|
||||
currentNodes << (JSON.parse(currentNode.to_json))
|
||||
# If text remain after match create new simple node with text after match
|
||||
after_node = {"text" => splitted_last_node.size > 1 ? splitted_last_node[1] : ""}
|
||||
current_nodes << (JSON.parse(after_node.to_json))
|
||||
afterNode = {"text" => splittedLastNode.size > 1 ? splittedLastNode[1] : ""}
|
||||
currentNodes << (JSON.parse(afterNode.to_json))
|
||||
end
|
||||
|
||||
# After processing of matches inside line
|
||||
# Add \n at end of last node for preserve carriage return
|
||||
last_node = current_nodes[-1].as_h
|
||||
last_node["text"] = JSON.parse("#{last_node["text"]}\n".to_json)
|
||||
current_nodes[-1] = JSON.parse(last_node.to_json)
|
||||
lastNode = currentNodes[currentNodes.size - 1].as_h
|
||||
lastNode["text"] = JSON.parse("#{currentNodes[currentNodes.size - 1]["text"]}\n".to_json)
|
||||
currentNodes[currentNodes.size - 1] = JSON.parse(lastNode.to_json)
|
||||
|
||||
# Finally add final nodes to nodes returned
|
||||
current_nodes.each do |node|
|
||||
currentNodes.each do |node|
|
||||
nodes << (node)
|
||||
end
|
||||
end
|
||||
@ -53,8 +53,8 @@ def content_to_comment_html(content, video_id : String? = "")
|
||||
|
||||
text = HTML.escape(run["text"].as_s)
|
||||
|
||||
if navigation_endpoint = run.dig?("navigationEndpoint")
|
||||
text = parse_link_endpoint(navigation_endpoint, text, video_id)
|
||||
if navigationEndpoint = run.dig?("navigationEndpoint")
|
||||
text = parse_link_endpoint(navigationEndpoint, text, video_id)
|
||||
end
|
||||
|
||||
text = "<b>#{text}</b>" if run["bold"]?
|
||||
|
@ -8,19 +8,11 @@ struct DBConfig
|
||||
property dbname : String
|
||||
end
|
||||
|
||||
struct SocketBindingConfig
|
||||
include YAML::Serializable
|
||||
|
||||
property path : String
|
||||
property permissions : String
|
||||
end
|
||||
|
||||
struct ConfigPreferences
|
||||
include YAML::Serializable
|
||||
|
||||
property annotations : Bool = false
|
||||
property annotations_subscribed : Bool = false
|
||||
property preload : Bool = true
|
||||
property autoplay : Bool = false
|
||||
property captions : Array(String) = ["", "", ""]
|
||||
property comments : Array(String) = ["youtube", ""]
|
||||
@ -35,7 +27,7 @@ struct ConfigPreferences
|
||||
property max_results : Int32 = 40
|
||||
property notifications_only : Bool = false
|
||||
property player_style : String = "invidious"
|
||||
property quality : String = "dash"
|
||||
property quality : String = "hd720"
|
||||
property quality_dash : String = "auto"
|
||||
property default_home : String? = "Popular"
|
||||
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
|
||||
@ -62,28 +54,9 @@ struct ConfigPreferences
|
||||
end
|
||||
end
|
||||
|
||||
struct HTTPProxyConfig
|
||||
include YAML::Serializable
|
||||
|
||||
property user : String
|
||||
property password : String
|
||||
property host : String
|
||||
property port : Int32
|
||||
end
|
||||
|
||||
class Config
|
||||
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)
|
||||
property channel_threads : Int32 = 1
|
||||
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
|
||||
@ -95,14 +68,14 @@ class Config
|
||||
property output : String = "STDOUT"
|
||||
# Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
|
||||
property log_level : LogLevel = LogLevel::Info
|
||||
# Enables colors in logs. Useful for debugging purposes
|
||||
property colorize_logs : Bool = false
|
||||
# Database configuration with separate parameters (username, hostname, etc)
|
||||
property db : DBConfig? = nil
|
||||
|
||||
# Database configuration using 12-Factor "Database URL" syntax
|
||||
@[YAML::Field(converter: Preferences::URIConverter)]
|
||||
property database_url : URI = URI.parse("")
|
||||
# Use polling to keep decryption function up to date
|
||||
property decrypt_polling : Bool = false
|
||||
# Used for crawling channels: threads should check all videos uploaded by a channel
|
||||
property full_refresh : Bool = false
|
||||
|
||||
@ -147,35 +120,16 @@ class Config
|
||||
# Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
|
||||
@[YAML::Field(converter: Preferences::FamilyConverter)]
|
||||
property force_resolve : Socket::Family = Socket::Family::UNSPEC
|
||||
|
||||
# External signature solver server socket (either a path to a UNIX domain socket or "<IP>:<Port>")
|
||||
property signature_server : String? = nil
|
||||
|
||||
# Port to listen for connections (overridden by command line argument)
|
||||
property port : Int32 = 3000
|
||||
# Host to bind (overridden by command line argument)
|
||||
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`)
|
||||
property pool_size : Int32 = 100
|
||||
# HTTP Proxy configuration
|
||||
property http_proxy : HTTPProxyConfig? = nil
|
||||
|
||||
# Use Innertube's transcripts API instead of timedtext for closed captions
|
||||
property use_innertube_for_captions : Bool = false
|
||||
|
||||
# visitor data ID for Google session
|
||||
property visitor_data : String? = nil
|
||||
# poToken for passing bot attestation
|
||||
property po_token : String? = nil
|
||||
|
||||
# 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
|
||||
@[YAML::Field(converter: Preferences::StringToCookies)]
|
||||
property cookies : HTTP::Cookies = HTTP::Cookies.new
|
||||
@ -209,9 +163,6 @@ class Config
|
||||
config = Config.from_yaml(config_yaml)
|
||||
|
||||
# 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 %}
|
||||
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
|
||||
|
||||
@ -248,40 +199,16 @@ class Config
|
||||
exit(1)
|
||||
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 %}
|
||||
|
||||
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
|
||||
# See: https://github.com/iv-org/invidious/issues/3854
|
||||
if config.hmac_key.empty?
|
||||
puts "Config: 'hmac_key' is required/can't be empty"
|
||||
exit(1)
|
||||
elsif config.hmac_key == "CHANGE_ME!!"
|
||||
puts "Config: The value of 'hmac_key' needs to be changed!!"
|
||||
exit(1)
|
||||
end
|
||||
|
||||
# Build database_url from db.* if it's not set directly
|
||||
@ -301,24 +228,6 @@ class Config
|
||||
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
|
||||
end
|
||||
end
|
||||
|
@ -140,7 +140,6 @@ module Invidious::Database::Playlists
|
||||
request = <<-SQL
|
||||
SELECT id,title FROM playlists
|
||||
WHERE author = $1 AND id LIKE 'IV%'
|
||||
ORDER BY title
|
||||
SQL
|
||||
|
||||
PG_DB.query_all(request, email, as: {String, String})
|
||||
|
@ -119,15 +119,15 @@ module Invidious::Database::Users
|
||||
# Update (notifs)
|
||||
# -------------------
|
||||
|
||||
def add_multiple_notifications(channel_id : String, video_ids : Array(String))
|
||||
def add_notification(video : ChannelVideo)
|
||||
request = <<-SQL
|
||||
UPDATE users
|
||||
SET notifications = array_cat(notifications, $1),
|
||||
SET notifications = array_append(notifications, $1),
|
||||
feed_needs_update = true
|
||||
WHERE $2 = ANY(subscriptions)
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, video_ids, channel_id)
|
||||
PG_DB.exec(request, video.id, video.ucid)
|
||||
end
|
||||
|
||||
def remove_notification(user : User, vid : String)
|
||||
@ -154,14 +154,14 @@ module Invidious::Database::Users
|
||||
# Update (misc)
|
||||
# -------------------
|
||||
|
||||
def feed_needs_update(channel_id : String)
|
||||
def feed_needs_update(video : ChannelVideo)
|
||||
request = <<-SQL
|
||||
UPDATE users
|
||||
SET feed_needs_update = true
|
||||
WHERE $1 = ANY(subscriptions)
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, channel_id)
|
||||
PG_DB.exec(request, video.ucid)
|
||||
end
|
||||
|
||||
def update_preferences(user : User)
|
||||
|
@ -7,9 +7,8 @@ module Invidious::Frontend::ChannelPage
|
||||
Streams
|
||||
Podcasts
|
||||
Releases
|
||||
Courses
|
||||
Playlists
|
||||
Posts
|
||||
Community
|
||||
Channels
|
||||
end
|
||||
|
||||
|
@ -149,12 +149,12 @@ module Invidious::Frontend::Comments
|
||||
|
||||
if comments["videoId"]?
|
||||
html << <<-END_HTML
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
<a href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
elsif comments["authorId"]?
|
||||
html << <<-END_HTML
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
<a href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
end
|
||||
|
@ -6,9 +6,9 @@ module Invidious::Frontend::Misc
|
||||
|
||||
if prefs.automatic_instance_redirect
|
||||
current_page = env.get?("current_page").as(String)
|
||||
return "/redirect?referer=#{current_page}"
|
||||
redirect_url = "/redirect?referer=#{current_page}"
|
||||
else
|
||||
return "https://redirect.invidious.io#{env.request.resource}"
|
||||
redirect_url = "https://redirect.invidious.io#{env.request.resource}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -3,24 +3,6 @@ require "uri"
|
||||
module Invidious::Frontend::Pagination
|
||||
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)
|
||||
# Link
|
||||
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
|
||||
@ -90,24 +72,18 @@ module Invidious::Frontend::Pagination
|
||||
end
|
||||
end
|
||||
|
||||
def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?, first_page : Bool, params : URI::Params)
|
||||
def nav_ctoken(locale : String?, *, base_url : String | URI, ctoken : String?)
|
||||
return String.build do |str|
|
||||
str << %(<div class="h-box">\n)
|
||||
str << %(<div class="page-nav-container flexible">\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-prev-container flex-left"></div>\n)
|
||||
|
||||
str << %(<div class="page-next-container flex-right">)
|
||||
|
||||
if !ctoken.nil?
|
||||
params["continuation"] = ctoken
|
||||
url_next = HttpServer::Utils.add_params_to_url(base_url, params)
|
||||
params_next = URI::Params{"continuation" => ctoken}
|
||||
url_next = HttpServer::Utils.add_params_to_url(base_url, params_next)
|
||||
|
||||
self.next_page(str, locale, url_next.to_s)
|
||||
end
|
||||
|
@ -13,7 +13,7 @@ module Invidious::Frontend::WatchPage
|
||||
@full_videos,
|
||||
@video_streams,
|
||||
@audio_streams,
|
||||
@captions,
|
||||
@captions
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -3,9 +3,9 @@
|
||||
# IPv6 addresses.
|
||||
#
|
||||
class TCPSocket
|
||||
def initialize(host, port, dns_timeout = nil, connect_timeout = nil, blocking = false, family = Socket::Family::UNSPEC)
|
||||
def initialize(host : String, port, dns_timeout = nil, connect_timeout = nil, family = Socket::Family::UNSPEC)
|
||||
Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo|
|
||||
super(addrinfo.family, addrinfo.type, addrinfo.protocol, blocking)
|
||||
super(addrinfo.family, addrinfo.type, addrinfo.protocol)
|
||||
connect(addrinfo, timeout: connect_timeout) do |error|
|
||||
close
|
||||
error
|
||||
@ -26,7 +26,7 @@ class HTTP::Client
|
||||
end
|
||||
|
||||
hostname = @host.starts_with?('[') && @host.ends_with?(']') ? @host[1..-2] : @host
|
||||
io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, family: @family
|
||||
io = TCPSocket.new hostname, @port, @dns_timeout, @connect_timeout, @family
|
||||
io.read_timeout = @read_timeout if @read_timeout
|
||||
io.write_timeout = @write_timeout if @write_timeout
|
||||
io.sync = false
|
||||
@ -35,7 +35,7 @@ class HTTP::Client
|
||||
if tls = @tls
|
||||
tcp_socket = io
|
||||
begin
|
||||
io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host.rchop('.'))
|
||||
io = OpenSSL::SSL::Socket::Client.new(tcp_socket, context: tls, sync_close: true, hostname: @host)
|
||||
rescue exc
|
||||
# don't leak the TCP socket when the SSL connection failed
|
||||
tcp_socket.close
|
||||
|
@ -43,8 +43,6 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
|
||||
# URLs for the error message below
|
||||
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 += "?q=is:issue+is:open+"
|
||||
url_search_issues += URI.encode_www_form("[Bug] #{issue_title}")
|
||||
|
||||
url_switch = "https://redirect.invidious.io" + env.request.resource
|
||||
|
||||
@ -130,7 +128,7 @@ def error_json_helper(
|
||||
env : HTTP::Server::Context,
|
||||
status_code : Int32,
|
||||
exception : Exception,
|
||||
additional_fields : Hash(String, Object) | Nil = nil,
|
||||
additional_fields : Hash(String, Object) | Nil = nil
|
||||
)
|
||||
if exception.is_a?(InfoException)
|
||||
return error_json_helper(env, status_code, exception.message || "", additional_fields)
|
||||
@ -152,7 +150,7 @@ def error_json_helper(
|
||||
env : HTTP::Server::Context,
|
||||
status_code : Int32,
|
||||
message : String,
|
||||
additional_fields : Hash(String, Object) | Nil = nil,
|
||||
additional_fields : Hash(String, Object) | Nil = nil
|
||||
)
|
||||
env.response.content_type = "application/json"
|
||||
env.response.status_code = status_code
|
||||
@ -192,7 +190,7 @@ def error_redirect_helper(env : HTTP::Server::Context)
|
||||
<a href="/redirect?referer=#{env.get("current_page")}">#{switch_instance}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
|
||||
<a href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
|
||||
</li>
|
||||
</ul>
|
||||
END_HTML
|
||||
|
@ -27,7 +27,6 @@ class Kemal::RouteHandler
|
||||
# Processes the route if it's a match. Otherwise renders 404.
|
||||
private def process_request(context)
|
||||
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
|
||||
return if context.response.closed?
|
||||
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)
|
||||
@ -98,7 +97,7 @@ class AuthHandler < Kemal::Handler
|
||||
if token = env.request.headers["Authorization"]?
|
||||
token = JSON.parse(URI.decode_www_form(token.lchop("Bearer ")))
|
||||
session = URI.decode_www_form(token["session"].as_s)
|
||||
scopes, _, _ = validate_request(token, session, env.request, HMAC_KEY, nil)
|
||||
scopes, expire, signature = validate_request(token, session, env.request, HMAC_KEY, nil)
|
||||
|
||||
if email = Invidious::Database::SessionIDs.select_email(session)
|
||||
user = Invidious::Database::Users.select!(email: email)
|
||||
|
@ -1,22 +1,8 @@
|
||||
# Languages requiring a better level of translation (at least 20%)
|
||||
# to be added to the list below:
|
||||
#
|
||||
# "af" => "", # Afrikaans
|
||||
# "az" => "", # Azerbaijani
|
||||
# "be" => "", # Belarusian
|
||||
# "bn_BD" => "", # Bengali (Bangladesh)
|
||||
# "ia" => "", # Interlingua
|
||||
# "or" => "", # Odia
|
||||
# "tk" => "", # Turkmen
|
||||
# "tok => "", # Toki Pona
|
||||
#
|
||||
LOCALES_LIST = {
|
||||
"ar" => "العربية", # Arabic
|
||||
"bg" => "български", # Bulgarian
|
||||
"bn" => "বাংলা", # Bengali
|
||||
"ca" => "Català", # Catalan
|
||||
"cs" => "Čeština", # Czech
|
||||
"cy" => "Cymraeg", # Welsh
|
||||
"da" => "Dansk", # Danish
|
||||
"de" => "Deutsch", # German
|
||||
"el" => "Ελληνικά", # Greek
|
||||
@ -37,7 +23,6 @@ LOCALES_LIST = {
|
||||
"it" => "Italiano", # Italian
|
||||
"ja" => "日本語", # Japanese
|
||||
"ko" => "한국어", # Korean
|
||||
"lmo" => "Lombard", # Lombard
|
||||
"lt" => "Lietuvių", # Lithuanian
|
||||
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
|
||||
"nl" => "Nederlands", # Dutch
|
||||
@ -54,7 +39,6 @@ LOCALES_LIST = {
|
||||
"sr" => "Srpski (latinica)", # Serbian (Latin)
|
||||
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
|
||||
"sv-SE" => "Svenska", # Swedish
|
||||
"ta" => "தமிழ்", # Tamil
|
||||
"tr" => "Türkçe", # Turkish
|
||||
"uk" => "Українська", # Ukrainian
|
||||
"vi" => "Tiếng Việt", # Vietnamese
|
||||
|
@ -95,6 +95,7 @@ module I18next::Plurals
|
||||
"hr" => PluralForms::Special_Hungarian_Serbian,
|
||||
"it" => PluralForms::Special_Spanish_Italian,
|
||||
"pt" => PluralForms::Special_French_Portuguese,
|
||||
"pt" => PluralForms::Special_French_Portuguese,
|
||||
"sr" => PluralForms::Special_Hungarian_Serbian,
|
||||
}
|
||||
|
||||
@ -188,7 +189,7 @@ module I18next::Plurals
|
||||
|
||||
# Emulate the `rule.numbers.size == 2 && rule.numbers[0] == 1` check
|
||||
# from original i18next code
|
||||
private def simple_plural?(form : PluralForms) : Bool
|
||||
private def is_simple_plural(form : PluralForms) : Bool
|
||||
case form
|
||||
when .single_gt_one? then return true
|
||||
when .single_not_one? then return true
|
||||
@ -210,7 +211,7 @@ module I18next::Plurals
|
||||
idx = SuffixIndex.get_index(plural_form, count)
|
||||
|
||||
# Simple plurals are handled differently in all versions (but v4)
|
||||
if @simplify_plural_suffix && simple_plural?(plural_form)
|
||||
if @simplify_plural_suffix && is_simple_plural(plural_form)
|
||||
return (idx == 1) ? "_plural" : ""
|
||||
end
|
||||
|
||||
@ -261,9 +262,9 @@ module I18next::Plurals
|
||||
when .special_hebrew? then return special_hebrew(count)
|
||||
when .special_odia? then return special_odia(count)
|
||||
# Mixed v3/v4 forms
|
||||
when .special_spanish_italian? then return special_cldr_spanish_italian(count)
|
||||
when .special_french_portuguese? then return special_cldr_french_portuguese(count)
|
||||
when .special_hungarian_serbian? then return special_cldr_hungarian_serbian(count)
|
||||
when .special_spanish_italian? then return special_cldr_Spanish_Italian(count)
|
||||
when .special_french_portuguese? then return special_cldr_French_Portuguese(count)
|
||||
when .special_hungarian_serbian? then return special_cldr_Hungarian_Serbian(count)
|
||||
else
|
||||
# default, if nothing matched above
|
||||
return 0_u8
|
||||
@ -534,7 +535,7 @@ module I18next::Plurals
|
||||
#
|
||||
# This rule is mostly compliant to CLDR v42
|
||||
#
|
||||
def self.special_cldr_spanish_italian(count : Int) : UInt8
|
||||
def self.special_cldr_Spanish_Italian(count : Int) : UInt8
|
||||
return 0_u8 if (count == 1) # one
|
||||
return 1_u8 if (count != 0 && count % 1_000_000 == 0) # many
|
||||
return 2_u8 # other
|
||||
@ -544,7 +545,7 @@ module I18next::Plurals
|
||||
#
|
||||
# This rule is mostly compliant to CLDR v42
|
||||
#
|
||||
def self.special_cldr_french_portuguese(count : Int) : UInt8
|
||||
def self.special_cldr_French_Portuguese(count : Int) : UInt8
|
||||
return 0_u8 if (count == 0 || count == 1) # one
|
||||
return 1_u8 if (count % 1_000_000 == 0) # many
|
||||
return 2_u8 # other
|
||||
@ -554,7 +555,7 @@ module I18next::Plurals
|
||||
#
|
||||
# This rule is mostly compliant to CLDR v42
|
||||
#
|
||||
def self.special_cldr_hungarian_serbian(count : Int) : UInt8
|
||||
def self.special_cldr_Hungarian_Serbian(count : Int) : UInt8
|
||||
n_mod_10 = count % 10
|
||||
n_mod_100 = count % 100
|
||||
|
||||
|
@ -1,5 +1,3 @@
|
||||
require "colorize"
|
||||
|
||||
enum LogLevel
|
||||
All = 0
|
||||
Trace = 1
|
||||
@ -12,9 +10,7 @@ enum LogLevel
|
||||
end
|
||||
|
||||
class Invidious::LogHandler < Kemal::BaseLogHandler
|
||||
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, use_color : Bool = true)
|
||||
Colorize.enabled = use_color
|
||||
Colorize.on_tty_only!
|
||||
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug)
|
||||
end
|
||||
|
||||
def call(context : HTTP::Server::Context)
|
||||
@ -38,27 +34,28 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
|
||||
context
|
||||
end
|
||||
|
||||
def puts(message : String)
|
||||
@io << message << '\n'
|
||||
@io.flush
|
||||
end
|
||||
|
||||
def write(message : String)
|
||||
@io << message
|
||||
@io.flush
|
||||
end
|
||||
|
||||
def color(level)
|
||||
case level
|
||||
when LogLevel::Trace then :cyan
|
||||
when LogLevel::Debug then :green
|
||||
when LogLevel::Info then :white
|
||||
when LogLevel::Warn then :yellow
|
||||
when LogLevel::Error then :red
|
||||
when LogLevel::Fatal then :magenta
|
||||
else :default
|
||||
end
|
||||
def set_log_level(level : String)
|
||||
@level = LogLevel.parse(level)
|
||||
end
|
||||
|
||||
def set_log_level(level : LogLevel)
|
||||
@level = level
|
||||
end
|
||||
|
||||
{% for level in %w(trace debug info warn error fatal) %}
|
||||
def {{level.id}}(message : String)
|
||||
if LogLevel::{{level.id.capitalize}} >= @level
|
||||
puts("#{Time.utc} [{{level.id}}] #{message}".colorize(color(LogLevel::{{level.id.capitalize}})))
|
||||
puts("#{Time.utc} [{{level.id}}] #{message}")
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
|
@ -1,16 +1,3 @@
|
||||
@[Flags]
|
||||
enum VideoBadges
|
||||
LiveNow
|
||||
Premium
|
||||
ThreeD
|
||||
FourK
|
||||
New
|
||||
EightK
|
||||
VR180
|
||||
VR360
|
||||
ClosedCaptions
|
||||
end
|
||||
|
||||
struct SearchVideo
|
||||
include DB::Serializable
|
||||
|
||||
@ -22,10 +9,10 @@ struct SearchVideo
|
||||
property views : Int64
|
||||
property description_html : String
|
||||
property length_seconds : Int32
|
||||
property live_now : Bool
|
||||
property premium : Bool
|
||||
property premiere_timestamp : Time?
|
||||
property author_verified : Bool
|
||||
property author_thumbnail : String?
|
||||
property badges : VideoBadges
|
||||
|
||||
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
||||
query_params["v"] = self.id
|
||||
@ -89,24 +76,6 @@ struct SearchVideo
|
||||
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||
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
|
||||
Invidious::JSONify::APIv1.thumbnails(json, self.id)
|
||||
end
|
||||
@ -119,20 +88,13 @@ struct SearchVideo
|
||||
json.field "published", self.published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
||||
json.field "lengthSeconds", self.length_seconds
|
||||
json.field "liveNow", self.badges.live_now?
|
||||
json.field "premium", self.badges.premium?
|
||||
json.field "isUpcoming", self.upcoming?
|
||||
json.field "liveNow", self.live_now
|
||||
json.field "premium", self.premium
|
||||
json.field "isUpcoming", self.is_upcoming
|
||||
|
||||
if self.premiere_timestamp
|
||||
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
|
||||
end
|
||||
json.field "isNew", self.badges.new?
|
||||
json.field "is4k", self.badges.four_k?
|
||||
json.field "is8k", self.badges.eight_k?
|
||||
json.field "isVr180", self.badges.vr180?
|
||||
json.field "isVr360", self.badges.vr360?
|
||||
json.field "is3d", self.badges.three_d?
|
||||
json.field "hasCaptions", self.badges.closed_captions?
|
||||
end
|
||||
end
|
||||
|
||||
@ -147,7 +109,7 @@ struct SearchVideo
|
||||
to_json(nil, json)
|
||||
end
|
||||
|
||||
def upcoming?
|
||||
def is_upcoming
|
||||
premiere_timestamp ? true : false
|
||||
end
|
||||
end
|
||||
@ -242,7 +204,7 @@ struct SearchChannel
|
||||
|
||||
qualities.each do |quality|
|
||||
json.object do
|
||||
json.field "url", self.author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
|
||||
json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
|
||||
json.field "width", quality
|
||||
json.field "height", quality
|
||||
end
|
||||
|
@ -1,349 +0,0 @@
|
||||
require "uri"
|
||||
require "socket"
|
||||
require "socket/tcp_socket"
|
||||
require "socket/unix_socket"
|
||||
|
||||
{% if flag?(:advanced_debug) %}
|
||||
require "io/hexdump"
|
||||
{% end %}
|
||||
|
||||
private alias NetworkEndian = IO::ByteFormat::NetworkEndian
|
||||
|
||||
module Invidious::SigHelper
|
||||
enum UpdateStatus
|
||||
Updated
|
||||
UpdateNotRequired
|
||||
Error
|
||||
end
|
||||
|
||||
# -------------------
|
||||
# Payload types
|
||||
# -------------------
|
||||
|
||||
abstract struct Payload
|
||||
end
|
||||
|
||||
struct StringPayload < Payload
|
||||
getter string : String
|
||||
|
||||
def initialize(str : String)
|
||||
raise Exception.new("SigHelper: String can't be empty") if str.empty?
|
||||
@string = str
|
||||
end
|
||||
|
||||
def self.from_bytes(slice : Bytes)
|
||||
size = IO::ByteFormat::NetworkEndian.decode(UInt16, slice)
|
||||
if size == 0 # Error code
|
||||
raise Exception.new("SigHelper: Server encountered an error")
|
||||
end
|
||||
|
||||
if (slice.bytesize - 2) != size
|
||||
raise Exception.new("SigHelper: String size mismatch")
|
||||
end
|
||||
|
||||
if str = String.new(slice[2..])
|
||||
return self.new(str)
|
||||
else
|
||||
raise Exception.new("SigHelper: Can't read string from socket")
|
||||
end
|
||||
end
|
||||
|
||||
def to_io(io)
|
||||
# `.to_u16` raises if there is an overflow during the conversion
|
||||
io.write_bytes(@string.bytesize.to_u16, NetworkEndian)
|
||||
io.write(@string.to_slice)
|
||||
end
|
||||
end
|
||||
|
||||
private enum Opcode
|
||||
FORCE_UPDATE = 0
|
||||
DECRYPT_N_SIGNATURE = 1
|
||||
DECRYPT_SIGNATURE = 2
|
||||
GET_SIGNATURE_TIMESTAMP = 3
|
||||
GET_PLAYER_STATUS = 4
|
||||
PLAYER_UPDATE_TIMESTAMP = 5
|
||||
end
|
||||
|
||||
private record Request,
|
||||
opcode : Opcode,
|
||||
payload : Payload?
|
||||
|
||||
# ----------------------
|
||||
# High-level functions
|
||||
# ----------------------
|
||||
|
||||
class Client
|
||||
@mux : Multiplexor
|
||||
|
||||
def initialize(uri_or_path)
|
||||
@mux = Multiplexor.new(uri_or_path)
|
||||
end
|
||||
|
||||
# Forces the server to re-fetch the YouTube player, and extract the necessary
|
||||
# components from it (nsig function code, sig function code, signature timestamp).
|
||||
def force_update : UpdateStatus
|
||||
request = Request.new(Opcode::FORCE_UPDATE, nil)
|
||||
|
||||
value = send_request(request) do |bytes|
|
||||
IO::ByteFormat::NetworkEndian.decode(UInt16, bytes)
|
||||
end
|
||||
|
||||
case value
|
||||
when 0x0000 then return UpdateStatus::Error
|
||||
when 0xFFFF then return UpdateStatus::UpdateNotRequired
|
||||
when 0xF44F then return UpdateStatus::Updated
|
||||
else
|
||||
code = value.nil? ? "nil" : value.to_s(base: 16)
|
||||
raise Exception.new("SigHelper: Invalid status code received #{code}")
|
||||
end
|
||||
end
|
||||
|
||||
# Decrypt a provided n signature using the server's current nsig function
|
||||
# code, and return the result (or an error).
|
||||
def decrypt_n_param(n : String) : String?
|
||||
request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n))
|
||||
|
||||
n_dec = self.send_request(request) do |bytes|
|
||||
StringPayload.from_bytes(bytes).string
|
||||
end
|
||||
|
||||
return n_dec
|
||||
end
|
||||
|
||||
# Decrypt a provided s signature using the server's current sig function
|
||||
# code, and return the result (or an error).
|
||||
def decrypt_sig(sig : String) : String?
|
||||
request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig))
|
||||
|
||||
sig_dec = self.send_request(request) do |bytes|
|
||||
StringPayload.from_bytes(bytes).string
|
||||
end
|
||||
|
||||
return sig_dec
|
||||
end
|
||||
|
||||
# Return the signature timestamp from the server's current player
|
||||
def get_signature_timestamp : UInt64?
|
||||
request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil)
|
||||
|
||||
return self.send_request(request) do |bytes|
|
||||
IO::ByteFormat::NetworkEndian.decode(UInt64, bytes)
|
||||
end
|
||||
end
|
||||
|
||||
# Return the current player's version
|
||||
def get_player : UInt32?
|
||||
request = Request.new(Opcode::GET_PLAYER_STATUS, nil)
|
||||
|
||||
return self.send_request(request) do |bytes|
|
||||
has_player = (bytes[0] == 0xFF)
|
||||
player_version = IO::ByteFormat::NetworkEndian.decode(UInt32, bytes[1..4])
|
||||
has_player ? player_version : nil
|
||||
end
|
||||
end
|
||||
|
||||
# Return when the player was last updated
|
||||
def get_player_timestamp : UInt64?
|
||||
request = Request.new(Opcode::PLAYER_UPDATE_TIMESTAMP, nil)
|
||||
|
||||
return self.send_request(request) do |bytes|
|
||||
IO::ByteFormat::NetworkEndian.decode(UInt64, bytes)
|
||||
end
|
||||
end
|
||||
|
||||
private def send_request(request : Request, &)
|
||||
channel = @mux.send(request)
|
||||
slice = channel.receive
|
||||
return yield slice
|
||||
rescue ex
|
||||
LOGGER.debug("SigHelper: Error when sending a request")
|
||||
LOGGER.trace(ex.inspect_with_backtrace)
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
# ---------------------
|
||||
# Low level functions
|
||||
# ---------------------
|
||||
|
||||
class Multiplexor
|
||||
alias TransactionID = UInt32
|
||||
record Transaction, channel = ::Channel(Bytes).new
|
||||
|
||||
@prng = Random.new
|
||||
@mutex = Mutex.new
|
||||
@queue = {} of TransactionID => Transaction
|
||||
|
||||
@conn : Connection
|
||||
@uri_or_path : String
|
||||
|
||||
def initialize(@uri_or_path)
|
||||
@conn = Connection.new(uri_or_path)
|
||||
listen
|
||||
end
|
||||
|
||||
def listen : Nil
|
||||
raise "Socket is closed" if @conn.closed?
|
||||
|
||||
LOGGER.debug("SigHelper: Multiplexor listening")
|
||||
|
||||
spawn do
|
||||
loop do
|
||||
begin
|
||||
receive_data
|
||||
rescue ex
|
||||
LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...")
|
||||
# We close the socket because for some reason is not closed.
|
||||
@conn.close
|
||||
loop do
|
||||
begin
|
||||
@conn = Connection.new(@uri_or_path)
|
||||
LOGGER.info("SigHelper: Reconnected to SigHelper!")
|
||||
rescue ex
|
||||
LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying")
|
||||
sleep 500.milliseconds
|
||||
next
|
||||
end
|
||||
break if !@conn.closed?
|
||||
end
|
||||
end
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def send(request : Request)
|
||||
transaction = Transaction.new
|
||||
transaction_id = @prng.rand(TransactionID)
|
||||
|
||||
# Add transaction to queue
|
||||
@mutex.synchronize do
|
||||
# On a 32-bits random integer, this should never happen. Though, just in case, ...
|
||||
if @queue[transaction_id]?
|
||||
raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!")
|
||||
end
|
||||
|
||||
@queue[transaction_id] = transaction
|
||||
end
|
||||
|
||||
write_packet(transaction_id, request)
|
||||
|
||||
return transaction.channel
|
||||
end
|
||||
|
||||
def receive_data
|
||||
transaction_id, slice = read_packet
|
||||
|
||||
@mutex.synchronize do
|
||||
if transaction = @queue.delete(transaction_id)
|
||||
# Remove transaction from queue and send data to the channel
|
||||
transaction.channel.send(slice)
|
||||
LOGGER.trace("SigHelper: Transaction unqueued and data sent to channel")
|
||||
else
|
||||
raise Exception.new("SigHelper: Received transaction was not in queue")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Read a single packet from the socket
|
||||
private def read_packet : {TransactionID, Bytes}
|
||||
# Header
|
||||
transaction_id = @conn.read_bytes(UInt32, NetworkEndian)
|
||||
length = @conn.read_bytes(UInt32, NetworkEndian)
|
||||
|
||||
LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} / length #{length}")
|
||||
|
||||
if length > 67_000
|
||||
raise Exception.new("SigHelper: Packet longer than expected (#{length})")
|
||||
end
|
||||
|
||||
# Payload
|
||||
slice = Bytes.new(length)
|
||||
@conn.read(slice) if length > 0
|
||||
|
||||
LOGGER.trace("SigHelper: payload = #{slice}")
|
||||
LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} - Done")
|
||||
|
||||
return transaction_id, slice
|
||||
end
|
||||
|
||||
# Write a single packet to the socket
|
||||
private def write_packet(transaction_id : TransactionID, request : Request)
|
||||
LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} / opcode #{request.opcode}")
|
||||
|
||||
io = IO::Memory.new(1024)
|
||||
io.write_bytes(request.opcode.to_u8, NetworkEndian)
|
||||
io.write_bytes(transaction_id, NetworkEndian)
|
||||
|
||||
if payload = request.payload
|
||||
payload.to_io(io)
|
||||
end
|
||||
|
||||
@conn.send(io)
|
||||
@conn.flush
|
||||
|
||||
LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} - Done")
|
||||
end
|
||||
end
|
||||
|
||||
class Connection
|
||||
@socket : UNIXSocket | TCPSocket
|
||||
|
||||
{% if flag?(:advanced_debug) %}
|
||||
@io : IO::Hexdump
|
||||
{% end %}
|
||||
|
||||
def initialize(host_or_path : String)
|
||||
case host_or_path
|
||||
when .starts_with?('/')
|
||||
# Make sure that the file exists
|
||||
if File.exists?(host_or_path)
|
||||
@socket = UNIXSocket.new(host_or_path)
|
||||
else
|
||||
raise Exception.new("SigHelper: '#{host_or_path}' no such file")
|
||||
end
|
||||
when .starts_with?("tcp://")
|
||||
uri = URI.parse(host_or_path)
|
||||
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
|
||||
else
|
||||
uri = URI.parse("tcp://#{host_or_path}")
|
||||
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
|
||||
end
|
||||
LOGGER.info("SigHelper: Using helper at '#{host_or_path}'")
|
||||
|
||||
{% if flag?(:advanced_debug) %}
|
||||
@io = IO::Hexdump.new(@socket, output: STDERR, read: true, write: true)
|
||||
{% end %}
|
||||
|
||||
@socket.sync = false
|
||||
@socket.blocking = false
|
||||
end
|
||||
|
||||
def closed? : Bool
|
||||
return @socket.closed?
|
||||
end
|
||||
|
||||
def close : Nil
|
||||
@socket.close if !@socket.closed?
|
||||
end
|
||||
|
||||
def flush(*args, **options)
|
||||
@socket.flush(*args, **options)
|
||||
end
|
||||
|
||||
def send(*args, **options)
|
||||
@socket.send(*args, **options)
|
||||
end
|
||||
|
||||
# Wrap IO functions, with added debug tooling if needed
|
||||
{% for function in %w(read read_bytes write write_bytes) %}
|
||||
def {{function.id}}(*args, **options)
|
||||
{% if flag?(:advanced_debug) %}
|
||||
@io.{{function.id}}(*args, **options)
|
||||
{% else %}
|
||||
@socket.{{function.id}}(*args, **options)
|
||||
{% end %}
|
||||
end
|
||||
{% end %}
|
||||
end
|
||||
end
|
@ -1,53 +1,73 @@
|
||||
require "http/params"
|
||||
require "./sig_helper"
|
||||
alias SigProc = Proc(Array(String), Int32, Array(String))
|
||||
|
||||
class Invidious::DecryptFunction
|
||||
@last_update : Time = Time.utc - 42.days
|
||||
struct DecryptFunction
|
||||
@decrypt_function = [] of {SigProc, Int32}
|
||||
@decrypt_time = Time.monotonic
|
||||
|
||||
def initialize(uri_or_path)
|
||||
@client = SigHelper::Client.new(uri_or_path)
|
||||
self.check_update
|
||||
def initialize(@use_polling = true)
|
||||
end
|
||||
|
||||
def check_update
|
||||
# If we have updated in the last 5 minutes, do nothing
|
||||
return if (Time.utc - @last_update) < 5.minutes
|
||||
def update_decrypt_function
|
||||
@decrypt_function = fetch_decrypt_function
|
||||
end
|
||||
|
||||
# Get the amount of time elapsed since when the player was updated, in the
|
||||
# event where multiple invidious processes are run in parallel.
|
||||
update_time_elapsed = (@client.get_player_timestamp || 301).seconds
|
||||
private def fetch_decrypt_function(id = "CvFH_6DNRCY")
|
||||
document = YT_POOL.client &.get("/watch?v=#{id}&gl=US&hl=en").body
|
||||
url = document.match(/src="(?<url>\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base.js)"/).not_nil!["url"]
|
||||
player = YT_POOL.client &.get(url).body
|
||||
|
||||
if update_time_elapsed > 5.minutes
|
||||
LOGGER.debug("Signature: Player might be outdated, updating")
|
||||
@client.force_update
|
||||
@last_update = Time.utc
|
||||
function_name = player.match(/^(?<name>[^=]+)=function\(\w\){\w=\w\.split\(""\);[^\. ]+\.[^( ]+/m).not_nil!["name"]
|
||||
function_body = player.match(/^#{Regex.escape(function_name)}=function\(\w\){(?<body>[^}]+)}/m).not_nil!["body"]
|
||||
function_body = function_body.split(";")[1..-2]
|
||||
|
||||
var_name = function_body[0][0, 2]
|
||||
var_body = player.delete("\n").match(/var #{Regex.escape(var_name)}={(?<body>(.*?))};/).not_nil!["body"]
|
||||
|
||||
operations = {} of String => SigProc
|
||||
var_body.split("},").each do |operation|
|
||||
op_name = operation.match(/^[^:]+/).not_nil![0]
|
||||
op_body = operation.match(/\{[^}]+/).not_nil![0]
|
||||
|
||||
case op_body
|
||||
when "{a.reverse()"
|
||||
operations[op_name] = ->(a : Array(String), _b : Int32) { a.reverse }
|
||||
when "{a.splice(0,b)"
|
||||
operations[op_name] = ->(a : Array(String), b : Int32) { a.delete_at(0..(b - 1)); a }
|
||||
else
|
||||
operations[op_name] = ->(a : Array(String), b : Int32) { c = a[0]; a[0] = a[b % a.size]; a[b % a.size] = c; a }
|
||||
end
|
||||
end
|
||||
|
||||
decrypt_function = [] of {SigProc, Int32}
|
||||
function_body.each do |function|
|
||||
function = function.lchop(var_name).delete("[].")
|
||||
|
||||
op_name = function.match(/[^\(]+/).not_nil![0]
|
||||
value = function.match(/\(\w,(?<value>[\d]+)\)/).not_nil!["value"].to_i
|
||||
|
||||
decrypt_function << {operations[op_name], value}
|
||||
end
|
||||
|
||||
return decrypt_function
|
||||
end
|
||||
|
||||
def decrypt_nsig(n : String) : String?
|
||||
self.check_update
|
||||
return @client.decrypt_n_param(n)
|
||||
rescue ex
|
||||
LOGGER.debug(ex.message || "Signature: Unknown error")
|
||||
LOGGER.trace(ex.inspect_with_backtrace)
|
||||
return nil
|
||||
end
|
||||
def decrypt_signature(fmt : Hash(String, JSON::Any))
|
||||
return "" if !fmt["s"]? || !fmt["sp"]?
|
||||
|
||||
def decrypt_signature(str : String) : String?
|
||||
self.check_update
|
||||
return @client.decrypt_sig(str)
|
||||
rescue ex
|
||||
LOGGER.debug(ex.message || "Signature: Unknown error")
|
||||
LOGGER.trace(ex.inspect_with_backtrace)
|
||||
return nil
|
||||
end
|
||||
sp = fmt["sp"].as_s
|
||||
sig = fmt["s"].as_s.split("")
|
||||
if !@use_polling
|
||||
now = Time.monotonic
|
||||
if now - @decrypt_time > 60.seconds || @decrypt_function.size == 0
|
||||
@decrypt_function = fetch_decrypt_function
|
||||
@decrypt_time = Time.monotonic
|
||||
end
|
||||
end
|
||||
|
||||
def get_sts : UInt64?
|
||||
self.check_update
|
||||
return @client.get_signature_timestamp
|
||||
rescue ex
|
||||
LOGGER.debug(ex.message || "Signature: Unknown error")
|
||||
LOGGER.trace(ex.inspect_with_backtrace)
|
||||
return nil
|
||||
@decrypt_function.each do |proc, value|
|
||||
sig = proc.call(sig, value)
|
||||
end
|
||||
|
||||
return "&#{sp}=#{sig.join("")}"
|
||||
end
|
||||
end
|
||||
|
@ -52,9 +52,9 @@ def recode_length_seconds(time)
|
||||
end
|
||||
|
||||
def decode_interval(string : String) : Time::Span
|
||||
raw_minutes = string.try &.to_i32?
|
||||
rawMinutes = string.try &.to_i32?
|
||||
|
||||
if !raw_minutes
|
||||
if !rawMinutes
|
||||
hours = /(?<hours>\d+)h/.match(string).try &.["hours"].try &.to_i32
|
||||
hours ||= 0
|
||||
|
||||
@ -63,7 +63,7 @@ def decode_interval(string : String) : Time::Span
|
||||
|
||||
time = Time::Span.new(hours: hours, minutes: minutes)
|
||||
else
|
||||
time = Time::Span.new(minutes: raw_minutes)
|
||||
time = Time::Span.new(minutes: rawMinutes)
|
||||
end
|
||||
|
||||
return time
|
||||
@ -323,6 +323,68 @@ def parse_range(range)
|
||||
return 0_i64, nil
|
||||
end
|
||||
|
||||
def fetch_random_instance
|
||||
begin
|
||||
instance_api_client = make_client(URI.parse("https://api.invidious.io"))
|
||||
|
||||
# Timeouts
|
||||
instance_api_client.connect_timeout = 10.seconds
|
||||
instance_api_client.dns_timeout = 10.seconds
|
||||
|
||||
instance_list = JSON.parse(instance_api_client.get("/instances.json").body).as_a
|
||||
instance_api_client.close
|
||||
rescue Socket::ConnectError | IO::TimeoutError | JSON::ParseException
|
||||
instance_list = [] of JSON::Any
|
||||
end
|
||||
|
||||
filtered_instance_list = [] of String
|
||||
|
||||
instance_list.each do |data|
|
||||
# TODO Check if current URL is onion instance and use .onion types if so.
|
||||
if data[1]["type"] == "https"
|
||||
# Instances can have statistics disabled, which is an requirement of version validation.
|
||||
# as_nil? doesn't exist. Thus we'll have to handle the error raised if as_nil fails.
|
||||
begin
|
||||
data[1]["stats"].as_nil
|
||||
next
|
||||
rescue TypeCastError
|
||||
end
|
||||
|
||||
# stats endpoint could also lack the software dict.
|
||||
next if data[1]["stats"]["software"]?.nil?
|
||||
|
||||
# Makes sure the instance isn't too outdated.
|
||||
if remote_version = data[1]["stats"]?.try &.["software"]?.try &.["version"]
|
||||
remote_commit_date = remote_version.as_s.match(/\d{4}\.\d{2}\.\d{2}/)
|
||||
next if !remote_commit_date
|
||||
|
||||
remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC)
|
||||
local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC)
|
||||
|
||||
next if (remote_commit_date - local_commit_date).abs.days > 30
|
||||
|
||||
begin
|
||||
data[1]["monitor"].as_nil
|
||||
health = data[1]["monitor"].as_h["dailyRatios"][0].as_h["ratio"]
|
||||
filtered_instance_list << data[0].as_s if health.to_s.to_f > 90
|
||||
rescue TypeCastError
|
||||
# We can't check the health if the monitoring is broken. Thus we'll just add it to the list
|
||||
# and move on. Ideally we'll ignore any instance that has broken health monitoring but due to the fact that
|
||||
# it's an error that often occurs with all the instances at the same time, we have to just skip the check.
|
||||
filtered_instance_list << data[0].as_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# If for some reason no instances managed to get fetched successfully then we'll just redirect to redirect.invidious.io
|
||||
if filtered_instance_list.size == 0
|
||||
return "redirect.invidious.io"
|
||||
end
|
||||
|
||||
return filtered_instance_list.sample(1)[0]
|
||||
end
|
||||
|
||||
def reduce_uri(uri : URI | String, max_length : Int32 = 50, suffix : String = "…") : String
|
||||
str = uri.to_s.sub(/^https?:\/\//, "")
|
||||
if str.size > max_length
|
||||
@ -383,22 +445,3 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
|
||||
end
|
||||
return text
|
||||
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
|
||||
|
@ -11,12 +11,11 @@ module Invidious::HttpServer
|
||||
params = url.query_params
|
||||
params["host"] = url.host.not_nil! # Should never be nil, in theory
|
||||
params["region"] = region if !region.nil?
|
||||
url.query_params = params
|
||||
|
||||
if absolute
|
||||
return "#{HOST_URL}#{url.request_target}"
|
||||
return "#{HOST_URL}#{url.request_target}?#{params}"
|
||||
else
|
||||
return url.request_target
|
||||
return "#{url.request_target}?#{params}"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,97 +0,0 @@
|
||||
class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
|
||||
# We update the internals of a constant as so it can be accessed from anywhere
|
||||
# within the codebase
|
||||
#
|
||||
# "INSTANCES" => Array(Tuple(String, String)) # region, instance
|
||||
|
||||
INSTANCES = {"INSTANCES" => [] of Tuple(String, String)}
|
||||
|
||||
def initialize
|
||||
end
|
||||
|
||||
def begin
|
||||
loop do
|
||||
refresh_instances
|
||||
LOGGER.info("InstanceListRefreshJob: Done, sleeping for 30 minutes")
|
||||
sleep 30.minute
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
|
||||
# Refreshes the list of instances used for redirects.
|
||||
#
|
||||
# Does the following three checks for each instance
|
||||
# - Is it a clear-net instance?
|
||||
# - Is it an instance with a good uptime?
|
||||
# - Is it an updated instance?
|
||||
private def refresh_instances
|
||||
raw_instance_list = self.fetch_instances
|
||||
filtered_instance_list = [] of Tuple(String, String)
|
||||
|
||||
raw_instance_list.each do |instance_data|
|
||||
# TODO allow Tor hidden service instances when the current instance
|
||||
# is also a hidden service. Same for i2p and any other non-clearnet instances.
|
||||
begin
|
||||
domain = instance_data[0]
|
||||
info = instance_data[1]
|
||||
stats = info["stats"]
|
||||
|
||||
next unless info["type"] == "https"
|
||||
next if bad_uptime?(info["monitor"])
|
||||
next if outdated?(stats["software"]["version"])
|
||||
|
||||
filtered_instance_list << {info["region"].as_s, domain.as_s}
|
||||
rescue ex
|
||||
if domain
|
||||
LOGGER.info("InstanceListRefreshJob: failed to parse information from '#{domain}' because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" ")
|
||||
else
|
||||
LOGGER.info("InstanceListRefreshJob: failed to parse information from an instance because \"#{ex}\"\n\"#{ex.backtrace.join('\n')}\" ")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if !filtered_instance_list.empty?
|
||||
INSTANCES["INSTANCES"] = filtered_instance_list
|
||||
end
|
||||
end
|
||||
|
||||
# Fetches information regarding instances from api.invidious.io or an otherwise configured URL
|
||||
private def fetch_instances : Array(JSON::Any)
|
||||
begin
|
||||
# We directly call the stdlib HTTP::Client here as it allows us to negate the effects
|
||||
# of the force_resolve config option. This is needed as api.invidious.io does not support ipv6
|
||||
# and as such the following request raises if we were to use force_resolve with the ipv6 value.
|
||||
instance_api_client = HTTP::Client.new(URI.parse("https://api.invidious.io"))
|
||||
|
||||
# Timeouts
|
||||
instance_api_client.connect_timeout = 10.seconds
|
||||
instance_api_client.dns_timeout = 10.seconds
|
||||
|
||||
raw_instance_list = JSON.parse(instance_api_client.get("/instances.json").body).as_a
|
||||
instance_api_client.close
|
||||
rescue ex : Socket::ConnectError | IO::TimeoutError | JSON::ParseException
|
||||
raw_instance_list = [] of JSON::Any
|
||||
end
|
||||
|
||||
return raw_instance_list
|
||||
end
|
||||
|
||||
# Checks if the given target instance is outdated
|
||||
private def outdated?(target_instance_version) : Bool
|
||||
remote_commit_date = target_instance_version.as_s.match(/\d{4}\.\d{2}\.\d{2}/)
|
||||
return false if !remote_commit_date
|
||||
|
||||
remote_commit_date = Time.parse(remote_commit_date[0], "%Y.%m.%d", Time::Location::UTC)
|
||||
local_commit_date = Time.parse(CURRENT_VERSION, "%Y.%m.%d", Time::Location::UTC)
|
||||
|
||||
return (remote_commit_date - local_commit_date).abs.days > 30
|
||||
end
|
||||
|
||||
# Checks if the uptime of the target instance is greater than 90% over a 30 day period
|
||||
private def bad_uptime?(target_instance_health_monitor) : Bool
|
||||
return true if !target_instance_health_monitor["down"].as_bool == false
|
||||
return true if target_instance_health_monitor["uptime"].as_f < 90
|
||||
|
||||
return false
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user