forked from Fijxu/invidious
Compare commits
8 commits
master
...
external-p
Author | SHA1 | Date | |
---|---|---|---|
e1a1bf0f79 | |||
7ded7455b0 | |||
53f0a5b1e0 | |||
34a11fe1bb | |||
2db9396dea | |||
|
0c3e5baab0 | ||
73ba53a327 | |||
00bcf53d0f |
87 changed files with 476 additions and 1306 deletions
|
@ -38,9 +38,6 @@ Style/RedundantBegin:
|
||||||
Style/RedundantReturn:
|
Style/RedundantReturn:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
Style/RedundantNext:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/ParenthesesAroundCondition:
|
Style/ParenthesesAroundCondition:
|
||||||
Enabled: false
|
Enabled: false
|
||||||
|
|
||||||
|
|
|
@ -8,8 +8,6 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- "master"
|
- "master"
|
||||||
- "experimental"
|
|
||||||
- "experimental2"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
@ -37,15 +35,13 @@ jobs:
|
||||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
|
|
||||||
- uses: https://code.forgejo.org/docker/build-push-action@v6
|
- uses: https://code.forgejo.org/docker/build-push-action@v5
|
||||||
name: Build images
|
name: Build images
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: docker/Dockerfile
|
file: docker/Dockerfile
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
# cache-from: type=gha
|
|
||||||
# cache-to: type=gha,mode=max
|
|
||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
"release=1"
|
"release=1"
|
||||||
|
|
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
|
@ -6,7 +6,7 @@ docker/ @unixfox
|
||||||
kubernetes/ @unixfox
|
kubernetes/ @unixfox
|
||||||
|
|
||||||
README.md @thefrenchghosty
|
README.md @thefrenchghosty
|
||||||
config/config.example.yml @SamantazFox @unixfox
|
config/config.example.yml @thefrenchghosty @SamantazFox @unixfox
|
||||||
|
|
||||||
scripts/ @syeopite
|
scripts/ @syeopite
|
||||||
shards.lock @syeopite
|
shards.lock @syeopite
|
||||||
|
|
8
.github/workflows/build-stable-container.yml
vendored
8
.github/workflows/build-stable-container.yml
vendored
|
@ -1,7 +1,6 @@
|
||||||
name: Build and release container
|
name: Build and release container
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
|
@ -47,11 +46,9 @@ jobs:
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
with:
|
with:
|
||||||
images: quay.io/invidious/invidious
|
images: quay.io/invidious/invidious
|
||||||
flavor: |
|
|
||||||
latest=false
|
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=raw,value=latest
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
labels: |
|
labels: |
|
||||||
quay.expires-after=12w
|
quay.expires-after=12w
|
||||||
|
|
||||||
|
@ -73,11 +70,10 @@ jobs:
|
||||||
with:
|
with:
|
||||||
images: quay.io/invidious/invidious
|
images: quay.io/invidious/invidious
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
|
||||||
suffix=-arm64
|
suffix=-arm64
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=raw,value=latest
|
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||||
labels: |
|
labels: |
|
||||||
quay.expires-after=12w
|
quay.expires-after=12w
|
||||||
|
|
||||||
|
|
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
|
@ -38,11 +38,10 @@ jobs:
|
||||||
matrix:
|
matrix:
|
||||||
stable: [true]
|
stable: [true]
|
||||||
crystal:
|
crystal:
|
||||||
|
- 1.9.2
|
||||||
- 1.10.1
|
- 1.10.1
|
||||||
- 1.11.2
|
- 1.11.2
|
||||||
- 1.12.1
|
- 1.12.1
|
||||||
- 1.13.2
|
|
||||||
- 1.14.0
|
|
||||||
include:
|
include:
|
||||||
- crystal: nightly
|
- crystal: nightly
|
||||||
stable: false
|
stable: false
|
||||||
|
@ -52,11 +51,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Install required APT packages
|
|
||||||
run: |
|
|
||||||
sudo apt install -y libsqlite3-dev
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Install Crystal
|
- name: Install Crystal
|
||||||
uses: crystal-lang/install-crystal@v1.8.0
|
uses: crystal-lang/install-crystal@v1.8.0
|
||||||
with:
|
with:
|
||||||
|
|
231
CHANGELOG.md
231
CHANGELOG.md
|
@ -1,235 +1,6 @@
|
||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
## vX.Y.0 (future)
|
## 2024-04-26
|
||||||
|
|
||||||
|
|
||||||
### Full list of pull requests merged since the last release (newest first)
|
|
||||||
|
|
||||||
* 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
|
|
||||||
[#4750]: https://github.com/iv-org/invidious/pull/4750
|
|
||||||
[#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
|
|
||||||
[#4942]: https://github.com/iv-org/invidious/pull/4942
|
|
||||||
[#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
|
|
||||||
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
||||||
Major bug fixes:
|
Major bug fixes:
|
||||||
* Videos: Use android test suite client (#4650, thanks @SamantazFox)
|
* Videos: Use android test suite client (#4650, thanks @SamantazFox)
|
||||||
|
|
9
Makefile
9
Makefile
|
@ -7,11 +7,6 @@ STATIC := 0
|
||||||
|
|
||||||
NO_DBG_SYMBOLS := 0
|
NO_DBG_SYMBOLS := 0
|
||||||
|
|
||||||
# Enable multi-threading.
|
|
||||||
# Warning: Experimental feature!!
|
|
||||||
# invidious is not stable when MT is enabled.
|
|
||||||
MT := 0
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS ?=
|
FLAGS ?=
|
||||||
|
|
||||||
|
@ -24,10 +19,6 @@ ifeq ($(STATIC), 1)
|
||||||
FLAGS += --static
|
FLAGS += --static
|
||||||
endif
|
endif
|
||||||
|
|
||||||
ifeq ($(MT), 1)
|
|
||||||
FLAGS += -Dpreview_mt
|
|
||||||
endif
|
|
||||||
|
|
||||||
|
|
||||||
ifeq ($(NO_DBG_SYMBOLS), 1)
|
ifeq ($(NO_DBG_SYMBOLS), 1)
|
||||||
FLAGS += --no-debug
|
FLAGS += --no-debug
|
||||||
|
|
|
@ -68,7 +68,6 @@
|
||||||
|
|
||||||
.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu {
|
.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu {
|
||||||
margin-bottom: 2em;
|
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;
|
.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control {height: 5px;
|
||||||
|
|
|
@ -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 video_data = JSON.parse(document.getElementById('video_data').textContent);
|
||||||
|
|
||||||
var options = {
|
var options = {
|
||||||
|
preload: 'auto',
|
||||||
liveui: true,
|
liveui: true,
|
||||||
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
|
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
|
||||||
controlBar: {
|
controlBar: {
|
||||||
|
@ -50,8 +51,8 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
|
||||||
return options;
|
return options;
|
||||||
};
|
};
|
||||||
|
|
||||||
videojs.Vhs.GOAL_BUFFER_LENGTH = 40;
|
videojs.Vhs.GOAL_BUFFER_LENGTH = 20;
|
||||||
videojs.Vhs.MAX_GOAL_BUFFER_LENGTH = 80;
|
videojs.Vhs.MAX_GOAL_BUFFER_LENGTH = 30;
|
||||||
|
|
||||||
var player = videojs('player', options);
|
var player = videojs('player', options);
|
||||||
|
|
||||||
|
|
|
@ -173,17 +173,6 @@ https_only: false
|
||||||
##
|
##
|
||||||
#force_resolve:
|
#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
|
## Use Innertube's transcripts API instead of timedtext for closed captions
|
||||||
|
@ -790,22 +779,6 @@ default_user_preferences:
|
||||||
# Video player behavior
|
# 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.
|
## Automatically play videos on page load.
|
||||||
##
|
##
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
import lldb
|
|
||||||
|
|
||||||
class CrystalArraySyntheticProvider:
|
|
||||||
def __init__(self, valobj, internal_dict):
|
|
||||||
self.valobj = valobj
|
|
||||||
self.buffer = None
|
|
||||||
self.size = 0
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
if self.valobj.type.is_pointer:
|
|
||||||
self.valobj = self.valobj.Dereference()
|
|
||||||
self.size = int(self.valobj.child[0].value)
|
|
||||||
self.type = self.valobj.type
|
|
||||||
self.buffer = self.valobj.child[3]
|
|
||||||
|
|
||||||
def num_children(self):
|
|
||||||
size = 0 if self.size is None else self.size
|
|
||||||
return size
|
|
||||||
|
|
||||||
def get_child_index(self, name):
|
|
||||||
try:
|
|
||||||
return int(name.lstrip('[').rstrip(']'))
|
|
||||||
except:
|
|
||||||
return -1
|
|
||||||
|
|
||||||
def get_child_at_index(self,index):
|
|
||||||
if index >= self.size:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
elementType = self.buffer.type.GetPointeeType()
|
|
||||||
offset = elementType.size * index
|
|
||||||
return self.buffer.CreateChildAtOffset('[' + str(index) + ']', offset, elementType)
|
|
||||||
except Exception as e:
|
|
||||||
print('Got exception %s' % (str(e)))
|
|
||||||
return None
|
|
||||||
|
|
||||||
def findType(name, module):
|
|
||||||
cachedTypes = module.GetTypes()
|
|
||||||
for idx in range(cachedTypes.GetSize()):
|
|
||||||
type = cachedTypes.GetTypeAtIndex(idx)
|
|
||||||
if type.name == name:
|
|
||||||
return type
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def CrystalString_SummaryProvider(value, dict):
|
|
||||||
error = lldb.SBError()
|
|
||||||
if value.TypeIsPointerType():
|
|
||||||
value = value.Dereference()
|
|
||||||
process = value.GetTarget().GetProcess()
|
|
||||||
byteSize = int(value.child[0].value)
|
|
||||||
len = int(value.child[1].value)
|
|
||||||
len = byteSize or len
|
|
||||||
strAddr = value.child[2].load_addr
|
|
||||||
val = process.ReadCStringFromMemory(strAddr, len + 1, error)
|
|
||||||
return '"%s"' % val
|
|
||||||
|
|
||||||
|
|
||||||
def __lldb_init_module(debugger, dict):
|
|
||||||
debugger.HandleCommand(r'type synthetic add -l crystal_formatters.CrystalArraySyntheticProvider -x "^Array\(.+\)(\s*\**)?" -w Crystal')
|
|
||||||
debugger.HandleCommand(r'type summary add -F crystal_formatters.CrystalString_SummaryProvider -x "^(String|\(String \| Nil\))(\s*\**)?$" -w Crystal')
|
|
||||||
debugger.HandleCommand(r'type category enable Crystal')
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM crystallang/crystal:1.14.0-alpine AS builder
|
FROM crystallang/crystal:1.12.1-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-static yaml-static
|
RUN apk add --no-cache sqlite-static yaml-static
|
||||||
|
|
||||||
|
|
|
@ -483,7 +483,7 @@
|
||||||
"comments_view_x_replies_3": "عرض رد {{count}}",
|
"comments_view_x_replies_3": "عرض رد {{count}}",
|
||||||
"comments_view_x_replies_4": "عرض الردود {{count}}",
|
"comments_view_x_replies_4": "عرض الردود {{count}}",
|
||||||
"comments_view_x_replies_5": "عرض رد {{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_0": "{{count}} نقطة",
|
||||||
"comments_points_count_1": "نقطة واحدة",
|
"comments_points_count_1": "نقطة واحدة",
|
||||||
"comments_points_count_2": "نقطتان",
|
"comments_points_count_2": "نقطتان",
|
||||||
|
|
|
@ -471,7 +471,7 @@
|
||||||
"search_filters_title": "Filtry",
|
"search_filters_title": "Filtry",
|
||||||
"search_filters_duration_option_medium": "Střední (4 - 20 minut)",
|
"search_filters_duration_option_medium": "Střední (4 - 20 minut)",
|
||||||
"search_filters_duration_option_long": "Dlouhá (> 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_label": "Vlastnosti",
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
"search_filters_features_option_three_sixty": "360°",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
|
|
|
@ -47,7 +47,6 @@
|
||||||
"Preferences": "Einstellungen",
|
"Preferences": "Einstellungen",
|
||||||
"preferences_category_player": "Wiedergabeeinstellungen",
|
"preferences_category_player": "Wiedergabeeinstellungen",
|
||||||
"preferences_video_loop_label": "Immer wiederholen: ",
|
"preferences_video_loop_label": "Immer wiederholen: ",
|
||||||
"preferences_preload_label": "Videodaten vorladen: ",
|
|
||||||
"preferences_autoplay_label": "Automatisch abspielen: ",
|
"preferences_autoplay_label": "Automatisch abspielen: ",
|
||||||
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
|
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
|
||||||
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
|
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
|
||||||
|
@ -323,7 +322,7 @@
|
||||||
"channel_tab_community_label": "Gemeinschaft",
|
"channel_tab_community_label": "Gemeinschaft",
|
||||||
"search_filters_sort_option_relevance": "Relevanz",
|
"search_filters_sort_option_relevance": "Relevanz",
|
||||||
"search_filters_sort_option_rating": "Bewertung",
|
"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_sort_option_views": "Aufrufe",
|
||||||
"search_filters_type_label": "Inhaltstyp",
|
"search_filters_type_label": "Inhaltstyp",
|
||||||
"search_filters_duration_label": "Dauer",
|
"search_filters_duration_label": "Dauer",
|
||||||
|
@ -455,7 +454,7 @@
|
||||||
"Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)",
|
"Portuguese (auto-generated)": "Portugiesisch (automatisch generiert)",
|
||||||
"search_filters_title": "Filtern",
|
"search_filters_title": "Filtern",
|
||||||
"search_message_change_filters_or_query": "Versuchen Sie, Ihre Suchanfrage zu erweitern und/oder die Filter zu ändern.",
|
"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: ",
|
"Popular enabled: ": "„Beliebt“-Seite aktiviert: ",
|
||||||
"search_message_no_results": "Keine Ergebnisse gefunden.",
|
"search_message_no_results": "Keine Ergebnisse gefunden.",
|
||||||
"search_filters_duration_option_medium": "Mittel (4 - 20 Minuten)",
|
"search_filters_duration_option_medium": "Mittel (4 - 20 Minuten)",
|
||||||
|
@ -494,8 +493,5 @@
|
||||||
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
|
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
|
||||||
"Search for videos": "Nach Videos suchen",
|
"Search for videos": "Nach Videos suchen",
|
||||||
"toggle_theme": "Thema wechseln",
|
"toggle_theme": "Thema wechseln",
|
||||||
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
|
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: "
|
||||||
"carousel_go_to": "Zu Folie `x` gehen",
|
|
||||||
"carousel_slide": "Folie {{current}} von {{total}}",
|
|
||||||
"carousel_skip": "Karussell überspringen"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -489,10 +489,5 @@
|
||||||
"search_filters_date_label": "Ημερομηνία αναφόρτωσης",
|
"search_filters_date_label": "Ημερομηνία αναφόρτωσης",
|
||||||
"Search for videos": "Αναζήτηση βίντεο",
|
"Search for videos": "Αναζήτηση βίντεο",
|
||||||
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
|
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
|
||||||
"Answer": "Απάντηση",
|
"Answer": "Απάντηση"
|
||||||
"Add to playlist": "Λίιστα αναπαραγωγής",
|
|
||||||
"Add to playlist: ": "Λίστα αναπαραγωγής: ",
|
|
||||||
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
|
|
||||||
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
|
|
||||||
"toggle_theme": "Αλλαγή θέματος"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,6 @@
|
||||||
"Preferences": "Preferences",
|
"Preferences": "Preferences",
|
||||||
"preferences_category_player": "Player preferences",
|
"preferences_category_player": "Player preferences",
|
||||||
"preferences_video_loop_label": "Always loop: ",
|
"preferences_video_loop_label": "Always loop: ",
|
||||||
"preferences_preload_label": "Preload video data: ",
|
|
||||||
"preferences_autoplay_label": "Autoplay: ",
|
"preferences_autoplay_label": "Autoplay: ",
|
||||||
"preferences_continue_label": "Play next by default: ",
|
"preferences_continue_label": "Play next by default: ",
|
||||||
"preferences_continue_autoplay_label": "Autoplay next video: ",
|
"preferences_continue_autoplay_label": "Autoplay next video: ",
|
||||||
|
@ -192,7 +191,7 @@
|
||||||
"Switch Invidious Instance": "Switch Invidious Instance",
|
"Switch Invidious Instance": "Switch Invidious Instance",
|
||||||
"search_message_no_results": "No results found.",
|
"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_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",
|
"Hide annotations": "Hide annotations",
|
||||||
"Show annotations": "Show annotations",
|
"Show annotations": "Show annotations",
|
||||||
"Genre: ": "Genre: ",
|
"Genre: ": "Genre: ",
|
||||||
|
@ -287,7 +286,6 @@
|
||||||
"Esperanto": "Esperanto",
|
"Esperanto": "Esperanto",
|
||||||
"Estonian": "Estonian",
|
"Estonian": "Estonian",
|
||||||
"Filipino": "Filipino",
|
"Filipino": "Filipino",
|
||||||
"Filipino (auto-generated)": "Filipino (auto-generated)",
|
|
||||||
"Finnish": "Finnish",
|
"Finnish": "Finnish",
|
||||||
"French": "French",
|
"French": "French",
|
||||||
"French (auto-generated)": "French (auto-generated)",
|
"French (auto-generated)": "French (auto-generated)",
|
||||||
|
@ -425,7 +423,7 @@
|
||||||
"search_filters_title": "Filters",
|
"search_filters_title": "Filters",
|
||||||
"search_filters_date_label": "Upload date",
|
"search_filters_date_label": "Upload date",
|
||||||
"search_filters_date_option_none": "Any 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_today": "Today",
|
||||||
"search_filters_date_option_week": "This week",
|
"search_filters_date_option_week": "This week",
|
||||||
"search_filters_date_option_month": "This month",
|
"search_filters_date_option_month": "This month",
|
||||||
|
@ -457,7 +455,7 @@
|
||||||
"search_filters_sort_label": "Sort By",
|
"search_filters_sort_label": "Sort By",
|
||||||
"search_filters_sort_option_relevance": "Relevance",
|
"search_filters_sort_option_relevance": "Relevance",
|
||||||
"search_filters_sort_option_rating": "Rating",
|
"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_sort_option_views": "View count",
|
||||||
"search_filters_apply_button": "Apply selected filters",
|
"search_filters_apply_button": "Apply selected filters",
|
||||||
"Current version: ": "Current version: ",
|
"Current version: ": "Current version: ",
|
||||||
|
|
|
@ -480,7 +480,7 @@
|
||||||
"tokens_count_0": "{{count}} token",
|
"tokens_count_0": "{{count}} token",
|
||||||
"tokens_count_1": "{{count}} tokens",
|
"tokens_count_1": "{{count}} tokens",
|
||||||
"tokens_count_2": "{{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? ",
|
"Popular enabled: ": "¿Habilitar la sección popular? ",
|
||||||
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
|
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
|
||||||
"channel_tab_streams_label": "Directos",
|
"channel_tab_streams_label": "Directos",
|
||||||
|
|
|
@ -360,7 +360,7 @@
|
||||||
"search_filters_duration_label": "مدت",
|
"search_filters_duration_label": "مدت",
|
||||||
"search_filters_features_label": "ویژگیها",
|
"search_filters_features_label": "ویژگیها",
|
||||||
"search_filters_sort_label": "به ترتیب",
|
"search_filters_sort_label": "به ترتیب",
|
||||||
"search_filters_date_option_hour": "ساعت گذشته",
|
"search_filters_date_option_hour": "یک ساعت گذشته",
|
||||||
"search_filters_date_option_today": "امروز",
|
"search_filters_date_option_today": "امروز",
|
||||||
"search_filters_date_option_week": "این هفته",
|
"search_filters_date_option_week": "این هفته",
|
||||||
"search_filters_date_option_month": "این ماه",
|
"search_filters_date_option_month": "این ماه",
|
||||||
|
@ -461,7 +461,7 @@
|
||||||
"Song: ": "آهنگ: ",
|
"Song: ": "آهنگ: ",
|
||||||
"Channel Sponsor": "اسپانسر کانال",
|
"Channel Sponsor": "اسپانسر کانال",
|
||||||
"Standard YouTube license": "پروانه استاندارد YouTube",
|
"Standard YouTube license": "پروانه استاندارد YouTube",
|
||||||
"search_message_use_another_instance": "همچنین میتوانید <a href=\"`x`\">در نمونهای دیگر هم جستوجو کنید</a>.",
|
"search_message_use_another_instance": " شما همچنین میتوانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>.",
|
||||||
"Download is disabled": "دریافت غیرفعال است",
|
"Download is disabled": "دریافت غیرفعال است",
|
||||||
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
|
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
|
||||||
"playlist_button_add_items": "افزودن ویدیو",
|
"playlist_button_add_items": "افزودن ویدیو",
|
||||||
|
|
|
@ -484,7 +484,7 @@
|
||||||
"search_filters_duration_option_medium": "Moyenne (de 4 à 20 minutes)",
|
"search_filters_duration_option_medium": "Moyenne (de 4 à 20 minutes)",
|
||||||
"search_filters_apply_button": "Appliquer les filtres",
|
"search_filters_apply_button": "Appliquer les filtres",
|
||||||
"search_message_no_results": "Aucun résultat.",
|
"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_type_option_all": "Tous les types",
|
||||||
"search_filters_date_label": "Date d'ajout",
|
"search_filters_date_label": "Date d'ajout",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
|
|
|
@ -449,30 +449,30 @@
|
||||||
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
|
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
|
||||||
"Chinese": "Kineski",
|
"Chinese": "Kineski",
|
||||||
"Chinese (Taiwan)": "Kineski (Tajvan)",
|
"Chinese (Taiwan)": "Kineski (Tajvan)",
|
||||||
"Dutch (auto-generated)": "Nizozemski (automatski generirano)",
|
"Dutch (auto-generated)": "Nizozemski (automatski generiran)",
|
||||||
"French (auto-generated)": "Francuski (automatski generirano)",
|
"French (auto-generated)": "Francuski (automatski generiran)",
|
||||||
"Indonesian (auto-generated)": "Indonezijski (automatski generirano)",
|
"Indonesian (auto-generated)": "Indonezijski (automatski generiran)",
|
||||||
"Interlingue": "Interlingua",
|
"Interlingue": "Interlingua",
|
||||||
"Japanese (auto-generated)": "Japanski (automatski generirano)",
|
"Japanese (auto-generated)": "Japanski (automatski generiran)",
|
||||||
"Russian (auto-generated)": "Ruski (automatski generirano)",
|
"Russian (auto-generated)": "Ruski (automatski generiran)",
|
||||||
"Turkish (auto-generated)": "Turski (automatski generirano)",
|
"Turkish (auto-generated)": "Turski (automatski generiran)",
|
||||||
"Vietnamese (auto-generated)": "Vijetnamski (automatski generirano)",
|
"Vietnamese (auto-generated)": "Vijetnamski (automatski generiran)",
|
||||||
"Spanish (Spain)": "Španjolski (Španjolska)",
|
"Spanish (Spain)": "Španjolski (Španjolska)",
|
||||||
"Italian (auto-generated)": "Talijanski (automatski generirano)",
|
"Italian (auto-generated)": "Talijanski (automatski generiran)",
|
||||||
"Portuguese (Brazil)": "Portugalski (Brazil)",
|
"Portuguese (Brazil)": "Portugalski (Brazil)",
|
||||||
"Spanish (Mexico)": "Španjolski (Meksiko)",
|
"Spanish (Mexico)": "Španjolski (Meksiko)",
|
||||||
"German (auto-generated)": "Njemački (automatski generirano)",
|
"German (auto-generated)": "Njemački (automatski generiran)",
|
||||||
"Chinese (China)": "Kineski (Kina)",
|
"Chinese (China)": "Kineski (Kina)",
|
||||||
"Chinese (Hong Kong)": "Kineski (Hong Kong)",
|
"Chinese (Hong Kong)": "Kineski (Hong Kong)",
|
||||||
"Korean (auto-generated)": "Korejski (automatski generirano)",
|
"Korean (auto-generated)": "Korejski (automatski generiran)",
|
||||||
"Portuguese (auto-generated)": "Portugalski (automatski generirano)",
|
"Portuguese (auto-generated)": "Portugalski (automatski generiran)",
|
||||||
"Spanish (auto-generated)": "Španjolski (automatski generirano)",
|
"Spanish (auto-generated)": "Španjolski (automatski generiran)",
|
||||||
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
|
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
|
||||||
"search_filters_title": "Filtri",
|
"search_filters_title": "Filtri",
|
||||||
"search_filters_date_option_none": "Bilo koji datum",
|
"search_filters_date_option_none": "Bilo koji datum",
|
||||||
"search_filters_date_label": "Datum prijenosa",
|
"search_filters_date_label": "Datum prijenosa",
|
||||||
"search_message_no_results": "Nema rezultata.",
|
"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_message_change_filters_or_query": "Pokušaj proširiti upit za pretragu i/ili promijeni filtre.",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_filters_duration_option_none": "Bilo koje duljine",
|
"search_filters_duration_option_none": "Bilo koje duljine",
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
"invidious": "Invidious",
|
"invidious": "Invidious",
|
||||||
"Image CAPTCHA": "Imagine CAPTCHA",
|
"Image CAPTCHA": "Imagine CAPTCHA",
|
||||||
"newest": "plus nove",
|
"newest": "plus nove",
|
||||||
"generic_button_save": "Salveguardar",
|
"generic_button_save": "Salvar",
|
||||||
"Dark mode: ": "Modo obscur: ",
|
"Dark mode: ": "Modo obscur: ",
|
||||||
"preferences_dark_mode_label": "Thema: ",
|
"preferences_dark_mode_label": "Thema: ",
|
||||||
"preferences_category_subscription": "Preferentias de subscription",
|
"preferences_category_subscription": "Preferentias de subscription",
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
"light": "clar",
|
"light": "clar",
|
||||||
"No": "Non",
|
"No": "Non",
|
||||||
"youtube": "YouTube",
|
"youtube": "YouTube",
|
||||||
"LIVE": "IN DIRECTO",
|
"LIVE": "IN DIRECTE",
|
||||||
"reddit": "Reddit",
|
"reddit": "Reddit",
|
||||||
"preferences_category_player": "Preferentias de reproductor",
|
"preferences_category_player": "Preferentias de reproductor",
|
||||||
"Preferences": "Preferentias",
|
"Preferences": "Preferentias",
|
||||||
|
|
|
@ -396,7 +396,7 @@
|
||||||
"toggle_theme": "Víxla þema",
|
"toggle_theme": "Víxla þema",
|
||||||
"carousel_skip": "Sleppa hringekjunni",
|
"carousel_skip": "Sleppa hringekjunni",
|
||||||
"preferences_quality_option_medium": "Miðlungs",
|
"preferences_quality_option_medium": "Miðlungs",
|
||||||
"search_message_use_another_instance": "Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.",
|
"search_message_use_another_instance": " Þú getur líka <a href=\"`x`\">leitað á öðrum netþjóni</a>.",
|
||||||
"footer_source_code": "Grunnkóði",
|
"footer_source_code": "Grunnkóði",
|
||||||
"English (United Kingdom)": "Enska (Bretland)",
|
"English (United Kingdom)": "Enska (Bretland)",
|
||||||
"English (United States)": "Enska (Bandarísk)",
|
"English (United States)": "Enska (Bandarísk)",
|
||||||
|
|
|
@ -449,7 +449,7 @@
|
||||||
"Portuguese (Brazil)": "Portoghese (Brasile)",
|
"Portuguese (Brazil)": "Portoghese (Brasile)",
|
||||||
"preferences_watch_history_label": "Attiva cronologia di riproduzione: ",
|
"preferences_watch_history_label": "Attiva cronologia di riproduzione: ",
|
||||||
"French (auto-generated)": "Francese (generati automaticamente)",
|
"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_no_results": "Nessun risultato trovato.",
|
||||||
"search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.",
|
"search_message_change_filters_or_query": "Prova ad ampliare la ricerca e/o modificare i filtri.",
|
||||||
"English (United States)": "Inglese (Stati Uniti)",
|
"English (United States)": "Inglese (Stati Uniti)",
|
||||||
|
|
|
@ -363,7 +363,7 @@
|
||||||
"search_filters_features_option_location": "場所",
|
"search_filters_features_option_location": "場所",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"Current version: ": "現在のバージョン: ",
|
"Current version: ": "現在のバージョン: ",
|
||||||
"next_steps_error_message": "以下をお試しください: ",
|
"next_steps_error_message": "以下をお試してください: ",
|
||||||
"next_steps_error_message_refresh": "再読み込み",
|
"next_steps_error_message_refresh": "再読み込み",
|
||||||
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
|
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
|
||||||
"search_filters_duration_option_short": "4分未満",
|
"search_filters_duration_option_short": "4分未満",
|
||||||
|
@ -396,7 +396,7 @@
|
||||||
"download_subtitles": "字幕 - `x` (.vtt)",
|
"download_subtitles": "字幕 - `x` (.vtt)",
|
||||||
"search_filters_features_option_purchased": "購入済み",
|
"search_filters_features_option_purchased": "購入済み",
|
||||||
"preferences_quality_option_dash": "DASH (適応的画質)",
|
"preferences_quality_option_dash": "DASH (適応的画質)",
|
||||||
"preferences_quality_dash_option_worst": "最低",
|
"preferences_quality_dash_option_worst": "最悪",
|
||||||
"preferences_quality_dash_option_best": "最高",
|
"preferences_quality_dash_option_best": "最高",
|
||||||
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
|
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
|
||||||
"videoinfo_watch_on_youTube": "YouTubeで視聴",
|
"videoinfo_watch_on_youTube": "YouTubeで視聴",
|
||||||
|
@ -434,7 +434,7 @@
|
||||||
"crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>を試す",
|
"crash_page_switch_instance": "<a href=\"`x`\">別のインスタンスを使用</a>を試す",
|
||||||
"crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む",
|
"crash_page_read_the_faq": "<a href=\"`x`\">よくある質問 (FAQ)</a> を読む",
|
||||||
"Popular enabled: ": "人気動画を有効化 ",
|
"Popular enabled: ": "人気動画を有効化 ",
|
||||||
"search_message_use_another_instance": "<a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
|
"search_message_use_another_instance": " <a href=\"`x`\">別のインスタンス上での検索</a>も可能です。",
|
||||||
"search_filters_apply_button": "選択したフィルターを適用",
|
"search_filters_apply_button": "選択したフィルターを適用",
|
||||||
"user_saved_playlists": "`x`個の保存済みの再生リスト",
|
"user_saved_playlists": "`x`個の保存済みの再生リスト",
|
||||||
"crash_page_you_found_a_bug": "Invidious のバグのようです!",
|
"crash_page_you_found_a_bug": "Invidious のバグのようです!",
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
"preferences_related_videos_label": "관련 동영상 보기: ",
|
"preferences_related_videos_label": "관련 동영상 보기: ",
|
||||||
"Fallback captions: ": "대체 자막: ",
|
"Fallback captions: ": "대체 자막: ",
|
||||||
"preferences_captions_label": "기본 자막: ",
|
"preferences_captions_label": "기본 자막: ",
|
||||||
"reddit": "레딧",
|
"reddit": "Reddit",
|
||||||
"youtube": "유튜브",
|
"youtube": "YouTube",
|
||||||
"preferences_comments_label": "기본 댓글: ",
|
"preferences_comments_label": "기본 댓글: ",
|
||||||
"preferences_volume_label": "플레이어 볼륨: ",
|
"preferences_volume_label": "플레이어 볼륨: ",
|
||||||
"preferences_quality_label": "선호하는 비디오 품질: ",
|
"preferences_quality_label": "선호하는 비디오 품질: ",
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
|
||||||
"History": "시청 기록",
|
"History": "시청 기록",
|
||||||
"Delete account?": "계정을 삭제 하시겠습니까?",
|
"Delete account?": "계정을 삭제 하시겠습니까?",
|
||||||
"Export data as JSON": "인비디어스 데이터 내보내기 (.json)",
|
"Export data as JSON": "JSON으로 데이터 내보내기",
|
||||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
|
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
|
||||||
"Export subscriptions as OPML": "OPML로 구독 내보내기",
|
"Export subscriptions as OPML": "OPML로 구독 내보내기",
|
||||||
"Export": "내보내기",
|
"Export": "내보내기",
|
||||||
|
@ -78,10 +78,10 @@
|
||||||
"Subscribe": "구독",
|
"Subscribe": "구독",
|
||||||
"Unsubscribe": "구독 취소",
|
"Unsubscribe": "구독 취소",
|
||||||
"LIVE": "실시간",
|
"LIVE": "실시간",
|
||||||
"generic_views_count_0": "{{count}} 조회수",
|
"generic_views_count_0": "조회수 {{count}}회",
|
||||||
"generic_videos_count_0": "{{count}} 동영상",
|
"generic_videos_count_0": "동영상 {{count}}개",
|
||||||
"generic_playlists_count_0": "{{count}} 재생목록",
|
"generic_playlists_count_0": "재생목록 {{count}}개",
|
||||||
"generic_subscribers_count_0": "{{count}} 구독자",
|
"generic_subscribers_count_0": "구독자 {{count}}명",
|
||||||
"generic_subscriptions_count_0": "{{count}} 구독",
|
"generic_subscriptions_count_0": "{{count}} 구독",
|
||||||
"search_filters_type_option_playlist": "재생목록",
|
"search_filters_type_option_playlist": "재생목록",
|
||||||
"Korean": "한국어",
|
"Korean": "한국어",
|
||||||
|
@ -109,14 +109,14 @@
|
||||||
"This channel does not exist.": "이 채널은 존재하지 않습니다.",
|
"This channel does not exist.": "이 채널은 존재하지 않습니다.",
|
||||||
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
|
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
|
||||||
"channel:`x`": "채널:`x`",
|
"channel:`x`": "채널:`x`",
|
||||||
"Show replies": "댓글 보기",
|
"Show replies": "댓글 보이기",
|
||||||
"Hide replies": "댓글 숨기기",
|
"Hide replies": "댓글 숨기기",
|
||||||
"Incorrect password": "잘못된 비밀번호",
|
"Incorrect password": "잘못된 비밀번호",
|
||||||
"License: ": "라이선스: ",
|
"License: ": "라이선스: ",
|
||||||
"Genre: ": "장르: ",
|
"Genre: ": "장르: ",
|
||||||
"Editing playlist `x`": "재생목록 `x` 수정하기",
|
"Editing playlist `x`": "재생목록 `x` 수정하기",
|
||||||
"Playlist privacy": "재생목록 공개 범위",
|
"Playlist privacy": "재생목록 공개 범위",
|
||||||
"Watch on YouTube": "유튜브에서 보기",
|
"Watch on YouTube": "YouTube에서 보기",
|
||||||
"Show less": "간략히",
|
"Show less": "간략히",
|
||||||
"Show more": "더보기",
|
"Show more": "더보기",
|
||||||
"Title": "제목",
|
"Title": "제목",
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
"Delete playlist": "재생목록 삭제",
|
"Delete playlist": "재생목록 삭제",
|
||||||
"Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?",
|
"Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?",
|
||||||
"Updated `x` ago": "`x` 전에 업데이트됨",
|
"Updated `x` ago": "`x` 전에 업데이트됨",
|
||||||
"Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.",
|
"Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.",
|
||||||
"View all playlists": "모든 재생목록 보기",
|
"View all playlists": "모든 재생목록 보기",
|
||||||
"Private": "비공개",
|
"Private": "비공개",
|
||||||
"Unlisted": "목록에 없음",
|
"Unlisted": "목록에 없음",
|
||||||
|
@ -135,12 +135,12 @@
|
||||||
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
|
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
|
||||||
"Log out": "로그아웃",
|
"Log out": "로그아웃",
|
||||||
"search": "검색",
|
"search": "검색",
|
||||||
"subscriptions_unseen_notifs_count_0": "{{count}} 읽지 않은 알림",
|
"subscriptions_unseen_notifs_count_0": "읽지 않은 알림 {{count}}개",
|
||||||
"Subscriptions": "구독",
|
"Subscriptions": "구독",
|
||||||
"revoke": "철회",
|
"revoke": "철회",
|
||||||
"unsubscribe": "구독 취소",
|
"unsubscribe": "구독 취소",
|
||||||
"Import/export": "가져오기/내보내기",
|
"Import/export": "가져오기/내보내기",
|
||||||
"tokens_count_0": "{{count}} 토큰",
|
"tokens_count_0": "토큰 {{count}}개",
|
||||||
"Token": "토큰",
|
"Token": "토큰",
|
||||||
"Token manager": "토큰 관리자",
|
"Token manager": "토큰 관리자",
|
||||||
"Subscription manager": "구독 관리자",
|
"Subscription manager": "구독 관리자",
|
||||||
|
@ -163,7 +163,7 @@
|
||||||
"Clear watch history": "시청 기록 지우기",
|
"Clear watch history": "시청 기록 지우기",
|
||||||
"preferences_category_data": "데이터 설정",
|
"preferences_category_data": "데이터 설정",
|
||||||
"`x` is live": "`x` 이(가) 라이브 중입니다",
|
"`x` is live": "`x` 이(가) 라이브 중입니다",
|
||||||
"`x` uploaded a video": "`x` 동영상 게시됨",
|
"`x` uploaded a video": "`x` 이(가) 동영상을 게시했습니다",
|
||||||
"Enable web notifications": "웹 알림 활성화",
|
"Enable web notifications": "웹 알림 활성화",
|
||||||
"preferences_notifications_only_label": "알림만 표시 (있는 경우): ",
|
"preferences_notifications_only_label": "알림만 표시 (있는 경우): ",
|
||||||
"preferences_unseen_only_label": "시청하지 않은 것만 표시: ",
|
"preferences_unseen_only_label": "시청하지 않은 것만 표시: ",
|
||||||
|
@ -241,7 +241,7 @@
|
||||||
"Could not create mix.": "믹스를 생성할 수 없습니다.",
|
"Could not create mix.": "믹스를 생성할 수 없습니다.",
|
||||||
"`x` ago": "`x` 전",
|
"`x` ago": "`x` 전",
|
||||||
"comments_view_x_replies_0": "답글 {{count}}개 보기",
|
"comments_view_x_replies_0": "답글 {{count}}개 보기",
|
||||||
"View Reddit comments": "레딧 댓글 보기",
|
"View Reddit comments": "Reddit 댓글 보기",
|
||||||
"Engagement: ": "약속: ",
|
"Engagement: ": "약속: ",
|
||||||
"Wilson score: ": "Wilson Score: ",
|
"Wilson score: ": "Wilson Score: ",
|
||||||
"Family friendly? ": "전연령 영상입니까? ",
|
"Family friendly? ": "전연령 영상입니까? ",
|
||||||
|
@ -267,8 +267,8 @@
|
||||||
"Bulgarian": "불가리아어",
|
"Bulgarian": "불가리아어",
|
||||||
"Bosnian": "보스니아어",
|
"Bosnian": "보스니아어",
|
||||||
"Belarusian": "벨라루스어",
|
"Belarusian": "벨라루스어",
|
||||||
"View more comments on Reddit": "레딧에서 댓글 더 보기",
|
"View more comments on Reddit": "Reddit에서 댓글 더 보기",
|
||||||
"View YouTube comments": "유튜브 댓글 보기",
|
"View YouTube comments": "YouTube 댓글 보기",
|
||||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
|
"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` 업로드",
|
"Shared `x`": "`x` 업로드",
|
||||||
"Whitelisted regions: ": "차단되지 않은 지역: ",
|
"Whitelisted regions: ": "차단되지 않은 지역: ",
|
||||||
|
@ -289,7 +289,7 @@
|
||||||
"Empty playlist": "재생목록 비어 있음",
|
"Empty playlist": "재생목록 비어 있음",
|
||||||
"Show annotations": "주석 보이기",
|
"Show annotations": "주석 보이기",
|
||||||
"Hide annotations": "주석 숨기기",
|
"Hide annotations": "주석 숨기기",
|
||||||
"Switch Invidious Instance": "인비디어스 인스턴스 변경",
|
"Switch Invidious Instance": "Invidious 인스턴스 변경",
|
||||||
"Spanish": "스페인어",
|
"Spanish": "스페인어",
|
||||||
"Southern Sotho": "소토어",
|
"Southern Sotho": "소토어",
|
||||||
"Somali": "소말리어",
|
"Somali": "소말리어",
|
||||||
|
@ -329,7 +329,7 @@
|
||||||
"Swedish": "스웨덴어",
|
"Swedish": "스웨덴어",
|
||||||
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
|
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
|
||||||
"comments_points_count_0": "{{count}} 포인트",
|
"comments_points_count_0": "{{count}} 포인트",
|
||||||
"Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드",
|
"Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드",
|
||||||
"Premieres `x`": "최초 공개 `x`",
|
"Premieres `x`": "최초 공개 `x`",
|
||||||
"Premieres in `x`": "`x` 후 최초 공개",
|
"Premieres in `x`": "`x` 후 최초 공개",
|
||||||
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
|
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
|
||||||
|
@ -408,7 +408,7 @@
|
||||||
"preferences_quality_dash_option_1080p": "1080p",
|
"preferences_quality_dash_option_1080p": "1080p",
|
||||||
"preferences_quality_dash_option_worst": "최저",
|
"preferences_quality_dash_option_worst": "최저",
|
||||||
"preferences_watch_history_label": "시청 기록 저장: ",
|
"preferences_watch_history_label": "시청 기록 저장: ",
|
||||||
"invidious": "인비디어스",
|
"invidious": "Invidious",
|
||||||
"preferences_quality_option_small": "낮음",
|
"preferences_quality_option_small": "낮음",
|
||||||
"preferences_quality_dash_option_auto": "자동",
|
"preferences_quality_dash_option_auto": "자동",
|
||||||
"preferences_quality_dash_option_480p": "480p",
|
"preferences_quality_dash_option_480p": "480p",
|
||||||
|
@ -453,7 +453,7 @@
|
||||||
"channel_tab_streams_label": "실시간 스트리밍",
|
"channel_tab_streams_label": "실시간 스트리밍",
|
||||||
"channel_tab_channels_label": "채널",
|
"channel_tab_channels_label": "채널",
|
||||||
"channel_tab_playlists_label": "재생목록",
|
"channel_tab_playlists_label": "재생목록",
|
||||||
"Standard YouTube license": "표준 유튜브 라이선스",
|
"Standard YouTube license": "표준 YouTube 라이선스",
|
||||||
"Song: ": "제목: ",
|
"Song: ": "제목: ",
|
||||||
"Channel Sponsor": "채널 스폰서",
|
"Channel Sponsor": "채널 스폰서",
|
||||||
"Album: ": "앨범: ",
|
"Album: ": "앨범: ",
|
||||||
|
|
|
@ -322,13 +322,13 @@
|
||||||
"channel_tab_community_label": "Gemenskap",
|
"channel_tab_community_label": "Gemenskap",
|
||||||
"search_filters_sort_option_relevance": "relevans",
|
"search_filters_sort_option_relevance": "relevans",
|
||||||
"search_filters_sort_option_rating": "vurdering",
|
"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_sort_option_views": "visninger",
|
||||||
"search_filters_type_label": "innholdstype",
|
"search_filters_type_label": "innholdstype",
|
||||||
"search_filters_duration_label": "varighet",
|
"search_filters_duration_label": "varighet",
|
||||||
"search_filters_features_label": "funksjoner",
|
"search_filters_features_label": "funksjoner",
|
||||||
"search_filters_sort_label": "sorter",
|
"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_today": "i dag",
|
||||||
"search_filters_date_option_week": "uke",
|
"search_filters_date_option_week": "uke",
|
||||||
"search_filters_date_option_month": "måned",
|
"search_filters_date_option_month": "måned",
|
||||||
|
@ -459,7 +459,7 @@
|
||||||
"search_message_no_results": "Resultatløst.",
|
"search_message_no_results": "Resultatløst.",
|
||||||
"search_filters_type_option_all": "Alle typer",
|
"search_filters_type_option_all": "Alle typer",
|
||||||
"search_filters_duration_option_none": "Enhver varighet",
|
"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_date_label": "Opplastningsdato",
|
||||||
"search_filters_apply_button": "Bruk valgte filtre",
|
"search_filters_apply_button": "Bruk valgte filtre",
|
||||||
"search_filters_date_option_none": "Siden begynnelsen",
|
"search_filters_date_option_none": "Siden begynnelsen",
|
||||||
|
@ -494,7 +494,5 @@
|
||||||
"carousel_slide": "Lysark {{current}} av {{total}}",
|
"carousel_slide": "Lysark {{current}} av {{total}}",
|
||||||
"carousel_skip": "Hopp over karusellen",
|
"carousel_skip": "Hopp over karusellen",
|
||||||
"Add to playlist": "Legg til i spilleliste",
|
"Add to playlist": "Legg til i spilleliste",
|
||||||
"Add to playlist: ": "Legg til i spilleliste: ",
|
"Add to playlist: ": "Legg til i spilleliste: "
|
||||||
"The Popular feed has been disabled by the administrator.": "Populært-kilden er koblet ut av administratoren.",
|
|
||||||
"toggle_theme": "Endre utseende"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -317,13 +317,13 @@
|
||||||
"channel_tab_community_label": "Gemeenschap",
|
"channel_tab_community_label": "Gemeenschap",
|
||||||
"search_filters_sort_option_relevance": "relevantie",
|
"search_filters_sort_option_relevance": "relevantie",
|
||||||
"search_filters_sort_option_rating": "beoordeling",
|
"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_sort_option_views": "keren bekeken",
|
||||||
"search_filters_type_label": "Type inhoud",
|
"search_filters_type_label": "Type inhoud",
|
||||||
"search_filters_duration_label": "duur",
|
"search_filters_duration_label": "duur",
|
||||||
"search_filters_features_label": "eigenschappen",
|
"search_filters_features_label": "eigenschappen",
|
||||||
"search_filters_sort_label": "sorteren",
|
"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_today": "vandaag",
|
||||||
"search_filters_date_option_week": "week",
|
"search_filters_date_option_week": "week",
|
||||||
"search_filters_date_option_month": "maand",
|
"search_filters_date_option_month": "maand",
|
||||||
|
@ -357,7 +357,7 @@
|
||||||
"footer_original_source_code": "Originele bron-code",
|
"footer_original_source_code": "Originele bron-code",
|
||||||
"footer_modfied_source_code": "Gewijzigde bron-code",
|
"footer_modfied_source_code": "Gewijzigde bron-code",
|
||||||
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
|
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
|
||||||
"next_steps_error_message": "Waarna u zou kunnen proberen om: ",
|
"next_steps_error_message": "Daarna moet u proberen om: ",
|
||||||
"footer_source_code": "Bron-code",
|
"footer_source_code": "Bron-code",
|
||||||
"search_filters_duration_option_long": "Lang (> 20 minuten)",
|
"search_filters_duration_option_long": "Lang (> 20 minuten)",
|
||||||
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
|
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
|
||||||
|
@ -450,7 +450,7 @@
|
||||||
"Chinese (Hong Kong)": "Chinees (Hongkong)",
|
"Chinese (Hong Kong)": "Chinees (Hongkong)",
|
||||||
"Korean (auto-generated)": "Koreaans (automatisch gegenereerd)",
|
"Korean (auto-generated)": "Koreaans (automatisch gegenereerd)",
|
||||||
"search_filters_apply_button": "Geselecteerde filters toepassen",
|
"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_message_use_another_instance": " Je kan ook <a href=\"`x`\">zoeken op een andere instantie</a>.",
|
||||||
"Cantonese (Hong Kong)": "Kantonees (Hongkong)",
|
"Cantonese (Hong Kong)": "Kantonees (Hongkong)",
|
||||||
"Chinese (China)": "Chinees (China)",
|
"Chinese (China)": "Chinees (China)",
|
||||||
"crash_page_read_the_faq": "de <a href=\"`x`\">veelgestelde vragen (FAQ)</a> gelezen hebt",
|
"crash_page_read_the_faq": "de <a href=\"`x`\">veelgestelde vragen (FAQ)</a> gelezen hebt",
|
||||||
|
@ -477,7 +477,7 @@
|
||||||
"Song: ": "Lied: ",
|
"Song: ": "Lied: ",
|
||||||
"generic_channels_count": "{{count}} kanaal",
|
"generic_channels_count": "{{count}} kanaal",
|
||||||
"generic_channels_count_plural": "{{count}} kanalen",
|
"generic_channels_count_plural": "{{count}} kanalen",
|
||||||
"Popular enabled: ": "Populair ingeschakeld: ",
|
"Popular enabled: ": "Populair geactiveerd: ",
|
||||||
"channel_tab_playlists_label": "Afspeellijsten",
|
"channel_tab_playlists_label": "Afspeellijsten",
|
||||||
"generic_button_edit": "Bewerken",
|
"generic_button_edit": "Bewerken",
|
||||||
"Music in this video": "Muziek in deze video",
|
"Music in this video": "Muziek in deze video",
|
||||||
|
|
|
@ -478,7 +478,7 @@
|
||||||
"search_filters_date_label": "Data przesłania",
|
"search_filters_date_label": "Data przesłania",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_filters_date_option_none": "Dowolna data",
|
"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_type_option_all": "Dowolny typ",
|
||||||
"search_filters_duration_option_none": "Dowolna długość",
|
"search_filters_duration_option_none": "Dowolna długość",
|
||||||
"search_filters_duration_option_medium": "Średnia (4-20 minut)",
|
"search_filters_duration_option_medium": "Średnia (4-20 minut)",
|
||||||
|
|
|
@ -474,7 +474,7 @@
|
||||||
"Spanish (auto-generated)": "Espanhol (gerado automaticamente)",
|
"Spanish (auto-generated)": "Espanhol (gerado automaticamente)",
|
||||||
"Spanish (Mexico)": "Espanhol (México)",
|
"Spanish (Mexico)": "Espanhol (México)",
|
||||||
"search_filters_duration_option_none": "Qualquer duração",
|
"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)",
|
"Spanish (Spain)": "Espanhol (Espanha)",
|
||||||
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
|
"Turkish (auto-generated)": "Turco (gerado automaticamente)",
|
||||||
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)",
|
"search_filters_duration_option_medium": "Médio (4 - 20 minutos)",
|
||||||
|
|
|
@ -448,7 +448,7 @@
|
||||||
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
"Chinese (Taiwan)": "Chinês (Taiwan)",
|
||||||
"search_message_no_results": "Nenhum resultado encontrado.",
|
"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_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 Kingdom)": "Inglês (Reino Unido)",
|
||||||
"English (United States)": "Inglês (Estados Unidos)",
|
"English (United States)": "Inglês (Estados Unidos)",
|
||||||
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
|
"Cantonese (Hong Kong)": "Cantonês (Hong Kong)",
|
||||||
|
@ -508,7 +508,7 @@
|
||||||
"toggle_theme": "Trocar tema",
|
"toggle_theme": "Trocar tema",
|
||||||
"Add to playlist": "Adicionar à lista de reprodução",
|
"Add to playlist": "Adicionar à lista de reprodução",
|
||||||
"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",
|
"Search for videos": "Procurar vídeos",
|
||||||
"carousel_slide": "Diapositivo {{current}} de{{total}}",
|
"carousel_slide": "Diapositivo {{current}} de{{total}}",
|
||||||
"carousel_skip": "Ignorar carrossel",
|
"carousel_skip": "Ignorar carrossel",
|
||||||
|
|
|
@ -509,9 +509,6 @@
|
||||||
"Add to playlist: ": "Добавить в плейлист: ",
|
"Add to playlist: ": "Добавить в плейлист: ",
|
||||||
"Answer": "Ответить",
|
"Answer": "Ответить",
|
||||||
"Search for videos": "Поиск видео",
|
"Search for videos": "Поиск видео",
|
||||||
"The Popular feed has been disabled by the administrator.": "Лента популярного была отключена администратором.",
|
"The Popular feed has been disabled by the administrator.": "Популярная лента была отключена администратором.",
|
||||||
"toggle_theme": "Переключатель тем",
|
"toggle_theme": "Переключатель тем"
|
||||||
"carousel_slide": "Пролистано {{current}} из {{total}}",
|
|
||||||
"carousel_skip": "Пропустить всё",
|
|
||||||
"carousel_go_to": "Перейти к странице `x`"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -257,13 +257,13 @@
|
||||||
"Video mode": "Mënyrë video",
|
"Video mode": "Mënyrë video",
|
||||||
"channel_tab_videos_label": "Video",
|
"channel_tab_videos_label": "Video",
|
||||||
"search_filters_sort_option_rating": "Vlerësim",
|
"search_filters_sort_option_rating": "Vlerësim",
|
||||||
"search_filters_sort_option_date": "Datë ngarkimi",
|
"search_filters_sort_option_date": "Datë Ngarkimi",
|
||||||
"search_filters_sort_option_views": "Numër parjesh",
|
"search_filters_sort_option_views": "Numër parjesh",
|
||||||
"search_filters_type_label": "Lloj",
|
"search_filters_type_label": "Lloj",
|
||||||
"search_filters_duration_label": "Kohëzgjatje",
|
"search_filters_duration_label": "Kohëzgjatje",
|
||||||
"search_filters_features_label": "Veçori",
|
"search_filters_features_label": "Veçori",
|
||||||
"search_filters_sort_label": "Renditi Sipas",
|
"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_date_option_today": "Sot",
|
||||||
"search_filters_duration_option_long": "E gjatë (> 20 minuta)",
|
"search_filters_duration_option_long": "E gjatë (> 20 minuta)",
|
||||||
"search_filters_features_option_hd": "HD",
|
"search_filters_features_option_hd": "HD",
|
||||||
|
@ -435,14 +435,14 @@
|
||||||
"tokens_count_plural": "{{count}} tokenë",
|
"tokens_count_plural": "{{count}} tokenë",
|
||||||
"preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ",
|
"preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ",
|
||||||
"Import Invidious data": "Importoni të dhëna JSON Invidious",
|
"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",
|
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
|
||||||
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
|
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
|
||||||
"Shared `x`": "Ndarë me të tjerë më `x`",
|
"Shared `x`": "Ndarë me të tjerë më `x`",
|
||||||
"search_filters_title": "Filtra",
|
"search_filters_title": "Filtra",
|
||||||
"Popular enabled: ": "Me populloret të aktivizuara: ",
|
"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>",
|
"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",
|
"search_filters_date_label": "Datë ngarkimi",
|
||||||
"preferences_watch_history_label": "Aktivizo historik parjesh: ",
|
"preferences_watch_history_label": "Aktivizo historik parjesh: ",
|
||||||
"Top enabled: ": "Me kryesueset të aktivizuara: ",
|
"Top enabled: ": "Me kryesueset të aktivizuara: ",
|
||||||
|
@ -484,13 +484,5 @@
|
||||||
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
|
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
|
||||||
"preferences_local_label": "Video përmes ndërmjetësi: ",
|
"preferences_local_label": "Video përmes ndërmjetësi: ",
|
||||||
"Fallback captions: ": "Titra nga halli: ",
|
"Fallback captions: ": "Titra nga halli: ",
|
||||||
"Erroneous challenge": "Zgjidhje e gabuar",
|
"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`"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -404,7 +404,7 @@
|
||||||
"generic_count_months_0": "{{count}} mesec",
|
"generic_count_months_0": "{{count}} mesec",
|
||||||
"generic_count_months_1": "{{count}} meseca",
|
"generic_count_months_1": "{{count}} meseca",
|
||||||
"generic_count_months_2": "{{count}} meseci",
|
"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_0": "{{count}} pratilac",
|
||||||
"generic_subscribers_count_1": "{{count}} pratioca",
|
"generic_subscribers_count_1": "{{count}} pratioca",
|
||||||
"generic_subscribers_count_2": "{{count}} pratilaca",
|
"generic_subscribers_count_2": "{{count}} pratilaca",
|
||||||
|
|
|
@ -404,7 +404,7 @@
|
||||||
"generic_count_months_0": "{{count}} месец",
|
"generic_count_months_0": "{{count}} месец",
|
||||||
"generic_count_months_1": "{{count}} месеца",
|
"generic_count_months_1": "{{count}} месеца",
|
||||||
"generic_count_months_2": "{{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_0": "{{count}} пратилац",
|
||||||
"generic_subscribers_count_1": "{{count}} пратиоца",
|
"generic_subscribers_count_1": "{{count}} пратиоца",
|
||||||
"generic_subscribers_count_2": "{{count}} пратилаца",
|
"generic_subscribers_count_2": "{{count}} пратилаца",
|
||||||
|
|
|
@ -320,13 +320,13 @@
|
||||||
"channel_tab_community_label": "Gemenskap",
|
"channel_tab_community_label": "Gemenskap",
|
||||||
"search_filters_sort_option_relevance": "Relevans",
|
"search_filters_sort_option_relevance": "Relevans",
|
||||||
"search_filters_sort_option_rating": "Rankning",
|
"search_filters_sort_option_rating": "Rankning",
|
||||||
"search_filters_sort_option_date": "Uppladdnings datum",
|
"search_filters_sort_option_date": "Uppladdnings Datum",
|
||||||
"search_filters_sort_option_views": "Visningar",
|
"search_filters_sort_option_views": "Visningar",
|
||||||
"search_filters_type_label": "Typ",
|
"search_filters_type_label": "Typ",
|
||||||
"search_filters_duration_label": "Varaktighet",
|
"search_filters_duration_label": "Varaktighet",
|
||||||
"search_filters_features_label": "Funktioner",
|
"search_filters_features_label": "Funktioner",
|
||||||
"search_filters_sort_label": "Sortera efter",
|
"search_filters_sort_label": "Sortera efter",
|
||||||
"search_filters_date_option_hour": "Senaste timmen",
|
"search_filters_date_option_hour": "Senaste Timmen",
|
||||||
"search_filters_date_option_today": "Idag",
|
"search_filters_date_option_today": "Idag",
|
||||||
"search_filters_date_option_week": "Denna vecka",
|
"search_filters_date_option_week": "Denna vecka",
|
||||||
"search_filters_date_option_month": "Denna månad",
|
"search_filters_date_option_month": "Denna månad",
|
||||||
|
@ -393,7 +393,7 @@
|
||||||
"Artist: ": "Artist: ",
|
"Artist: ": "Artist: ",
|
||||||
"generic_count_months": "{{count}}månad",
|
"generic_count_months": "{{count}}månad",
|
||||||
"generic_count_months_plural": "{{count}}månader",
|
"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": "{{count}} prenumerant",
|
||||||
"generic_subscribers_count_plural": "{{count}} prenumeranter",
|
"generic_subscribers_count_plural": "{{count}} prenumeranter",
|
||||||
"download_subtitles": "Undertexter - `x` (.vtt)",
|
"download_subtitles": "Undertexter - `x` (.vtt)",
|
||||||
|
|
|
@ -322,13 +322,13 @@
|
||||||
"channel_tab_community_label": "Topluluk",
|
"channel_tab_community_label": "Topluluk",
|
||||||
"search_filters_sort_option_relevance": "İlgi",
|
"search_filters_sort_option_relevance": "İlgi",
|
||||||
"search_filters_sort_option_rating": "Değerlendirme",
|
"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_sort_option_views": "Görüntüleme Sayısı",
|
||||||
"search_filters_type_label": "Tür",
|
"search_filters_type_label": "Tür",
|
||||||
"search_filters_duration_label": "Süre",
|
"search_filters_duration_label": "Süre",
|
||||||
"search_filters_features_label": "Özellikler",
|
"search_filters_features_label": "Özellikler",
|
||||||
"search_filters_sort_label": "Sıralama Ölçütü",
|
"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_today": "Bugün",
|
||||||
"search_filters_date_option_week": "Bu Hafta",
|
"search_filters_date_option_week": "Bu Hafta",
|
||||||
"search_filters_date_option_month": "Bu Ay",
|
"search_filters_date_option_month": "Bu Ay",
|
||||||
|
@ -452,7 +452,7 @@
|
||||||
"Spanish (Spain)": "İspanyolca (İspanya)",
|
"Spanish (Spain)": "İspanyolca (İspanya)",
|
||||||
"Vietnamese (auto-generated)": "Vietnamca (Otomatik Oluşturuldu)",
|
"Vietnamese (auto-generated)": "Vietnamca (Otomatik Oluşturuldu)",
|
||||||
"preferences_watch_history_label": "İzleme Geçmişini Etkinleştir: ",
|
"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_type_option_all": "Herhangi Bir Tür",
|
||||||
"search_filters_duration_option_none": "Herhangi Bir Süre",
|
"search_filters_duration_option_none": "Herhangi Bir Süre",
|
||||||
"search_message_no_results": "Sonuç bulunamadı.",
|
"search_message_no_results": "Sonuç bulunamadı.",
|
||||||
|
|
|
@ -455,7 +455,7 @@
|
||||||
"search_filters_date_option_week": "Цей тиждень",
|
"search_filters_date_option_week": "Цей тиждень",
|
||||||
"search_filters_type_label": "Тип",
|
"search_filters_type_label": "Тип",
|
||||||
"search_filters_type_option_channel": "Канал",
|
"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_title": "Фільтри",
|
||||||
"search_filters_date_option_hour": "Остання година",
|
"search_filters_date_option_hour": "Остання година",
|
||||||
"search_filters_date_option_month": "Цей місяць",
|
"search_filters_date_option_month": "Цей місяць",
|
||||||
|
@ -472,7 +472,7 @@
|
||||||
"search_filters_features_option_three_sixty": "360°",
|
"search_filters_features_option_three_sixty": "360°",
|
||||||
"search_filters_features_option_hdr": "HDR",
|
"search_filters_features_option_hdr": "HDR",
|
||||||
"search_filters_sort_label": "Спершу",
|
"search_filters_sort_label": "Спершу",
|
||||||
"search_filters_sort_option_date": "Дата вивантаження",
|
"search_filters_sort_option_date": "Нещодавні",
|
||||||
"search_filters_apply_button": "Застосувати фільтри",
|
"search_filters_apply_button": "Застосувати фільтри",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"search_filters_features_option_vr180": "VR180",
|
||||||
"search_filters_features_option_purchased": "Придбано",
|
"search_filters_features_option_purchased": "Придбано",
|
||||||
|
|
|
@ -436,7 +436,7 @@
|
||||||
"Turkish (auto-generated)": "土耳其语 (自动生成)",
|
"Turkish (auto-generated)": "土耳其语 (自动生成)",
|
||||||
"Spanish (Spain)": "西班牙语 (西班牙)",
|
"Spanish (Spain)": "西班牙语 (西班牙)",
|
||||||
"preferences_watch_history_label": "启用观看历史: ",
|
"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_title": "过滤器",
|
||||||
"search_filters_date_label": "上传日期",
|
"search_filters_date_label": "上传日期",
|
||||||
"search_filters_apply_button": "应用所选过滤器",
|
"search_filters_apply_button": "应用所选过滤器",
|
||||||
|
|
|
@ -338,13 +338,13 @@
|
||||||
"channel_tab_community_label": "社群",
|
"channel_tab_community_label": "社群",
|
||||||
"search_filters_sort_option_relevance": "關聯",
|
"search_filters_sort_option_relevance": "關聯",
|
||||||
"search_filters_sort_option_rating": "評分",
|
"search_filters_sort_option_rating": "評分",
|
||||||
"search_filters_sort_option_date": "上傳日期",
|
"search_filters_sort_option_date": "日期",
|
||||||
"search_filters_sort_option_views": "檢視",
|
"search_filters_sort_option_views": "檢視",
|
||||||
"search_filters_type_label": "內容類型",
|
"search_filters_type_label": "內容類型",
|
||||||
"search_filters_duration_label": "時長",
|
"search_filters_duration_label": "時長",
|
||||||
"search_filters_features_label": "特色",
|
"search_filters_features_label": "特色",
|
||||||
"search_filters_sort_label": "排序",
|
"search_filters_sort_label": "排序",
|
||||||
"search_filters_date_option_hour": "最後一小時",
|
"search_filters_date_option_hour": "小時",
|
||||||
"search_filters_date_option_today": "今天",
|
"search_filters_date_option_today": "今天",
|
||||||
"search_filters_date_option_week": "週",
|
"search_filters_date_option_week": "週",
|
||||||
"search_filters_date_option_month": "月",
|
"search_filters_date_option_month": "月",
|
||||||
|
@ -442,7 +442,7 @@
|
||||||
"search_filters_duration_option_none": "任何時長",
|
"search_filters_duration_option_none": "任何時長",
|
||||||
"search_filters_duration_option_medium": "中等(4到20分鐘)",
|
"search_filters_duration_option_medium": "中等(4到20分鐘)",
|
||||||
"search_filters_features_option_vr180": "VR180",
|
"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_title": "過濾條件",
|
||||||
"search_filters_date_label": "上傳日期",
|
"search_filters_date_label": "上傳日期",
|
||||||
"search_filters_type_option_all": "任何類型",
|
"search_filters_type_option_all": "任何類型",
|
||||||
|
|
2
mocks
2
mocks
|
@ -1 +1 @@
|
||||||
Subproject commit b55d58dea94f7144ff0205857dfa70ec14eaa872
|
Subproject commit 11ec372f72747c09d48ffef04843f72be67d5b54
|
11
shard.lock
11
shard.lock
|
@ -10,7 +10,7 @@ shards:
|
||||||
|
|
||||||
backtracer:
|
backtracer:
|
||||||
git: https://github.com/sija/backtracer.cr.git
|
git: https://github.com/sija/backtracer.cr.git
|
||||||
version: 1.2.2
|
version: 1.2.1
|
||||||
|
|
||||||
db:
|
db:
|
||||||
git: https://github.com/crystal-lang/crystal-db.git
|
git: https://github.com/crystal-lang/crystal-db.git
|
||||||
|
@ -20,13 +20,6 @@ shards:
|
||||||
git: https://github.com/crystal-loot/exception_page.git
|
git: https://github.com/crystal-loot/exception_page.git
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
|
|
||||||
inotify:
|
|
||||||
git: https://github.com/petoem/inotify.cr.git
|
|
||||||
version: 1.0.3
|
|
||||||
http_proxy:
|
|
||||||
git: https://github.com/mamantoha/http_proxy.git
|
|
||||||
version: 0.10.3
|
|
||||||
|
|
||||||
kemal:
|
kemal:
|
||||||
git: https://github.com/kemalcr/kemal.git
|
git: https://github.com/kemalcr/kemal.git
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
|
@ -57,7 +50,7 @@ shards:
|
||||||
|
|
||||||
spectator:
|
spectator:
|
||||||
git: https://github.com/icy-arctic-fox/spectator.git
|
git: https://github.com/icy-arctic-fox/spectator.git
|
||||||
version: 0.10.6
|
version: 0.10.4
|
||||||
|
|
||||||
sqlite3:
|
sqlite3:
|
||||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||||
|
|
|
@ -30,12 +30,6 @@ dependencies:
|
||||||
version: ~> 0.1.1
|
version: ~> 0.1.1
|
||||||
redis:
|
redis:
|
||||||
github: stefanwille/crystal-redis
|
github: stefanwille/crystal-redis
|
||||||
inotify:
|
|
||||||
github: petoem/inotify.cr
|
|
||||||
version: 1.0.3
|
|
||||||
http_proxy:
|
|
||||||
github: mamantoha/http_proxy
|
|
||||||
version: ~> 0.10.3
|
|
||||||
|
|
||||||
development_dependencies:
|
development_dependencies:
|
||||||
spectator:
|
spectator:
|
||||||
|
|
|
@ -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.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
|
||||||
expect(video_11.views).to eq(40_504_893)
|
expect(video_11.views).to eq(40_504_893)
|
||||||
|
|
||||||
expect(video_11.badges.live_now?).to be_false
|
expect(video_11.live_now).to be_false
|
||||||
expect(video_11.badges.premium?).to be_false
|
expect(video_11.premium).to be_false
|
||||||
expect(video_11.premiere_timestamp).to be_nil
|
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.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
|
||||||
expect(video_35.views).to eq(30_790_049)
|
expect(video_35.views).to eq(30_790_049)
|
||||||
|
|
||||||
expect(video_35.badges.live_now?).to be_false
|
expect(video_35.live_now).to be_false
|
||||||
expect(video_35.badges.premium?).to be_false
|
expect(video_35.premium).to be_false
|
||||||
expect(video_35.premiere_timestamp).to be_nil
|
expect(video_35.premiere_timestamp).to be_nil
|
||||||
end
|
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.length_seconds).to eq((1.hour).total_seconds.to_i32)
|
||||||
expect(video_41.views).to eq(63_240)
|
expect(video_41.views).to eq(63_240)
|
||||||
|
|
||||||
expect(video_41.badges.live_now?).to be_false
|
expect(video_41.live_now).to be_false
|
||||||
expect(video_41.badges.premium?).to be_false
|
expect(video_41.premium).to be_false
|
||||||
expect(video_41.premiere_timestamp).to be_nil
|
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.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
|
||||||
expect(video_48.views).to eq(68_704)
|
expect(video_48.views).to eq(68_704)
|
||||||
|
|
||||||
expect(video_48.badges.live_now?).to be_false
|
expect(video_48.live_now).to be_false
|
||||||
expect(video_48.badges.premium?).to be_false
|
expect(video_48.premium).to be_false
|
||||||
expect(video_48.premiere_timestamp).to be_nil
|
expect(video_48.premiere_timestamp).to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do
|
||||||
# Basic video infos
|
# Basic video infos
|
||||||
|
|
||||||
expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island")
|
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["views"].as_i).to eq(126_573_823)
|
||||||
expect(info["likes"].as_i).to eq(6_870_691)
|
expect(info["likes"].as_i).to eq(5_157_654)
|
||||||
|
|
||||||
# For some reason the video length from VideoDetails and the
|
# For some reason the video length from VideoDetails and the
|
||||||
# one from microformat differs by 1s...
|
# 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"].as_a.size).to eq(20)
|
||||||
|
|
||||||
expect(info["relatedVideos"][0]["id"]).to eq("krsBRQbOPQ4")
|
expect(info["relatedVideos"][0]["id"]).to eq("Hwybp38GnZw")
|
||||||
expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $250,000,000 Private Island!")
|
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]["author"]).to eq("MrBeast")
|
||||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
||||||
expect(info["relatedVideos"][0]["view_count"]).to eq("230617484")
|
expect(info["relatedVideos"][0]["view_count"]).to eq("179877630")
|
||||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M")
|
expect(info["relatedVideos"][0]["short_view_count"]).to eq("179M")
|
||||||
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
|
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do
|
||||||
expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
|
||||||
|
|
||||||
expect(info["authorThumbnail"].as_s).to eq(
|
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["authorVerified"].as_bool).to be_true
|
||||||
expect(info["subCountText"].as_s).to eq("320M")
|
expect(info["subCountText"].as_s).to eq("143M")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "parses a regular video with no descrition/comments" do
|
it "parses a regular video with no descrition/comments" do
|
||||||
|
@ -99,8 +99,8 @@ Spectator.describe "parse_video_info" do
|
||||||
# Basic video infos
|
# Basic video infos
|
||||||
|
|
||||||
expect(info["title"].as_s).to eq("Chris Rea - Auberge")
|
expect(info["title"].as_s).to eq("Chris Rea - Auberge")
|
||||||
expect(info["views"].as_i).to eq(14_324_584)
|
expect(info["views"].as_i).to eq(10_943_126)
|
||||||
expect(info["likes"].as_i).to eq(35_870)
|
expect(info["likes"].as_i).to eq(0)
|
||||||
expect(info["lengthSeconds"].as_i).to eq(283_i64)
|
expect(info["lengthSeconds"].as_i).to eq(283_i64)
|
||||||
expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
|
expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
|
||||||
|
|
||||||
|
@ -132,14 +132,14 @@ Spectator.describe "parse_video_info" do
|
||||||
|
|
||||||
# Related videos
|
# 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]["id"]).to eq("Ww3KeZ2_Yv4")
|
||||||
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version")
|
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea")
|
||||||
expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH")
|
expect(info["relatedVideos"][0]["author"]).to eq("PanMusic")
|
||||||
expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ")
|
expect(info["relatedVideos"][0]["ucid"]).to eq("UCsKAPSuh1iNbLWUga_igPyA")
|
||||||
expect(info["relatedVideos"][0]["view_count"]).to eq("53298661")
|
expect(info["relatedVideos"][0]["view_count"]).to eq("31581")
|
||||||
expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M")
|
expect(info["relatedVideos"][0]["short_view_count"]).to eq("31K")
|
||||||
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
|
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
|
||||||
|
|
||||||
# Description
|
# Description
|
||||||
|
@ -156,13 +156,11 @@ Spectator.describe "parse_video_info" do
|
||||||
|
|
||||||
# Author infos
|
# 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["ucid"].as_s).to eq("UC_5q6nWPbD30-y6oiWF_oNA")
|
||||||
|
|
||||||
expect(info["authorThumbnail"].as_s).to eq(
|
expect(info["authorThumbnail"].as_s).to be_empty
|
||||||
"https://yt3.ggpht.com/ytc/AIdro_n71nsegpKfjeRKwn1JJmK5IVMh_7j5m_h3_1KnUUg=s48-c-k-c0x00ffffff-no-rj"
|
|
||||||
)
|
|
||||||
expect(info["authorVerified"].as_bool).to be_false
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,7 +23,6 @@ require "kilt"
|
||||||
require "./ext/kemal_content_for.cr"
|
require "./ext/kemal_content_for.cr"
|
||||||
require "./ext/kemal_static_file_handler.cr"
|
require "./ext/kemal_static_file_handler.cr"
|
||||||
|
|
||||||
require "http_proxy"
|
|
||||||
require "athena-negotiation"
|
require "athena-negotiation"
|
||||||
require "openssl/hmac"
|
require "openssl/hmac"
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
|
@ -33,7 +32,6 @@ require "yaml"
|
||||||
require "compress/zip"
|
require "compress/zip"
|
||||||
require "protodec/utils"
|
require "protodec/utils"
|
||||||
require "redis"
|
require "redis"
|
||||||
require "inotify"
|
|
||||||
|
|
||||||
require "./invidious/database/*"
|
require "./invidious/database/*"
|
||||||
require "./invidious/database/migrations/*"
|
require "./invidious/database/migrations/*"
|
||||||
|
@ -60,20 +58,7 @@ end
|
||||||
# Simple alias to make code easier to read
|
# Simple alias to make code easier to read
|
||||||
alias IV = Invidious
|
alias IV = Invidious
|
||||||
|
|
||||||
CONFIG = Config.load
|
CONFIG = Config.load
|
||||||
|
|
||||||
Signal::HUP.trap do
|
|
||||||
Config.reload
|
|
||||||
end
|
|
||||||
|
|
||||||
{% if flag?(:linux) %}
|
|
||||||
if CONFIG.reload_config_automatically
|
|
||||||
Inotify.watch("config/config.yml") do |event|
|
|
||||||
Config.reload
|
|
||||||
end
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
HMAC_KEY = CONFIG.hmac_key
|
HMAC_KEY = CONFIG.hmac_key
|
||||||
|
|
||||||
PG_DB = DB.open CONFIG.database_url
|
PG_DB = DB.open CONFIG.database_url
|
||||||
|
@ -82,13 +67,11 @@ REDIS_DB = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url:
|
||||||
if REDIS_DB.ping
|
if REDIS_DB.ping
|
||||||
puts "Connected to redis"
|
puts "Connected to redis"
|
||||||
end
|
end
|
||||||
ARCHIVE_URL = URI.parse("https://archive.org")
|
ARCHIVE_URL = URI.parse("https://archive.org")
|
||||||
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
||||||
REDDIT_URL = URI.parse("https://www.reddit.com")
|
REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||||
YT_URL = URI.parse("https://www.youtube.com")
|
YT_URL = URI.parse("https://www.youtube.com")
|
||||||
PUBSUB_HOST_URL = CONFIG.pubsub_domain
|
HOST_URL = make_host_url(Kemal.config)
|
||||||
HOST_URL = make_host_url(Kemal.config)
|
|
||||||
EXT_VIDEOP_LIST = gen_videoplayback_proxy_list()
|
|
||||||
|
|
||||||
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
||||||
|
@ -115,10 +98,6 @@ SOFTWARE = {
|
||||||
|
|
||||||
YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
|
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)
|
|
||||||
|
|
||||||
# CLI
|
# CLI
|
||||||
Kemal.config.extra_options do |parser|
|
Kemal.config.extra_options do |parser|
|
||||||
parser.banner = "Usage: invidious [arguments]"
|
parser.banner = "Usage: invidious [arguments]"
|
||||||
|
@ -209,14 +188,6 @@ Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
||||||
|
|
||||||
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
|
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
|
||||||
|
|
||||||
if !CONFIG.external_videoplayback_proxy.empty?
|
|
||||||
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
|
|
||||||
end
|
|
||||||
|
|
||||||
if CONFIG.refresh_tokens
|
|
||||||
Invidious::Jobs.register Invidious::Jobs::RefreshTokens.new
|
|
||||||
end
|
|
||||||
|
|
||||||
Invidious::Jobs.start_all
|
Invidious::Jobs.start_all
|
||||||
|
|
||||||
def popular_videos
|
def popular_videos
|
||||||
|
|
|
@ -223,7 +223,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||||
length_seconds = channel_video.try &.length_seconds
|
length_seconds = channel_video.try &.length_seconds
|
||||||
length_seconds ||= 0
|
length_seconds ||= 0
|
||||||
|
|
||||||
live_now = channel_video.try &.badges.live_now?
|
live_now = channel_video.try &.live_now
|
||||||
live_now ||= false
|
live_now ||= false
|
||||||
|
|
||||||
premiere_timestamp = channel_video.try &.premiere_timestamp
|
premiere_timestamp = channel_video.try &.premiere_timestamp
|
||||||
|
@ -251,8 +251,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
|
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
|
||||||
if CONFIG.enable_user_notifications
|
if CONFIG.enable_user_notifications
|
||||||
Invidious::Database::Users.add_notification(video)
|
Invidious::Database::Users.add_notification(video)
|
||||||
# else
|
else
|
||||||
# Invidious::Database::Users.feed_needs_update(video)
|
Invidious::Database::Users.feed_needs_update(video)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
|
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
|
||||||
|
@ -275,7 +275,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||||
ucid: video.ucid,
|
ucid: video.ucid,
|
||||||
author: video.author,
|
author: video.author,
|
||||||
length_seconds: video.length_seconds,
|
length_seconds: video.length_seconds,
|
||||||
live_now: video.badges.live_now?,
|
live_now: video.live_now,
|
||||||
premiere_timestamp: video.premiere_timestamp,
|
premiere_timestamp: video.premiere_timestamp,
|
||||||
views: video.views,
|
views: video.views,
|
||||||
})
|
})
|
||||||
|
@ -287,8 +287,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
||||||
if was_insert
|
if was_insert
|
||||||
if CONFIG.enable_user_notifications
|
if CONFIG.enable_user_notifications
|
||||||
Invidious::Database::Users.add_notification(video)
|
Invidious::Database::Users.add_notification(video)
|
||||||
# else
|
else
|
||||||
# Invidious::Database::Users.feed_needs_update(video)
|
Invidious::Database::Users.feed_needs_update(video)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,31 +23,14 @@ def produce_channel_content_continuation(ucid, content_type, page = 1, auto_gene
|
||||||
else 15 # Fallback to "videos"
|
else 15 # Fallback to "videos"
|
||||||
end
|
end
|
||||||
|
|
||||||
sort_type_numerical =
|
sort_by_numerical =
|
||||||
case content_type
|
case sort_by
|
||||||
when "videos" then 3
|
when "newest" then 1_i64
|
||||||
when "livestreams" then 5
|
when "popular" then 2_i64
|
||||||
else 3 # Fallback to "videos"
|
when "oldest" then 4_i64
|
||||||
|
else 1_i64 # Fallback to "newest"
|
||||||
end
|
end
|
||||||
|
|
||||||
if content_type == "livestreams"
|
|
||||||
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
|
|
||||||
else
|
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
object_inner_1 = {
|
object_inner_1 = {
|
||||||
"110:embedded" => {
|
"110:embedded" => {
|
||||||
"3:embedded" => {
|
"3:embedded" => {
|
||||||
|
@ -58,7 +41,7 @@ def produce_channel_content_continuation(ucid, content_type, page = 1, auto_gene
|
||||||
"2:embedded" => {
|
"2:embedded" => {
|
||||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||||
},
|
},
|
||||||
"#{sort_type_numerical}:varint" => sort_by_numerical,
|
"3:varint" => sort_by_numerical,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,7 +13,6 @@ struct ConfigPreferences
|
||||||
|
|
||||||
property annotations : Bool = false
|
property annotations : Bool = false
|
||||||
property annotations_subscribed : Bool = false
|
property annotations_subscribed : Bool = false
|
||||||
property preload : Bool = true
|
|
||||||
property autoplay : Bool = false
|
property autoplay : Bool = false
|
||||||
property captions : Array(String) = ["", "", ""]
|
property captions : Array(String) = ["", "", ""]
|
||||||
property comments : Array(String) = ["youtube", ""]
|
property comments : Array(String) = ["youtube", ""]
|
||||||
|
@ -45,8 +44,6 @@ struct ConfigPreferences
|
||||||
property vr_mode : Bool = true
|
property vr_mode : Bool = true
|
||||||
property show_nick : Bool = true
|
property show_nick : Bool = true
|
||||||
property save_player_pos : Bool = false
|
property save_player_pos : Bool = false
|
||||||
property po_token : String = ""
|
|
||||||
property visitor_data : String = ""
|
|
||||||
|
|
||||||
def to_tuple
|
def to_tuple
|
||||||
{% begin %}
|
{% begin %}
|
||||||
|
@ -57,15 +54,6 @@ struct ConfigPreferences
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct HTTPProxyConfig
|
|
||||||
include YAML::Serializable
|
|
||||||
|
|
||||||
property user : String
|
|
||||||
property password : String
|
|
||||||
property host : String
|
|
||||||
property port : Int32
|
|
||||||
end
|
|
||||||
|
|
||||||
class Config
|
class Config
|
||||||
include YAML::Serializable
|
include YAML::Serializable
|
||||||
|
|
||||||
|
@ -98,14 +86,10 @@ class Config
|
||||||
|
|
||||||
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
||||||
property https_only : Bool?
|
property https_only : Bool?
|
||||||
# Enable or disable CSP
|
|
||||||
property csp : Bool? = true
|
|
||||||
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
||||||
property hmac_key : String = ""
|
property hmac_key : String = ""
|
||||||
# Domain to be used for links to resources on the site where an absolute URL is required
|
# Domain to be used for links to resources on the site where an absolute URL is required
|
||||||
property domain : String?
|
property domain : String?
|
||||||
# Materialious redirects
|
|
||||||
property materialious_domain : String?
|
|
||||||
# Alternative domains. You can add other domains, like TOR and I2P addresses
|
# Alternative domains. You can add other domains, like TOR and I2P addresses
|
||||||
property alternative_domains : Array(String) = [] of String
|
property alternative_domains : Array(String) = [] of String
|
||||||
property donation_url : String?
|
property donation_url : String?
|
||||||
|
@ -166,8 +150,6 @@ class Config
|
||||||
property host_binding : String = "0.0.0.0"
|
property host_binding : String = "0.0.0.0"
|
||||||
# Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
|
# Pool size for HTTP requests to youtube.com and ytimg.com (each domain has a separate pool of `pool_size`)
|
||||||
property pool_size : Int32 = 100
|
property pool_size : Int32 = 100
|
||||||
# HTTP Proxy configuration
|
|
||||||
property http_proxy : HTTPProxyConfig? = nil
|
|
||||||
|
|
||||||
# Use Innertube's transcripts API instead of timedtext for closed captions
|
# Use Innertube's transcripts API instead of timedtext for closed captions
|
||||||
property use_innertube_for_captions : Bool = false
|
property use_innertube_for_captions : Bool = false
|
||||||
|
@ -189,24 +171,11 @@ class Config
|
||||||
|
|
||||||
# List of names of the backends
|
# List of names of the backends
|
||||||
property backends : Array(String) = [] of String
|
property backends : Array(String) = [] of String
|
||||||
# Character used to separate the backend number from the description/note
|
|
||||||
# of the backend
|
|
||||||
property backends_delimiter : String = "|"
|
|
||||||
|
|
||||||
# External videoplayback proxies list. They should include `https://`
|
property external_videoplayback_proxy : String?
|
||||||
# at the start of the URI
|
|
||||||
property external_videoplayback_proxy : Array(NamedTuple(url: String, balance: Bool)) = [] of NamedTuple(url: String, balance: Bool)
|
|
||||||
|
|
||||||
# Job to refresh tokens from a Redis compatible DB
|
# Materialious redirects
|
||||||
property refresh_tokens : Bool = true
|
property materialious_domain : String?
|
||||||
|
|
||||||
property pubsub_domain : String = ""
|
|
||||||
|
|
||||||
property ignore_user_tokens : Bool = false
|
|
||||||
|
|
||||||
{% if flag?(:linux) %}
|
|
||||||
property reload_config_automatically : Bool = true
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
def disabled?(option)
|
def disabled?(option)
|
||||||
case disabled = CONFIG.disable_proxy
|
case disabled = CONFIG.disable_proxy
|
||||||
|
@ -223,64 +192,6 @@ class Config
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.reload
|
|
||||||
LOGGER.info("Config: Reloading configuration")
|
|
||||||
# Load config from file or YAML string env var
|
|
||||||
env_config_file = "INVIDIOUS_CONFIG_FILE"
|
|
||||||
env_config_yaml = "INVIDIOUS_CONFIG"
|
|
||||||
|
|
||||||
config_file = ENV.has_key?(env_config_file) ? ENV.fetch(env_config_file) : "config/config.yml"
|
|
||||||
config_yaml = ENV.has_key?(env_config_yaml) ? ENV.fetch(env_config_yaml) : File.read(config_file)
|
|
||||||
|
|
||||||
begin
|
|
||||||
config = Config.from_yaml(config_yaml)
|
|
||||||
rescue ex
|
|
||||||
LOGGER.error("Config: Error when reloading configuration: '#{ex.message}'")
|
|
||||||
config = CONFIG
|
|
||||||
end
|
|
||||||
|
|
||||||
# TODO: Preserve old config and don't exit on fail
|
|
||||||
{% for ivar in Config.instance_vars %}
|
|
||||||
CONFIG.{{ivar}} = config.{{ivar}}
|
|
||||||
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
|
|
||||||
|
|
||||||
if ENV.has_key?({{env_id}})
|
|
||||||
env_value = ENV.fetch({{env_id}})
|
|
||||||
success = false
|
|
||||||
|
|
||||||
# Use YAML converter if specified
|
|
||||||
{% ann = ivar.annotation(::YAML::Field) %}
|
|
||||||
{% if ann && ann[:converter] %}
|
|
||||||
CONFIG.{{ivar.id}} = {{ann[:converter]}}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{env_id}})).nodes[0])
|
|
||||||
success = true
|
|
||||||
|
|
||||||
# Use regular YAML parser otherwise
|
|
||||||
{% else %}
|
|
||||||
{% ivar_types = ivar.type.union? ? ivar.type.union_types : [ivar.type] %}
|
|
||||||
# Sort types to avoid parsing nulls and numbers as strings
|
|
||||||
{% ivar_types = ivar_types.sort_by { |ivar_type| ivar_type == Nil ? 0 : ivar_type == Int32 ? 1 : 2 } %}
|
|
||||||
{{ivar_types}}.each do |ivar_type|
|
|
||||||
if !success
|
|
||||||
begin
|
|
||||||
CONFIG.{{ivar.id}} = ivar_type.from_yaml(env_value)
|
|
||||||
success = true
|
|
||||||
rescue
|
|
||||||
# nop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
# Exit on fail
|
|
||||||
if !success
|
|
||||||
LOGGER.error("Config: Error when reloading environment variables for the configuration, exiting (fixme!)")
|
|
||||||
exit(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
LOGGER.info("Config: Reload successfull")
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.load
|
def self.load
|
||||||
# Load config from file or YAML string env var
|
# Load config from file or YAML string env var
|
||||||
env_config_file = "INVIDIOUS_CONFIG_FILE"
|
env_config_file = "INVIDIOUS_CONFIG_FILE"
|
||||||
|
|
|
@ -154,17 +154,15 @@ module Invidious::Database::Users
|
||||||
# Update (misc)
|
# Update (misc)
|
||||||
# -------------------
|
# -------------------
|
||||||
|
|
||||||
# Feeds never need update. PubSubHubBub is the one that sends videos to
|
def feed_needs_update(video : ChannelVideo)
|
||||||
# invidious.
|
request = <<-SQL
|
||||||
# def feed_needs_update(video : ChannelVideo)
|
UPDATE users
|
||||||
# request = <<-SQL
|
SET feed_needs_update = true
|
||||||
# UPDATE users
|
WHERE $1 = ANY(subscriptions)
|
||||||
# SET feed_needs_update = true
|
SQL
|
||||||
# WHERE $1 = ANY(subscriptions)
|
|
||||||
# SQL
|
|
||||||
|
|
||||||
# PG_DB.exec(request, video.ucid)
|
PG_DB.exec(request, video.ucid)
|
||||||
# end
|
end
|
||||||
|
|
||||||
def update_preferences(user : User)
|
def update_preferences(user : User)
|
||||||
request = <<-SQL
|
request = <<-SQL
|
||||||
|
|
|
@ -10,8 +10,8 @@ module Invidious::Database::Videos
|
||||||
ON CONFLICT (id) DO NOTHING
|
ON CONFLICT (id) DO NOTHING
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
REDIS_DB.set(video.id, video.info.to_json, ex: 14400)
|
REDIS_DB.set(video.id, video.info.to_json, ex: 3600)
|
||||||
REDIS_DB.set(video.id + ":time", video.updated, ex: 14400)
|
REDIS_DB.set(video.id + ":time", video.updated, ex: 3600)
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete(id)
|
def delete(id)
|
||||||
|
|
|
@ -18,40 +18,6 @@ end
|
||||||
class HTTP::Client
|
class HTTP::Client
|
||||||
property family : Socket::Family = Socket::Family::UNSPEC
|
property family : Socket::Family = Socket::Family::UNSPEC
|
||||||
|
|
||||||
# Override stdlib to automatically initialize proxy if configured
|
|
||||||
#
|
|
||||||
# Accurate as of crystal 1.12.1
|
|
||||||
|
|
||||||
def initialize(@host : String, port = nil, tls : TLSContext = nil)
|
|
||||||
check_host_only(@host)
|
|
||||||
|
|
||||||
{% if flag?(:without_openssl) %}
|
|
||||||
if tls
|
|
||||||
raise "HTTP::Client TLS is disabled because `-D without_openssl` was passed at compile time"
|
|
||||||
end
|
|
||||||
@tls = nil
|
|
||||||
{% else %}
|
|
||||||
@tls = case tls
|
|
||||||
when true
|
|
||||||
OpenSSL::SSL::Context::Client.new
|
|
||||||
when OpenSSL::SSL::Context::Client
|
|
||||||
tls
|
|
||||||
when false, nil
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
{% end %}
|
|
||||||
|
|
||||||
@port = (port || (@tls ? 443 : 80)).to_i
|
|
||||||
|
|
||||||
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(@io : IO, @host = "", @port = 80)
|
|
||||||
@reconnect = false
|
|
||||||
|
|
||||||
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
|
||||||
end
|
|
||||||
|
|
||||||
private def io
|
private def io
|
||||||
io = @io
|
io = @io
|
||||||
return io if io
|
return io if io
|
||||||
|
|
|
@ -43,8 +43,6 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
|
||||||
# URLs for the error message below
|
# URLs for the error message below
|
||||||
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
|
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
|
||||||
url_search_issues = "https://github.com/iv-org/invidious/issues"
|
url_search_issues = "https://github.com/iv-org/invidious/issues"
|
||||||
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
|
url_switch = "https://redirect.invidious.io" + env.request.resource
|
||||||
|
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
module Tokens
|
|
||||||
extend self
|
|
||||||
@@po_token : String | Nil
|
|
||||||
@@visitor_data : String | Nil
|
|
||||||
|
|
||||||
def refresh_tokens
|
|
||||||
@@po_token = REDIS_DB.get("invidious:po_token")
|
|
||||||
@@visitor_data = REDIS_DB.get("invidious:visitor_data")
|
|
||||||
if !@@po_token.nil? && !@@visitor_data.nil?
|
|
||||||
LOGGER.debug("RefreshTokens: Successfully updated tokens")
|
|
||||||
else
|
|
||||||
LOGGER.warn("RefreshTokens: Tokens are empty!")
|
|
||||||
end
|
|
||||||
LOGGER.trace("RefreshTokens: Tokens are:")
|
|
||||||
LOGGER.trace("RefreshTokens: po_token: #{@@po_token}")
|
|
||||||
LOGGER.trace("RefreshTokens: visitor_data: #{@@visitor_data}")
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_tokens
|
|
||||||
return {@@po_token, @@visitor_data}
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_po_token
|
|
||||||
return @@po_token
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_visitor_data
|
|
||||||
return @@visitor_data
|
|
||||||
end
|
|
||||||
|
|
||||||
def generate_tokens(user : String)
|
|
||||||
po_token = ""
|
|
||||||
visitor_data = ""
|
|
||||||
attempts = 0
|
|
||||||
|
|
||||||
LOGGER.debug("Generating po_token and visitor_data for user: '#{user}'")
|
|
||||||
REDIS_DB.publish("generate-token", "#{user}")
|
|
||||||
|
|
||||||
while REDIS_DB.get("invidious:#{user}:po_token").nil? && REDIS_DB.get("invidious:#{user}:visitor_data").nil?
|
|
||||||
if attempts > 50
|
|
||||||
break
|
|
||||||
end
|
|
||||||
LOGGER.debug("Waiting for tokens to arrive at redis for user: '#{user}'")
|
|
||||||
attempts += 1
|
|
||||||
sleep 250.milliseconds
|
|
||||||
end
|
|
||||||
|
|
||||||
po_token = REDIS_DB.get("invidious:#{user}:po_token")
|
|
||||||
visitor_data = REDIS_DB.get("invidious:#{user}:visitor_data")
|
|
||||||
|
|
||||||
LOGGER.debug("Tokens successfully generated for user: '#{user}'")
|
|
||||||
return {po_token, visitor_data}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,16 +1,3 @@
|
||||||
@[Flags]
|
|
||||||
enum VideoBadges
|
|
||||||
LiveNow
|
|
||||||
Premium
|
|
||||||
ThreeD
|
|
||||||
FourK
|
|
||||||
New
|
|
||||||
EightK
|
|
||||||
VR180
|
|
||||||
VR360
|
|
||||||
ClosedCaptions
|
|
||||||
end
|
|
||||||
|
|
||||||
struct SearchVideo
|
struct SearchVideo
|
||||||
include DB::Serializable
|
include DB::Serializable
|
||||||
|
|
||||||
|
@ -22,9 +9,10 @@ struct SearchVideo
|
||||||
property views : Int64
|
property views : Int64
|
||||||
property description_html : String
|
property description_html : String
|
||||||
property length_seconds : Int32
|
property length_seconds : Int32
|
||||||
|
property live_now : Bool
|
||||||
|
property premium : Bool
|
||||||
property premiere_timestamp : Time?
|
property premiere_timestamp : Time?
|
||||||
property author_verified : Bool
|
property author_verified : Bool
|
||||||
property badges : VideoBadges
|
|
||||||
|
|
||||||
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
def to_xml(auto_generated, query_params, xml : XML::Builder)
|
||||||
query_params["v"] = self.id
|
query_params["v"] = self.id
|
||||||
|
@ -100,20 +88,13 @@ struct SearchVideo
|
||||||
json.field "published", self.published.to_unix
|
json.field "published", self.published.to_unix
|
||||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
||||||
json.field "lengthSeconds", self.length_seconds
|
json.field "lengthSeconds", self.length_seconds
|
||||||
json.field "liveNow", self.badges.live_now?
|
json.field "liveNow", self.live_now
|
||||||
json.field "premium", self.badges.premium?
|
json.field "premium", self.premium
|
||||||
json.field "isUpcoming", self.upcoming?
|
json.field "isUpcoming", self.upcoming?
|
||||||
|
|
||||||
if self.premiere_timestamp
|
if self.premiere_timestamp
|
||||||
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
|
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,7 @@ module Invidious::SigHelper
|
||||||
@queue = {} of TransactionID => Transaction
|
@queue = {} of TransactionID => Transaction
|
||||||
|
|
||||||
@conn : Connection
|
@conn : Connection
|
||||||
|
|
||||||
@uri_or_path : String
|
@uri_or_path : String
|
||||||
|
|
||||||
def initialize(@uri_or_path)
|
def initialize(@uri_or_path)
|
||||||
|
@ -191,6 +192,7 @@ module Invidious::SigHelper
|
||||||
loop do
|
loop do
|
||||||
begin
|
begin
|
||||||
receive_data
|
receive_data
|
||||||
|
# Handle all errors
|
||||||
rescue ex
|
rescue ex
|
||||||
LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...")
|
LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...")
|
||||||
# We close the socket because for some reason is not closed.
|
# We close the socket because for some reason is not closed.
|
||||||
|
@ -200,8 +202,8 @@ module Invidious::SigHelper
|
||||||
@conn = Connection.new(@uri_or_path)
|
@conn = Connection.new(@uri_or_path)
|
||||||
LOGGER.info("SigHelper: Reconnected to SigHelper!")
|
LOGGER.info("SigHelper: Reconnected to SigHelper!")
|
||||||
rescue ex
|
rescue ex
|
||||||
LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying")
|
LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}' retrying")
|
||||||
sleep 500.milliseconds
|
sleep 0.5
|
||||||
next
|
next
|
||||||
end
|
end
|
||||||
break if !@conn.closed?
|
break if !@conn.closed?
|
||||||
|
@ -323,6 +325,11 @@ module Invidious::SigHelper
|
||||||
return @socket.closed?
|
return @socket.closed?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def remote_address : Socket::IPAddress
|
||||||
|
|
||||||
|
return @socket.remote_address
|
||||||
|
end
|
||||||
|
|
||||||
def close : Nil
|
def close : Nil
|
||||||
@socket.close if !@socket.closed?
|
@socket.close if !@socket.closed?
|
||||||
end
|
end
|
||||||
|
|
|
@ -294,7 +294,7 @@ def subscribe_pubsub(topic, key)
|
||||||
signature = "#{time}:#{nonce}"
|
signature = "#{time}:#{nonce}"
|
||||||
|
|
||||||
body = {
|
body = {
|
||||||
"hub.callback" => "#{PUBSUB_HOST_URL}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}",
|
"hub.callback" => "#{HOST_URL}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}",
|
||||||
"hub.topic" => "https://www.youtube.com/xml/feeds/videos.xml?#{topic}",
|
"hub.topic" => "https://www.youtube.com/xml/feeds/videos.xml?#{topic}",
|
||||||
"hub.verify" => "async",
|
"hub.verify" => "async",
|
||||||
"hub.mode" => "subscribe",
|
"hub.mode" => "subscribe",
|
||||||
|
@ -383,17 +383,3 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
|
||||||
end
|
end
|
||||||
return text
|
return text
|
||||||
end
|
end
|
||||||
|
|
||||||
# Generates a list of external videoplayback proxies for
|
|
||||||
# CSP
|
|
||||||
def gen_videoplayback_proxy_list
|
|
||||||
if !CONFIG.external_videoplayback_proxy.empty?
|
|
||||||
external_videoplayback_proxy = ""
|
|
||||||
CONFIG.external_videoplayback_proxy.each do |proxy|
|
|
||||||
external_videoplayback_proxy += " #{proxy[:url]}"
|
|
||||||
end
|
|
||||||
else
|
|
||||||
external_videoplayback_proxy = ""
|
|
||||||
end
|
|
||||||
return external_videoplayback_proxy
|
|
||||||
end
|
|
||||||
|
|
|
@ -4,30 +4,6 @@ module Invidious::HttpServer
|
||||||
module Utils
|
module Utils
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
@@proxy_alive : String = ""
|
|
||||||
|
|
||||||
def check_external_proxy
|
|
||||||
CONFIG.external_videoplayback_proxy.each do |proxy|
|
|
||||||
begin
|
|
||||||
response = HTTP::Client.get("#{proxy[:url]}/health")
|
|
||||||
if response.status_code == 200
|
|
||||||
@@proxy_alive = proxy[:url]
|
|
||||||
LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy[:url]}'")
|
|
||||||
break
|
|
||||||
end
|
|
||||||
rescue
|
|
||||||
LOGGER.debug("CheckExternalProxy: Proxy '#{proxy[:url]}' is not available")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
if @@proxy_alive.empty?
|
|
||||||
LOGGER.warn("CheckExternalProxy: No proxies alive! Using own server proxy")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_external_proxy
|
|
||||||
return @@proxy_alive
|
|
||||||
end
|
|
||||||
|
|
||||||
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
|
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
|
||||||
url = URI.parse(raw_url)
|
url = URI.parse(raw_url)
|
||||||
|
|
||||||
|
@ -38,11 +14,7 @@ module Invidious::HttpServer
|
||||||
url.query_params = params
|
url.query_params = params
|
||||||
|
|
||||||
if absolute
|
if absolute
|
||||||
if !@@proxy_alive.empty?
|
return "#{HOST_URL}#{url.request_target}"
|
||||||
return "#{@@proxy_alive}#{url.request_target}"
|
|
||||||
else
|
|
||||||
return "#{HOST_URL}#{url.request_target}"
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
return url.request_target
|
return url.request_target
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
class Invidious::Jobs::CheckExternalProxy < Invidious::Jobs::BaseJob
|
|
||||||
def initialize
|
|
||||||
end
|
|
||||||
|
|
||||||
def begin
|
|
||||||
loop do
|
|
||||||
HttpServer::Utils.check_external_proxy
|
|
||||||
LOGGER.info("CheckExternalProxy: Done, sleeping for 10 seconds")
|
|
||||||
sleep 10.seconds
|
|
||||||
Fiber.yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,13 +0,0 @@
|
||||||
class Invidious::Jobs::RefreshTokens < Invidious::Jobs::BaseJob
|
|
||||||
def initialize
|
|
||||||
end
|
|
||||||
|
|
||||||
def begin
|
|
||||||
loop do
|
|
||||||
Tokens.refresh_tokens
|
|
||||||
LOGGER.info("RefreshTokens: Done, sleeping for 5 seconds")
|
|
||||||
sleep 5.seconds
|
|
||||||
Fiber.yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -30,8 +30,6 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob
|
||||||
spawn do
|
spawn do
|
||||||
begin
|
begin
|
||||||
response = subscribe_pubsub(ucid, hmac_key)
|
response = subscribe_pubsub(ucid, hmac_key)
|
||||||
LOGGER.debug("SubscribeToFeedsJob: Subscribed to #{ucid}.")
|
|
||||||
LOGGER.trace("SubscribeToFeedsJob: response.body: #{response.body}")
|
|
||||||
|
|
||||||
if response.status_code >= 400
|
if response.status_code >= 400
|
||||||
LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{response.body}")
|
LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{response.body}")
|
||||||
|
|
|
@ -270,7 +270,7 @@ end
|
||||||
|
|
||||||
def subscribe_playlist(user, playlist)
|
def subscribe_playlist(user, playlist)
|
||||||
playlist = InvidiousPlaylist.new({
|
playlist = InvidiousPlaylist.new({
|
||||||
title: playlist.title[..150],
|
title: playlist.title.byte_slice(0, 150),
|
||||||
id: playlist.id,
|
id: playlist.id,
|
||||||
author: user.email,
|
author: user.email,
|
||||||
description: "", # Max 5000 characters
|
description: "", # Max 5000 characters
|
||||||
|
|
|
@ -349,40 +349,4 @@ module Invidious::Routes::Account
|
||||||
return "{}"
|
return "{}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# -------------------
|
|
||||||
# poToken and visitorData tokens generation
|
|
||||||
# -------------------
|
|
||||||
|
|
||||||
# Generates a poToken & visitorData for the user, server side
|
|
||||||
def generate_tokens(env)
|
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
|
||||||
preferences = env.get("preferences").as(Preferences)
|
|
||||||
|
|
||||||
user = env.get? "user"
|
|
||||||
sid = env.get? "sid"
|
|
||||||
referer = get_referer(env)
|
|
||||||
|
|
||||||
if !user
|
|
||||||
return env.redirect referer
|
|
||||||
end
|
|
||||||
|
|
||||||
user = user.as(User)
|
|
||||||
|
|
||||||
po_token, visitor_data = Tokens.generate_tokens(user.email)
|
|
||||||
|
|
||||||
if po_token.nil? || visitor_data.nil?
|
|
||||||
return error_template(500, "Internal server error. Please submit an issue here IF THE ISSUE PERSISTS: https://git.nadeko.net/Fijxu/invidious/issues")
|
|
||||||
end
|
|
||||||
|
|
||||||
user.preferences.po_token = po_token
|
|
||||||
user.preferences.visitor_data = visitor_data
|
|
||||||
|
|
||||||
Invidious::Database::Users.update_preferences(user)
|
|
||||||
|
|
||||||
REDIS_DB.del("invidious:#{user.email}:po_token")
|
|
||||||
REDIS_DB.del("invidious:#{user.email}:visitor_data")
|
|
||||||
|
|
||||||
templated "user/tokens"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,21 +27,36 @@ module Invidious::Routes::API::Manifest
|
||||||
haltf env, status_code: response.status_code
|
haltf env, status_code: response.status_code
|
||||||
end
|
end
|
||||||
|
|
||||||
# Proxy URLs for video playback on invidious.
|
manifest = response.body
|
||||||
# Other API clients can get the original URLs by omiting `local=true`.
|
|
||||||
manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
||||||
url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
|
url = baseurl.lchop("<BaseURL>")
|
||||||
url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
|
url = url.rchop("</BaseURL>")
|
||||||
|
|
||||||
|
if local
|
||||||
|
uri = URI.parse(url)
|
||||||
|
if CONFIG.external_videoplayback_proxy
|
||||||
|
url = "#{CONFIG.external_videoplayback_proxy}#{uri.request_target}host/#{uri.host}/"
|
||||||
|
else
|
||||||
|
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
"<BaseURL>#{url}</BaseURL>"
|
"<BaseURL>#{url}</BaseURL>"
|
||||||
end
|
end
|
||||||
|
|
||||||
return manifest
|
return manifest
|
||||||
end
|
end
|
||||||
|
|
||||||
# Ditto, only proxify URLs if `local=true` is used
|
adaptive_fmts = video.adaptive_fmts
|
||||||
|
|
||||||
if local
|
if local
|
||||||
video.adaptive_fmts.each do |fmt|
|
adaptive_fmts.each do |fmt|
|
||||||
fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
|
if CONFIG.external_videoplayback_proxy
|
||||||
|
fmt["url"] = JSON::Any.new("#{CONFIG.external_videoplayback_proxy}#{URI.parse(fmt["url"].as_s).request_target}")
|
||||||
|
else
|
||||||
|
fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -55,10 +70,6 @@ module Invidious::Routes::API::Manifest
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
audio_streams.reject! do |z|
|
|
||||||
z if z.dig?("audioTrack", "audioIsDefault") == false
|
|
||||||
end
|
|
||||||
|
|
||||||
manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||||
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
|
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
|
||||||
"profiles": "urn:mpeg:dash:profile:full:2011", minBufferTime: "PT1.5S", type: "static",
|
"profiles": "urn:mpeg:dash:profile:full:2011", minBufferTime: "PT1.5S", type: "static",
|
||||||
|
@ -181,9 +192,8 @@ module Invidious::Routes::API::Manifest
|
||||||
manifest = response.body
|
manifest = response.body
|
||||||
|
|
||||||
if local
|
if local
|
||||||
manifest = manifest.gsub(/^https:\/\/[^\n"]*/m) do |match|
|
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
|
||||||
uri = URI.parse(match)
|
path = URI.parse(match).path
|
||||||
path = uri.path
|
|
||||||
|
|
||||||
path = path.lchop("/videoplayback/")
|
path = path.lchop("/videoplayback/")
|
||||||
path = path.rchop("/")
|
path = path.rchop("/")
|
||||||
|
@ -212,15 +222,9 @@ module Invidious::Routes::API::Manifest
|
||||||
raw_params["fvip"] = fvip["fvip"]
|
raw_params["fvip"] = fvip["fvip"]
|
||||||
end
|
end
|
||||||
|
|
||||||
raw_params["host"] = uri.host.not_nil!
|
raw_params["local"] = "true"
|
||||||
|
|
||||||
proxy = Invidious::HttpServer::Utils.get_external_proxy
|
"#{HOST_URL}/videoplayback?#{raw_params}"
|
||||||
|
|
||||||
if !proxy.empty?
|
|
||||||
"#{proxy}/videoplayback?#{raw_params}"
|
|
||||||
else
|
|
||||||
"#{HOST_URL}/videoplayback?#{raw_params}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -263,60 +263,59 @@ module Invidious::Routes::API::V1::Videos
|
||||||
|
|
||||||
annotations = ""
|
annotations = ""
|
||||||
|
|
||||||
# case source
|
case source
|
||||||
# when "archive"
|
when "archive"
|
||||||
# if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
|
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
|
||||||
# annotations = cached_annotation.annotations
|
annotations = cached_annotation.annotations
|
||||||
# else
|
else
|
||||||
# index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
|
index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
|
||||||
|
|
||||||
# # IA doesn't handle leading hyphens,
|
# IA doesn't handle leading hyphens,
|
||||||
# # so we use https://archive.org/details/youtubeannotations_64
|
# so we use https://archive.org/details/youtubeannotations_64
|
||||||
# if index == "62"
|
if index == "62"
|
||||||
# index = "64"
|
index = "64"
|
||||||
# id = id.sub(/^-/, 'A')
|
id = id.sub(/^-/, 'A')
|
||||||
# end
|
end
|
||||||
|
|
||||||
# file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
|
file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
|
||||||
|
|
||||||
# location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
||||||
|
|
||||||
# if !location.headers["Location"]?
|
if !location.headers["Location"]?
|
||||||
# env.response.status_code = location.status_code
|
env.response.status_code = location.status_code
|
||||||
# end
|
end
|
||||||
|
|
||||||
# response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"]))
|
response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"]))
|
||||||
|
|
||||||
# if response.body.empty?
|
if response.body.empty?
|
||||||
# haltf env, 404
|
haltf env, 404
|
||||||
# end
|
end
|
||||||
|
|
||||||
# if response.status_code != 200
|
if response.status_code != 200
|
||||||
# haltf env, response.status_code
|
haltf env, response.status_code
|
||||||
# end
|
end
|
||||||
|
|
||||||
# annotations = response.body
|
annotations = response.body
|
||||||
|
|
||||||
# cache_annotation(id, annotations)
|
cache_annotation(id, annotations)
|
||||||
# end
|
end
|
||||||
# else # "youtube"
|
else # "youtube"
|
||||||
# response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
|
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
|
||||||
|
|
||||||
# if response.status_code != 200
|
if response.status_code != 200
|
||||||
# haltf env, response.status_code
|
haltf env, response.status_code
|
||||||
# end
|
end
|
||||||
|
|
||||||
# annotations = response.body
|
annotations = response.body
|
||||||
# end
|
end
|
||||||
|
|
||||||
# etag = sha256(annotations)[0, 16]
|
etag = sha256(annotations)[0, 16]
|
||||||
# if env.request.headers["If-None-Match"]?.try &.== etag
|
if env.request.headers["If-None-Match"]?.try &.== etag
|
||||||
# haltf env, 304
|
haltf env, 304
|
||||||
# else
|
else
|
||||||
# env.response.headers["ETag"] = etag
|
env.response.headers["ETag"] = etag
|
||||||
# annotations
|
annotations
|
||||||
# end
|
end
|
||||||
annotations
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.comments(env)
|
def self.comments(env)
|
||||||
|
|
|
@ -28,6 +28,12 @@ module Invidious::Routes::BeforeAll
|
||||||
extra_media_csp = ""
|
extra_media_csp = ""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if CONFIG.external_videoplayback_proxy
|
||||||
|
external_videoplayback_proxy = " #{CONFIG.external_videoplayback_proxy}"
|
||||||
|
else
|
||||||
|
external_videoplayback_proxy = ""
|
||||||
|
end
|
||||||
|
|
||||||
# Only allow the pages at /embed/* to be embedded
|
# Only allow the pages at /embed/* to be embedded
|
||||||
if env.request.resource.starts_with?("/embed")
|
if env.request.resource.starts_with?("/embed")
|
||||||
frame_ancestors = "'self' file: http: https:"
|
frame_ancestors = "'self' file: http: https:"
|
||||||
|
@ -43,9 +49,9 @@ module Invidious::Routes::BeforeAll
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' data:",
|
"img-src 'self' data:",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
"connect-src 'self'" + EXT_VIDEOP_LIST,
|
"connect-src 'self'" + external_videoplayback_proxy,
|
||||||
"manifest-src 'self'",
|
"manifest-src 'self'",
|
||||||
"media-src 'self' blob:" + extra_media_csp + EXT_VIDEOP_LIST,
|
"media-src 'self' blob:" + extra_media_csp,
|
||||||
"child-src 'self' blob:",
|
"child-src 'self' blob:",
|
||||||
"frame-src 'self'",
|
"frame-src 'self'",
|
||||||
"frame-ancestors " + frame_ancestors,
|
"frame-ancestors " + frame_ancestors,
|
||||||
|
|
|
@ -157,12 +157,10 @@ module Invidious::Routes::Embed
|
||||||
adaptive_fmts = video.adaptive_fmts
|
adaptive_fmts = video.adaptive_fmts
|
||||||
|
|
||||||
if params.local
|
if params.local
|
||||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||||
|
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
|
||||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
|
||||||
|
|
||||||
video_streams = video.video_streams
|
video_streams = video.video_streams
|
||||||
audio_streams = video.audio_streams
|
audio_streams = video.audio_streams
|
||||||
|
|
||||||
|
|
|
@ -192,9 +192,11 @@ module Invidious::Routes::Feeds
|
||||||
views: views,
|
views: views,
|
||||||
description_html: description_html,
|
description_html: description_html,
|
||||||
length_seconds: 0,
|
length_seconds: 0,
|
||||||
|
live_now: false,
|
||||||
|
paid: false,
|
||||||
|
premium: false,
|
||||||
premiere_timestamp: nil,
|
premiere_timestamp: nil,
|
||||||
author_verified: false,
|
author_verified: false,
|
||||||
badges: VideoBadges::None,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -450,8 +452,8 @@ module Invidious::Routes::Feeds
|
||||||
if was_insert
|
if was_insert
|
||||||
if CONFIG.enable_user_notifications
|
if CONFIG.enable_user_notifications
|
||||||
Invidious::Database::Users.add_notification(video)
|
Invidious::Database::Users.add_notification(video)
|
||||||
# else
|
else
|
||||||
# Invidious::Database::Users.feed_needs_update(video)
|
Invidious::Database::Users.feed_needs_update(video)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,9 +11,29 @@ module Invidious::Routes::Images
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# We're encapsulating this into a proc in order to easily reuse this
|
||||||
|
# portion of the code for each request block below.
|
||||||
|
request_proc = ->(response : HTTP::Client::Response) {
|
||||||
|
env.response.status_code = response.status_code
|
||||||
|
response.headers.each do |key, value|
|
||||||
|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
|
||||||
|
env.response.headers[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
|
||||||
|
if response.status_code >= 300
|
||||||
|
env.response.headers.delete("Transfer-Encoding")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
proxy_file(response, env)
|
||||||
|
}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
GGPHT_POOL.client &.get(url, headers) do |resp|
|
HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp|
|
||||||
return self.proxy_image(env, resp)
|
return request_proc.call(resp)
|
||||||
end
|
end
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
|
@ -41,10 +61,27 @@ module Invidious::Routes::Images
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
request_proc = ->(response : HTTP::Client::Response) {
|
||||||
|
env.response.status_code = response.status_code
|
||||||
|
response.headers.each do |key, value|
|
||||||
|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
|
||||||
|
env.response.headers[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
env.response.headers["Connection"] = "close"
|
||||||
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
|
||||||
|
if response.status_code >= 300
|
||||||
|
return env.response.headers.delete("Transfer-Encoding")
|
||||||
|
end
|
||||||
|
|
||||||
|
proxy_file(response, env)
|
||||||
|
}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
get_ytimg_pool(authority).client &.get(url, headers) do |resp|
|
HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp|
|
||||||
env.response.headers["Connection"] = "close"
|
return request_proc.call(resp)
|
||||||
return self.proxy_image(env, resp)
|
|
||||||
end
|
end
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
|
@ -64,9 +101,26 @@ module Invidious::Routes::Images
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
request_proc = ->(response : HTTP::Client::Response) {
|
||||||
|
env.response.status_code = response.status_code
|
||||||
|
response.headers.each do |key, value|
|
||||||
|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
|
||||||
|
env.response.headers[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
|
||||||
|
if response.status_code >= 300 && response.status_code != 404
|
||||||
|
return env.response.headers.delete("Transfer-Encoding")
|
||||||
|
end
|
||||||
|
|
||||||
|
proxy_file(response, env)
|
||||||
|
}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
get_ytimg_pool("i9").client &.get(url, headers) do |resp|
|
HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp|
|
||||||
return self.proxy_image(env, resp)
|
return request_proc.call(resp)
|
||||||
end
|
end
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
|
@ -111,7 +165,8 @@ module Invidious::Routes::Images
|
||||||
if name == "maxres.jpg"
|
if name == "maxres.jpg"
|
||||||
build_thumbnails(id).each do |thumb|
|
build_thumbnails(id).each do |thumb|
|
||||||
thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg"
|
thumbnail_resource_path = "/vi/#{id}/#{thumb[:url]}.jpg"
|
||||||
if get_ytimg_pool("i9").client &.head(thumbnail_resource_path, headers).status_code == 200
|
# This can likely be optimized into a (small) pool sometime in the future.
|
||||||
|
if HTTP::Client.head("https://i.ytimg.com#{thumbnail_resource_path}").status_code == 200
|
||||||
name = thumb[:url] + ".jpg"
|
name = thumb[:url] + ".jpg"
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
|
@ -126,28 +181,29 @@ module Invidious::Routes::Images
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
request_proc = ->(response : HTTP::Client::Response) {
|
||||||
|
env.response.status_code = response.status_code
|
||||||
|
response.headers.each do |key, value|
|
||||||
|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
|
||||||
|
env.response.headers[key] = value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
|
||||||
|
if response.status_code >= 300 && response.status_code != 404
|
||||||
|
return env.response.headers.delete("Transfer-Encoding")
|
||||||
|
end
|
||||||
|
|
||||||
|
proxy_file(response, env)
|
||||||
|
}
|
||||||
|
|
||||||
begin
|
begin
|
||||||
get_ytimg_pool("i").client &.get(url, headers) do |resp|
|
# This can likely be optimized into a (small) pool sometime in the future.
|
||||||
return self.proxy_image(env, resp)
|
HTTP::Client.get("https://i.ytimg.com#{url}") do |resp|
|
||||||
|
return request_proc.call(resp)
|
||||||
end
|
end
|
||||||
rescue ex
|
rescue ex
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private def self.proxy_image(env, response)
|
|
||||||
env.response.status_code = response.status_code
|
|
||||||
response.headers.each do |key, value|
|
|
||||||
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
|
|
||||||
env.response.headers[key] = value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
|
||||||
|
|
||||||
if response.status_code >= 300
|
|
||||||
return env.response.headers.delete("Transfer-Encoding")
|
|
||||||
end
|
|
||||||
|
|
||||||
return proxy_file(response, env)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,10 +27,6 @@ module Invidious::Routes::PreferencesRoute
|
||||||
annotations_subscribed ||= "off"
|
annotations_subscribed ||= "off"
|
||||||
annotations_subscribed = annotations_subscribed == "on"
|
annotations_subscribed = annotations_subscribed == "on"
|
||||||
|
|
||||||
preload = env.params.body["preload"]?.try &.as(String)
|
|
||||||
preload ||= "off"
|
|
||||||
preload = preload == "on"
|
|
||||||
|
|
||||||
autoplay = env.params.body["autoplay"]?.try &.as(String)
|
autoplay = env.params.body["autoplay"]?.try &.as(String)
|
||||||
autoplay ||= "off"
|
autoplay ||= "off"
|
||||||
autoplay = autoplay == "on"
|
autoplay = autoplay == "on"
|
||||||
|
@ -86,12 +82,6 @@ module Invidious::Routes::PreferencesRoute
|
||||||
show_nick ||= "off"
|
show_nick ||= "off"
|
||||||
show_nick = show_nick == "on"
|
show_nick = show_nick == "on"
|
||||||
|
|
||||||
po_token = env.params.body["po_token"]?.try &.as(String)
|
|
||||||
po_token ||= CONFIG.default_user_preferences.po_token
|
|
||||||
|
|
||||||
visitor_data = env.params.body["visitor_data"]?.try &.as(String)
|
|
||||||
visitor_data ||= CONFIG.default_user_preferences.visitor_data
|
|
||||||
|
|
||||||
comments = [] of String
|
comments = [] of String
|
||||||
2.times do |i|
|
2.times do |i|
|
||||||
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
|
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
|
||||||
|
@ -154,7 +144,6 @@ module Invidious::Routes::PreferencesRoute
|
||||||
preferences = Preferences.from_json({
|
preferences = Preferences.from_json({
|
||||||
annotations: annotations,
|
annotations: annotations,
|
||||||
annotations_subscribed: annotations_subscribed,
|
annotations_subscribed: annotations_subscribed,
|
||||||
preload: preload,
|
|
||||||
autoplay: autoplay,
|
autoplay: autoplay,
|
||||||
captions: captions,
|
captions: captions,
|
||||||
comments: comments,
|
comments: comments,
|
||||||
|
@ -186,8 +175,6 @@ module Invidious::Routes::PreferencesRoute
|
||||||
vr_mode: vr_mode,
|
vr_mode: vr_mode,
|
||||||
show_nick: show_nick,
|
show_nick: show_nick,
|
||||||
save_player_pos: save_player_pos,
|
save_player_pos: save_player_pos,
|
||||||
po_token: po_token,
|
|
||||||
visitor_data: visitor_data,
|
|
||||||
}.to_json)
|
}.to_json)
|
||||||
|
|
||||||
if user = env.get? "user"
|
if user = env.get? "user"
|
||||||
|
|
|
@ -3,11 +3,6 @@ module Invidious::Routes::VideoPlayback
|
||||||
def self.get_video_playback(env)
|
def self.get_video_playback(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
query_params = env.params.query
|
query_params = env.params.query
|
||||||
array = UInt8[0x78, 0]
|
|
||||||
protobuf = Bytes.new(array.size)
|
|
||||||
array.each_with_index do |byte, index|
|
|
||||||
protobuf[index] = byte
|
|
||||||
end
|
|
||||||
|
|
||||||
fvip = query_params["fvip"]? || "3"
|
fvip = query_params["fvip"]? || "3"
|
||||||
mns = query_params["mn"]?.try &.split(",")
|
mns = query_params["mn"]?.try &.split(",")
|
||||||
|
@ -47,9 +42,9 @@ module Invidious::Routes::VideoPlayback
|
||||||
headers["Range"] = "bytes=#{range_for_head}"
|
headers["Range"] = "bytes=#{range_for_head}"
|
||||||
end
|
end
|
||||||
|
|
||||||
headers["Origin"] = "https://www.youtube.com"
|
headers["Origin"] = "https://www.youtube.com"
|
||||||
headers["Referer"] = "https://www.youtube.com/"
|
headers["Referer"] = "https://www.youtube.com/"
|
||||||
headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
|
headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0"
|
||||||
|
|
||||||
client = make_client(URI.parse(host), region, force_resolve = true)
|
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||||
response = HTTP::Client::Response.new(500)
|
response = HTTP::Client::Response.new(500)
|
||||||
|
@ -105,7 +100,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
client.post(url, headers, protobuf) do |resp|
|
client.get(url, headers) do |resp|
|
||||||
resp.headers.each do |key, value|
|
resp.headers.each do |key, value|
|
||||||
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
|
||||||
env.response.headers[key] = value
|
env.response.headers[key] = value
|
||||||
|
@ -156,7 +151,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
headers["Range"] = "bytes=#{chunk_start}-#{chunk_end}"
|
headers["Range"] = "bytes=#{chunk_start}-#{chunk_end}"
|
||||||
|
|
||||||
begin
|
begin
|
||||||
client.post(url, headers, protobuf) do |resp|
|
client.get(url, headers) do |resp|
|
||||||
if first_chunk
|
if first_chunk
|
||||||
if !env.request.headers["Range"]? && resp.status_code == 206
|
if !env.request.headers["Range"]? && resp.status_code == 206
|
||||||
env.response.status_code = 200
|
env.response.status_code = 200
|
||||||
|
@ -303,16 +298,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
end
|
end
|
||||||
|
|
||||||
if local
|
if local
|
||||||
external_proxy = Invidious::HttpServer::Utils.get_external_proxy
|
url = URI.parse(url).request_target.not_nil!
|
||||||
if !external_proxy.empty?
|
|
||||||
url = URI.parse(url)
|
|
||||||
external_proxy = URI.parse(external_proxy)
|
|
||||||
url.host = external_proxy.host
|
|
||||||
url.port = external_proxy.port
|
|
||||||
url = url.to_s
|
|
||||||
else
|
|
||||||
url = URI.parse(url).request_target.not_nil!
|
|
||||||
end
|
|
||||||
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
|
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,6 @@
|
||||||
module Invidious::Routes::Watch
|
module Invidious::Routes::Watch
|
||||||
def self.handle(env)
|
def self.handle(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
if !CONFIG.ignore_user_tokens
|
|
||||||
user_po_token = env.get("preferences").as(Preferences).po_token
|
|
||||||
user_visitor_data = env.get("preferences").as(Preferences).visitor_data
|
|
||||||
else
|
|
||||||
user_po_token = ""
|
|
||||||
user_visitor_data = ""
|
|
||||||
end
|
|
||||||
|
|
||||||
region = env.params.query["region"]?
|
region = env.params.query["region"]?
|
||||||
|
|
||||||
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
|
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
|
||||||
|
@ -60,7 +52,7 @@ module Invidious::Routes::Watch
|
||||||
env.params.query.delete_all("listen")
|
env.params.query.delete_all("listen")
|
||||||
|
|
||||||
begin
|
begin
|
||||||
video = get_video(id, region: params.region, po_token: user_po_token, visitor_data: user_visitor_data)
|
video = get_video(id, region: params.region)
|
||||||
rescue ex : NotFoundException
|
rescue ex : NotFoundException
|
||||||
LOGGER.error("get_video not found: #{id} : #{ex.message}")
|
LOGGER.error("get_video not found: #{id} : #{ex.message}")
|
||||||
return error_template(404, ex)
|
return error_template(404, ex)
|
||||||
|
@ -136,12 +128,10 @@ module Invidious::Routes::Watch
|
||||||
end
|
end
|
||||||
|
|
||||||
if params.local
|
if params.local
|
||||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||||
|
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||||
end
|
end
|
||||||
|
|
||||||
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
|
||||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
|
||||||
|
|
||||||
video_streams = video.video_streams
|
video_streams = video.video_streams
|
||||||
audio_streams = video.audio_streams
|
audio_streams = video.audio_streams
|
||||||
|
|
||||||
|
@ -152,11 +142,6 @@ module Invidious::Routes::Watch
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Removes non default audio tracks
|
|
||||||
audio_streams.reject! do |z|
|
|
||||||
z if z.dig?("audioTrack", "audioIsDefault") == false
|
|
||||||
end
|
|
||||||
|
|
||||||
# Older videos may not have audio sources available.
|
# Older videos may not have audio sources available.
|
||||||
# We redirect here so they're not unplayable
|
# We redirect here so they're not unplayable
|
||||||
if audio_streams.empty? && !video.live_now
|
if audio_streams.empty? && !video.live_now
|
||||||
|
@ -219,12 +204,6 @@ module Invidious::Routes::Watch
|
||||||
captions: video.captions
|
captions: video.captions
|
||||||
)
|
)
|
||||||
|
|
||||||
begin
|
|
||||||
video_url = fmt_stream[0]["url"].to_s
|
|
||||||
rescue
|
|
||||||
video_url = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
templated "watch"
|
templated "watch"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -77,7 +77,6 @@ module Invidious::Routing
|
||||||
post "/authorize_token", Routes::Account, :post_authorize_token
|
post "/authorize_token", Routes::Account, :post_authorize_token
|
||||||
get "/token_manager", Routes::Account, :token_manager
|
get "/token_manager", Routes::Account, :token_manager
|
||||||
post "/token_ajax", Routes::Account, :token_ajax
|
post "/token_ajax", Routes::Account, :token_ajax
|
||||||
get "/generate_tokens", Routes::Account, :generate_tokens
|
|
||||||
post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription
|
post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription
|
||||||
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
|
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,6 @@ struct Preferences
|
||||||
|
|
||||||
property annotations : Bool = CONFIG.default_user_preferences.annotations
|
property annotations : Bool = CONFIG.default_user_preferences.annotations
|
||||||
property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed
|
property annotations_subscribed : Bool = CONFIG.default_user_preferences.annotations_subscribed
|
||||||
property preload : Bool = CONFIG.default_user_preferences.preload
|
|
||||||
property autoplay : Bool = CONFIG.default_user_preferences.autoplay
|
property autoplay : Bool = CONFIG.default_user_preferences.autoplay
|
||||||
property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect
|
property automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect
|
||||||
|
|
||||||
|
@ -57,10 +56,6 @@ struct Preferences
|
||||||
property volume : Int32 = CONFIG.default_user_preferences.volume
|
property volume : Int32 = CONFIG.default_user_preferences.volume
|
||||||
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
|
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
|
||||||
|
|
||||||
@[YAML::Field(converter: Preferences::ProcessString)]
|
|
||||||
property po_token : String = ""
|
|
||||||
property visitor_data : String = ""
|
|
||||||
|
|
||||||
module BoolToString
|
module BoolToString
|
||||||
def self.to_json(value : String, json : JSON::Builder)
|
def self.to_json(value : String, json : JSON::Builder)
|
||||||
json.string value
|
json.string value
|
||||||
|
|
|
@ -26,6 +26,12 @@ struct Video
|
||||||
@[DB::Field(ignore: true)]
|
@[DB::Field(ignore: true)]
|
||||||
@captions = [] of Invidious::Videos::Captions::Metadata
|
@captions = [] of Invidious::Videos::Captions::Metadata
|
||||||
|
|
||||||
|
@[DB::Field(ignore: true)]
|
||||||
|
property adaptive_fmts : Array(Hash(String, JSON::Any))?
|
||||||
|
|
||||||
|
@[DB::Field(ignore: true)]
|
||||||
|
property fmt_stream : Array(Hash(String, JSON::Any))?
|
||||||
|
|
||||||
@[DB::Field(ignore: true)]
|
@[DB::Field(ignore: true)]
|
||||||
property description : String?
|
property description : String?
|
||||||
|
|
||||||
|
@ -92,24 +98,72 @@ struct Video
|
||||||
|
|
||||||
# Methods for parsing streaming data
|
# Methods for parsing streaming data
|
||||||
|
|
||||||
def fmt_stream : Array(Hash(String, JSON::Any))
|
def convert_url(fmt)
|
||||||
if formats = info.dig?("streamingData", "formats")
|
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
||||||
return formats
|
sp = cfr["sp"]
|
||||||
.as_a.map(&.as_h)
|
url = URI.parse(cfr["url"])
|
||||||
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
params = url.query_params
|
||||||
|
|
||||||
|
LOGGER.debug("Videos: Decoding '#{cfr}'")
|
||||||
|
|
||||||
|
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
|
||||||
|
params[sp] = unsig if unsig
|
||||||
else
|
else
|
||||||
return [] of Hash(String, JSON::Any)
|
url = URI.parse(fmt["url"].as_s)
|
||||||
|
params = url.query_params
|
||||||
end
|
end
|
||||||
|
|
||||||
|
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
|
||||||
|
params["n"] = n if n
|
||||||
|
|
||||||
|
if token = CONFIG.po_token
|
||||||
|
params["pot"] = token
|
||||||
|
end
|
||||||
|
|
||||||
|
params["host"] = url.host.not_nil!
|
||||||
|
if region = self.info["region"]?.try &.as_s
|
||||||
|
params["region"] = region
|
||||||
|
end
|
||||||
|
|
||||||
|
url.query_params = params
|
||||||
|
LOGGER.trace("Videos: new url is '#{url}'")
|
||||||
|
|
||||||
|
return url.to_s
|
||||||
|
rescue ex
|
||||||
|
LOGGER.debug("Videos: Error when parsing video URL")
|
||||||
|
LOGGER.trace(ex.inspect_with_backtrace)
|
||||||
|
return ""
|
||||||
end
|
end
|
||||||
|
|
||||||
def adaptive_fmts : Array(Hash(String, JSON::Any))
|
def fmt_stream
|
||||||
if formats = info.dig?("streamingData", "adaptiveFormats")
|
return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream
|
||||||
return formats
|
|
||||||
.as_a.map(&.as_h)
|
fmt_stream = info.dig?("streamingData", "formats")
|
||||||
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
|
||||||
else
|
|
||||||
return [] of Hash(String, JSON::Any)
|
fmt_stream.each do |fmt|
|
||||||
|
fmt["url"] = JSON::Any.new(self.convert_url(fmt))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
||||||
|
@fmt_stream = fmt_stream
|
||||||
|
return @fmt_stream.as(Array(Hash(String, JSON::Any)))
|
||||||
|
end
|
||||||
|
|
||||||
|
def adaptive_fmts
|
||||||
|
return @adaptive_fmts.as(Array(Hash(String, JSON::Any))) if @adaptive_fmts
|
||||||
|
|
||||||
|
fmt_stream = info.dig("streamingData", "adaptiveFormats")
|
||||||
|
.try &.as_a.map &.as_h || [] of Hash(String, JSON::Any)
|
||||||
|
|
||||||
|
fmt_stream.each do |fmt|
|
||||||
|
fmt["url"] = JSON::Any.new(self.convert_url(fmt))
|
||||||
|
end
|
||||||
|
|
||||||
|
fmt_stream.sort_by! { |f| f["width"]?.try &.as_i || 0 }
|
||||||
|
@adaptive_fmts = fmt_stream
|
||||||
|
|
||||||
|
return @adaptive_fmts.as(Array(Hash(String, JSON::Any)))
|
||||||
end
|
end
|
||||||
|
|
||||||
def video_streams
|
def video_streams
|
||||||
|
@ -294,7 +348,7 @@ struct Video
|
||||||
predicate_bool upcoming, isUpcoming
|
predicate_bool upcoming, isUpcoming
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_video(id, refresh = true, region = nil, force_refresh = false, po_token = "", visitor_data = "")
|
def get_video(id, refresh = true, region = nil, force_refresh = false)
|
||||||
if (video = Invidious::Database::Videos.select(id)) && !region
|
if (video = Invidious::Database::Videos.select(id)) && !region
|
||||||
# If record was last updated over 10 minutes ago, or video has since premiered,
|
# If record was last updated over 10 minutes ago, or video has since premiered,
|
||||||
# refresh (expire param in response lasts for 6 hours)
|
# refresh (expire param in response lasts for 6 hours)
|
||||||
|
@ -304,7 +358,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false, po_token
|
||||||
force_refresh ||
|
force_refresh ||
|
||||||
video.schema_version != Video::SCHEMA_VERSION # cache control
|
video.schema_version != Video::SCHEMA_VERSION # cache control
|
||||||
begin
|
begin
|
||||||
video = fetch_video(id, region, po_token, visitor_data)
|
video = fetch_video(id, region)
|
||||||
Invidious::Database::Videos.insert(video)
|
Invidious::Database::Videos.insert(video)
|
||||||
rescue ex
|
rescue ex
|
||||||
Invidious::Database::Videos.delete(id)
|
Invidious::Database::Videos.delete(id)
|
||||||
|
@ -312,7 +366,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false, po_token
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
video = fetch_video(id, region, po_token, visitor_data)
|
video = fetch_video(id, region)
|
||||||
Invidious::Database::Videos.insert(video) if !region
|
Invidious::Database::Videos.insert(video) if !region
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -320,11 +374,11 @@ def get_video(id, refresh = true, region = nil, force_refresh = false, po_token
|
||||||
rescue DB::Error
|
rescue DB::Error
|
||||||
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
|
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
|
||||||
# Note: All DB errors inherit from `DB::Error`
|
# Note: All DB errors inherit from `DB::Error`
|
||||||
return fetch_video(id, region, po_token, visitor_data)
|
return fetch_video(id, region)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_video(id, region, po_token, visitor_data)
|
def fetch_video(id, region)
|
||||||
info = extract_video_info(video_id: id, user_po_token: po_token, user_visitor_data: visitor_data)
|
info = extract_video_info(video_id: id)
|
||||||
|
|
||||||
if reason = info["reason"]?
|
if reason = info["reason"]?
|
||||||
if reason == "Video unavailable"
|
if reason == "Video unavailable"
|
||||||
|
|
|
@ -123,7 +123,6 @@ module Invidious::Videos
|
||||||
"Esperanto",
|
"Esperanto",
|
||||||
"Estonian",
|
"Estonian",
|
||||||
"Filipino",
|
"Filipino",
|
||||||
"Filipino (auto-generated)",
|
|
||||||
"Finnish",
|
"Finnish",
|
||||||
"French",
|
"French",
|
||||||
"French (auto-generated)",
|
"French (auto-generated)",
|
||||||
|
|
|
@ -50,17 +50,12 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_video_info(video_id : String, user_po_token, user_visitor_data)
|
def extract_video_info(video_id : String)
|
||||||
# Init client config for the API
|
# Init client config for the API
|
||||||
client_config = YoutubeAPI::ClientConfig.new
|
client_config = YoutubeAPI::ClientConfig.new
|
||||||
|
|
||||||
redis_po_token, redis_visitor_data = Tokens.get_tokens
|
|
||||||
|
|
||||||
po_token = (user_po_token if !user_po_token.empty?) || redis_po_token || CONFIG.po_token
|
|
||||||
visitor_data = (user_visitor_data if !user_visitor_data.empty?) || redis_visitor_data || CONFIG.visitor_data
|
|
||||||
|
|
||||||
# Fetch data from the player endpoint
|
# Fetch data from the player endpoint
|
||||||
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
|
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
|
||||||
|
|
||||||
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
||||||
|
|
||||||
|
@ -109,22 +104,22 @@ def extract_video_info(video_id : String, user_po_token, user_visitor_data)
|
||||||
|
|
||||||
# Don't use Android client if po_token is passed because po_token doesn't
|
# Don't use Android client if po_token is passed because po_token doesn't
|
||||||
# work for Android client.
|
# work for Android client.
|
||||||
if reason.nil? && po_token.nil?
|
if reason.nil? && CONFIG.po_token.nil?
|
||||||
# Fetch the video streams using an Android client in order to get the
|
# Fetch the video streams using an Android client in order to get the
|
||||||
# decrypted URLs and maybe fix throttling issues (#2194). See the
|
# decrypted URLs and maybe fix throttling issues (#2194). See the
|
||||||
# following issue for an explanation about decrypted URLs:
|
# following issue for an explanation about decrypted URLs:
|
||||||
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
|
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
|
||||||
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
|
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
|
||||||
new_player_response = try_fetch_streaming_data(video_id, client_config, po_token, visitor_data)
|
new_player_response = try_fetch_streaming_data(video_id, client_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Last hope
|
# Last hope
|
||||||
# Only trigger if reason found and po_token or didn't work wth Android client.
|
# Only trigger if reason found and po_token or didn't work wth Android client.
|
||||||
# TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required
|
# TvHtml5ScreenEmbed now requires sig helper for it to work but po_token is not required
|
||||||
# if the IP address is not blocked.
|
# if the IP address is not blocked.
|
||||||
if po_token.nil? && reason || po_token.nil? && new_player_response.nil?
|
if CONFIG.po_token && reason || CONFIG.po_token.nil? && new_player_response.nil?
|
||||||
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
|
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
|
||||||
new_player_response = try_fetch_streaming_data(video_id, client_config, po_token, visitor_data)
|
new_player_response = try_fetch_streaming_data(video_id, client_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Replace player response and reset reason
|
# Replace player response and reset reason
|
||||||
|
@ -137,30 +132,19 @@ def extract_video_info(video_id : String, user_po_token, user_visitor_data)
|
||||||
params.delete("reason")
|
params.delete("reason")
|
||||||
end
|
end
|
||||||
|
|
||||||
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
|
{"captions", "playabilityStatus", "playerConfig", "storyboards", "streamingData"}.each do |f|
|
||||||
params[f] = player_response[f] if player_response[f]?
|
params[f] = player_response[f] if player_response[f]?
|
||||||
end
|
end
|
||||||
|
|
||||||
# Convert URLs, if those are present
|
|
||||||
if streaming_data = player_response["streamingData"]?
|
|
||||||
%w[formats adaptiveFormats].each do |key|
|
|
||||||
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
|
||||||
format.as_h["url"] = JSON::Any.new(convert_url(format, po_token))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
params["streamingData"] = streaming_data
|
|
||||||
end
|
|
||||||
|
|
||||||
# Data structure version, for cache control
|
# Data structure version, for cache control
|
||||||
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
|
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
|
||||||
|
|
||||||
return params
|
return params
|
||||||
end
|
end
|
||||||
|
|
||||||
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig, po_token, visitor_data) : Hash(String, JSON::Any)?
|
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
||||||
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
|
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)
|
||||||
|
|
||||||
playability_status = response["playabilityStatus"]["status"]
|
playability_status = response["playabilityStatus"]["status"]
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
||||||
|
@ -459,37 +443,3 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
|
||||||
|
|
||||||
return params
|
return params
|
||||||
end
|
end
|
||||||
|
|
||||||
private def convert_url(fmt, po_token)
|
|
||||||
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
|
||||||
sp = cfr["sp"]
|
|
||||||
url = URI.parse(cfr["url"])
|
|
||||||
params = url.query_params
|
|
||||||
|
|
||||||
LOGGER.debug("convert_url: Decoding '#{cfr}'")
|
|
||||||
|
|
||||||
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
|
|
||||||
params[sp] = unsig if unsig
|
|
||||||
else
|
|
||||||
url = URI.parse(fmt["url"].as_s)
|
|
||||||
params = url.query_params
|
|
||||||
end
|
|
||||||
|
|
||||||
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
|
|
||||||
params["n"] = n if n
|
|
||||||
|
|
||||||
if !po_token.nil?
|
|
||||||
params["pot"] = po_token
|
|
||||||
elsif token = CONFIG.po_token
|
|
||||||
params["pot"] = token
|
|
||||||
end
|
|
||||||
|
|
||||||
url.query_params = params
|
|
||||||
LOGGER.trace("convert_url: new url is '#{url}'")
|
|
||||||
|
|
||||||
return url.to_s
|
|
||||||
rescue ex
|
|
||||||
LOGGER.debug("convert_url: Error when parsing video URL")
|
|
||||||
LOGGER.trace(ex.inspect_with_backtrace)
|
|
||||||
return ""
|
|
||||||
end
|
|
||||||
|
|
|
@ -2,7 +2,6 @@ struct VideoPreferences
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
property annotations : Bool
|
property annotations : Bool
|
||||||
property preload : Bool
|
|
||||||
property autoplay : Bool
|
property autoplay : Bool
|
||||||
property comments : Array(String)
|
property comments : Array(String)
|
||||||
property continue : Bool
|
property continue : Bool
|
||||||
|
@ -29,7 +28,6 @@ end
|
||||||
|
|
||||||
def process_video_params(query, preferences)
|
def process_video_params(query, preferences)
|
||||||
annotations = query["iv_load_policy"]?.try &.to_i?
|
annotations = query["iv_load_policy"]?.try &.to_i?
|
||||||
preload = query["preload"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
|
||||||
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
comments = query["comments"]?.try &.split(",").map(&.downcase)
|
comments = query["comments"]?.try &.split(",").map(&.downcase)
|
||||||
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
@ -52,7 +50,6 @@ def process_video_params(query, preferences)
|
||||||
if preferences
|
if preferences
|
||||||
# region ||= preferences.region
|
# region ||= preferences.region
|
||||||
annotations ||= preferences.annotations.to_unsafe
|
annotations ||= preferences.annotations.to_unsafe
|
||||||
preload ||= preferences.preload.to_unsafe
|
|
||||||
autoplay ||= preferences.autoplay.to_unsafe
|
autoplay ||= preferences.autoplay.to_unsafe
|
||||||
comments ||= preferences.comments
|
comments ||= preferences.comments
|
||||||
continue ||= preferences.continue.to_unsafe
|
continue ||= preferences.continue.to_unsafe
|
||||||
|
@ -73,7 +70,6 @@ def process_video_params(query, preferences)
|
||||||
end
|
end
|
||||||
|
|
||||||
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
|
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
|
||||||
preload ||= CONFIG.default_user_preferences.preload.to_unsafe
|
|
||||||
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
|
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
|
||||||
comments ||= CONFIG.default_user_preferences.comments
|
comments ||= CONFIG.default_user_preferences.comments
|
||||||
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
|
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
|
||||||
|
@ -93,7 +89,6 @@ def process_video_params(query, preferences)
|
||||||
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe
|
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe
|
||||||
|
|
||||||
annotations = annotations == 1
|
annotations = annotations == 1
|
||||||
preload = preload == 1
|
|
||||||
autoplay = autoplay == 1
|
autoplay = autoplay == 1
|
||||||
continue = continue == 1
|
continue = continue == 1
|
||||||
continue_autoplay = continue_autoplay == 1
|
continue_autoplay = continue_autoplay == 1
|
||||||
|
@ -133,7 +128,6 @@ def process_video_params(query, preferences)
|
||||||
|
|
||||||
params = VideoPreferences.new({
|
params = VideoPreferences.new({
|
||||||
annotations: annotations,
|
annotations: annotations,
|
||||||
preload: preload,
|
|
||||||
autoplay: autoplay,
|
autoplay: autoplay,
|
||||||
comments: comments,
|
comments: comments,
|
||||||
continue: continue,
|
continue: continue,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>"
|
<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>"
|
||||||
id="player" class="on-video_player video-js player-style-<%= params.player_style %>"
|
id="player" class="on-video_player video-js player-style-<%= params.player_style %>"
|
||||||
preload="<% if params.preload %>auto<% else %>none<% end %>"
|
|
||||||
<% if params.autoplay %>autoplay<% end %>
|
<% if params.autoplay %>autoplay<% end %>
|
||||||
<% if params.video_loop %>loop<% end %>
|
<% if params.video_loop %>loop<% end %>
|
||||||
<% if params.controls %>controls<% end %>>
|
<% if params.controls %>controls<% end %>>
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<%
|
<%
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
||||||
current_backend = env.request.cookies["SERVER_ID"]?.try &.value
|
|
||||||
%>
|
%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="<%= locale %>">
|
<html lang="<%= locale %>">
|
||||||
|
@ -105,26 +104,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if !CONFIG.backends.empty? %>
|
<% if CONFIG.backends %>
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<b>Switch Backend:</b>
|
<b>Switch Backend:</b>
|
||||||
<% CONFIG.backends.each do | backend | %>
|
<% CONFIG.backends.each do | backend | %>
|
||||||
<% backend = backend.split(CONFIG.backends_delimiter) %>
|
<a href="/switchbackend?backend_id=<%= backend %>">
|
||||||
<% if current_backend == backend[0] %>
|
Backend<%= HTML.escape(backend) %>
|
||||||
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="text-decoration-line: underline; display: inline-block;">
|
</a> |
|
||||||
Backend<%= HTML.escape(backend[0]) %>
|
|
||||||
<% if backend.size == 2 %>
|
|
||||||
<%= HTML.escape(backend[1]) %>
|
|
||||||
<% end %>
|
|
||||||
</a> <span> | </span>
|
|
||||||
<% else %>
|
|
||||||
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="display: inline-block;">
|
|
||||||
Backend<%= HTML.escape(backend[0]) %>
|
|
||||||
<% if backend.size == 2 %>
|
|
||||||
<%= HTML.escape(backend[1]) %>
|
|
||||||
<% end %>
|
|
||||||
</a> <span> | </span>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -310,7 +296,6 @@
|
||||||
</div>
|
</div>
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="footer-footer">
|
<div class="footer-footer">
|
||||||
<div class="box">You are currently using Backend: <%= current_backend %></p>
|
|
||||||
<span class="left">
|
<span class="left">
|
||||||
<% if CONFIG.modified_source_code_url %>
|
<% if CONFIG.modified_source_code_url %>
|
||||||
<%= translate(locale, "footer_current_version_modified") %>
|
<%= translate(locale, "footer_current_version_modified") %>
|
||||||
|
|
|
@ -12,11 +12,6 @@
|
||||||
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
|
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="preload"><%= translate(locale, "preferences_preload_label") %></label>
|
|
||||||
<input name="preload" id="preload" type="checkbox" <% if preferences.preload %>checked<% end %>>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
|
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
|
||||||
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>
|
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>
|
||||||
|
@ -126,24 +121,6 @@
|
||||||
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
|
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if !CONFIG.ignore_user_tokens %>
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="po_token"><%= translate(locale, "preferences_po_token") %></label>
|
|
||||||
<input name="po_token" id="po_token" type="text" value="<%= preferences.po_token %>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<label for="visitor_data"><%= translate(locale, "preferences_visitor_data") %></label>
|
|
||||||
<input name="visitor_data" id="visitor_data" type="text" value="<%= preferences.visitor_data %>">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<% if env.get?("user") %>
|
|
||||||
<div class="pure-control-group">
|
|
||||||
<a href="/generate_tokens?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Generate po_token and visitor_data for your account") %></a>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<legend><%= translate(locale, "preferences_category_visual") %></legend>
|
<legend><%= translate(locale, "preferences_category_visual") %></legend>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
<% content_for "header" do %>
|
|
||||||
<title><%= translate(locale, "Invidious token generator") %> - Invidious</title>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="pure-g">
|
|
||||||
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
|
||||||
<div class="pure-u-1 pure-u-lg-3-5">
|
|
||||||
<div class="h-box">
|
|
||||||
<p>po_token and visitor_data successfully generated!</p>
|
|
||||||
<p>po_token: <%= po_token %></p>
|
|
||||||
<p>visitor_data: <%= visitor_data %></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
|
||||||
</div>
|
|
|
@ -13,13 +13,11 @@
|
||||||
<meta property="og:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
|
<meta property="og:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
|
||||||
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>">
|
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>">
|
||||||
<meta property="og:type" content="video.other">
|
<meta property="og:type" content="video.other">
|
||||||
<!-- This shouldn't be empty, ever. -->
|
<meta property="og:video:url" content="<%= HOST_URL %>/embed/<%= video.id %>">
|
||||||
<meta property="og:video" content="<%= video_url %>">
|
<meta property="og:video:secure_url" content="<%= HOST_URL %>/embed/<%= video.id %>">
|
||||||
<meta property="og:video:url" content="<%= video_url %>">
|
<meta property="og:video:type" content="text/html">
|
||||||
<meta property="og:video:secure_url" content="<%= video_url %>">
|
<meta property="og:video:width" content="1280">
|
||||||
<meta property="og:video:type" content="video/mp4">
|
<meta property="og:video:height" content="720">
|
||||||
<meta property="og:video:width" content="640">
|
|
||||||
<meta property="og:video:height" content="360">
|
|
||||||
<meta name="twitter:card" content="player">
|
<meta name="twitter:card" content="player">
|
||||||
<meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
|
<meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
|
||||||
<meta name="twitter:title" content="<%= title %>">
|
<meta name="twitter:title" content="<%= title %>">
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
# Mapping of subdomain => YoutubeConnectionPool
|
def add_yt_headers(request)
|
||||||
# This is needed as we may need to access arbitrary subdomains of ytimg
|
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
|
||||||
private YTIMG_POOLS = {} of String => YoutubeConnectionPool
|
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
|
||||||
|
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||||
|
request.headers["Accept-Language"] ||= "en-us,en;q=0.5"
|
||||||
|
|
||||||
|
# Preserve original cookies and add new YT consent cookie for EU servers
|
||||||
|
request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}"
|
||||||
|
if !CONFIG.cookies.empty?
|
||||||
|
request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
struct YoutubeConnectionPool
|
struct YoutubeConnectionPool
|
||||||
property! url : URI
|
property! url : URI
|
||||||
|
@ -15,16 +26,12 @@ struct YoutubeConnectionPool
|
||||||
|
|
||||||
def client(&)
|
def client(&)
|
||||||
conn = pool.checkout
|
conn = pool.checkout
|
||||||
# Proxy needs to be reinstated every time we get a client from the pool
|
|
||||||
conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
response = yield conn
|
response = yield conn
|
||||||
rescue ex
|
rescue ex
|
||||||
conn.close
|
conn.close
|
||||||
|
|
||||||
conn = HTTP::Client.new(url)
|
conn = HTTP::Client.new(url)
|
||||||
conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
|
||||||
conn.family = CONFIG.force_resolve
|
conn.family = CONFIG.force_resolve
|
||||||
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
||||||
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||||
|
@ -47,21 +54,6 @@ struct YoutubeConnectionPool
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def add_yt_headers(request)
|
|
||||||
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
|
|
||||||
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"
|
|
||||||
|
|
||||||
request.headers["Accept-Charset"] ||= "ISO-8859-1,utf-8;q=0.7,*;q=0.7"
|
|
||||||
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
|
||||||
request.headers["Accept-Language"] ||= "en-us,en;q=0.5"
|
|
||||||
|
|
||||||
# Preserve original cookies and add new YT consent cookie for EU servers
|
|
||||||
request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}"
|
|
||||||
if !CONFIG.cookies.empty?
|
|
||||||
request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def make_client(url : URI, region = nil, force_resolve : Bool = false)
|
def make_client(url : URI, region = nil, force_resolve : Bool = false)
|
||||||
client = HTTP::Client.new(url)
|
client = HTTP::Client.new(url)
|
||||||
|
|
||||||
|
@ -85,31 +77,3 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false, &)
|
||||||
client.close
|
client.close
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_configured_http_proxy_client
|
|
||||||
# This method is only called when configuration for an HTTP proxy are set
|
|
||||||
config_proxy = CONFIG.http_proxy.not_nil!
|
|
||||||
|
|
||||||
return HTTP::Proxy::Client.new(
|
|
||||||
config_proxy.host,
|
|
||||||
config_proxy.port,
|
|
||||||
|
|
||||||
username: config_proxy.user,
|
|
||||||
password: config_proxy.password,
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Fetches a HTTP pool for the specified subdomain of ytimg.com
|
|
||||||
#
|
|
||||||
# Creates a new one when the specified pool for the subdomain does not exist
|
|
||||||
def get_ytimg_pool(subdomain)
|
|
||||||
if pool = YTIMG_POOLS[subdomain]?
|
|
||||||
return pool
|
|
||||||
else
|
|
||||||
LOGGER.info("ytimg_pool: Creating a new HTTP pool for \"https://#{subdomain}.ytimg.com\"")
|
|
||||||
pool = YoutubeConnectionPool.new(URI.parse("https://#{subdomain}.ytimg.com"), capacity: CONFIG.pool_size)
|
|
||||||
YTIMG_POOLS[subdomain] = pool
|
|
||||||
|
|
||||||
return pool
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -108,30 +108,21 @@ private module Parsers
|
||||||
length_seconds = 0
|
length_seconds = 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
live_now = false
|
||||||
|
premium = false
|
||||||
|
|
||||||
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
|
premiere_timestamp = item_contents.dig?("upcomingEventData", "startTime").try { |t| Time.unix(t.as_s.to_i64) }
|
||||||
badges = VideoBadges::None
|
|
||||||
item_contents["badges"]?.try &.as_a.each do |badge|
|
item_contents["badges"]?.try &.as_a.each do |badge|
|
||||||
b = badge["metadataBadgeRenderer"]
|
b = badge["metadataBadgeRenderer"]
|
||||||
case b["label"].as_s
|
case b["label"].as_s
|
||||||
when "LIVE"
|
when "LIVE NOW"
|
||||||
badges |= VideoBadges::LiveNow
|
live_now = true
|
||||||
when "New"
|
when "New", "4K", "CC"
|
||||||
badges |= VideoBadges::New
|
# TODO
|
||||||
when "4K"
|
|
||||||
badges |= VideoBadges::FourK
|
|
||||||
when "8K"
|
|
||||||
badges |= VideoBadges::EightK
|
|
||||||
when "VR180"
|
|
||||||
badges |= VideoBadges::VR180
|
|
||||||
when "360°"
|
|
||||||
badges |= VideoBadges::VR360
|
|
||||||
when "3D"
|
|
||||||
badges |= VideoBadges::ThreeD
|
|
||||||
when "CC"
|
|
||||||
badges |= VideoBadges::ClosedCaptions
|
|
||||||
when "Premium"
|
when "Premium"
|
||||||
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
|
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
|
||||||
badges |= VideoBadges::Premium
|
premium = true
|
||||||
else nil # Ignore
|
else nil # Ignore
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -145,9 +136,10 @@ private module Parsers
|
||||||
views: view_count,
|
views: view_count,
|
||||||
description_html: description_html,
|
description_html: description_html,
|
||||||
length_seconds: length_seconds,
|
length_seconds: length_seconds,
|
||||||
|
live_now: live_now,
|
||||||
|
premium: premium,
|
||||||
premiere_timestamp: premiere_timestamp,
|
premiere_timestamp: premiere_timestamp,
|
||||||
author_verified: author_verified,
|
author_verified: author_verified,
|
||||||
badges: badges,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -571,9 +563,10 @@ private module Parsers
|
||||||
views: view_count,
|
views: view_count,
|
||||||
description_html: "",
|
description_html: "",
|
||||||
length_seconds: duration,
|
length_seconds: duration,
|
||||||
|
live_now: false,
|
||||||
|
premium: false,
|
||||||
premiere_timestamp: Time.unix(0),
|
premiere_timestamp: Time.unix(0),
|
||||||
author_verified: false,
|
author_verified: false,
|
||||||
badges: VideoBadges::None,
|
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ module UrlSanitizer
|
||||||
new_uri.path = "/watch"
|
new_uri.path = "/watch"
|
||||||
|
|
||||||
new_params = copy_params(unsafe_uri.query_params, :watch)
|
new_params = copy_params(unsafe_uri.query_params, :watch)
|
||||||
new_params["v"] = breadcrumbs[0]
|
new_params["id"] = breadcrumbs[0]
|
||||||
|
|
||||||
new_uri.query_params = new_params
|
new_uri.query_params = new_params
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,8 +3,6 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
module YoutubeAPI
|
module YoutubeAPI
|
||||||
@@visitor_data : String = ""
|
|
||||||
|
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
|
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
|
||||||
|
@ -322,9 +320,7 @@ module YoutubeAPI
|
||||||
client_context["client"]["platform"] = platform
|
client_context["client"]["platform"] = platform
|
||||||
end
|
end
|
||||||
|
|
||||||
if !@@visitor_data.empty?
|
if CONFIG.visitor_data.is_a?(String)
|
||||||
client_context["client"]["visitorData"] = @@visitor_data
|
|
||||||
elsif CONFIG.visitor_data.is_a?(String)
|
|
||||||
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
|
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -459,13 +455,8 @@ module YoutubeAPI
|
||||||
video_id : String,
|
video_id : String,
|
||||||
*, # Force the following parameters to be passed by name
|
*, # Force the following parameters to be passed by name
|
||||||
params : String,
|
params : String,
|
||||||
client_config : ClientConfig | Nil = nil,
|
client_config : ClientConfig | Nil = nil
|
||||||
po_token : String | Nil,
|
|
||||||
visitor_data : String | Nil,
|
|
||||||
)
|
)
|
||||||
if visitor_data
|
|
||||||
@@visitor_data = visitor_data
|
|
||||||
end
|
|
||||||
# Playback context, separate because it can be different between clients
|
# Playback context, separate because it can be different between clients
|
||||||
playback_ctx = {
|
playback_ctx = {
|
||||||
"html5Preference" => "HTML5_PREF_WANTS",
|
"html5Preference" => "HTML5_PREF_WANTS",
|
||||||
|
@ -491,7 +482,7 @@ module YoutubeAPI
|
||||||
"contentPlaybackContext" => playback_ctx,
|
"contentPlaybackContext" => playback_ctx,
|
||||||
},
|
},
|
||||||
"serviceIntegrityDimensions" => {
|
"serviceIntegrityDimensions" => {
|
||||||
"poToken" => po_token || CONFIG.po_token,
|
"poToken" => CONFIG.po_token,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -605,7 +596,7 @@ module YoutubeAPI
|
||||||
def _post_json(
|
def _post_json(
|
||||||
endpoint : String,
|
endpoint : String,
|
||||||
data : Hash,
|
data : Hash,
|
||||||
client_config : ClientConfig | Nil,
|
client_config : ClientConfig | Nil
|
||||||
) : Hash(String, JSON::Any)
|
) : Hash(String, JSON::Any)
|
||||||
# Use the default client config if nil is passed
|
# Use the default client config if nil is passed
|
||||||
client_config ||= DEFAULT_CLIENT_CONFIG
|
client_config ||= DEFAULT_CLIENT_CONFIG
|
||||||
|
@ -625,9 +616,7 @@ module YoutubeAPI
|
||||||
headers["User-Agent"] = user_agent
|
headers["User-Agent"] = user_agent
|
||||||
end
|
end
|
||||||
|
|
||||||
if !@@visitor_data.empty?
|
if CONFIG.visitor_data.is_a?(String)
|
||||||
headers["X-Goog-Visitor-Id"] = @@visitor_data
|
|
||||||
elsif CONFIG.visitor_data.is_a?(String)
|
|
||||||
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
|
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue