Compare commits

..

8 commits

Author SHA1 Message Date
e1a1bf0f79
Feat: Experimental support for external videoplayback proxies 2024-09-27 19:09:50 -03:00
7ded7455b0
Logger: Add color support for different log levels
All checks were successful
Invidious CI / build (push) Successful in 7m41s
2024-09-19 21:53:01 -03:00
53f0a5b1e0
fixup! SigHelper: Handle all errors, lower time between retries
All checks were successful
Invidious CI / build (push) Successful in 8m43s
2024-09-19 15:17:04 -03:00
34a11fe1bb
SigHelper: Handle all errors, lower time between retries
All checks were successful
Invidious CI / build (push) Successful in 9m1s
2024-09-19 14:43:47 -03:00
2db9396dea
SigHelper: Reconnect to signature helper
All checks were successful
Invidious CI / build (push) Successful in 14m43s
2024-09-18 18:14:28 -03:00
Emilien Devos
0c3e5baab0
decrease buffer seconds for saving bandwidth
All checks were successful
Invidious CI / build (push) Successful in 7m43s
2024-09-17 18:02:09 -03:00
73ba53a327
fixup! Feat: backend supports with cookies
All checks were successful
Invidious CI / build (push) Successful in 23m13s
2024-09-16 23:52:14 -03:00
00bcf53d0f
Feat: backend supports with cookies
All checks were successful
Invidious CI / build (push) Successful in 13m45s
2024-09-15 14:42:06 -03:00
98 changed files with 736 additions and 1889 deletions

View file

@ -38,9 +38,6 @@ Style/RedundantBegin:
Style/RedundantReturn:
Enabled: false
Style/RedundantNext:
Enabled: false
Style/ParenthesesAroundCondition:
Enabled: false

View file

@ -8,8 +8,6 @@ on:
push:
branches:
- "master"
- "experimental"
- "experimental2"
jobs:
build:
@ -37,15 +35,13 @@ jobs:
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') }}
- uses: https://code.forgejo.org/docker/build-push-action@v6
- uses: https://code.forgejo.org/docker/build-push-action@v5
name: Build images
with:
context: .
file: docker/Dockerfile
tags: ${{ steps.meta.outputs.tags }}
platforms: linux/amd64
# cache-from: type=gha
# cache-to: type=gha,mode=max
push: true
build-args: |
"release=1"

2
.github/CODEOWNERS vendored
View file

@ -6,7 +6,7 @@ docker/ @unixfox
kubernetes/ @unixfox
README.md @thefrenchghosty
config/config.example.yml @SamantazFox @unixfox
config/config.example.yml @thefrenchghosty @SamantazFox @unixfox
scripts/ @syeopite
shards.lock @syeopite

View file

@ -1,7 +1,6 @@
name: Build and release container
on:
workflow_dispatch:
push:
tags:
- "v*"
@ -47,11 +46,9 @@ jobs:
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
flavor: |
latest=false
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
@ -73,11 +70,10 @@ jobs:
with:
images: quay.io/invidious/invidious
flavor: |
latest=false
suffix=-arm64
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w

View file

@ -38,11 +38,10 @@ jobs:
matrix:
stable: [true]
crystal:
- 1.9.2
- 1.10.1
- 1.11.2
- 1.12.1
- 1.13.2
- 1.14.0
include:
- crystal: nightly
stable: false
@ -52,11 +51,6 @@ jobs:
with:
submodules: true
- name: Install required APT packages
run: |
sudo apt install -y libsqlite3-dev
shell: bash
- name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.0
with:
@ -65,9 +59,7 @@ jobs:
- name: Cache Shards
uses: actions/cache@v3
with:
path: |
./lib
./bin
path: ./lib
key: shards-${{ hashFiles('shard.lock') }}
- name: Install Shards
@ -79,6 +71,14 @@ jobs:
- name: Run tests
run: crystal spec
- name: Run lint
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
- name: Build
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
@ -124,12 +124,8 @@ jobs:
- name: Test Docker
run: while curl -Isf http://localhost:3000; do sleep 1; done
lint:
ameba_lint:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v4
with:
@ -149,18 +145,7 @@ jobs:
key: shards-${{ hashFiles('shard.lock') }}
- name: Install Shards
run: |
if ! shards check; then
shards install
fi
- name: Check Crystal formatter compliance
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
run: shards install
- name: Run Ameba linter
run: bin/ameba

View file

@ -13,11 +13,14 @@ jobs:
- uses: actions/stale@v8
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 730
days-before-pr-stale: -1
days-before-close: 60
days-before-stale: 365
days-before-pr-stale: 90
days-before-close: 30
exempt-pr-labels: blocked,exempt-stale
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
stale-issue-label: "stale"
stale-pr-label: "stale"
ascending: true
# Exempt the following types of issues from being staled
exempt-issue-labels: "feature-request,enhancement,discussion,exempt-stale"
# Never mark feature requests/enhancements as stale
exempt-issue-labels: "feature-request,enhancement,exempt-stale"

View file

@ -1,333 +1,6 @@
# CHANGELOG
## vX.Y.0 (future)
## v2.20241110.0
### Wrap-up
This release is most importantly here to fix to the annoying "Youtube API returned error 400"
error that prevented all channel pages from loading.
If you're updating from the previous release, it provides no improvements on the ability to play
videos. If updating from a commit in-between release, it removes the "Please sign in" error caused
by a previous attempt at restoring video playback on large instances.
In the preferences, a new option allows for control of video preload. When enabled, this option
tells the browser to load the video as soon as the page is loaded (this used to be the default).
When disabled, the video starts loading only when the "play" button is pressed.
New interface languages available: Bulgarian, Welsh and Lombard
New dependency required: `tzdata`.
An HTTP proxy can be configured directly in Invidious, if needed. \
**NOTE:** In that case, it is recommended to comment out `force_resolve`.
### New features & important changes
#### For users
* Channels: Fix "Youtube API returned error 400" error preventing channel pages from loading
* Channels: Shorts can now be sorted by "newest", "oldest" and "popular"
* Preferences: Addition of the new "preload" option
* New interface languages available: Bulgarian, Welsh and Lombard
* Added "Filipino (auto-generated)" to the list of caption languages available
* Lots of new translations from Weblate
#### For instance owners
* Allow the configuration of an HTTP proxy to talk to Youtube
* Invidious tries to reconnect to `inv_sig_helper` if the socket is closed
* The instance list is downloaded in the background to improve redirection speed
* New `colorize_logs` option makes each log level a different color
#### For developpers
* `/api/v1/channels/{id}/shorts` now supports the `sort-by` parameter with the following values:
`newest`, `oldest` and `popular`
* Older `/api/v1/channels/xyz/{id}` (tab name before UCID) were removed
* API/Search: New video metadata available: `isNew`, `is4k`, `is8k`, `isVr180`, `isVr360`,
`is3d` and `hasCaptions`
### Bugs fixed
#### User-side
* Channels: The second page of shorts now loads as expected
* Channels: Fixed intermittent empty "playlists" tab
* Search: Fixed `youtu.be` URLs not being properly redirected to the watch page
* Fixed `DB::MappingException` error on the subscriptions feed (due to missing `tzdata` in docker)
* Switching to another instance is much faster
* Fixed an "invalid byte sequence" error when subscribing to a playlist
* Videos: Playback URLs were sometimes broken when cached and `inv_sig_helper` was used
#### For instance owners
* Fix `force_resolve` being ignored in some cases
#### API
* API/Videos: Fixed `live_now` and `premiere_timestamp` sometimes not having the right values
### Full list of pull requests merged since the last release (newest first)
* API: Add "sort_by" parameter to channels/shorts endpoint ([#5071], thanks @iBicha)
* Docker: Install tzdata in Dockerfile ([#5070], by @SamantazFox)
* Videos: Stop using TVHTML5_SIMPLY_EMBEDDED_PLAYER ([#5063], thanks @unixfox)
* Routing: Deprecate old channel API routes ([#5045], by @SamantazFox)
* Videos: use WEB client instead of WEB CREATOR ([#4984], thanks @unixfox)
* Parsers: Fix parsing live_now and premiere_timestamp ([#4934], thanks @absidue)
* Stale bot updates ([#5060], thanks @syeopite)
* Channels: Fix "Youtube API returned error 400" ([#5059], by @SamantazFox)
* Channels: Fix for live videos ([#5027], thanks @iBicha)
* Locales: Add Bulgarian, Welsh and Lombard to the list ([#5046], by @SamantazFox)
* Shards: Update database dependencies ([#5034], by @SamantazFox)
* Logger: Add color support for different log levels ([#4931], thanks @Fijxu)
* Fix named arg syntax when passing force_resolve ([#4754], thanks @syeopite)
* Use make_client instead of calling HTTP::Client ([#4709], thanks @syeopite)
* Add "Filipino (auto-generated)" to the list of caption languages ([#4995], by @SamantazFox)
* Makefile: Add MT option to enable the 'preview_mt' flag ([#4993], by @SamantazFox)
* SigHelper: Reconnect to signature helper ([#4991], thanks @Fijxu)
* Fix player menus hiding onHover ready ([#4750], thanks @giacomocerquone)
* Use connection pools when requesting images from YouTube ([#4326], thanks @syeopite)
* Add support for using Invidious through a HTTP Proxy ([#4270], thanks @syeopite)
* Search: Fix 'youtu.be' URLs in sanitizer ([#4894], by @SamantazFox)
* Ameba: Disable Style/RedundantNext rule ([#4888], thanks @syeopite)
* Playlists: Fix 'invalid byte sequence' error when subscribing ([#4887], thanks @DmitrySandalov)
* Parse more metadata badges for SearchVideos ([#4863], thanks @ChunkyProgrammer)
* Translations update from Hosted Weblate ([#4862], thanks to our many translators)
* Videos: Convert URL before putting result into cache ([#4850], by @SamantazFox)
* HTML: Add error message to "search issues on GitHub" link ([#4652], thanks @tracedgod)
* Preferences: Add option to control preloading of video data ([#4122], thanks @Nerdmind)
* Performance: Improve speed of automatic instance redirection ([#4193], thanks @syeopite)
* Remove myself from CODEOWNERS on the config file ([#4942], by @TheFrenchGhosty)
* Update latest version WEB_CREATOR + fix comment web embed ([#4930], thanks @unixfox)
* use WEB_CREATOR when po_token with WEB_EMBED as a fallback ([#4928], thanks @unixfox)
* Revert "use web screen embed for fixing potoken functionality"
* use web screen embed for fixing potoken functionality ([#4923], thanks @unixfox)
[#4122]: https://github.com/iv-org/invidious/pull/4122
[#4193]: https://github.com/iv-org/invidious/pull/4193
[#4270]: https://github.com/iv-org/invidious/pull/4270
[#4326]: https://github.com/iv-org/invidious/pull/4326
[#4652]: https://github.com/iv-org/invidious/pull/4652
[#4709]: https://github.com/iv-org/invidious/pull/4709
[#4750]: https://github.com/iv-org/invidious/pull/4750
[#4754]: https://github.com/iv-org/invidious/pull/4754
[#4850]: https://github.com/iv-org/invidious/pull/4850
[#4862]: https://github.com/iv-org/invidious/pull/4862
[#4863]: https://github.com/iv-org/invidious/pull/4863
[#4887]: https://github.com/iv-org/invidious/pull/4887
[#4888]: https://github.com/iv-org/invidious/pull/4888
[#4894]: https://github.com/iv-org/invidious/pull/4894
[#4923]: https://github.com/iv-org/invidious/pull/4923
[#4928]: https://github.com/iv-org/invidious/pull/4928
[#4930]: https://github.com/iv-org/invidious/pull/4930
[#4931]: https://github.com/iv-org/invidious/pull/4931
[#4934]: https://github.com/iv-org/invidious/pull/4934
[#4942]: https://github.com/iv-org/invidious/pull/4942
[#4984]: https://github.com/iv-org/invidious/pull/4984
[#4991]: https://github.com/iv-org/invidious/pull/4991
[#4993]: https://github.com/iv-org/invidious/pull/4993
[#4995]: https://github.com/iv-org/invidious/pull/4995
[#5027]: https://github.com/iv-org/invidious/pull/5027
[#5034]: https://github.com/iv-org/invidious/pull/5034
[#5045]: https://github.com/iv-org/invidious/pull/5045
[#5046]: https://github.com/iv-org/invidious/pull/5046
[#5059]: https://github.com/iv-org/invidious/pull/5059
[#5060]: https://github.com/iv-org/invidious/pull/5060
[#5063]: https://github.com/iv-org/invidious/pull/5063
[#5070]: https://github.com/iv-org/invidious/pull/5070
[#5071]: https://github.com/iv-org/invidious/pull/5071
## v2.20240825.2 (2024-08-26)
This releases fixes the container tags pushed on quay.io.
Previously, the ARM64 build was released under the `latest` tag, instead of `latest-arm64`.
### Full list of pull requests merged since the last release (newest first)
CI: Fix docker container tags ([#4883], by @SamantazFox)
[#4877]: https://github.com/iv-org/invidious/pull/4877
## v2.20240825.1 (2024-08-25)
Add patch component to be [semver] compliant and make github actions happy.
[semver]: https://semver.org/
### Full list of pull requests merged since the last release (newest first)
Allow manual trigger of release-container build ([#4877], thanks @syeopite)
[#4877]: https://github.com/iv-org/invidious/pull/4877
## v2.20240825.0 (2024-08-25)
### New features & important changes
#### For users
* The search bar now has a button that you can click!
* Youtube URLs can be pasted directly in the search bar. Prepend search query with a
backslash (`\`) to disable that feature (useful if you need to search for a video whose
title contains some youtube URL).
* On the channel page the "streams" tab can be sorted by either: "newest", "oldest" or "popular"
* Lots of translations have been updated (thanks to our contributors on Weblate!)
* Videos embedded in local HTML files (e.g: a webpage saved from a blog) can now be played
#### For instance owners
* Invidious now has the ability to provide a `po_token` and `visitordata` to Youtube in order to
circumvent current Youtube restrictions.
* Invidious can use an (optional) external signature server like [inv_sig_helper]. Please note that
some videos can't be played without that signature server.
* The Helm charts were moved to a separate repo: https://github.com/iv-org/invidious-helm-chart
* We have changed how containers are released: the `latest` tag now tracks tagged releases, whereas
the `master` tag tracks the most recent commits of the `master` branch ("nightly" builds).
[inv_sig_helper]: https://github.com/iv-org/inv_sig_helper
#### For developpers
* The versions of Crystal that we test in CI/CD are now: `1.9.2`, `1.10.1`, `1.11.2`, `1.12.1`.
Please note that due to a bug in the `libxml` bindings (See [#4256]), versions prior to `1.10.0`
are not recommended to use.
* Thanks to @syeopite, the code is now [ameba] compliant.
* Ameba is part of our CI/CD pipeline, and its rules will be enforced in future PRs.
* The transcript code has been rewritten to permit transcripts as a feature rather than being
only a workaround for captions. Trancripts feature is coming soon!
* Various fixes regarding the logic interacting with Youtube
* The `sort_by` parameter can be used on the `/api/v1/channels/{id}/streams` endpoint. Accepted
values are: "newest", "oldest" and "popular"
[ameba]: https://github.com/crystal-ameba/ameba
[#4256]: https://github.com/iv-org/invidious/issues/4256
### Bugs fixed
#### User-side
* Channels: fixed broken "subscribers" and "views" counters
* Watch page: playback position is reset at the end of a video, so that the next time this video
is watched, it will start from the beginning rather than 15 seconds before the end
* Watch page: the items in the "add to playlist" drop down are now sorted alphabetically
* Videos: the "genre" URL is now always pointing to a valid webpage
* Playlists: Fixed `Could not parse N episodes` error on podcast playlists
* All external links should now have the [`rel`] attibute set to `noreferrer noopener` for
increased privacy.
* Preferences: Fixed the admin-only "modified source code" input being ignored
* Watch/channel pages: use the full image URL in `og:image` and `twitter:image` meta tags
[`rel`]: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel
#### API
* fixed the `local` parameter not applying to `formatStreams` on `/api/v1/videos/{id}`
* fixed an `Index out of bounds` error hapenning when a playlist had no videos
* fixed duplicated query parameters in proxied video URLs
* Return actual video height/width/fps rather than hard coded values
* Fixed the `/api/v1/popular` endpoint not returning a proper error code/message when the
popular page/endpoint are disabled.
### Full list of pull requests merged since the last release (newest first)
* HTML: Sort playlists alphabetically in watch page drop down ([#4853], by @SamantazFox)
* Videos: Fix XSS vulnerability in description/comments ([#4852], thanks _anonymous_)
* YtAPI: Bump client versions ([#4849], by @SamantazFox)
* SigHelper: Fix inverted time comparison in 'check_update' ([#4845], by @SamantazFox)
* Storyboards: Various fixes and code cleaning ([#4153], by SamantazFox)
* Fix lint errors introduced in #4146 and #4295 ([#4876], thanks @syeopite)
* Search: Add support for Youtube URLs ([#4146], by @SamantazFox)
* Channel: Render age restricted channels ([#4295], thanks @ChunkyProgrammer)
* Ameba: Miscellaneous fixes ([#4807], thanks @syeopite)
* API: Proxy formatStreams URLs too ([#4859], thanks @colinleroy)
* UI: Add search button to search bar ([#4706], thanks @thansk)
* Add ability to set po_token and visitordata ID ([#4789], thanks @unixfox)
* Add support for an external signature server ([#4772], by @SamantazFox)
* Ameba: Fix Naming/VariableNames ([#4790], thanks @syeopite)
* Translations update from Hosted Weblate ([#4659])
* Ameba: Fix Lint/UselessAssign ([#4795], thanks @syeopite)
* HTML: Add rel="noreferrer noopener" to external links ([#4667], thanks @ulmemxpoc)
* Remove unused methods in Invidious::LogHandler ([#4812], thanks @syeopite)
* Ameba: Fix Lint/NotNilAfterNoBang ([#4796], thanks @syeopite)
* Ameba: Fix unused argument Lint warnings ([#4805], thanks @syeopite)
* Ameba: i18next.cr fixes ([#4806], thanks @syeopite)
* Ameba: Disable rules ([#4792], thanks @syeopite)
* Channel: parse subscriber count and channel banner ([#4785], thanks @ChunkyProgrammer)
* Player: Fix playback position of already watched videos ([#4731], thanks @Fijxu)
* Videos: Fix genre url being unusable ([#4717], thanks @meatball133)
* API: Fix out of bound error on empty playlists ([#4696], thanks @Fijxu)
* Handle playlists cataloged as Podcast ([#4695], thanks @Fijxu)
* API: Fix duplicated query parameters in proxied video URLs ([#4587], thanks @absidue)
* API: Return actual stream height, width and fps ([#4586], thanks @absidue)
* Preferences: Fix handling of modified source code URL ([#4437], thanks @nooptek)
* API: Fix URL for vtt subtitles ([#4221], thanks @karelrooted)
* Channels: Add sort options to streams ([#4224], thanks @src-tinkerer)
* API: Fix error code for disabled popular endpoint ([#4296], thanks @iBicha)
* Allow embedding videos in local HTML files ([#4450], thanks @tomasz1986)
* CI: Bump Crystal version matrix ([#4654], by @SamantazFox)
* YtAPI: Remove API keys like official clients ([#4655], by @SamantazFox)
* HTML: Use full URL in the og:image property ([#4675], thanks @Fijxu)
* Rewrite transcript logic to be more generic ([#4747], thanks @syeopite)
* CI: Run Ameba ([#4753], thanks @syeopite)
* CI: Add release based containers ([#4763], thanks @syeopite)
* move helm chart to a dedicated github repository ([#4711], thanks @unixfox)
[#4146]: https://github.com/iv-org/invidious/pull/4146
[#4153]: https://github.com/iv-org/invidious/pull/4153
[#4221]: https://github.com/iv-org/invidious/pull/4221
[#4224]: https://github.com/iv-org/invidious/pull/4224
[#4295]: https://github.com/iv-org/invidious/pull/4295
[#4296]: https://github.com/iv-org/invidious/pull/4296
[#4437]: https://github.com/iv-org/invidious/pull/4437
[#4450]: https://github.com/iv-org/invidious/pull/4450
[#4586]: https://github.com/iv-org/invidious/pull/4586
[#4587]: https://github.com/iv-org/invidious/pull/4587
[#4654]: https://github.com/iv-org/invidious/pull/4654
[#4655]: https://github.com/iv-org/invidious/pull/4655
[#4659]: https://github.com/iv-org/invidious/pull/4659
[#4667]: https://github.com/iv-org/invidious/pull/4667
[#4675]: https://github.com/iv-org/invidious/pull/4675
[#4695]: https://github.com/iv-org/invidious/pull/4695
[#4696]: https://github.com/iv-org/invidious/pull/4696
[#4706]: https://github.com/iv-org/invidious/pull/4706
[#4711]: https://github.com/iv-org/invidious/pull/4711
[#4717]: https://github.com/iv-org/invidious/pull/4717
[#4731]: https://github.com/iv-org/invidious/pull/4731
[#4747]: https://github.com/iv-org/invidious/pull/4747
[#4753]: https://github.com/iv-org/invidious/pull/4753
[#4763]: https://github.com/iv-org/invidious/pull/4763
[#4772]: https://github.com/iv-org/invidious/pull/4772
[#4785]: https://github.com/iv-org/invidious/pull/4785
[#4789]: https://github.com/iv-org/invidious/pull/4789
[#4790]: https://github.com/iv-org/invidious/pull/4790
[#4792]: https://github.com/iv-org/invidious/pull/4792
[#4795]: https://github.com/iv-org/invidious/pull/4795
[#4796]: https://github.com/iv-org/invidious/pull/4796
[#4805]: https://github.com/iv-org/invidious/pull/4805
[#4806]: https://github.com/iv-org/invidious/pull/4806
[#4807]: https://github.com/iv-org/invidious/pull/4807
[#4812]: https://github.com/iv-org/invidious/pull/4812
[#4845]: https://github.com/iv-org/invidious/pull/4845
[#4849]: https://github.com/iv-org/invidious/pull/4849
[#4852]: https://github.com/iv-org/invidious/pull/4852
[#4853]: https://github.com/iv-org/invidious/pull/4853
[#4859]: https://github.com/iv-org/invidious/pull/4859
[#4876]: https://github.com/iv-org/invidious/pull/4876
## v2.20240427 (2024-04-27)
## 2024-04-26
Major bug fixes:
* Videos: Use android test suite client (#4650, thanks @SamantazFox)

View file

@ -7,11 +7,6 @@ STATIC := 0
NO_DBG_SYMBOLS := 0
# Enable multi-threading.
# Warning: Experimental feature!!
# invidious is not stable when MT is enabled.
MT := 0
FLAGS ?=
@ -24,10 +19,6 @@ ifeq ($(STATIC), 1)
FLAGS += --static
endif
ifeq ($(MT), 1)
FLAGS += -Dpreview_mt
endif
ifeq ($(NO_DBG_SYMBOLS), 1)
FLAGS += --no-debug

View file

@ -68,7 +68,6 @@
.video-js.player-style-youtube .vjs-menu-button-popup .vjs-menu {
margin-bottom: 2em;
padding-top: 2em
}
.video-js.player-style-youtube .vjs-progress-control .vjs-progress-holder, .video-js.player-style-youtube .vjs-progress-control {height: 5px;

View file

@ -3,6 +3,7 @@ var player_data = JSON.parse(document.getElementById('player_data').textContent)
var video_data = JSON.parse(document.getElementById('video_data').textContent);
var options = {
preload: 'auto',
liveui: true,
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
controlBar: {
@ -50,8 +51,8 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
return options;
};
videojs.Vhs.GOAL_BUFFER_LENGTH = 40;
videojs.Vhs.MAX_GOAL_BUFFER_LENGTH = 80;
videojs.Vhs.GOAL_BUFFER_LENGTH = 20;
videojs.Vhs.MAX_GOAL_BUFFER_LENGTH = 30;
var player = videojs('player', options);

View file

@ -54,53 +54,6 @@ db:
##
#signature_server:
##
## Invidious companion is an external program
## for loading the video streams from YouTube servers.
##
## When this setting is commented out, Invidious companion is not used.
## Otherwise, Invidious will proxy the requests to Invidious companion.
##
## Note: multiple URL can be configured. In this case, invidious will
## randomly pick one every time video data needs to be retrieved. This
## URL is then kept in the video metadata cache to allow video playback
## to work. Once said cache has expired, requesting that video's data
## again will cause a new companion URL to be picked.
##
## The parameter private_url needs to be configured for the internal
## communication between the companion and Invidious.
## And public_url is the public URL from which companion is listening
## to the requests from the user(s).
##
## If you are using a reverse proxy then you will probably need to
## configure the public_url to be the same as the domain used for Invidious.
## Also apply when used from an external IP address (without a domain).
## Examples: https://MYINVIDIOUSDOMAIN or http://192.168.1.100:8282
##
## Both parameter can have identical URL when Invidious is hosted in
## an internal network or at home or locally (localhost).
##
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
## Default: <none>
##
#invidious_companion:
# - private_url: "http://localhost:8282"
# public_url: "http://localhost:8282"
##
## API key for Invidious companion, used for securing the communication
## between Invidious and Invidious companion.
## The size of the key needs to be more or equal to 16.
##
## Note: This parameter is mandatory when Invidious companion is enabled
## and should be a random string.
## Such random string can be generated on linux with the following
## command: `pwgen 16 1`
##
## Accepted values: a string
## Default: <none>
##
#invidious_companion_key: "CHANGE_ME!!"
#########################################
#
@ -220,17 +173,6 @@ https_only: false
##
#force_resolve:
##
## Configuration for using a HTTP proxy
##
## If unset, then no HTTP proxy will be used.
##
http_proxy:
user:
password:
host:
port:
##
## Use Innertube's transcripts API instead of timedtext for closed captions
@ -284,11 +226,9 @@ http_proxy:
## Enables colors in logs. Useful for debugging purposes
## This is overridden if "-k" or "--colorize"
## are passed on the command line.
## Colors are also disabled if the environment variable
## NO_COLOR is present and has any value
##
## Accepted values: true, false
## Default: true
## Default: false
##
#colorize_logs: false
@ -839,22 +779,6 @@ default_user_preferences:
# Video player behavior
# -----------------------------
##
## This option controls the value of the HTML5 <video> element's
## "preload" attribute.
##
## If set to 'false', no video data will be loaded until the user
## explicitly starts the video by clicking the "Play" button.
## If set to 'true', the web browser will buffer some video data
## while the page is loading.
##
## See: https://www.w3schools.com/tags/att_video_preload.asp
##
## Accepted values: true, false
## Default: true
##
#preload: true
##
## Automatically play videos on page load.
##

View file

@ -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')

View file

@ -1,4 +1,4 @@
FROM mirror.gcr.io/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
@ -23,7 +23,7 @@ RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
RUN if [[ "${release}" == 1 ]] ; then \
crystal build ./src/invidious.cr \
--release --mcpu=x86-64-v2 \
--release --mcpu=x86-64-v3 \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
else \
@ -32,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
--link-flags "-lxml2 -llzma"; \
fi
FROM mirror.gcr.io/alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
FROM alpine:3.18
RUN apk add --no-cache rsvg-convert ttf-opensans tini
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious

View file

@ -1,6 +1,5 @@
FROM alpine:3.20 AS builder
RUN apk add --no-cache 'crystal=1.12.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \
zlib-static openssl-libs-static openssl-dev musl-dev xz-static
FROM alpine:3.19 AS builder
RUN apk add --no-cache 'crystal=1.10.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-static zlib-static openssl-libs-static openssl-dev musl-dev xz-static
ARG release
@ -33,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
FROM alpine:3.18
RUN apk add --no-cache rsvg-convert ttf-opensans tini
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious

View file

@ -47,7 +47,6 @@
"Preferences": "Einstellungen",
"preferences_category_player": "Wiedergabeeinstellungen",
"preferences_video_loop_label": "Immer wiederholen: ",
"preferences_preload_label": "Videodaten vorladen: ",
"preferences_autoplay_label": "Automatisch abspielen: ",
"preferences_continue_label": "Immer automatisch nächstes Video abspielen: ",
"preferences_continue_autoplay_label": "Nächstes Video automatisch abspielen: ",
@ -323,7 +322,7 @@
"channel_tab_community_label": "Gemeinschaft",
"search_filters_sort_option_relevance": "Relevanz",
"search_filters_sort_option_rating": "Bewertung",
"search_filters_sort_option_date": "Hochladedatum",
"search_filters_sort_option_date": "Datum",
"search_filters_sort_option_views": "Aufrufe",
"search_filters_type_label": "Inhaltstyp",
"search_filters_duration_label": "Dauer",
@ -494,8 +493,5 @@
"Add to playlist": "Einer Wiedergabeliste hinzufügen",
"Search for videos": "Nach Videos suchen",
"toggle_theme": "Thema wechseln",
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: ",
"carousel_go_to": "Zu Folie `x` gehen",
"carousel_slide": "Folie {{current}} von {{total}}",
"carousel_skip": "Karussell überspringen"
"Add to playlist: ": "Einer Wiedergabeliste hinzufügen: "
}

View file

@ -489,10 +489,5 @@
"search_filters_date_label": "Ημερομηνία αναφόρτωσης",
"Search for videos": "Αναζήτηση βίντεο",
"The Popular feed has been disabled by the administrator.": "Η δημοφιλής ροή έχει απενεργοποιηθεί από τον διαχειριστή.",
"Answer": "Απάντηση",
"Add to playlist": "Λίιστα αναπαραγωγής",
"Add to playlist: ": "Λίστα αναπαραγωγής: ",
"carousel_slide": "Εικόνα {{current}}απο {{total}}",
"carousel_go_to": "Πήγαινε στην εικόνα`x`",
"toggle_theme": "Αλλαγή θέματος"
"Answer": "Απάντηση"
}

View file

@ -71,7 +71,6 @@
"Preferences": "Preferences",
"preferences_category_player": "Player preferences",
"preferences_video_loop_label": "Always loop: ",
"preferences_preload_label": "Preload video data: ",
"preferences_autoplay_label": "Autoplay: ",
"preferences_continue_label": "Play next by default: ",
"preferences_continue_autoplay_label": "Autoplay next video: ",
@ -287,7 +286,6 @@
"Esperanto": "Esperanto",
"Estonian": "Estonian",
"Filipino": "Filipino",
"Filipino (auto-generated)": "Filipino (auto-generated)",
"Finnish": "Finnish",
"French": "French",
"French (auto-generated)": "French (auto-generated)",
@ -425,7 +423,7 @@
"search_filters_title": "Filters",
"search_filters_date_label": "Upload date",
"search_filters_date_option_none": "Any date",
"search_filters_date_option_hour": "Last hour",
"search_filters_date_option_hour": "Last Hour",
"search_filters_date_option_today": "Today",
"search_filters_date_option_week": "This week",
"search_filters_date_option_month": "This month",
@ -457,7 +455,7 @@
"search_filters_sort_label": "Sort By",
"search_filters_sort_option_relevance": "Relevance",
"search_filters_sort_option_rating": "Rating",
"search_filters_sort_option_date": "Upload date",
"search_filters_sort_option_date": "Upload Date",
"search_filters_sort_option_views": "View count",
"search_filters_apply_button": "Apply selected filters",
"Current version: ": "Current version: ",

View file

@ -480,7 +480,7 @@
"tokens_count_0": "{{count}} token",
"tokens_count_1": "{{count}} tokens",
"tokens_count_2": "{{count}} tokens",
"search_message_use_another_instance": "También puedes <a href=\"`x`\">buscar en otra instancia</a>.",
"search_message_use_another_instance": " También puede <a href=\"`x`\">buscar en otra instancia</a>.",
"Popular enabled: ": "¿Habilitar la sección popular? ",
"error_video_not_in_playlist": "El video que solicitaste no existe en esta lista de reproducción. <a href=\"`x`\">Haz clic aquí para acceder a la página de inicio de la lista de reproducción.</a>",
"channel_tab_streams_label": "Directos",

View file

@ -360,7 +360,7 @@
"search_filters_duration_label": "مدت",
"search_filters_features_label": "ویژگی‌ها",
"search_filters_sort_label": "به ترتیب",
"search_filters_date_option_hour": "ساعت گذشته",
"search_filters_date_option_hour": "یک ساعت گذشته",
"search_filters_date_option_today": "امروز",
"search_filters_date_option_week": "این هفته",
"search_filters_date_option_month": "این ماه",
@ -461,7 +461,7 @@
"Song: ": "آهنگ: ",
"Channel Sponsor": "اسپانسر کانال",
"Standard YouTube license": "پروانه استاندارد YouTube",
"search_message_use_another_instance": "همچنین می‌توانید <a href=\"`x`\">در نمونه‌ای دیگر هم جست‌وجو کنید</a>.",
"search_message_use_another_instance": " شما همچنین می‌توانید <a href=\"`x`\">در نمونه دیگر هم جستجو کنید</a>.",
"Download is disabled": "دریافت غیرفعال است",
"crash_page_before_reporting": "پیش از گزارش ایراد، مطمئنید شوید که:",
"playlist_button_add_items": "افزودن ویدیو",

View file

@ -449,24 +449,24 @@
"Cantonese (Hong Kong)": "Kantonski (Hong Kong)",
"Chinese": "Kineski",
"Chinese (Taiwan)": "Kineski (Tajvan)",
"Dutch (auto-generated)": "Nizozemski (automatski generirano)",
"French (auto-generated)": "Francuski (automatski generirano)",
"Indonesian (auto-generated)": "Indonezijski (automatski generirano)",
"Dutch (auto-generated)": "Nizozemski (automatski generiran)",
"French (auto-generated)": "Francuski (automatski generiran)",
"Indonesian (auto-generated)": "Indonezijski (automatski generiran)",
"Interlingue": "Interlingua",
"Japanese (auto-generated)": "Japanski (automatski generirano)",
"Russian (auto-generated)": "Ruski (automatski generirano)",
"Turkish (auto-generated)": "Turski (automatski generirano)",
"Vietnamese (auto-generated)": "Vijetnamski (automatski generirano)",
"Japanese (auto-generated)": "Japanski (automatski generiran)",
"Russian (auto-generated)": "Ruski (automatski generiran)",
"Turkish (auto-generated)": "Turski (automatski generiran)",
"Vietnamese (auto-generated)": "Vijetnamski (automatski generiran)",
"Spanish (Spain)": "Španjolski (Španjolska)",
"Italian (auto-generated)": "Talijanski (automatski generirano)",
"Italian (auto-generated)": "Talijanski (automatski generiran)",
"Portuguese (Brazil)": "Portugalski (Brazil)",
"Spanish (Mexico)": "Španjolski (Meksiko)",
"German (auto-generated)": "Njemački (automatski generirano)",
"German (auto-generated)": "Njemački (automatski generiran)",
"Chinese (China)": "Kineski (Kina)",
"Chinese (Hong Kong)": "Kineski (Hong Kong)",
"Korean (auto-generated)": "Korejski (automatski generirano)",
"Portuguese (auto-generated)": "Portugalski (automatski generirano)",
"Spanish (auto-generated)": "Španjolski (automatski generirano)",
"Korean (auto-generated)": "Korejski (automatski generiran)",
"Portuguese (auto-generated)": "Portugalski (automatski generiran)",
"Spanish (auto-generated)": "Španjolski (automatski generiran)",
"preferences_watch_history_label": "Aktiviraj povijest gledanja: ",
"search_filters_title": "Filtri",
"search_filters_date_option_none": "Bilo koji datum",

View file

@ -7,7 +7,7 @@
"invidious": "Invidious",
"Image CAPTCHA": "Imagine CAPTCHA",
"newest": "plus nove",
"generic_button_save": "Salveguardar",
"generic_button_save": "Salvar",
"Dark mode: ": "Modo obscur: ",
"preferences_dark_mode_label": "Thema: ",
"preferences_category_subscription": "Preferentias de subscription",
@ -23,7 +23,7 @@
"light": "clar",
"No": "Non",
"youtube": "YouTube",
"LIVE": "IN DIRECTO",
"LIVE": "IN DIRECTE",
"reddit": "Reddit",
"preferences_category_player": "Preferentias de reproductor",
"Preferences": "Preferentias",

View file

@ -363,7 +363,7 @@
"search_filters_features_option_location": "場所",
"search_filters_features_option_hdr": "HDR",
"Current version: ": "現在のバージョン: ",
"next_steps_error_message": "以下をお試しください: ",
"next_steps_error_message": "以下をお試しください: ",
"next_steps_error_message_refresh": "再読み込み",
"next_steps_error_message_go_to_youtube": "YouTubeを開く",
"search_filters_duration_option_short": "4分未満",
@ -396,7 +396,7 @@
"download_subtitles": "字幕 - `x` (.vtt)",
"search_filters_features_option_purchased": "購入済み",
"preferences_quality_option_dash": "DASH (適応的画質)",
"preferences_quality_dash_option_worst": "最",
"preferences_quality_dash_option_worst": "最",
"preferences_quality_dash_option_best": "最高",
"videoinfo_started_streaming_x_ago": "`x`前に配信を開始",
"videoinfo_watch_on_youTube": "YouTubeで視聴",

View file

@ -18,8 +18,8 @@
"preferences_related_videos_label": "관련 동영상 보기: ",
"Fallback captions: ": "대체 자막: ",
"preferences_captions_label": "기본 자막: ",
"reddit": "레딧",
"youtube": "유튜브",
"reddit": "Reddit",
"youtube": "YouTube",
"preferences_comments_label": "기본 댓글: ",
"preferences_volume_label": "플레이어 볼륨: ",
"preferences_quality_label": "선호하는 비디오 품질: ",
@ -48,7 +48,7 @@
"An alternative front-end to YouTube": "유튜브의 프론트엔드 대안",
"History": "시청 기록",
"Delete account?": "계정을 삭제 하시겠습니까?",
"Export data as JSON": "인비디어스 데이터 내보내기 (.json)",
"Export data as JSON": "JSON으로 데이터 내보내기",
"Export subscriptions as OPML (for NewPipe & FreeTube)": "OPML로 구독 내보내기 (뉴파이프 및 프리튜브)",
"Export subscriptions as OPML": "OPML로 구독 내보내기",
"Export": "내보내기",
@ -78,10 +78,10 @@
"Subscribe": "구독",
"Unsubscribe": "구독 취소",
"LIVE": "실시간",
"generic_views_count_0": "{{count}} 조회수",
"generic_videos_count_0": "{{count}} 동영상",
"generic_playlists_count_0": "{{count}} 재생목록",
"generic_subscribers_count_0": "{{count}} 구독자",
"generic_views_count_0": "조회수 {{count}}회",
"generic_videos_count_0": "동영상 {{count}}개",
"generic_playlists_count_0": "재생목록 {{count}}개",
"generic_subscribers_count_0": "구독자 {{count}}명",
"generic_subscriptions_count_0": "{{count}} 구독",
"search_filters_type_option_playlist": "재생목록",
"Korean": "한국어",
@ -109,14 +109,14 @@
"This channel does not exist.": "이 채널은 존재하지 않습니다.",
"Deleted or invalid channel": "삭제되었거나 더 이상 존재하지 않는 채널",
"channel:`x`": "채널:`x`",
"Show replies": "댓글 보기",
"Show replies": "댓글 보기",
"Hide replies": "댓글 숨기기",
"Incorrect password": "잘못된 비밀번호",
"License: ": "라이선스: ",
"Genre: ": "장르: ",
"Editing playlist `x`": "재생목록 `x` 수정하기",
"Playlist privacy": "재생목록 공개 범위",
"Watch on YouTube": "유튜브에서 보기",
"Watch on YouTube": "YouTube에서 보기",
"Show less": "간략히",
"Show more": "더보기",
"Title": "제목",
@ -125,7 +125,7 @@
"Delete playlist": "재생목록 삭제",
"Delete playlist `x`?": "재생목록 `x` 를 삭제하시겠습니까?",
"Updated `x` ago": "`x` 전에 업데이트됨",
"Released under the AGPLv3 on Github.": "깃허브에 AGPLv3 으로 배포됩니다.",
"Released under the AGPLv3 on Github.": "GitHub에 AGPLv3 으로 배포됩니다.",
"View all playlists": "모든 재생목록 보기",
"Private": "비공개",
"Unlisted": "목록에 없음",
@ -135,12 +135,12 @@
"Source available here.": "소스는 여기에서 사용할 수 있습니다.",
"Log out": "로그아웃",
"search": "검색",
"subscriptions_unseen_notifs_count_0": "{{count}} 읽지 않은 알림",
"subscriptions_unseen_notifs_count_0": "읽지 않은 알림 {{count}}개",
"Subscriptions": "구독",
"revoke": "철회",
"unsubscribe": "구독 취소",
"Import/export": "가져오기/내보내기",
"tokens_count_0": "{{count}} 토큰",
"tokens_count_0": "토큰 {{count}}개",
"Token": "토큰",
"Token manager": "토큰 관리자",
"Subscription manager": "구독 관리자",
@ -163,7 +163,7 @@
"Clear watch history": "시청 기록 지우기",
"preferences_category_data": "데이터 설정",
"`x` is live": "`x` 이(가) 라이브 중입니다",
"`x` uploaded a video": "`x` 동영상 게시됨",
"`x` uploaded a video": "`x` 이(가) 동영상을 게시했습니다",
"Enable web notifications": "웹 알림 활성화",
"preferences_notifications_only_label": "알림만 표시 (있는 경우): ",
"preferences_unseen_only_label": "시청하지 않은 것만 표시: ",
@ -241,7 +241,7 @@
"Could not create mix.": "믹스를 생성할 수 없습니다.",
"`x` ago": "`x` 전",
"comments_view_x_replies_0": "답글 {{count}}개 보기",
"View Reddit comments": "레딧 댓글 보기",
"View Reddit comments": "Reddit 댓글 보기",
"Engagement: ": "약속: ",
"Wilson score: ": "Wilson Score: ",
"Family friendly? ": "전연령 영상입니까? ",
@ -267,8 +267,8 @@
"Bulgarian": "불가리아어",
"Bosnian": "보스니아어",
"Belarusian": "벨라루스어",
"View more comments on Reddit": "레딧에서 댓글 더 보기",
"View YouTube comments": "유튜브 댓글 보기",
"View more comments on Reddit": "Reddit에서 댓글 더 보기",
"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.": "자바스크립트가 꺼져 있는 것 같습니다! 댓글을 보려면 여기를 클릭하세요. 댓글을 로드하는 데 시간이 조금 더 걸릴 수 있습니다.",
"Shared `x`": "`x` 업로드",
"Whitelisted regions: ": "차단되지 않은 지역: ",
@ -289,7 +289,7 @@
"Empty playlist": "재생목록 비어 있음",
"Show annotations": "주석 보이기",
"Hide annotations": "주석 숨기기",
"Switch Invidious Instance": "인비디어스 인스턴스 변경",
"Switch Invidious Instance": "Invidious 인스턴스 변경",
"Spanish": "스페인어",
"Southern Sotho": "소토어",
"Somali": "소말리어",
@ -329,7 +329,7 @@
"Swedish": "스웨덴어",
"Spanish (Latin America)": "스페인어 (라틴 아메리카)",
"comments_points_count_0": "{{count}} 포인트",
"Invidious Private Feed for `x`": "`x` 에 대한 인비디어스 비공개 피드",
"Invidious Private Feed for `x`": "`x` 에 대한 Invidious 비공개 피드",
"Premieres `x`": "최초 공개 `x`",
"Premieres in `x`": "`x` 후 최초 공개",
"next_steps_error_message": "다음 방법을 시도해 보세요: ",
@ -408,7 +408,7 @@
"preferences_quality_dash_option_1080p": "1080p",
"preferences_quality_dash_option_worst": "최저",
"preferences_watch_history_label": "시청 기록 저장: ",
"invidious": "인비디어스",
"invidious": "Invidious",
"preferences_quality_option_small": "낮음",
"preferences_quality_dash_option_auto": "자동",
"preferences_quality_dash_option_480p": "480p",
@ -453,7 +453,7 @@
"channel_tab_streams_label": "실시간 스트리밍",
"channel_tab_channels_label": "채널",
"channel_tab_playlists_label": "재생목록",
"Standard YouTube license": "표준 유튜브 라이선스",
"Standard YouTube license": "표준 YouTube 라이선스",
"Song: ": "제목: ",
"Channel Sponsor": "채널 스폰서",
"Album: ": "앨범: ",

View file

@ -322,13 +322,13 @@
"channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "relevans",
"search_filters_sort_option_rating": "vurdering",
"search_filters_sort_option_date": "Opplastingsdato",
"search_filters_sort_option_date": "dato",
"search_filters_sort_option_views": "visninger",
"search_filters_type_label": "innholdstype",
"search_filters_duration_label": "varighet",
"search_filters_features_label": "funksjoner",
"search_filters_sort_label": "sorter",
"search_filters_date_option_hour": "Siste time",
"search_filters_date_option_hour": "time",
"search_filters_date_option_today": "i dag",
"search_filters_date_option_week": "uke",
"search_filters_date_option_month": "måned",
@ -494,7 +494,5 @@
"carousel_slide": "Lysark {{current}} av {{total}}",
"carousel_skip": "Hopp over karusellen",
"Add to playlist": "Legg til i spilleliste",
"Add to playlist: ": "Legg til i spilleliste: ",
"The Popular feed has been disabled by the administrator.": "Populært-kilden er koblet ut av administratoren.",
"toggle_theme": "Endre utseende"
"Add to playlist: ": "Legg til i spilleliste: "
}

View file

@ -317,13 +317,13 @@
"channel_tab_community_label": "Gemeenschap",
"search_filters_sort_option_relevance": "relevantie",
"search_filters_sort_option_rating": "beoordeling",
"search_filters_sort_option_date": "Upload datum",
"search_filters_sort_option_date": "datum",
"search_filters_sort_option_views": "keren bekeken",
"search_filters_type_label": "Type inhoud",
"search_filters_duration_label": "duur",
"search_filters_features_label": "eigenschappen",
"search_filters_sort_label": "sorteren",
"search_filters_date_option_hour": "Laatste uur",
"search_filters_date_option_hour": "uur",
"search_filters_date_option_today": "vandaag",
"search_filters_date_option_week": "week",
"search_filters_date_option_month": "maand",
@ -357,7 +357,7 @@
"footer_original_source_code": "Originele bron-code",
"footer_modfied_source_code": "Gewijzigde bron-code",
"adminprefs_modified_source_code_url_label": "URL naar gewijzigde bron-code-opslagplaats",
"next_steps_error_message": "Waarna u zou kunnen proberen om: ",
"next_steps_error_message": "Daarna moet u proberen om: ",
"footer_source_code": "Bron-code",
"search_filters_duration_option_long": "Lang (> 20 minuten)",
"preferences_quality_option_dash": "DASH (adaptieve kwaliteit)",
@ -477,7 +477,7 @@
"Song: ": "Lied: ",
"generic_channels_count": "{{count}} kanaal",
"generic_channels_count_plural": "{{count}} kanalen",
"Popular enabled: ": "Populair ingeschakeld: ",
"Popular enabled: ": "Populair geactiveerd: ",
"channel_tab_playlists_label": "Afspeellijsten",
"generic_button_edit": "Bewerken",
"Music in this video": "Muziek in deze video",

View file

@ -508,7 +508,7 @@
"toggle_theme": "Trocar tema",
"Add to playlist": "Adicionar à lista de reprodução",
"Add to playlist: ": "Adicionar à lista de reprodução: ",
"Answer": "Responder",
"Answer": "Resposta",
"Search for videos": "Procurar vídeos",
"carousel_slide": "Diapositivo {{current}} de{{total}}",
"carousel_skip": "Ignorar carrossel",

View file

@ -509,9 +509,6 @@
"Add to playlist: ": "Добавить в плейлист: ",
"Answer": "Ответить",
"Search for videos": "Поиск видео",
"The Popular feed has been disabled by the administrator.": "Лента популярного была отключена администратором.",
"toggle_theme": "Переключатель тем",
"carousel_slide": "Пролистано {{current}} из {{total}}",
"carousel_skip": "Пропустить всё",
"carousel_go_to": "Перейти к странице `x`"
"The Popular feed has been disabled by the administrator.": "Популярная лента была отключена администратором.",
"toggle_theme": "Переключатель тем"
}

View file

@ -257,13 +257,13 @@
"Video mode": "Mënyrë video",
"channel_tab_videos_label": "Video",
"search_filters_sort_option_rating": "Vlerësim",
"search_filters_sort_option_date": "Datë ngarkimi",
"search_filters_sort_option_date": "Datë Ngarkimi",
"search_filters_sort_option_views": "Numër parjesh",
"search_filters_type_label": "Lloj",
"search_filters_duration_label": "Kohëzgjatje",
"search_filters_features_label": "Veçori",
"search_filters_sort_label": "Renditi Sipas",
"search_filters_date_option_hour": "Orën e fundit",
"search_filters_date_option_hour": "Orën e Fundit",
"search_filters_date_option_today": "Sot",
"search_filters_duration_option_long": "E gjatë (> 20 minuta)",
"search_filters_features_option_hd": "HD",
@ -435,7 +435,7 @@
"tokens_count_plural": "{{count}} tokenë",
"preferences_save_player_pos_label": "Mba mend pozicionin e luajtjes: ",
"Import Invidious data": "Importoni të dhëna JSON Invidious",
"Import YouTube subscriptions": "Importoni pajtime YouTube CSV ose OPML",
"Import YouTube subscriptions": "Importoni pajtime YouTube/OPML",
"Export data as JSON": "Eksportoji të dhënat Invidious si JSON",
"preferences_vr_mode_label": "Video me ndërveprim 360 gradë (lyp WebGL): ",
"Shared `x`": "Ndarë me të tjerë më `x`",
@ -484,13 +484,5 @@
"Import YouTube watch history (.json)": "Importo historik parjesh YouTube (.json)",
"preferences_local_label": "Video përmes ndërmjetësi: ",
"Fallback captions: ": "Titra nga halli: ",
"Erroneous challenge": "Zgjidhje e gabuar",
"Add to playlist: ": "Shtoje te luajlistë: ",
"Add to playlist": "Shtoje te luajlistë",
"Answer": "Përgjigje",
"Search for videos": "Kërko për video",
"The Popular feed has been disabled by the administrator.": "Prurja Popullore është çaktivizuar nga përgjegjësi.",
"carousel_skip": "Anashkaloje Rrotullamen",
"carousel_slide": "Diapozitiv {{current}} nga {{total}}",
"carousel_go_to": "Kalo te diapozitivi `x`"
"Erroneous challenge": "Zgjidhje e gabuar"
}

View file

@ -320,13 +320,13 @@
"channel_tab_community_label": "Gemenskap",
"search_filters_sort_option_relevance": "Relevans",
"search_filters_sort_option_rating": "Rankning",
"search_filters_sort_option_date": "Uppladdnings datum",
"search_filters_sort_option_date": "Uppladdnings Datum",
"search_filters_sort_option_views": "Visningar",
"search_filters_type_label": "Typ",
"search_filters_duration_label": "Varaktighet",
"search_filters_features_label": "Funktioner",
"search_filters_sort_label": "Sortera efter",
"search_filters_date_option_hour": "Senaste timmen",
"search_filters_date_option_hour": "Senaste Timmen",
"search_filters_date_option_today": "Idag",
"search_filters_date_option_week": "Denna vecka",
"search_filters_date_option_month": "Denna månad",

View file

@ -322,13 +322,13 @@
"channel_tab_community_label": "Topluluk",
"search_filters_sort_option_relevance": "İlgi",
"search_filters_sort_option_rating": "Değerlendirme",
"search_filters_sort_option_date": "Yükleme tarihi",
"search_filters_sort_option_date": "Yükleme Tarihi",
"search_filters_sort_option_views": "Görüntüleme Sayısı",
"search_filters_type_label": "Tür",
"search_filters_duration_label": "Süre",
"search_filters_features_label": "Özellikler",
"search_filters_sort_label": "Sıralama Ölçütü",
"search_filters_date_option_hour": "Son saat",
"search_filters_date_option_hour": "Son Saat",
"search_filters_date_option_today": "Bugün",
"search_filters_date_option_week": "Bu Hafta",
"search_filters_date_option_month": "Bu Ay",

View file

@ -455,7 +455,7 @@
"search_filters_date_option_week": "Цей тиждень",
"search_filters_type_label": "Тип",
"search_filters_type_option_channel": "Канал",
"search_message_use_another_instance": "Можете також <a href=\"`x`\">пошукати на іншому сервері</a>.",
"search_message_use_another_instance": " Можете також <a href=\"`x`\">пошукати іншим сервером</a>.",
"search_filters_title": "Фільтри",
"search_filters_date_option_hour": "Остання година",
"search_filters_date_option_month": "Цей місяць",
@ -472,7 +472,7 @@
"search_filters_features_option_three_sixty": "360°",
"search_filters_features_option_hdr": "HDR",
"search_filters_sort_label": "Спершу",
"search_filters_sort_option_date": "Дата вивантаження",
"search_filters_sort_option_date": "Нещодавні",
"search_filters_apply_button": "Застосувати фільтри",
"search_filters_features_option_vr180": "VR180",
"search_filters_features_option_purchased": "Придбано",

View file

@ -338,13 +338,13 @@
"channel_tab_community_label": "社群",
"search_filters_sort_option_relevance": "關聯",
"search_filters_sort_option_rating": "評分",
"search_filters_sort_option_date": "上傳日期",
"search_filters_sort_option_date": "日期",
"search_filters_sort_option_views": "檢視",
"search_filters_type_label": "內容類型",
"search_filters_duration_label": "時長",
"search_filters_features_label": "特色",
"search_filters_sort_label": "排序",
"search_filters_date_option_hour": "最後一小時",
"search_filters_date_option_hour": "小時",
"search_filters_date_option_today": "今天",
"search_filters_date_option_week": "週",
"search_filters_date_option_month": "月",

2
mocks

@ -1 +1 @@
Subproject commit b55d58dea94f7144ff0205857dfa70ec14eaa872
Subproject commit 11ec372f72747c09d48ffef04843f72be67d5b54

View file

@ -10,23 +10,16 @@ shards:
backtracer:
git: https://github.com/sija/backtracer.cr.git
version: 1.2.2
version: 1.2.1
db:
git: https://github.com/crystal-lang/crystal-db.git
version: 0.13.1
version: 0.10.1
exception_page:
git: https://github.com/crystal-loot/exception_page.git
version: 0.2.2
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:
git: https://github.com/kemalcr/kemal.git
version: 1.1.2
@ -37,7 +30,7 @@ shards:
pg:
git: https://github.com/will/crystal-pg.git
version: 0.28.0
version: 0.24.0
pool:
git: https://github.com/ysbaddaden/pool.git
@ -57,9 +50,9 @@ shards:
spectator:
git: https://github.com/icy-arctic-fox/spectator.git
version: 0.10.6
version: 0.10.4
sqlite3:
git: https://github.com/crystal-lang/crystal-sqlite3.git
version: 0.21.0
version: 0.18.0

View file

@ -1,20 +1,21 @@
name: invidious
version: 2.20241110.0-dev
version: 0.20.1
authors:
- Invidious team <contact@invidious.io>
- Contributors!
- Omar Roth <omarroth@protonmail.com>
- Invidious team
description: |
Invidious is an alternative front-end to YouTube
targets:
invidious:
main: src/invidious.cr
dependencies:
pg:
github: will/crystal-pg
version: ~> 0.28.0
version: ~> 0.24.0
sqlite3:
github: crystal-lang/crystal-sqlite3
version: ~> 0.21.0
version: ~> 0.18.0
kemal:
github: kemalcr/kemal
version: ~> 1.1.2
@ -29,12 +30,6 @@ dependencies:
version: ~> 0.1.1
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:
spectator:
@ -44,10 +39,6 @@ development_dependencies:
github: crystal-ameba/ameba
version: ~> 1.6.1
crystal: ">= 1.10.0, < 2.0.0"
crystal: ">= 1.0.0, < 2.0.0"
license: AGPLv3
repository: https://github.com/iv-org/invidious
homepage: https://invidious.io
documentation: https://docs.invidious.io

View file

@ -27,8 +27,8 @@ Spectator.describe Invidious::Hashtag do
expect(video_11.length_seconds).to eq((56.minutes + 41.seconds).total_seconds.to_i32)
expect(video_11.views).to eq(40_504_893)
expect(video_11.badges.live_now?).to be_false
expect(video_11.badges.premium?).to be_false
expect(video_11.live_now).to be_false
expect(video_11.premium).to be_false
expect(video_11.premiere_timestamp).to be_nil
#
@ -49,8 +49,8 @@ Spectator.describe Invidious::Hashtag do
expect(video_35.length_seconds).to eq((3.minutes + 14.seconds).total_seconds.to_i32)
expect(video_35.views).to eq(30_790_049)
expect(video_35.badges.live_now?).to be_false
expect(video_35.badges.premium?).to be_false
expect(video_35.live_now).to be_false
expect(video_35.premium).to be_false
expect(video_35.premiere_timestamp).to be_nil
end
@ -80,8 +80,8 @@ Spectator.describe Invidious::Hashtag do
expect(video_41.length_seconds).to eq((1.hour).total_seconds.to_i32)
expect(video_41.views).to eq(63_240)
expect(video_41.badges.live_now?).to be_false
expect(video_41.badges.premium?).to be_false
expect(video_41.live_now).to be_false
expect(video_41.premium).to be_false
expect(video_41.premiere_timestamp).to be_nil
#
@ -102,8 +102,8 @@ Spectator.describe Invidious::Hashtag do
expect(video_48.length_seconds).to eq((35.minutes + 46.seconds).total_seconds.to_i32)
expect(video_48.views).to eq(68_704)
expect(video_48.badges.live_now?).to be_false
expect(video_48.badges.premium?).to be_false
expect(video_48.live_now).to be_false
expect(video_48.premium).to be_false
expect(video_48.premiere_timestamp).to be_nil
end
end

View file

@ -17,8 +17,8 @@ Spectator.describe "parse_video_info" do
# Basic video infos
expect(info["title"].as_s).to eq("I Gave My 100,000,000th Subscriber An Island")
expect(info["views"].as_i).to eq(220_226_287)
expect(info["likes"].as_i).to eq(6_870_691)
expect(info["views"].as_i).to eq(126_573_823)
expect(info["likes"].as_i).to eq(5_157_654)
# For some reason the video length from VideoDetails and the
# one from microformat differs by 1s...
@ -48,12 +48,12 @@ Spectator.describe "parse_video_info" do
expect(info["relatedVideos"].as_a.size).to eq(20)
expect(info["relatedVideos"][0]["id"]).to eq("krsBRQbOPQ4")
expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $250,000,000 Private Island!")
expect(info["relatedVideos"][0]["id"]).to eq("Hwybp38GnZw")
expect(info["relatedVideos"][0]["title"]).to eq("I Built Willy Wonka's Chocolate Factory!")
expect(info["relatedVideos"][0]["author"]).to eq("MrBeast")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
expect(info["relatedVideos"][0]["view_count"]).to eq("230617484")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M")
expect(info["relatedVideos"][0]["view_count"]).to eq("179877630")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("179M")
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
# Description
@ -76,11 +76,11 @@ Spectator.describe "parse_video_info" do
expect(info["ucid"].as_s).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
expect(info["authorThumbnail"].as_s).to eq(
"https://yt3.ggpht.com/fxGKYucJAVme-Yz4fsdCroCFCrANWqw0ql4GYuvx8Uq4l_euNJHgE-w9MTkLQA805vWCi-kE0g=s48-c-k-c0x00ffffff-no-rj"
"https://yt3.ggpht.com/ytc/AL5GRJVuqw82ERvHzsmBxL7avr1dpBtsVIXcEzBPZaloFg=s48-c-k-c0x00ffffff-no-rj"
)
expect(info["authorVerified"].as_bool).to be_true
expect(info["subCountText"].as_s).to eq("320M")
expect(info["subCountText"].as_s).to eq("143M")
end
it "parses a regular video with no descrition/comments" do
@ -99,8 +99,8 @@ Spectator.describe "parse_video_info" do
# Basic video infos
expect(info["title"].as_s).to eq("Chris Rea - Auberge")
expect(info["views"].as_i).to eq(14_324_584)
expect(info["likes"].as_i).to eq(35_870)
expect(info["views"].as_i).to eq(10_943_126)
expect(info["likes"].as_i).to eq(0)
expect(info["lengthSeconds"].as_i).to eq(283_i64)
expect(info["published"].as_s).to eq("2012-05-21T00:00:00Z")
@ -132,14 +132,14 @@ Spectator.describe "parse_video_info" do
# Related videos
expect(info["relatedVideos"].as_a.size).to eq(20)
expect(info["relatedVideos"].as_a.size).to eq(19)
expect(info["relatedVideos"][0]["id"]).to eq("gUUdQfnshJ4")
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version")
expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ")
expect(info["relatedVideos"][0]["view_count"]).to eq("53298661")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M")
expect(info["relatedVideos"][0]["id"]).to eq("Ww3KeZ2_Yv4")
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea")
expect(info["relatedVideos"][0]["author"]).to eq("PanMusic")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCsKAPSuh1iNbLWUga_igPyA")
expect(info["relatedVideos"][0]["view_count"]).to eq("31581")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("31K")
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")
# Description
@ -156,13 +156,11 @@ Spectator.describe "parse_video_info" do
# Author infos
expect(info["author"].as_s).to eq("ChrisReaVideos")
expect(info["author"].as_s).to eq("ChrisReaOfficial")
expect(info["ucid"].as_s).to eq("UC_5q6nWPbD30-y6oiWF_oNA")
expect(info["authorThumbnail"].as_s).to eq(
"https://yt3.ggpht.com/ytc/AIdro_n71nsegpKfjeRKwn1JJmK5IVMh_7j5m_h3_1KnUUg=s48-c-k-c0x00ffffff-no-rj"
)
expect(info["authorThumbnail"].as_s).to be_empty
expect(info["authorVerified"].as_bool).to be_false
expect(info["subCountText"].as_s).to eq("3.11K")
expect(info["subCountText"].as_s).to eq("-")
end
end

View file

@ -23,7 +23,6 @@ require "kilt"
require "./ext/kemal_content_for.cr"
require "./ext/kemal_static_file_handler.cr"
require "http_proxy"
require "athena-negotiation"
require "openssl/hmac"
require "option_parser"
@ -33,7 +32,6 @@ require "yaml"
require "compress/zip"
require "protodec/utils"
require "redis"
require "inotify"
require "./invidious/database/*"
require "./invidious/database/migrations/*"
@ -61,19 +59,6 @@ end
alias IV = Invidious
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
PG_DB = DB.open CONFIG.database_url
@ -86,7 +71,6 @@ ARCHIVE_URL = URI.parse("https://archive.org")
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
REDDIT_URL = URI.parse("https://www.reddit.com")
YT_URL = URI.parse("https://www.youtube.com")
PUBSUB_HOST_URL = CONFIG.pubsub_domain
HOST_URL = make_host_url(Kemal.config)
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
@ -114,10 +98,6 @@ SOFTWARE = {
YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
# Image request pool
GGPHT_POOL = YoutubeConnectionPool.new(URI.parse("https://yt3.ggpht.com"), capacity: CONFIG.pool_size)
# CLI
Kemal.config.extra_options do |parser|
parser.banner = "Usage: invidious [arguments]"
@ -208,20 +188,6 @@ Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
if !CONFIG.external_videoplayback_proxy.empty?
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
else
# Invidious will it's own videoplayback proxy unless the admin decides to rewrite
# the /videoplayback location in the reverse proxy configuration (NGINX, Caddy, etc)
LOGGER.info("jobs: Disabling CheckExternalProxy job. Invidious will it's own videoplayback proxy")
end
if !CONFIG.tokens_server.empty?
Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new
else
LOGGER.info("jobs: Disabling RefreshSessionTokens job. Invidious will use the tokens that are on the configuration file")
end
Invidious::Jobs.start_all
def popular_videos

View file

@ -223,7 +223,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
length_seconds = channel_video.try &.length_seconds
length_seconds ||= 0
live_now = channel_video.try &.badges.live_now?
live_now = channel_video.try &.live_now
live_now ||= false
premiere_timestamp = channel_video.try &.premiere_timestamp
@ -251,8 +251,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
# else
# Invidious::Database::Users.feed_needs_update(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
else
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
@ -275,7 +275,7 @@ def fetch_channel(ucid, pull_all_videos : Bool)
ucid: video.ucid,
author: video.author,
length_seconds: video.length_seconds,
live_now: video.badges.live_now?,
live_now: video.live_now,
premiere_timestamp: video.premiere_timestamp,
views: video.views,
})
@ -287,8 +287,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
# else
# Invidious::Database::Users.feed_needs_update(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
end
end

View file

@ -1,3 +1,78 @@
def produce_channel_content_continuation(ucid, content_type, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
object_inner_2 = {
"2:0:embedded" => {
"1:0:varint" => 0_i64,
},
"5:varint" => 50_i64,
"6:varint" => 1_i64,
"7:varint" => (page * 30).to_i64,
"9:varint" => 1_i64,
"10:varint" => 0_i64,
}
object_inner_2_encoded = object_inner_2
.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
content_type_numerical =
case content_type
when "videos" then 15
when "livestreams" then 14
else 15 # Fallback to "videos"
end
sort_by_numerical =
case sort_by
when "newest" then 1_i64
when "popular" then 2_i64
when "oldest" then 4_i64
else 1_i64 # Fallback to "newest"
end
object_inner_1 = {
"110:embedded" => {
"3:embedded" => {
"#{content_type_numerical}:embedded" => {
"1:embedded" => {
"1:string" => object_inner_2_encoded,
},
"2:embedded" => {
"1:string" => "00000000-0000-0000-0000-000000000000",
},
"3:varint" => sort_by_numerical,
},
},
},
}
object_inner_1_encoded = object_inner_1
.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:string" => object_inner_1_encoded,
"35:string" => "browse-feed#{ucid}videos102",
},
}
continuation = object.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return continuation
end
def make_initial_content_ctoken(ucid, content_type, sort_by) : String
return produce_channel_content_continuation(ucid, content_type, sort_by: sort_by)
end
module Invidious::Channel::Tabs
extend self
@ -26,7 +101,7 @@ module Invidious::Channel::Tabs
end
def get_videos(author : String, ucid : String, *, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_videos_ctoken(ucid, sort_by)
continuation ||= make_initial_content_ctoken(ucid, "videos", sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, author, ucid)
@ -55,10 +130,14 @@ module Invidious::Channel::Tabs
# Shorts
# -------------------
def get_shorts(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_shorts_ctoken(channel.ucid, sort_by)
def get_shorts(channel : AboutChannel, continuation : String? = nil)
if continuation.nil?
# EgZzaG9ydHPyBgUKA5oBAA%3D%3D is the protobuf object to load "shorts"
# TODO: try to extract the continuation tokens that allows other sorting options
initial_data = YoutubeAPI.browse(channel.ucid, params: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D")
else
initial_data = YoutubeAPI.browse(continuation: continuation)
end
return extract_items(initial_data, channel.author, channel.ucid)
end
@ -66,8 +145,9 @@ module Invidious::Channel::Tabs
# Livestreams
# -------------------
def get_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_livestreams_ctoken(channel.ucid, sort_by)
def get_livestreams(channel : AboutChannel, continuation : String? = nil, sort_by = "newest")
continuation ||= make_initial_content_ctoken(channel.ucid, "livestreams", sort_by)
initial_data = YoutubeAPI.browse(continuation: continuation)
return extract_items(initial_data, channel.author, channel.ucid)
@ -91,102 +171,4 @@ module Invidious::Channel::Tabs
return items, next_continuation
end
# -------------------
# C-tokens
# -------------------
private def sort_options_videos_short(sort_by : String)
case sort_by
when "newest" then return 4_i64
when "popular" then return 2_i64
when "oldest" then return 5_i64
else return 4_i64 # Fallback to "newest"
end
end
# Generate the initial "continuation token" to get the first page of the
# "videos" tab. The following page requires the ctoken provided in that
# first page, and so on.
private def make_initial_videos_ctoken(ucid : String, sort_by = "newest")
object = {
"15:embedded" => {
"2:embedded" => {
"1:string" => "00000000-0000-0000-0000-000000000000",
},
"4:varint" => sort_options_videos_short(sort_by),
},
}
return channel_ctoken_wrap(ucid, object)
end
# Generate the initial "continuation token" to get the first page of the
# "shorts" tab. The following page requires the ctoken provided in that
# first page, and so on.
private def make_initial_shorts_ctoken(ucid : String, sort_by = "newest")
object = {
"10:embedded" => {
"2:embedded" => {
"1:string" => "00000000-0000-0000-0000-000000000000",
},
"4:varint" => sort_options_videos_short(sort_by),
},
}
return channel_ctoken_wrap(ucid, object)
end
# Generate the initial "continuation token" to get the first page of the
# "livestreams" tab. The following page requires the ctoken provided in that
# first page, and so on.
private def make_initial_livestreams_ctoken(ucid : String, sort_by = "newest")
sort_by_numerical =
case sort_by
when "newest" then 12_i64
when "popular" then 14_i64
when "oldest" then 13_i64
else 12_i64 # Fallback to "newest"
end
object = {
"14:embedded" => {
"2:embedded" => {
"1:string" => "00000000-0000-0000-0000-000000000000",
},
"5:varint" => sort_by_numerical,
},
}
return channel_ctoken_wrap(ucid, object)
end
# The protobuf structure common between videos/shorts/livestreams
private def channel_ctoken_wrap(ucid : String, object)
object_inner = {
"110:embedded" => {
"3:embedded" => object,
},
}
object_inner_encoded = object_inner
.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
object = {
"80226972:embedded" => {
"2:string" => ucid,
"3:string" => object_inner_encoded,
},
}
continuation = object.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) }
return continuation
end
end

View file

@ -13,7 +13,6 @@ struct ConfigPreferences
property annotations : Bool = false
property annotations_subscribed : Bool = false
property preload : Bool = true
property autoplay : Bool = false
property captions : Array(String) = ["", "", ""]
property comments : Array(String) = ["youtube", ""]
@ -55,28 +54,9 @@ struct ConfigPreferences
end
end
struct HTTPProxyConfig
include YAML::Serializable
property user : String
property password : String
property host : String
property port : Int32
end
class Config
include YAML::Serializable
class CompanionConfig
include YAML::Serializable
@[YAML::Field(converter: Preferences::URIConverter)]
property private_url : URI = URI.parse("")
@[YAML::Field(converter: Preferences::URIConverter)]
property public_url : URI = URI.parse("")
end
# Number of threads to use for crawling videos from channels (for updating subscriptions)
property channel_threads : Int32 = 1
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
@ -106,22 +86,18 @@ class Config
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
property https_only : Bool?
# Enable or disable CSP
property csp : Bool? = true
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
property hmac_key : String = ""
# Domain to be used for links to resources on the site where an absolute URL is required
property domain : String?
# Materialious redirects
property materialious_domain : String?
# Alternative domains. You can add other domains, like TOR and I2P addresses
property alternative_domains : Array(String) = [] of String
# Backend domains. Domains for numbered backends
property backend_domains : Array(String) = [] of String
property donation_url : String?
property contact_url : String?
property home_domain : String?
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
property use_pubsub_feeds : Bool | Int32 = false
property use_innertube_for_feeds : Bool = true
property popular_enabled : Bool = true
property captcha_enabled : Bool = true
property login_enabled : Bool = true
@ -174,8 +150,6 @@ class Config
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`)
property pool_size : Int32 = 100
# HTTP Proxy configuration
property http_proxy : HTTPProxyConfig? = nil
# Use Innertube's transcripts API instead of timedtext for closed captions
property use_innertube_for_captions : Bool = false
@ -185,12 +159,6 @@ class Config
# poToken for passing bot attestation
property po_token : String? = nil
# Invidious companion
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
# Invidious companion API key
property invidious_companion_key : String = ""
# Saved cookies in "name1=value1; name2=value2..." format
@[YAML::Field(converter: Preferences::StringToCookies)]
property cookies : HTTP::Cookies = HTTP::Cookies.new
@ -203,25 +171,11 @@ class Config
# List of names of the backends
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://`
# at the start of the URI
property external_videoplayback_proxy : Array(String) = [] of String
property external_videoplayback_proxy : String?
property pubsub_domain : String = ""
property ignore_user_tokens : Bool = false
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
property tokens_server : String = ""
{% if flag?(:linux) %}
property reload_config_automatically : Bool = true
{% end %}
# Materialious redirects
property materialious_domain : String?
def disabled?(option)
case disabled = CONFIG.disable_proxy
@ -238,64 +192,6 @@ class Config
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
# Load config from file or YAML string env var
env_config_file = "INVIDIOUS_CONFIG_FILE"
@ -345,23 +241,6 @@ class Config
end
{% end %}
if config.invidious_companion.present?
# invidious_companion and signature_server can't work together
if config.signature_server
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."
exit(1)
elsif config.invidious_companion_key.empty?
puts "Config: Please configure a key if you are using invidious companion."
exit(1)
elsif config.invidious_companion_key == "CHANGE_ME!!"
puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
exit(1)
elsif config.invidious_companion_key.size < 16
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 or more."
exit(1)
end
end
# HMAC_key is mandatory
# See: https://github.com/iv-org/invidious/issues/3854
if config.hmac_key.empty?

View file

@ -154,17 +154,15 @@ module Invidious::Database::Users
# Update (misc)
# -------------------
# Feeds never need update. PubSubHubBub is the one that sends videos to
# invidious.
# def feed_needs_update(video : ChannelVideo)
# request = <<-SQL
# UPDATE users
# SET feed_needs_update = true
# WHERE $1 = ANY(subscriptions)
# SQL
def feed_needs_update(video : ChannelVideo)
request = <<-SQL
UPDATE users
SET feed_needs_update = true
WHERE $1 = ANY(subscriptions)
SQL
# PG_DB.exec(request, video.ucid)
# end
PG_DB.exec(request, video.ucid)
end
def update_preferences(user : User)
request = <<-SQL

View file

@ -10,8 +10,8 @@ module Invidious::Database::Videos
ON CONFLICT (id) DO NOTHING
SQL
REDIS_DB.set(video.id, video.info.to_json, ex: 14400)
REDIS_DB.set(video.id + ":time", video.updated, ex: 14400)
REDIS_DB.set(video.id, video.info.to_json, ex: 3600)
REDIS_DB.set(video.id + ":time", video.updated, ex: 3600)
end
def delete(id)

View file

@ -18,40 +18,6 @@ end
class HTTP::Client
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
io = @io
return io if io

View file

@ -43,8 +43,6 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
# URLs for the error message below
url_faq = "https://github.com/iv-org/documentation/blob/master/docs/faq.md"
url_search_issues = "https://github.com/iv-org/invidious/issues"
url_search_issues += "?q=is:issue+is:open+"
url_search_issues += URI.encode_www_form("[Bug] #{issue_title}")
url_switch = "https://redirect.invidious.io" + env.request.resource

View file

@ -1,22 +1,8 @@
# Languages requiring a better level of translation (at least 20%)
# to be added to the list below:
#
# "af" => "", # Afrikaans
# "az" => "", # Azerbaijani
# "be" => "", # Belarusian
# "bn_BD" => "", # Bengali (Bangladesh)
# "ia" => "", # Interlingua
# "or" => "", # Odia
# "tk" => "", # Turkmen
# "tok => "", # Toki Pona
#
LOCALES_LIST = {
"ar" => "العربية", # Arabic
"bg" => "български", # Bulgarian
"bn" => "বাংলা", # Bengali
"ca" => "Català", # Catalan
"cs" => "Čeština", # Czech
"cy" => "Cymraeg", # Welsh
"da" => "Dansk", # Danish
"de" => "Deutsch", # German
"el" => "Ελληνικά", # Greek
@ -37,7 +23,6 @@ LOCALES_LIST = {
"it" => "Italiano", # Italian
"ja" => "日本語", # Japanese
"ko" => "한국어", # Korean
"lmo" => "Lombard", # Lombard
"lt" => "Lietuvių", # Lithuanian
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
"nl" => "Nederlands", # Dutch

View file

@ -12,9 +12,7 @@ enum LogLevel
end
class Invidious::LogHandler < Kemal::BaseLogHandler
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, use_color : Bool = true)
Colorize.enabled = use_color
Colorize.on_tty_only!
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, @color : Bool = true)
end
def call(context : HTTP::Server::Context)
@ -58,7 +56,8 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
{% for level in %w(trace debug info warn error fatal) %}
def {{level.id}}(message : String)
if LogLevel::{{level.id.capitalize}} >= @level
puts("#{Time.utc} [{{level.id}}] #{message}".colorize(color(LogLevel::{{level.id.capitalize}})))
puts("#{Time.utc} [{{level.id}}] #{message}".colorize(color(LogLevel::{{level.id.capitalize}})).toggle(@color))
end
end
{% end %}

View file

@ -1,16 +1,3 @@
@[Flags]
enum VideoBadges
LiveNow
Premium
ThreeD
FourK
New
EightK
VR180
VR360
ClosedCaptions
end
struct SearchVideo
include DB::Serializable
@ -22,9 +9,10 @@ struct SearchVideo
property views : Int64
property description_html : String
property length_seconds : Int32
property live_now : Bool
property premium : Bool
property premiere_timestamp : Time?
property author_verified : Bool
property badges : VideoBadges
def to_xml(auto_generated, query_params, xml : XML::Builder)
query_params["v"] = self.id
@ -100,20 +88,13 @@ struct SearchVideo
json.field "published", self.published.to_unix
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
json.field "lengthSeconds", self.length_seconds
json.field "liveNow", self.badges.live_now?
json.field "premium", self.badges.premium?
json.field "liveNow", self.live_now
json.field "premium", self.premium
json.field "isUpcoming", self.upcoming?
if self.premiere_timestamp
json.field "premiereTimestamp", self.premiere_timestamp.try &.to_unix
end
json.field "isNew", self.badges.new?
json.field "is4k", self.badges.four_k?
json.field "is8k", self.badges.eight_k?
json.field "isVr180", self.badges.vr180?
json.field "isVr360", self.badges.vr360?
json.field "is3d", self.badges.three_d?
json.field "hasCaptions", self.badges.closed_captions?
end
end

View file

@ -1,35 +0,0 @@
module SessionTokens
extend self
@@po_token : String | Nil
@@visitor_data : String | Nil
def refresh_tokens
begin
response = HTTP::Client.get "#{CONFIG.tokens_server}/generate"
if !response.status_code == 200
LOGGER.error("RefreshSessionTokens: Expected response to have status code 200 but got #{response.status_code} from #{CONFIG.tokens_server}")
end
json = JSON.parse(response.body)
@@po_token = json.try &.["potoken"].as_s || nil
@@visitor_data = json.try &.["visitorData"].as_s || nil
rescue ex
LOGGER.error("RefreshSessionTokens: Failed to fetch tokens from #{CONFIG.tokens_server}: #{ex.message}")
return
end
if !@@po_token.nil? && !@@visitor_data.nil?
set_tokens
LOGGER.debug("RefreshSessionTokens: Successfully updated po_token and visitor_data")
else
LOGGER.warn("RefreshSessionTokens: Tokens are empty!. Invidious will use the tokens that are on the configuration file")
end
LOGGER.trace("RefreshSessionTokens: Tokens are:")
LOGGER.trace("RefreshSessionTokens: po_token: #{CONFIG.po_token}")
LOGGER.trace("RefreshSessionTokens: visitor_data: #{CONFIG.visitor_data}")
end
def set_tokens
CONFIG.po_token = @@po_token
CONFIG.visitor_data = @@visitor_data
end
end

View file

@ -175,6 +175,7 @@ module Invidious::SigHelper
@queue = {} of TransactionID => Transaction
@conn : Connection
@uri_or_path : String
def initialize(@uri_or_path)
@ -191,6 +192,7 @@ module Invidious::SigHelper
loop do
begin
receive_data
# Handle all errors
rescue ex
LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...")
# We close the socket because for some reason is not closed.
@ -200,8 +202,8 @@ module Invidious::SigHelper
@conn = Connection.new(@uri_or_path)
LOGGER.info("SigHelper: Reconnected to SigHelper!")
rescue ex
LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying")
sleep 500.milliseconds
LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}' retrying")
sleep 0.5
next
end
break if !@conn.closed?
@ -323,6 +325,11 @@ module Invidious::SigHelper
return @socket.closed?
end
def remote_address : Socket::IPAddress
return @socket.remote_address
end
def close : Nil
@socket.close if !@socket.closed?
end

View file

@ -294,7 +294,7 @@ def subscribe_pubsub(topic, key)
signature = "#{time}:#{nonce}"
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.verify" => "async",
"hub.mode" => "subscribe",
@ -383,36 +383,3 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
end
return text
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}"
end
else
external_videoplayback_proxy = ""
end
return external_videoplayback_proxy
end
def encrypt_ecb_without_salt(data, key)
cipher = OpenSSL::Cipher.new("aes-128-ecb")
cipher.encrypt
cipher.key = key
io = IO::Memory.new
io.write(cipher.update(data))
io.write(cipher.final)
io.rewind
return io
end
def invidious_companion_encrypt(data)
timestamp = Time.utc.to_unix
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
return Base64.urlsafe_encode(encrypted_data)
end

View file

@ -4,30 +4,6 @@ module Invidious::HttpServer
module Utils
extend self
@@proxy_alive : String = ""
def check_external_proxy
CONFIG.external_videoplayback_proxy.each do |proxy|
begin
response = HTTP::Client.get("#{proxy}/health")
if response.status_code == 200
@@proxy_alive = proxy
LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy}'")
break
end
rescue
LOGGER.debug("CheckExternalProxy: Proxy '#{proxy}' 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)
url = URI.parse(raw_url)
@ -38,11 +14,7 @@ module Invidious::HttpServer
url.query_params = params
if absolute
if !@@proxy_alive.empty?
return "#{@@proxy_alive}#{url.request_target}"
else
return "#{HOST_URL}#{url.request_target}"
end
else
return url.request_target
end

View file

@ -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

View file

@ -1,13 +0,0 @@
class Invidious::Jobs::RefreshSessionTokens < Invidious::Jobs::BaseJob
def initialize
end
def begin
loop do
SessionTokens.refresh_tokens
LOGGER.info("RefreshTokens: Done, sleeping for 5 seconds")
sleep 5.seconds
Fiber.yield
end
end
end

View file

@ -30,8 +30,6 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob
spawn do
begin
response = subscribe_pubsub(ucid, hmac_key)
LOGGER.debug("SubscribeToFeedsJob: Subscribed to #{ucid}.")
LOGGER.trace("SubscribeToFeedsJob: response.body: #{response.body}")
if response.status_code >= 400
LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{response.body}")

View file

@ -8,7 +8,7 @@ module Invidious::JSONify::APIv1
build_thumbnails(id).each do |thumbnail|
json.object do
json.field "quality", thumbnail[:name]
json.field "url", "/vi/#{id}/#{thumbnail["url"]}.jpg"
json.field "url", "#{thumbnail[:host]}/vi/#{id}/#{thumbnail["url"]}.jpg"
json.field "width", thumbnail[:width]
json.field "height", thumbnail[:height]
end

View file

@ -270,7 +270,7 @@ end
def subscribe_playlist(user, playlist)
playlist = InvidiousPlaylist.new({
title: playlist.title[..150],
title: playlist.title.byte_slice(0, 150),
id: playlist.id,
author: user.email,
description: "", # Max 5000 characters

View file

@ -8,11 +8,6 @@ module Invidious::Routes::API::Manifest
id = env.params.url["id"]
region = env.params.query["region"]?
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/api/manifest/dash/id/#{id}?#{env.params.query}"
end
# Since some implementations create playlists based on resolution regardless of different codecs,
# we can opt to only add a source to a representation if it has a unique height within that representation
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }
@ -32,21 +27,36 @@ module Invidious::Routes::API::Manifest
haltf env, status_code: response.status_code
end
# Proxy URLs for video playback on invidious.
# Other API clients can get the original URLs by omiting `local=true`.
manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
manifest = response.body
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>")
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>"
end
return manifest
end
# Ditto, only proxify URLs if `local=true` is used
adaptive_fmts = video.adaptive_fmts
if local
video.adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
adaptive_fmts.each do |fmt|
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
@ -60,10 +70,6 @@ module Invidious::Routes::API::Manifest
end
end
audio_streams.reject! do |z|
z if z.dig?("audioTrack", "audioIsDefault") == false
end
manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml|
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
"profiles": "urn:mpeg:dash:profile:full:2011", minBufferTime: "PT1.5S", type: "static",
@ -218,19 +224,7 @@ module Invidious::Routes::API::Manifest
raw_params["local"] = "true"
proxy = Invidious::HttpServer::Utils.get_external_proxy
if CONFIG.https_only
scheme = "https://"
else
scheme = "http://"
end
if !proxy.empty?
"#{proxy}/videoplayback?#{raw_params}"
else
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}"
end
"#{HOST_URL}/videoplayback?#{raw_params}"
end
end
@ -253,12 +247,7 @@ module Invidious::Routes::API::Manifest
manifest = response.body
if local
if CONFIG.https_only
scheme = "https://"
else
scheme = "http://"
end
manifest = manifest.gsub("https://www.youtube.com", "#{scheme}#{env.request.headers["Host"]}")
manifest = manifest.gsub("https://www.youtube.com", HOST_URL)
manifest = manifest.gsub("index.m3u8", "index.m3u8?local=true")
end

View file

@ -226,7 +226,7 @@ module Invidious::Routes::API::V1::Authenticated
end
playlist = create_playlist(title, privacy, user)
env.response.headers["Location"] = "#{env.request.headers["Host"]}/api/v1/auth/playlists/#{playlist.id}"
env.response.headers["Location"] = "#{HOST_URL}/api/v1/auth/playlists/#{playlist.id}"
env.response.status_code = 201
{
"title" => title,

View file

@ -197,7 +197,6 @@ module Invidious::Routes::API::V1::Channels
get_channel()
# Retrieve continuation from URL parameters
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
continuation = env.params.query["continuation"]?
if channel.is_age_gated
@ -212,7 +211,7 @@ module Invidious::Routes::API::V1::Channels
else
begin
videos, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation, sort_by: sort_by
channel, continuation: continuation
)
rescue ex
return error_json(500, ex)

View file

@ -31,7 +31,9 @@ module Invidious::Routes::API::V1::Search
query = env.params.query["q"]? || ""
begin
client = make_client(URI.parse("https://suggestqueries-clients6.youtube.com"), force_youtube_headers: true)
client = HTTP::Client.new("suggestqueries-clients6.youtube.com")
client.before_request { |r| add_yt_headers(r) }
url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt"
response = client.get(url).body

View file

@ -263,61 +263,60 @@ module Invidious::Routes::API::V1::Videos
annotations = ""
# case source
# when "archive"
# if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
# annotations = cached_annotation.annotations
# else
# index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
case source
when "archive"
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
annotations = cached_annotation.annotations
else
index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
# # IA doesn't handle leading hyphens,
# # so we use https://archive.org/details/youtubeannotations_64
# if index == "62"
# index = "64"
# id = id.sub(/^-/, 'A')
# end
# IA doesn't handle leading hyphens,
# so we use https://archive.org/details/youtubeannotations_64
if index == "62"
index = "64"
id = id.sub(/^-/, 'A')
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"]?
# env.response.status_code = location.status_code
# end
if !location.headers["Location"]?
env.response.status_code = location.status_code
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?
# haltf env, 404
# end
if response.body.empty?
haltf env, 404
end
# if response.status_code != 200
# haltf env, response.status_code
# end
if response.status_code != 200
haltf env, response.status_code
end
# annotations = response.body
annotations = response.body
# cache_annotation(id, annotations)
# end
# else # "youtube"
# response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
cache_annotation(id, annotations)
end
else # "youtube"
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
# if response.status_code != 200
# haltf env, response.status_code
# end
if response.status_code != 200
haltf env, response.status_code
end
# annotations = response.body
# end
annotations = response.body
end
# etag = sha256(annotations)[0, 16]
# if env.request.headers["If-None-Match"]?.try &.== etag
# haltf env, 304
# else
# env.response.headers["ETag"] = etag
# annotations
# end
etag = sha256(annotations)[0, 16]
if env.request.headers["If-None-Match"]?.try &.== etag
haltf env, 304
else
env.response.headers["ETag"] = etag
annotations
end
end
def self.comments(env)
locale = env.get("preferences").as(Preferences).locale

View file

@ -20,25 +20,18 @@ module Invidious::Routes::BeforeAll
env.response.headers["X-XSS-Protection"] = "1; mode=block"
env.response.headers["X-Content-Type-Options"] = "nosniff"
extra_media_csp = ""
extra_connect_csp = ""
if CONFIG.invidious_companion.present?
extra_media_csp = " #{CONFIG.invidious_companion.sample.public_url}"
extra_connect_csp = " #{CONFIG.invidious_companion.sample.public_url}"
end
if !CONFIG.external_videoplayback_proxy.empty?
CONFIG.external_videoplayback_proxy.each do |proxy|
extra_media_csp += " #{proxy}"
extra_connect_csp += " #{proxy}"
end
end
# Allow media resources to be loaded from google servers
# TODO: check if *.youtube.com can be removed
if CONFIG.disabled?("local") || !preferences.local
extra_media_csp += " https://*.googlevideo.com:443 https://*.youtube.com:443"
extra_media_csp = " https://*.googlevideo.com:443 https://*.youtube.com:443"
else
extra_media_csp = ""
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
@ -56,7 +49,7 @@ module Invidious::Routes::BeforeAll
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"font-src 'self' data:",
"connect-src 'self'" + extra_connect_csp,
"connect-src 'self'" + external_videoplayback_proxy,
"manifest-src 'self'",
"media-src 'self' blob:" + extra_media_csp,
"child-src 'self' blob:",

View file

@ -20,11 +20,10 @@ module Invidious::Routes::Channels
sort_by = env.params.query["sort_by"]?.try &.downcase
if channel.auto_generated
sort_by ||= "last"
sort_options = {"last", "oldest", "newest"}
items, next_continuation = fetch_channel_playlists(
channel.ucid, channel.author, continuation, sort_by
channel.ucid, channel.author, continuation, (sort_by || "last")
)
items.uniq! do |item|
@ -50,11 +49,9 @@ module Invidious::Routes::Channels
end
next_continuation = nil
else
sort_by ||= "newest"
sort_options = {"newest", "oldest", "popular"}
items, next_continuation = Channel::Tabs.get_60_videos(
channel, continuation: continuation, sort_by: sort_by
items, next_continuation = Channel::Tabs.get_videos(
channel, continuation: continuation, sort_by: (sort_by || "newest")
)
end
end
@ -85,12 +82,13 @@ module Invidious::Routes::Channels
end
next_continuation = nil
else
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
sort_options = {"newest", "oldest", "popular"}
# TODO: support sort option for shorts
sort_by = ""
sort_options = [] of String
# Fetch items and continuation token
items, next_continuation = Channel::Tabs.get_shorts(
channel, continuation: continuation, sort_by: sort_by
channel, continuation: continuation
)
end

View file

@ -157,12 +157,10 @@ module Invidious::Routes::Embed
adaptive_fmts = video.adaptive_fmts
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
# 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
audio_streams = video.audio_streams

View file

@ -192,9 +192,11 @@ module Invidious::Routes::Feeds
views: views,
description_html: description_html,
length_seconds: 0,
live_now: false,
paid: false,
premium: false,
premiere_timestamp: nil,
author_verified: false,
badges: VideoBadges::None,
})
end
@ -416,22 +418,18 @@ module Invidious::Routes::Feeds
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
published = Time.parse_rfc3339(entry.xpath_node("default:published", namespaces).not_nil!.content)
updated = Time.parse_rfc3339(entry.xpath_node("default:updated", namespaces).not_nil!.content)
ucid = entry.xpath_node("yt:channelId", namespaces).not_nil!.content
title = entry.xpath_node("default:title", namespaces).not_nil!.content
if CONFIG.use_innertube_for_feeds
begin
video_ = get_video(id, force_refresh: true)
video = get_video(id, force_refresh: true)
rescue
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
end
end
if CONFIG.enable_user_notifications
# Deliver notifications to `/api/v1/auth/notifications`
payload = {
"topic" => ucid,
"videoId" => id,
"topic" => video.ucid,
"videoId" => video.id,
"published" => published.to_unix,
}.to_json
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
@ -439,23 +437,23 @@ module Invidious::Routes::Feeds
video = ChannelVideo.new({
id: id,
title: title,
title: video.title,
published: published,
updated: updated,
ucid: ucid,
ucid: video.ucid,
author: author,
length_seconds: video_.try &.length_seconds || 0,
live_now: video_.try &.live_now || false,
premiere_timestamp: video_.try &.premiere_timestamp || nil,
views: video_.try &.views || nil,
length_seconds: video.length_seconds,
live_now: video.live_now,
premiere_timestamp: video.premiere_timestamp,
views: video.views,
})
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
if was_insert
if CONFIG.enable_user_notifications
Invidious::Database::Users.add_notification(video)
# else
# Invidious::Database::Users.feed_needs_update(video)
else
Invidious::Database::Users.feed_needs_update(video)
end
end
end

View file

@ -11,9 +11,29 @@ module Invidious::Routes::Images
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
GGPHT_POOL.client &.get(url, headers) do |resp|
return self.proxy_image(env, resp)
HTTP::Client.get("https://yt3.ggpht.com#{url}") do |resp|
return request_proc.call(resp)
end
rescue ex
end
@ -41,10 +61,27 @@ module Invidious::Routes::Images
end
end
begin
get_ytimg_pool(authority).client &.get(url, headers) do |resp|
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"
return self.proxy_image(env, resp)
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
HTTP::Client.get("https://#{authority}.ytimg.com#{url}") do |resp|
return request_proc.call(resp)
end
rescue ex
end
@ -64,9 +101,26 @@ module Invidious::Routes::Images
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
get_ytimg_pool("i9").client &.get(url, headers) do |resp|
return self.proxy_image(env, resp)
HTTP::Client.get("https://i9.ytimg.com#{url}") do |resp|
return request_proc.call(resp)
end
rescue ex
end
@ -111,7 +165,8 @@ module Invidious::Routes::Images
if name == "maxres.jpg"
build_thumbnails(id).each do |thumb|
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"
break
end
@ -126,15 +181,7 @@ module Invidious::Routes::Images
end
end
begin
get_ytimg_pool("i").client &.get(url, headers) do |resp|
return self.proxy_image(env, resp)
end
rescue ex
end
end
private def self.proxy_image(env, response)
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)
@ -144,10 +191,19 @@ module Invidious::Routes::Images
env.response.headers["Access-Control-Allow-Origin"] = "*"
if response.status_code >= 300
if response.status_code >= 300 && response.status_code != 404
return env.response.headers.delete("Transfer-Encoding")
end
return proxy_file(response, env)
proxy_file(response, env)
}
begin
# This can likely be optimized into a (small) pool sometime in the future.
HTTP::Client.get("https://i.ytimg.com#{url}") do |resp|
return request_proc.call(resp)
end
rescue ex
end
end
end

View file

@ -64,8 +64,6 @@ module Invidious::Routes::Login
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.backend_domains[alt], sid)
else
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
end
@ -172,8 +170,6 @@ module Invidious::Routes::Login
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.backend_domains[alt], sid)
else
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
end

View file

@ -27,10 +27,6 @@ module Invidious::Routes::PreferencesRoute
annotations_subscribed ||= "off"
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 ||= "off"
autoplay = autoplay == "on"
@ -148,7 +144,6 @@ module Invidious::Routes::PreferencesRoute
preferences = Preferences.from_json({
annotations: annotations,
annotations_subscribed: annotations_subscribed,
preload: preload,
autoplay: autoplay,
captions: captions,
comments: comments,
@ -228,8 +223,6 @@ module Invidious::Routes::PreferencesRoute
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.backend_domains[alt], preferences)
else
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
end
@ -271,8 +264,6 @@ module Invidious::Routes::PreferencesRoute
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
elsif alt = CONFIG.backend_domains.index(env.request.headers["Host"])
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.backend_domains[alt], preferences)
else
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
end

View file

@ -0,0 +1,18 @@
{% skip_file if flag?(:api_only) %}
module Invidious::Routes::BackendSwitcher
def self.switch(env)
referer = get_referer(env)
backend_id = env.params.query["backend_id"]
# Checks if there is any alternative domain, like a second domain name,
# TOR or I2P address
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
env.response.cookies["SERVER_ID"] = Invidious::User::Cookies.server_id(CONFIG.alternative_domains[alt], backend_id)
else
env.response.cookies["SERVER_ID"] = Invidious::User::Cookies.server_id(CONFIG.domain, backend_id)
end
env.redirect referer
end
end

View file

@ -3,11 +3,6 @@ module Invidious::Routes::VideoPlayback
def self.get_video_playback(env)
locale = env.get("preferences").as(Preferences).locale
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"
mns = query_params["mn"]?.try &.split(",")
@ -47,7 +42,11 @@ module Invidious::Routes::VideoPlayback
headers["Range"] = "bytes=#{range_for_head}"
end
client = make_client(URI.parse(host), region, force_resolve: true)
headers["Origin"] = "https://www.youtube.com"
headers["Referer"] = "https://www.youtube.com/"
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)
response = HTTP::Client::Response.new(500)
error = ""
5.times do
@ -62,7 +61,7 @@ module Invidious::Routes::VideoPlayback
if new_host != host
host = new_host
client.close
client = make_client(URI.parse(new_host), region, force_resolve: true)
client = make_client(URI.parse(new_host), region, force_resolve = true)
end
url = "#{location.request_target}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
@ -76,7 +75,7 @@ module Invidious::Routes::VideoPlayback
fvip = "3"
host = "https://r#{fvip}---#{mn}.googlevideo.com"
client = make_client(URI.parse(host), region, force_resolve: true)
client = make_client(URI.parse(host), region, force_resolve = true)
rescue ex
error = ex.message
end
@ -101,7 +100,7 @@ module Invidious::Routes::VideoPlayback
end
begin
client.post(url, headers, protobuf) do |resp|
client.get(url, headers) do |resp|
resp.headers.each do |key, value|
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
env.response.headers[key] = value
@ -152,7 +151,7 @@ module Invidious::Routes::VideoPlayback
headers["Range"] = "bytes=#{chunk_start}-#{chunk_end}"
begin
client.post(url, headers, protobuf) do |resp|
client.get(url, headers) do |resp|
if first_chunk
if !env.request.headers["Range"]? && resp.status_code == 206
env.response.status_code = 200
@ -201,7 +200,7 @@ module Invidious::Routes::VideoPlayback
break
else
client.close
client = make_client(URI.parse(host), region, force_resolve: true)
client = make_client(URI.parse(host), region, force_resolve = true)
end
end
@ -258,11 +257,6 @@ module Invidious::Routes::VideoPlayback
# YouTube /videoplayback links expire after 6 hours,
# so we have a mechanism here to redirect to the latest version
def self.latest_version(env)
if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
end
id = env.params.query["id"]?
itag = env.params.query["itag"]?.try &.to_i?
@ -304,16 +298,7 @@ module Invidious::Routes::VideoPlayback
end
if local
external_proxy = Invidious::HttpServer::Utils.get_external_proxy
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
end

View file

@ -128,12 +128,10 @@ module Invidious::Routes::Watch
end
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
# 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
audio_streams = video.audio_streams
@ -144,11 +142,6 @@ module Invidious::Routes::Watch
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.
# We redirect here so they're not unplayable
if audio_streams.empty? && !video.live_now
@ -211,12 +204,6 @@ module Invidious::Routes::Watch
captions: video.captions
)
begin
video_url = fmt_stream[0]["url"].to_s
rescue
video_url = nil
end
templated "watch"
end
@ -347,18 +334,14 @@ module Invidious::Routes::Watch
env.params.query["label"] = URI.decode_www_form(label.as_s)
return Invidious::Routes::API::V1::Videos.captions(env)
elsif itag = download_widget["itag"]?.try &.as_i.to_s
elsif itag = download_widget["itag"]?.try &.as_i
# URL params specific to /latest_version
env.params.query["id"] = video_id
env.params.query["itag"] = itag.to_s
env.params.query["title"] = filename
env.params.query["local"] = "true"
if (CONFIG.invidious_companion.present?)
video = get_video(video_id)
return env.redirect "#{video.invidious_companion["baseUrl"].as_s}/latest_version?#{env.params.query}"
else
return Invidious::Routes::VideoPlayback.latest_version(env)
end
else
return error_template(400, "Invalid label or itag")
end

View file

@ -21,6 +21,7 @@ module Invidious::Routing
get "/privacy", Routes::Misc, :privacy
get "/licenses", Routes::Misc, :licenses
get "/redirect", Routes::Misc, :cross_instance_redirect
get "/switchbackend", Routes::BackendSwitcher, :switch
self.register_channel_routes
self.register_watch_routes
@ -243,16 +244,17 @@ module Invidious::Routing
# Channels
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home
get "/api/v1/channels/:ucid/latest", {{namespace}}::Channels, :latest
get "/api/v1/channels/:ucid/videos", {{namespace}}::Channels, :videos
get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels
get "/api/v1/channels/:ucid/search", {{namespace}}::Channels, :search
{% for route in {"videos", "latest", "playlists", "community", "search"} %}
get "/api/v1/channels/#{{{route}}}/:ucid", {{namespace}}::Channels, :{{route}}
get "/api/v1/channels/:ucid/#{{{route}}}", {{namespace}}::Channels, :{{route}}
{% end %}
# Posts
get "/api/v1/post/:id", {{namespace}}::Channels, :post
@ -270,6 +272,11 @@ module Invidious::Routing
# Authenticated
# The notification APIs cannot be extracted yet! They require the *local* notifications constant defined in invidious.cr
#
# Invidious::Routing.get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
# Invidious::Routing.post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences

View file

@ -45,5 +45,18 @@ struct Invidious::User
samesite: HTTP::Cookie::SameSite::Lax
)
end
# Server ID (SERVER_ID) cookie used for Sticky Sessions
# Parameter "domain" comes from the global config
def server_id(domain : String?, server_id) : HTTP::Cookie
return HTTP::Cookie.new(
name: "SERVER_ID",
domain: domain,
value: server_id,
secure: false,
http_only: true,
samesite: HTTP::Cookie::SameSite::Lax
)
end
end
end

View file

@ -4,7 +4,6 @@ struct Preferences
property annotations : Bool = CONFIG.default_user_preferences.annotations
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 automatic_instance_redirect : Bool = CONFIG.default_user_preferences.automatic_instance_redirect

View file

@ -15,7 +15,7 @@ struct Video
# NOTE: don't forget to bump this number if any change is made to
# the `params` structure in videos/parser.cr!!!
#
SCHEMA_VERSION = 3
SCHEMA_VERSION = 2
property id : String
@ -26,6 +26,12 @@ struct Video
@[DB::Field(ignore: true)]
@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)]
property description : String?
@ -92,24 +98,72 @@ struct Video
# Methods for parsing streaming data
def fmt_stream : Array(Hash(String, JSON::Any))
if formats = info.dig?("streamingData", "formats")
return formats
.as_a.map(&.as_h)
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
def convert_url(fmt)
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("Videos: Decoding '#{cfr}'")
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
params[sp] = unsig if unsig
else
return [] of Hash(String, JSON::Any)
end
url = URI.parse(fmt["url"].as_s)
params = url.query_params
end
def adaptive_fmts : Array(Hash(String, JSON::Any))
if formats = info.dig?("streamingData", "adaptiveFormats")
return formats
.as_a.map(&.as_h)
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
else
return [] of Hash(String, JSON::Any)
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
def fmt_stream
return @fmt_stream.as(Array(Hash(String, JSON::Any))) if @fmt_stream
fmt_stream = info.dig?("streamingData", "formats")
.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 }
@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
def video_streams
@ -192,10 +246,6 @@ struct Video
}
end
def invidious_companion : Hash(String, JSON::Any)?
info["invidiousCompanion"]?.try &.as_h || {} of String => JSON::Any
end
# Macros defining getters/setters for various types of data
private macro getset_string(name)

View file

@ -123,7 +123,6 @@ module Invidious::Videos
"Esperanto",
"Estonian",
"Filipino",
"Filipino (auto-generated)",
"Finnish",
"French",
"French (auto-generated)",

View file

@ -100,11 +100,10 @@ def extract_video_info(video_id : String)
params = parse_video_info(video_id, player_response)
params["reason"] = JSON::Any.new(reason) if reason
if CONFIG.invidious_companion.present?
new_player_response = nil
# Don't use Android test suite client if po_token is passed because po_token doesn't
# work for Android test suite client.
# Don't use Android client if po_token is passed because po_token doesn't
# work for Android client.
if reason.nil? && CONFIG.po_token.nil?
# Fetch the video streams using an Android client in order to get the
# decrypted URLs and maybe fix throttling issues (#2194). See the
@ -114,6 +113,15 @@ def extract_video_info(video_id : String)
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# Last hope
# 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
# if the IP address is not blocked.
if CONFIG.po_token && reason || CONFIG.po_token.nil? && new_player_response.nil?
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# Replace player response and reset reason
if !new_player_response.nil?
# Preserve captions & storyboard data before replacement
@ -123,23 +131,11 @@ def extract_video_info(video_id : String)
player_response = new_player_response
params.delete("reason")
end
end
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f|
{"captions", "playabilityStatus", "playerConfig", "storyboards", "streamingData"}.each do |f|
params[f] = player_response[f] if player_response[f]?
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))
end
end
params["streamingData"] = streaming_data
end
# Data structure version, for cache control
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
@ -217,17 +213,8 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?(
"playabilityStatus", "liveStreamability",
"liveStreamabilityRenderer", "offlineSlate",
"liveStreamOfflineSlateRenderer", "scheduledStartTime"
)
.try &.as_s.to_i64
.try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool
live_now ||= video_details.dig?("isLive").try &.as_bool || false
.try &.as_bool || false
post_live_dvr = video_details.dig?("isPostLiveDvr")
.try &.as_bool || false
@ -456,35 +443,3 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
return params
end
private def convert_url(fmt)
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 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

View file

@ -2,7 +2,6 @@ struct VideoPreferences
include JSON::Serializable
property annotations : Bool
property preload : Bool
property autoplay : Bool
property comments : Array(String)
property continue : Bool
@ -29,7 +28,6 @@ end
def process_video_params(query, preferences)
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 }
comments = query["comments"]?.try &.split(",").map(&.downcase)
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
@ -52,7 +50,6 @@ def process_video_params(query, preferences)
if preferences
# region ||= preferences.region
annotations ||= preferences.annotations.to_unsafe
preload ||= preferences.preload.to_unsafe
autoplay ||= preferences.autoplay.to_unsafe
comments ||= preferences.comments
continue ||= preferences.continue.to_unsafe
@ -73,7 +70,6 @@ def process_video_params(query, preferences)
end
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
preload ||= CONFIG.default_user_preferences.preload.to_unsafe
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
comments ||= CONFIG.default_user_preferences.comments
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
annotations = annotations == 1
preload = preload == 1
autoplay = autoplay == 1
continue = continue == 1
continue_autoplay = continue_autoplay == 1
@ -133,7 +128,6 @@ def process_video_params(query, preferences)
params = VideoPreferences.new({
annotations: annotations,
preload: preload,
autoplay: autoplay,
comments: comments,
continue: continue,

View file

@ -2,7 +2,6 @@
ucid = channel.ucid
author = HTML.escape(channel.author)
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
host = env.request.headers["Host"]
relative_url =
case selected_tab
@ -29,15 +28,15 @@
<%- if selected_tab.videos? -%>
<meta name="description" content="<%= channel.description %>">
<meta property="og:site_name" content="Invidious">
<meta property="og:url" content="<%= host %>/channel/<%= ucid %>">
<meta property="og:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
<meta property="og:title" content="<%= author %>">
<meta property="og:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<meta property="og:image" content="<%= HOST_URL %>/ggpht<%= channel_profile_pic %>">
<meta property="og:description" content="<%= channel.description %>">
<meta name="twitter:card" content="summary">
<meta name="twitter:url" content="<%= host %>/channel/<%= ucid %>">
<meta name="twitter:url" content="<%= HOST_URL %>/channel/<%= ucid %>">
<meta name="twitter:title" content="<%= author %>">
<meta name="twitter:description" content="<%= channel.description %>">
<meta name="twitter:image" content="<%= host %>/ggpht<%= channel_profile_pic %>">
<meta name="twitter:image" content="<%= HOST_URL %>/ggpht<%= channel_profile_pic %>">
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
<%- end -%>

View file

@ -1,6 +1,5 @@
<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 %>"
preload="<% if params.preload %>auto<% else %>none<% end %>"
<% if params.autoplay %>autoplay<% end %>
<% if params.video_loop %>loop<% end %>
<% if params.controls %>controls<% end %>>
@ -22,8 +21,6 @@
audio_streams.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s)
@ -36,12 +33,8 @@
<% end %>
<% end %>
<% else %>
<% if params.quality == "dash"
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1"
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
%>
<source src="<%= src_url %>" type='application/dash+xml' label="dash">
<% if params.quality == "dash" %>
<source src="/api/manifest/dash/id/<%= video.id %>?local=true&unique_res=1" type='application/dash+xml' label="dash">
<% end %>
<%
@ -50,8 +43,6 @@
fmt_stream.each_with_index do |fmt, i|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local
src_url = video.invidious_companion["baseUrl"].as_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (CONFIG.invidious_companion.present?)
quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s)

View file

@ -1,8 +1,6 @@
<%
locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value || env.request.headers["Host"]
current_external_videoplayback_proxy = Invidious::HttpServer::Utils.get_external_proxy()
%>
<!DOCTYPE html>
<html lang="<%= locale %>">
@ -106,31 +104,16 @@
</div>
</div>
<% if !CONFIG.backends.empty? %>
<% if !CONFIG.backend_domains.includes?(env.request.headers["Host"]) %>
<% if CONFIG.backends %>
<div class="h-box">
<b>Switch Backend:</b>
<% CONFIG.backends.each do | backend | %>
<% backend = backend.split(CONFIG.backends_delimiter) %>
<% if current_backend == backend[0] %>
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="text-decoration-line: underline; display: inline-block;">
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 %>
<a href="/switchbackend?backend_id=<%= backend %>">
Backend<%= HTML.escape(backend) %>
</a> |
<% end %>
</div>
<% end %>
<% end %>
<% if CONFIG.banner %>
<div class="h-box">
@ -313,10 +296,6 @@
</div>
<hr/>
<div class="footer-footer">
<div class="box">You are currently using Backend: <%= current_backend %></div>
<% if !current_external_videoplayback_proxy.empty? %>
<div class="box">External Videoplayback Proxy: <%= current_external_videoplayback_proxy %></div>
<% end %>
<span class="left">
<% if CONFIG.modified_source_code_url %>
<%= translate(locale, "footer_current_version_modified") %>

View file

@ -12,11 +12,6 @@
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
</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">
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>

View file

@ -1,7 +1,6 @@
<% ucid = video.ucid %>
<% title = HTML.escape(video.title) %>
<% author = HTML.escape(video.author) %>
<% host = env.request.headers["Host"] %>
<% content_for "header" do %>
@ -9,24 +8,22 @@
<meta name="description" content="<%= HTML.escape(video.short_description) %>">
<meta name="keywords" content="<%= video.keywords.join(",") %>">
<meta property="og:site_name" content="<%= author %> | Invidious">
<meta property="og:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta property="og:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
<meta property="og:title" content="<%= title %>">
<meta property="og:image" content="<%= host %>/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:type" content="video.other">
<!-- This shouldn't be empty, ever. -->
<meta property="og:video" content="<%= video_url %>">
<meta property="og:video:url" content="<%= video_url %>">
<meta property="og:video:secure_url" content="<%= video_url %>">
<meta property="og:video:type" content="video/mp4">
<meta property="og:video:width" content="640">
<meta property="og:video:height" content="360">
<meta property="og:video:url" content="<%= HOST_URL %>/embed/<%= video.id %>">
<meta property="og:video:secure_url" content="<%= HOST_URL %>/embed/<%= video.id %>">
<meta property="og:video:type" content="text/html">
<meta property="og:video:width" content="1280">
<meta property="og:video:height" content="720">
<meta name="twitter:card" content="player">
<meta name="twitter:url" content="<%= host %>/watch?v=<%= video.id %>">
<meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
<meta name="twitter:title" content="<%= title %>">
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
<meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= host %>/embed/<%= video.id %>">
<meta name="twitter:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
<meta name="twitter:player" content="<%= HOST_URL %>/embed/<%= video.id %>">
<meta name="twitter:player:width" content="1280">
<meta name="twitter:player:height" content="720">
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">

View file

@ -1,51 +1,3 @@
# Mapping of subdomain => YoutubeConnectionPool
# This is needed as we may need to access arbitrary subdomains of ytimg
private YTIMG_POOLS = {} of String => YoutubeConnectionPool
struct YoutubeConnectionPool
property! url : URI
property! capacity : Int32
property! timeout : Float64
property pool : DB::Pool(HTTP::Client)
def initialize(url : URI, @capacity = 5, @timeout = 5.0)
@url = url
@pool = build_pool()
end
def client(&)
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
response = yield conn
rescue ex
conn.close
conn = make_client(url, force_resolve: true)
response = yield conn
ensure
pool.release(conn)
end
response
end
private def build_pool
options = DB::Pool::Options.new(
initial_pool_size: 0,
max_pool_size: capacity,
max_idle_pool_size: capacity,
checkout_timeout: timeout
)
DB::Pool(HTTP::Client).new(options) do
next make_client(url, force_resolve: true)
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"
@ -61,56 +13,67 @@ def add_yt_headers(request)
end
end
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false, use_http_proxy : Bool = true)
struct YoutubeConnectionPool
property! url : URI
property! capacity : Int32
property! timeout : Float64
property pool : DB::Pool(HTTP::Client)
def initialize(url : URI, @capacity = 5, @timeout = 5.0)
@url = url
@pool = build_pool()
end
def client(&)
conn = pool.checkout
begin
response = yield conn
rescue ex
conn.close
conn = HTTP::Client.new(url)
conn.family = CONFIG.force_resolve
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"
response = yield conn
ensure
pool.release(conn)
end
response
end
private def build_pool
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
conn = HTTP::Client.new(url)
conn.family = CONFIG.force_resolve
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
end
end
end
def make_client(url : URI, region = nil, force_resolve : Bool = false)
client = HTTP::Client.new(url)
client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy && use_http_proxy
# Force the usage of a specific configured IP Family
if force_resolve
client.family = CONFIG.force_resolve
client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC
end
client.before_request { |r| add_yt_headers(r) } if url.host.try &.ends_with?("youtube.com") || force_youtube_headers
client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
client.read_timeout = 10.seconds
client.connect_timeout = 10.seconds
return client
end
def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &)
client = make_client(url, region, force_resolve: force_resolve, use_http_proxy: use_http_proxy)
def make_client(url : URI, region = nil, force_resolve : Bool = false, &)
client = make_client(url, region, force_resolve)
begin
yield client
ensure
client.close
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

View file

@ -21,7 +21,6 @@ private ITEM_PARSERS = {
Parsers::ItemSectionRendererParser,
Parsers::ContinuationItemRendererParser,
Parsers::HashtagRendererParser,
Parsers::LockupViewModelParser,
}
private alias InitialData = Hash(String, JSON::Any)
@ -109,30 +108,21 @@ private module Parsers
length_seconds = 0
end
live_now = false
premium = false
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|
b = badge["metadataBadgeRenderer"]
case b["label"].as_s
when "LIVE"
badges |= VideoBadges::LiveNow
when "New"
badges |= VideoBadges::New
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 "LIVE NOW"
live_now = true
when "New", "4K", "CC"
# TODO
when "Premium"
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
badges |= VideoBadges::Premium
premium = true
else nil # Ignore
end
end
@ -146,9 +136,10 @@ private module Parsers
views: view_count,
description_html: description_html,
length_seconds: length_seconds,
live_now: live_now,
premium: premium,
premiere_timestamp: premiere_timestamp,
author_verified: author_verified,
badges: badges,
})
end
@ -468,9 +459,9 @@ private module Parsers
# Parses an InnerTube richItemRenderer into a SearchVideo.
# Returns nil when the given object isn't a RichItemRenderer
#
# A richItemRenderer seems to be a simple wrapper for a various other types,
# used on the hashtags result page and the channel podcast tab. It is located
# itself inside a richGridRenderer container.
# A richItemRenderer seems to be a simple wrapper for a videoRenderer, used
# by the result page for hashtags and for the podcast tab on channels.
# It is located inside a continuationItems container for hashtags.
#
module RichItemRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
@ -483,8 +474,6 @@ private module Parsers
child = VideoRendererParser.process(item_contents, author_fallback)
child ||= ReelItemRendererParser.process(item_contents, author_fallback)
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
child ||= LockupViewModelParser.process(item_contents, author_fallback)
child ||= ShortsLockupViewModelParser.process(item_contents, author_fallback)
return child
end
@ -499,9 +488,6 @@ private module Parsers
# reelItemRenderer items are used in the new (2022) channel layout,
# in the "shorts" tab.
#
# NOTE: As of 10/2024, it might have been fully replaced by shortsLockupViewModel
# TODO: Confirm that hypothesis
#
module ReelItemRendererParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["reelItemRenderer"]?
@ -577,138 +563,10 @@ private module Parsers
views: view_count,
description_html: "",
length_seconds: duration,
live_now: false,
premium: false,
premiere_timestamp: Time.unix(0),
author_verified: false,
badges: VideoBadges::None,
})
end
def self.parser_name
return {{@type.name}}
end
end
# Parses an InnerTube lockupViewModel into a SearchPlaylist.
# Returns nil when the given object is not a lockupViewModel.
#
# This structure is present since November 2024 on the "podcasts" and
# "playlists" tabs of the channel page. It is usually encapsulated in either
# a richItemRenderer or a richGridRenderer.
#
module LockupViewModelParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["lockupViewModel"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
playlist_id = item_contents["contentId"].as_s
thumbnail_view_model = item_contents.dig(
"contentImage", "collectionThumbnailViewModel",
"primaryThumbnail", "thumbnailViewModel"
)
thumbnail = thumbnail_view_model.dig("image", "sources", 0, "url").as_s
# This complicated sequences tries to extract the following data structure:
# "overlays": [{
# "thumbnailOverlayBadgeViewModel": {
# "thumbnailBadges": [{
# "thumbnailBadgeViewModel": {
# "text": "430 episodes",
# "badgeStyle": "THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT"
# }
# }]
# }
# }]
#
# NOTE: this simplistic `.to_i` conversion might not work on larger
# playlists and hasn't been tested.
video_count = thumbnail_view_model.dig("overlays").as_a
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
.flatten
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try { |node|
{"episodes", "videos"}.any? { |str| node.as_s.ends_with?(str) }
})
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
title = metadata.dig("title", "content").as_s
# TODO: Retrieve "updated" info from metadata parts
# rows = metadata.dig("metadata", "contentMetadataViewModel", "metadataRows").as_a
# parts_text = rows.map(&.dig?("metadataParts", "text", "content").try &.as_s)
# One of these parts should contain a string like: "Updated 2 days ago"
# TODO: Maybe add a button to access the first video of the playlist?
# item_contents.dig("rendererContext", "commandContext", "onTap", "innertubeCommand", "watchEndpoint")
# Available fields: "videoId", "playlistId", "params"
return SearchPlaylist.new({
title: title,
id: playlist_id,
author: author_fallback.name,
ucid: author_fallback.id,
video_count: video_count || -1,
videos: [] of SearchPlaylistVideo,
thumbnail: thumbnail,
author_verified: false,
})
end
def self.parser_name
return {{@type.name}}
end
end
# Parses an InnerTube shortsLockupViewModel into a SearchVideo.
# Returns nil when the given object is not a shortsLockupViewModel.
#
# This structure is present since around October 2024 on the "shorts" tab of
# the channel page and likely replaces the reelItemRenderer structure. It is
# usually (always?) encapsulated in a richItemRenderer.
#
module ShortsLockupViewModelParser
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
if item_contents = item["shortsLockupViewModel"]?
return self.parse(item_contents, author_fallback)
end
end
private def self.parse(item_contents, author_fallback)
# TODO: Maybe add support for "oardefault.jpg" thumbnails?
# thumbnail = item_contents.dig("thumbnail", "sources", 0, "url").as_s
# Gives: https://i.ytimg.com/vi/{video_id}/oardefault.jpg?...
video_id = item_contents.dig(
"onTap", "innertubeCommand", "reelWatchEndpoint", "videoId"
).as_s
title = item_contents.dig("overlayMetadata", "primaryText", "content").as_s
view_count = short_text_to_number(
item_contents.dig("overlayMetadata", "secondaryText", "content").as_s
)
# Approximate to one minute, as "shorts" generally don't exceed that.
# NOTE: The actual duration is not provided by Youtube anymore.
# TODO: Maybe use -1 as an error value and handle that on the frontend?
duration = 60_i32
SearchVideo.new({
title: title,
id: video_id,
author: author_fallback.name,
ucid: author_fallback.id,
published: Time.unix(0),
views: view_count,
description_html: "",
length_seconds: duration,
premiere_timestamp: Time.unix(0),
author_verified: false,
badges: VideoBadges::None,
})
end

View file

@ -111,7 +111,7 @@ module UrlSanitizer
new_uri.path = "/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
end

View file

@ -455,7 +455,7 @@ module YoutubeAPI
video_id : String,
*, # Force the following parameters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil,
client_config : ClientConfig | Nil = nil
)
# Playback context, separate because it can be different between clients
playback_ctx = {
@ -491,12 +491,8 @@ module YoutubeAPI
data["params"] = params
end
if CONFIG.invidious_companion.present?
return self._post_invidious_companion("/youtubei/v1/player", data)
else
return self._post_json("/youtubei/v1/player", data, client_config)
end
end
####################################################################
# resolve_url(url, client_config?)
@ -600,7 +596,7 @@ module YoutubeAPI
def _post_json(
endpoint : String,
data : Hash,
client_config : ClientConfig | Nil,
client_config : ClientConfig | Nil
) : Hash(String, JSON::Any)
# Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG
@ -632,11 +628,6 @@ module YoutubeAPI
# Send the POST request
body = YT_POOL.client() do |client|
client.post(url, headers: headers, body: data.to_json) do |response|
if response.status_code != 200
raise InfoException.new("Error: non 200 status code. Youtube API returned \
status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
end
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
end
end
@ -661,51 +652,6 @@ module YoutubeAPI
return initial_data
end
####################################################################
# _post_invidious_companion(endpoint, data)
#
# Internal function that does the actual request to Invidious companion
# and handles errors.
#
# The requested data is an endpoint (URL without the domain part)
# and the data as a Hash object.
#
def _post_invidious_companion(
endpoint : String,
data : Hash
) : Hash(String, JSON::Any)
headers = HTTP::Headers{
"Content-Type" => "application/json; charset=UTF-8",
"Authorization" => "Bearer #{CONFIG.invidious_companion_key}",
}
# Logging
LOGGER.debug("Invidious companion: Using endpoint: \"#{endpoint}\"")
LOGGER.trace("Invidious companion: POST data: #{data}")
# Send the POST request
begin
invidious_companion = CONFIG.invidious_companion.sample
response = make_client(invidious_companion.private_url, use_http_proxy: false,
&.post(endpoint, headers: headers, body: data.to_json))
body = response.body
if (response.status_code != 200)
raise Exception.new(
"Error while communicating with Invidious companion: \
status code: #{response.status_code} and body: #{body.dump}"
)
end
rescue ex
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
end
# Convert result to Hash
initial_data = JSON.parse(body).as_h
return initial_data
end
####################################################################
# _decompress(body_io, headers)
#