Compare commits
6 commits
master
...
history-te
Author | SHA1 | Date | |
---|---|---|---|
7f16feaff6 | |||
ee6d2b5620 | |||
4da3936b5a | |||
b95a8bfbd3 | |||
4849286814 | |||
|
e3da8f408d |
49 changed files with 452 additions and 647 deletions
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
|
@ -65,9 +65,7 @@ jobs:
|
||||||
- name: Cache Shards
|
- name: Cache Shards
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ./lib
|
||||||
./lib
|
|
||||||
./bin
|
|
||||||
key: shards-${{ hashFiles('shard.lock') }}
|
key: shards-${{ hashFiles('shard.lock') }}
|
||||||
|
|
||||||
- name: Install Shards
|
- name: Install Shards
|
||||||
|
@ -79,6 +77,14 @@ jobs:
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: crystal spec
|
run: crystal spec
|
||||||
|
|
||||||
|
- name: Run lint
|
||||||
|
run: |
|
||||||
|
if ! crystal tool format --check; then
|
||||||
|
crystal tool format
|
||||||
|
git diff
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
|
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
|
||||||
|
|
||||||
|
@ -124,12 +130,8 @@ jobs:
|
||||||
- name: Test Docker
|
- name: Test Docker
|
||||||
run: while curl -Isf http://localhost:3000; do sleep 1; done
|
run: while curl -Isf http://localhost:3000; do sleep 1; done
|
||||||
|
|
||||||
lint:
|
ameba_lint:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
@ -149,18 +151,7 @@ jobs:
|
||||||
key: shards-${{ hashFiles('shard.lock') }}
|
key: shards-${{ hashFiles('shard.lock') }}
|
||||||
|
|
||||||
- name: Install Shards
|
- name: Install Shards
|
||||||
run: |
|
run: shards install
|
||||||
if ! shards check; then
|
|
||||||
shards install
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Check Crystal formatter compliance
|
|
||||||
run: |
|
|
||||||
if ! crystal tool format --check; then
|
|
||||||
crystal tool format
|
|
||||||
git diff
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run Ameba linter
|
- name: Run Ameba linter
|
||||||
run: bin/ameba
|
run: bin/ameba
|
||||||
|
|
13
.github/workflows/stale.yml
vendored
13
.github/workflows/stale.yml
vendored
|
@ -13,11 +13,14 @@ jobs:
|
||||||
- uses: actions/stale@v8
|
- uses: actions/stale@v8
|
||||||
with:
|
with:
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
days-before-stale: 730
|
days-before-stale: 365
|
||||||
days-before-pr-stale: -1
|
days-before-pr-stale: 90
|
||||||
days-before-close: 60
|
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-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-issue-label: "stale"
|
||||||
|
stale-pr-label: "stale"
|
||||||
ascending: true
|
ascending: true
|
||||||
# Exempt the following types of issues from being staled
|
# Never mark feature requests/enhancements as stale
|
||||||
exempt-issue-labels: "feature-request,enhancement,discussion,exempt-stale"
|
exempt-issue-labels: "feature-request,enhancement,exempt-stale"
|
||||||
|
|
98
CHANGELOG.md
98
CHANGELOG.md
|
@ -3,92 +3,8 @@
|
||||||
## vX.Y.0 (future)
|
## 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)
|
### 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)
|
* 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)
|
* Makefile: Add MT option to enable the 'preview_mt' flag ([#4993], by @SamantazFox)
|
||||||
* SigHelper: Reconnect to signature helper ([#4991], thanks @Fijxu)
|
* SigHelper: Reconnect to signature helper ([#4991], thanks @Fijxu)
|
||||||
|
@ -115,9 +31,7 @@ An HTTP proxy can be configured directly in Invidious, if needed. \
|
||||||
[#4270]: https://github.com/iv-org/invidious/pull/4270
|
[#4270]: https://github.com/iv-org/invidious/pull/4270
|
||||||
[#4326]: https://github.com/iv-org/invidious/pull/4326
|
[#4326]: https://github.com/iv-org/invidious/pull/4326
|
||||||
[#4652]: https://github.com/iv-org/invidious/pull/4652
|
[#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
|
[#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
|
[#4850]: https://github.com/iv-org/invidious/pull/4850
|
||||||
[#4862]: https://github.com/iv-org/invidious/pull/4862
|
[#4862]: https://github.com/iv-org/invidious/pull/4862
|
||||||
[#4863]: https://github.com/iv-org/invidious/pull/4863
|
[#4863]: https://github.com/iv-org/invidious/pull/4863
|
||||||
|
@ -127,22 +41,10 @@ An HTTP proxy can be configured directly in Invidious, if needed. \
|
||||||
[#4923]: https://github.com/iv-org/invidious/pull/4923
|
[#4923]: https://github.com/iv-org/invidious/pull/4923
|
||||||
[#4928]: https://github.com/iv-org/invidious/pull/4928
|
[#4928]: https://github.com/iv-org/invidious/pull/4928
|
||||||
[#4930]: https://github.com/iv-org/invidious/pull/4930
|
[#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
|
[#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
|
[#4991]: https://github.com/iv-org/invidious/pull/4991
|
||||||
[#4993]: https://github.com/iv-org/invidious/pull/4993
|
[#4993]: https://github.com/iv-org/invidious/pull/4993
|
||||||
[#4995]: https://github.com/iv-org/invidious/pull/4995
|
[#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)
|
## v2.20240825.2 (2024-08-26)
|
||||||
|
|
|
@ -54,53 +54,6 @@ db:
|
||||||
##
|
##
|
||||||
#signature_server:
|
#signature_server:
|
||||||
|
|
||||||
##
|
|
||||||
## Invidious companion is an external program
|
|
||||||
## for loading the video streams from YouTube servers.
|
|
||||||
##
|
|
||||||
## When this setting is commented out, Invidious companion is not used.
|
|
||||||
## Otherwise, Invidious will proxy the requests to Invidious companion.
|
|
||||||
##
|
|
||||||
## Note: multiple URL can be configured. In this case, invidious will
|
|
||||||
## randomly pick one every time video data needs to be retrieved. This
|
|
||||||
## URL is then kept in the video metadata cache to allow video playback
|
|
||||||
## to work. Once said cache has expired, requesting that video's data
|
|
||||||
## again will cause a new companion URL to be picked.
|
|
||||||
##
|
|
||||||
## The parameter private_url needs to be configured for the internal
|
|
||||||
## communication between the companion and Invidious.
|
|
||||||
## And public_url is the public URL from which companion is listening
|
|
||||||
## to the requests from the user(s).
|
|
||||||
##
|
|
||||||
## If you are using a reverse proxy then you will probably need to
|
|
||||||
## configure the public_url to be the same as the domain used for Invidious.
|
|
||||||
## Also apply when used from an external IP address (without a domain).
|
|
||||||
## Examples: https://MYINVIDIOUSDOMAIN or http://192.168.1.100:8282
|
|
||||||
##
|
|
||||||
## Both parameter can have identical URL when Invidious is hosted in
|
|
||||||
## an internal network or at home or locally (localhost).
|
|
||||||
##
|
|
||||||
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
|
|
||||||
## Default: <none>
|
|
||||||
##
|
|
||||||
#invidious_companion:
|
|
||||||
# - private_url: "http://localhost:8282"
|
|
||||||
# public_url: "http://localhost:8282"
|
|
||||||
|
|
||||||
##
|
|
||||||
## API key for Invidious companion, used for securing the communication
|
|
||||||
## between Invidious and Invidious companion.
|
|
||||||
## The 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!!"
|
|
||||||
|
|
||||||
#########################################
|
#########################################
|
||||||
#
|
#
|
||||||
|
@ -284,11 +237,9 @@ http_proxy:
|
||||||
## Enables colors in logs. Useful for debugging purposes
|
## Enables colors in logs. Useful for debugging purposes
|
||||||
## This is overridden if "-k" or "--colorize"
|
## This is overridden if "-k" or "--colorize"
|
||||||
## are passed on the command line.
|
## 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
|
## Accepted values: true, false
|
||||||
## Default: true
|
## Default: false
|
||||||
##
|
##
|
||||||
#colorize_logs: false
|
#colorize_logs: false
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM mirror.gcr.io/crystallang/crystal:1.14.0-alpine AS builder
|
FROM crystallang/crystal:1.14.0-alpine AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache sqlite-static yaml-static
|
RUN apk add --no-cache sqlite-static yaml-static
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ RUN crystal spec --warnings all \
|
||||||
--link-flags "-lxml2 -llzma"
|
--link-flags "-lxml2 -llzma"
|
||||||
RUN if [[ "${release}" == 1 ]] ; then \
|
RUN if [[ "${release}" == 1 ]] ; then \
|
||||||
crystal build ./src/invidious.cr \
|
crystal build ./src/invidious.cr \
|
||||||
--release --mcpu=x86-64-v2 \
|
--release --mcpu=x86-64-v3 \
|
||||||
--static --warnings all \
|
--static --warnings all \
|
||||||
--link-flags "-lxml2 -llzma"; \
|
--link-flags "-lxml2 -llzma"; \
|
||||||
else \
|
else \
|
||||||
|
@ -32,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
||||||
--link-flags "-lxml2 -llzma"; \
|
--link-flags "-lxml2 -llzma"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM mirror.gcr.io/alpine:3.20
|
FROM alpine:3.18
|
||||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
adduser -u 1000 -S invidious -G invidious
|
adduser -u 1000 -S invidious -G invidious
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
FROM alpine:3.20 AS builder
|
FROM alpine:3.19 AS builder
|
||||||
RUN apk add --no-cache 'crystal=1.12.2-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \
|
RUN apk add --no-cache 'crystal=1.10.1-r0' shards sqlite-static yaml-static yaml-dev libxml2-static zlib-static openssl-libs-static openssl-dev musl-dev xz-static
|
||||||
zlib-static openssl-libs-static openssl-dev musl-dev xz-static
|
|
||||||
|
|
||||||
ARG release
|
ARG release
|
||||||
|
|
||||||
|
@ -33,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
|
||||||
--link-flags "-lxml2 -llzma"; \
|
--link-flags "-lxml2 -llzma"; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
FROM alpine:3.20
|
FROM alpine:3.18
|
||||||
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
|
RUN apk add --no-cache rsvg-convert ttf-opensans tini
|
||||||
WORKDIR /invidious
|
WORKDIR /invidious
|
||||||
RUN addgroup -g 1000 -S invidious && \
|
RUN addgroup -g 1000 -S invidious && \
|
||||||
adduser -u 1000 -S invidious -G invidious
|
adduser -u 1000 -S invidious -G invidious
|
||||||
|
|
|
@ -14,7 +14,7 @@ shards:
|
||||||
|
|
||||||
db:
|
db:
|
||||||
git: https://github.com/crystal-lang/crystal-db.git
|
git: https://github.com/crystal-lang/crystal-db.git
|
||||||
version: 0.13.1
|
version: 0.10.1
|
||||||
|
|
||||||
exception_page:
|
exception_page:
|
||||||
git: https://github.com/crystal-loot/exception_page.git
|
git: https://github.com/crystal-loot/exception_page.git
|
||||||
|
@ -37,7 +37,7 @@ shards:
|
||||||
|
|
||||||
pg:
|
pg:
|
||||||
git: https://github.com/will/crystal-pg.git
|
git: https://github.com/will/crystal-pg.git
|
||||||
version: 0.28.0
|
version: 0.24.0
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
git: https://github.com/ysbaddaden/pool.git
|
git: https://github.com/ysbaddaden/pool.git
|
||||||
|
@ -61,5 +61,5 @@ shards:
|
||||||
|
|
||||||
sqlite3:
|
sqlite3:
|
||||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||||
version: 0.21.0
|
version: 0.18.0
|
||||||
|
|
||||||
|
|
21
shard.yml
21
shard.yml
|
@ -1,20 +1,21 @@
|
||||||
name: invidious
|
name: invidious
|
||||||
version: 2.20241110.0-dev
|
version: 0.20.1
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Invidious team <contact@invidious.io>
|
- Omar Roth <omarroth@protonmail.com>
|
||||||
- Contributors!
|
- Invidious team
|
||||||
|
|
||||||
description: |
|
targets:
|
||||||
Invidious is an alternative front-end to YouTube
|
invidious:
|
||||||
|
main: src/invidious.cr
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
pg:
|
pg:
|
||||||
github: will/crystal-pg
|
github: will/crystal-pg
|
||||||
version: ~> 0.28.0
|
version: ~> 0.24.0
|
||||||
sqlite3:
|
sqlite3:
|
||||||
github: crystal-lang/crystal-sqlite3
|
github: crystal-lang/crystal-sqlite3
|
||||||
version: ~> 0.21.0
|
version: ~> 0.18.0
|
||||||
kemal:
|
kemal:
|
||||||
github: kemalcr/kemal
|
github: kemalcr/kemal
|
||||||
version: ~> 1.1.2
|
version: ~> 1.1.2
|
||||||
|
@ -44,10 +45,6 @@ development_dependencies:
|
||||||
github: crystal-ameba/ameba
|
github: crystal-ameba/ameba
|
||||||
version: ~> 1.6.1
|
version: ~> 1.6.1
|
||||||
|
|
||||||
crystal: ">= 1.10.0, < 2.0.0"
|
crystal: ">= 1.0.0, < 2.0.0"
|
||||||
|
|
||||||
license: AGPLv3
|
license: AGPLv3
|
||||||
|
|
||||||
repository: https://github.com/iv-org/invidious
|
|
||||||
homepage: https://invidious.io
|
|
||||||
documentation: https://docs.invidious.io
|
|
||||||
|
|
|
@ -88,6 +88,7 @@ REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||||
YT_URL = URI.parse("https://www.youtube.com")
|
YT_URL = URI.parse("https://www.youtube.com")
|
||||||
PUBSUB_HOST_URL = CONFIG.pubsub_domain
|
PUBSUB_HOST_URL = CONFIG.pubsub_domain
|
||||||
HOST_URL = make_host_url(Kemal.config)
|
HOST_URL = make_host_url(Kemal.config)
|
||||||
|
EXT_VIDEOP_LIST = gen_videoplayback_proxy_list()
|
||||||
|
|
||||||
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||||
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
||||||
|
@ -210,16 +211,10 @@ Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
|
||||||
|
|
||||||
if !CONFIG.external_videoplayback_proxy.empty?
|
if !CONFIG.external_videoplayback_proxy.empty?
|
||||||
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
|
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
|
end
|
||||||
|
|
||||||
if !CONFIG.tokens_server.empty?
|
if CONFIG.refresh_tokens
|
||||||
Invidious::Jobs.register Invidious::Jobs::RefreshSessionTokens.new
|
Invidious::Jobs.register Invidious::Jobs::RefreshTokens.new
|
||||||
else
|
|
||||||
LOGGER.info("jobs: Disabling RefreshSessionTokens job. Invidious will use the tokens that are on the configuration file")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Invidious::Jobs.start_all
|
Invidious::Jobs.start_all
|
||||||
|
|
|
@ -111,9 +111,7 @@ module Invidious::Channel::Tabs
|
||||||
private def make_initial_videos_ctoken(ucid : String, sort_by = "newest")
|
private def make_initial_videos_ctoken(ucid : String, sort_by = "newest")
|
||||||
object = {
|
object = {
|
||||||
"15:embedded" => {
|
"15:embedded" => {
|
||||||
"2:embedded" => {
|
"2:string" => "\n$00000000-0000-0000-0000-000000000000",
|
||||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
|
||||||
},
|
|
||||||
"4:varint" => sort_options_videos_short(sort_by),
|
"4:varint" => sort_options_videos_short(sort_by),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,6 +45,8 @@ struct ConfigPreferences
|
||||||
property vr_mode : Bool = true
|
property vr_mode : Bool = true
|
||||||
property show_nick : Bool = true
|
property show_nick : Bool = true
|
||||||
property save_player_pos : Bool = false
|
property save_player_pos : Bool = false
|
||||||
|
property po_token : String = ""
|
||||||
|
property visitor_data : String = ""
|
||||||
|
|
||||||
def to_tuple
|
def to_tuple
|
||||||
{% begin %}
|
{% begin %}
|
||||||
|
@ -67,16 +69,6 @@ end
|
||||||
class Config
|
class Config
|
||||||
include YAML::Serializable
|
include YAML::Serializable
|
||||||
|
|
||||||
class CompanionConfig
|
|
||||||
include YAML::Serializable
|
|
||||||
|
|
||||||
@[YAML::Field(converter: Preferences::URIConverter)]
|
|
||||||
property private_url : URI = URI.parse("")
|
|
||||||
|
|
||||||
@[YAML::Field(converter: Preferences::URIConverter)]
|
|
||||||
property public_url : URI = URI.parse("")
|
|
||||||
end
|
|
||||||
|
|
||||||
# Number of threads to use for crawling videos from channels (for updating subscriptions)
|
# Number of threads to use for crawling videos from channels (for updating subscriptions)
|
||||||
property channel_threads : Int32 = 1
|
property channel_threads : Int32 = 1
|
||||||
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
|
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
|
||||||
|
@ -116,12 +108,12 @@ class Config
|
||||||
property materialious_domain : String?
|
property materialious_domain : String?
|
||||||
# Alternative domains. You can add other domains, like TOR and I2P addresses
|
# Alternative domains. You can add other domains, like TOR and I2P addresses
|
||||||
property alternative_domains : Array(String) = [] of String
|
property alternative_domains : Array(String) = [] of String
|
||||||
# Backend domains. Domains for numbered backends
|
property donation_url : String?
|
||||||
property backend_domains : Array(String) = [] of String
|
property contact_url : String?
|
||||||
|
property home_domain : String?
|
||||||
|
|
||||||
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
|
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
|
||||||
property use_pubsub_feeds : Bool | Int32 = false
|
property use_pubsub_feeds : Bool | Int32 = false
|
||||||
property use_innertube_for_feeds : Bool = true
|
|
||||||
property popular_enabled : Bool = true
|
property popular_enabled : Bool = true
|
||||||
property captcha_enabled : Bool = true
|
property captcha_enabled : Bool = true
|
||||||
property login_enabled : Bool = true
|
property login_enabled : Bool = true
|
||||||
|
@ -185,12 +177,6 @@ class Config
|
||||||
# poToken for passing bot attestation
|
# poToken for passing bot attestation
|
||||||
property po_token : String? = nil
|
property po_token : String? = nil
|
||||||
|
|
||||||
# Invidious companion
|
|
||||||
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
|
|
||||||
|
|
||||||
# Invidious companion API key
|
|
||||||
property invidious_companion_key : String = ""
|
|
||||||
|
|
||||||
# Saved cookies in "name1=value1; name2=value2..." format
|
# Saved cookies in "name1=value1; name2=value2..." format
|
||||||
@[YAML::Field(converter: Preferences::StringToCookies)]
|
@[YAML::Field(converter: Preferences::StringToCookies)]
|
||||||
property cookies : HTTP::Cookies = HTTP::Cookies.new
|
property cookies : HTTP::Cookies = HTTP::Cookies.new
|
||||||
|
@ -209,7 +195,10 @@ class Config
|
||||||
|
|
||||||
# External videoplayback proxies list. They should include `https://`
|
# External videoplayback proxies list. They should include `https://`
|
||||||
# at the start of the URI
|
# at the start of the URI
|
||||||
property external_videoplayback_proxy : Array(String) = [] of String
|
property external_videoplayback_proxy : Array(NamedTuple(url: String, balance: Bool)) = [] of NamedTuple(url: String, balance: Bool)
|
||||||
|
|
||||||
|
# Job to refresh tokens from a Redis compatible DB
|
||||||
|
property refresh_tokens : Bool = true
|
||||||
|
|
||||||
property pubsub_domain : String = ""
|
property pubsub_domain : String = ""
|
||||||
|
|
||||||
|
@ -217,8 +206,6 @@ class Config
|
||||||
|
|
||||||
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
|
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
|
||||||
|
|
||||||
property tokens_server : String = ""
|
|
||||||
|
|
||||||
{% if flag?(:linux) %}
|
{% if flag?(:linux) %}
|
||||||
property reload_config_automatically : Bool = true
|
property reload_config_automatically : Bool = true
|
||||||
{% end %}
|
{% end %}
|
||||||
|
@ -345,23 +332,6 @@ class Config
|
||||||
end
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
if config.invidious_companion.present?
|
|
||||||
# invidious_companion and signature_server can't work together
|
|
||||||
if config.signature_server
|
|
||||||
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."
|
|
||||||
exit(1)
|
|
||||||
elsif config.invidious_companion_key.empty?
|
|
||||||
puts "Config: Please configure a key if you are using invidious companion."
|
|
||||||
exit(1)
|
|
||||||
elsif config.invidious_companion_key == "CHANGE_ME!!"
|
|
||||||
puts "Config: The value of 'invidious_companion_key' needs to be changed!!"
|
|
||||||
exit(1)
|
|
||||||
elsif config.invidious_companion_key.size < 16
|
|
||||||
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 or more."
|
|
||||||
exit(1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# HMAC_key is mandatory
|
# HMAC_key is mandatory
|
||||||
# See: https://github.com/iv-org/invidious/issues/3854
|
# See: https://github.com/iv-org/invidious/issues/3854
|
||||||
if config.hmac_key.empty?
|
if config.hmac_key.empty?
|
||||||
|
|
|
@ -49,14 +49,14 @@ module Invidious::Database::Users
|
||||||
PG_DB.exec(request, user.watched, user.email)
|
PG_DB.exec(request, user.watched, user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_watched(user : User, vid : String)
|
def mark_watched(user : User, history_details : JSON::Any)
|
||||||
request = <<-SQL
|
request = <<-SQL
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET watched = array_append(array_remove(watched, $1), $1)
|
SET watched = array_append(array_remove(watched, $1::jsonb), $1::jsonb)
|
||||||
WHERE email = $2
|
WHERE email = $2
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
PG_DB.exec(request, vid, user.email)
|
PG_DB.exec(request, history_details, user.email)
|
||||||
end
|
end
|
||||||
|
|
||||||
def mark_unwatched(user : User, vid : String)
|
def mark_unwatched(user : User, vid : String)
|
||||||
|
|
|
@ -4,11 +4,22 @@ module Invidious::Database::Videos
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
def insert(video : Video)
|
def insert(video : Video)
|
||||||
|
request = <<-SQL
|
||||||
|
INSERT INTO videos
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (id) DO NOTHING
|
||||||
|
SQL
|
||||||
|
|
||||||
REDIS_DB.set(video.id, video.info.to_json, ex: 14400)
|
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 + ":time", video.updated, ex: 14400)
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete(id)
|
def delete(id)
|
||||||
|
request = <<-SQL
|
||||||
|
DELETE FROM videos *
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
REDIS_DB.del(id)
|
REDIS_DB.del(id)
|
||||||
REDIS_DB.del(id + ":time")
|
REDIS_DB.del(id + ":time")
|
||||||
end
|
end
|
||||||
|
@ -33,6 +44,11 @@ module Invidious::Database::Videos
|
||||||
end
|
end
|
||||||
|
|
||||||
def select(id : String) : Video?
|
def select(id : String) : Video?
|
||||||
|
request = <<-SQL
|
||||||
|
SELECT * FROM videos
|
||||||
|
WHERE id = $1
|
||||||
|
SQL
|
||||||
|
|
||||||
if ((info = REDIS_DB.get(id)) && (time = REDIS_DB.get(id + ":time")))
|
if ((info = REDIS_DB.get(id)) && (time = REDIS_DB.get(id + ":time")))
|
||||||
return Video.new({
|
return Video.new({
|
||||||
id: id,
|
id: id,
|
||||||
|
|
|
@ -180,7 +180,6 @@ def error_redirect_helper(env : HTTP::Server::Context)
|
||||||
next_steps_text = translate(locale, "next_steps_error_message")
|
next_steps_text = translate(locale, "next_steps_error_message")
|
||||||
refresh = translate(locale, "next_steps_error_message_refresh")
|
refresh = translate(locale, "next_steps_error_message_refresh")
|
||||||
go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube")
|
go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube")
|
||||||
go_to_youtube_embed = translate(locale, "videoinfo_youTube_embed_link")
|
|
||||||
switch_instance = translate(locale, "Switch Invidious Instance")
|
switch_instance = translate(locale, "Switch Invidious Instance")
|
||||||
|
|
||||||
return <<-END_HTML
|
return <<-END_HTML
|
||||||
|
@ -194,7 +193,6 @@ def error_redirect_helper(env : HTTP::Server::Context)
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
|
<a rel="noreferrer noopener" href="https://youtube.com#{env.request.resource}">#{go_to_youtube}</a>
|
||||||
(<a rel="noreferrer noopener" href="https://youtube.com/embed/#{env.params.query["v"]}">#{go_to_youtube_embed}</a>)
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
END_HTML
|
END_HTML
|
||||||
|
|
|
@ -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 = {
|
LOCALES_LIST = {
|
||||||
"ar" => "العربية", # Arabic
|
"ar" => "العربية", # Arabic
|
||||||
"bg" => "български", # Bulgarian
|
|
||||||
"bn" => "বাংলা", # Bengali
|
"bn" => "বাংলা", # Bengali
|
||||||
"ca" => "Català", # Catalan
|
"ca" => "Català", # Catalan
|
||||||
"cs" => "Čeština", # Czech
|
"cs" => "Čeština", # Czech
|
||||||
"cy" => "Cymraeg", # Welsh
|
|
||||||
"da" => "Dansk", # Danish
|
"da" => "Dansk", # Danish
|
||||||
"de" => "Deutsch", # German
|
"de" => "Deutsch", # German
|
||||||
"el" => "Ελληνικά", # Greek
|
"el" => "Ελληνικά", # Greek
|
||||||
|
@ -37,7 +23,6 @@ LOCALES_LIST = {
|
||||||
"it" => "Italiano", # Italian
|
"it" => "Italiano", # Italian
|
||||||
"ja" => "日本語", # Japanese
|
"ja" => "日本語", # Japanese
|
||||||
"ko" => "한국어", # Korean
|
"ko" => "한국어", # Korean
|
||||||
"lmo" => "Lombard", # Lombard
|
|
||||||
"lt" => "Lietuvių", # Lithuanian
|
"lt" => "Lietuvių", # Lithuanian
|
||||||
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
|
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
|
||||||
"nl" => "Nederlands", # Dutch
|
"nl" => "Nederlands", # Dutch
|
||||||
|
|
|
@ -12,9 +12,7 @@ enum LogLevel
|
||||||
end
|
end
|
||||||
|
|
||||||
class Invidious::LogHandler < Kemal::BaseLogHandler
|
class Invidious::LogHandler < Kemal::BaseLogHandler
|
||||||
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, use_color : Bool = true)
|
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, @color : Bool = true)
|
||||||
Colorize.enabled = use_color
|
|
||||||
Colorize.on_tty_only!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def call(context : HTTP::Server::Context)
|
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) %}
|
{% for level in %w(trace debug info warn error fatal) %}
|
||||||
def {{level.id}}(message : String)
|
def {{level.id}}(message : String)
|
||||||
if LogLevel::{{level.id.capitalize}} >= @level
|
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
|
end
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
54
src/invidious/helpers/redis_tokens.cr
Normal file
54
src/invidious/helpers/redis_tokens.cr
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
module Tokens
|
||||||
|
extend self
|
||||||
|
@@po_token : String | Nil
|
||||||
|
@@visitor_data : String | Nil
|
||||||
|
|
||||||
|
def refresh_tokens
|
||||||
|
@@po_token = REDIS_DB.get("invidious:po_token")
|
||||||
|
@@visitor_data = REDIS_DB.get("invidious:visitor_data")
|
||||||
|
if !@@po_token.nil? && !@@visitor_data.nil?
|
||||||
|
LOGGER.debug("RefreshTokens: Successfully updated tokens")
|
||||||
|
else
|
||||||
|
LOGGER.warn("RefreshTokens: Tokens are empty!")
|
||||||
|
end
|
||||||
|
LOGGER.trace("RefreshTokens: Tokens are:")
|
||||||
|
LOGGER.trace("RefreshTokens: po_token: #{@@po_token}")
|
||||||
|
LOGGER.trace("RefreshTokens: visitor_data: #{@@visitor_data}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_tokens
|
||||||
|
return {@@po_token, @@visitor_data}
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_po_token
|
||||||
|
return @@po_token
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_visitor_data
|
||||||
|
return @@visitor_data
|
||||||
|
end
|
||||||
|
|
||||||
|
def generate_tokens(user : String)
|
||||||
|
po_token = ""
|
||||||
|
visitor_data = ""
|
||||||
|
attempts = 0
|
||||||
|
|
||||||
|
LOGGER.debug("Generating po_token and visitor_data for user: '#{user}'")
|
||||||
|
REDIS_DB.publish("generate-token", "#{user}")
|
||||||
|
|
||||||
|
while REDIS_DB.get("invidious:#{user}:po_token").nil? && REDIS_DB.get("invidious:#{user}:visitor_data").nil?
|
||||||
|
if attempts > 50
|
||||||
|
break
|
||||||
|
end
|
||||||
|
LOGGER.debug("Waiting for tokens to arrive at redis for user: '#{user}'")
|
||||||
|
attempts += 1
|
||||||
|
sleep 250.milliseconds
|
||||||
|
end
|
||||||
|
|
||||||
|
po_token = REDIS_DB.get("invidious:#{user}:po_token")
|
||||||
|
visitor_data = REDIS_DB.get("invidious:#{user}:visitor_data")
|
||||||
|
|
||||||
|
LOGGER.debug("Tokens successfully generated for user: '#{user}'")
|
||||||
|
return {po_token, visitor_data}
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,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
|
|
|
@ -384,21 +384,16 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
|
||||||
return text
|
return text
|
||||||
end
|
end
|
||||||
|
|
||||||
def encrypt_ecb_without_salt(data, key)
|
# Generates a list of external videoplayback proxies for
|
||||||
cipher = OpenSSL::Cipher.new("aes-128-ecb")
|
# CSP
|
||||||
cipher.encrypt
|
def gen_videoplayback_proxy_list
|
||||||
cipher.key = key
|
if !CONFIG.external_videoplayback_proxy.empty?
|
||||||
|
external_videoplayback_proxy = ""
|
||||||
io = IO::Memory.new
|
CONFIG.external_videoplayback_proxy.each do |proxy|
|
||||||
io.write(cipher.update(data))
|
external_videoplayback_proxy += " #{proxy[:url]}"
|
||||||
io.write(cipher.final)
|
|
||||||
io.rewind
|
|
||||||
|
|
||||||
return io
|
|
||||||
end
|
end
|
||||||
|
else
|
||||||
def invidious_companion_encrypt(data)
|
external_videoplayback_proxy = ""
|
||||||
timestamp = Time.utc.to_unix
|
end
|
||||||
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
|
return external_videoplayback_proxy
|
||||||
return Base64.urlsafe_encode(encrypted_data)
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,14 +9,14 @@ module Invidious::HttpServer
|
||||||
def check_external_proxy
|
def check_external_proxy
|
||||||
CONFIG.external_videoplayback_proxy.each do |proxy|
|
CONFIG.external_videoplayback_proxy.each do |proxy|
|
||||||
begin
|
begin
|
||||||
response = HTTP::Client.get("#{proxy}/health")
|
response = HTTP::Client.get("#{proxy[:url]}/health")
|
||||||
if response.status_code == 200
|
if response.status_code == 200
|
||||||
@@proxy_alive = proxy
|
@@proxy_alive = proxy[:url]
|
||||||
LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy}'")
|
LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy[:url]}'")
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
LOGGER.debug("CheckExternalProxy: Proxy '#{proxy}' is not available")
|
LOGGER.debug("CheckExternalProxy: Proxy '#{proxy[:url]}' is not available")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if @@proxy_alive.empty?
|
if @@proxy_alive.empty?
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
class Invidious::Jobs::RefreshSessionTokens < Invidious::Jobs::BaseJob
|
class Invidious::Jobs::RefreshTokens < Invidious::Jobs::BaseJob
|
||||||
def initialize
|
def initialize
|
||||||
end
|
end
|
||||||
|
|
||||||
def begin
|
def begin
|
||||||
loop do
|
loop do
|
||||||
SessionTokens.refresh_tokens
|
Tokens.refresh_tokens
|
||||||
LOGGER.info("RefreshTokens: Done, sleeping for 5 seconds")
|
LOGGER.info("RefreshTokens: Done, sleeping for 5 seconds")
|
||||||
sleep 5.seconds
|
sleep 5.seconds
|
||||||
Fiber.yield
|
Fiber.yield
|
||||||
|
|
|
@ -8,7 +8,7 @@ module Invidious::JSONify::APIv1
|
||||||
build_thumbnails(id).each do |thumbnail|
|
build_thumbnails(id).each do |thumbnail|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "quality", thumbnail[:name]
|
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 "width", thumbnail[:width]
|
||||||
json.field "height", thumbnail[:height]
|
json.field "height", thumbnail[:height]
|
||||||
end
|
end
|
||||||
|
|
|
@ -349,4 +349,40 @@ module Invidious::Routes::Account
|
||||||
return "{}"
|
return "{}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# -------------------
|
||||||
|
# poToken and visitorData tokens generation
|
||||||
|
# -------------------
|
||||||
|
|
||||||
|
# Generates a poToken & visitorData for the user, server side
|
||||||
|
def generate_tokens(env)
|
||||||
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
preferences = env.get("preferences").as(Preferences)
|
||||||
|
|
||||||
|
user = env.get? "user"
|
||||||
|
sid = env.get? "sid"
|
||||||
|
referer = get_referer(env)
|
||||||
|
|
||||||
|
if !user
|
||||||
|
return env.redirect referer
|
||||||
|
end
|
||||||
|
|
||||||
|
user = user.as(User)
|
||||||
|
|
||||||
|
po_token, visitor_data = Tokens.generate_tokens(user.email)
|
||||||
|
|
||||||
|
if po_token.nil? || visitor_data.nil?
|
||||||
|
return error_template(500, "Internal server error. Please submit an issue here IF THE ISSUE PERSISTS: https://git.nadeko.net/Fijxu/invidious/issues")
|
||||||
|
end
|
||||||
|
|
||||||
|
user.preferences.po_token = po_token
|
||||||
|
user.preferences.visitor_data = visitor_data
|
||||||
|
|
||||||
|
Invidious::Database::Users.update_preferences(user)
|
||||||
|
|
||||||
|
REDIS_DB.del("invidious:#{user.email}:po_token")
|
||||||
|
REDIS_DB.del("invidious:#{user.email}:visitor_data")
|
||||||
|
|
||||||
|
templated "user/tokens"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,11 +8,6 @@ module Invidious::Routes::API::Manifest
|
||||||
id = env.params.url["id"]
|
id = env.params.url["id"]
|
||||||
region = env.params.query["region"]?
|
region = env.params.query["region"]?
|
||||||
|
|
||||||
if CONFIG.invidious_companion.present?
|
|
||||||
invidious_companion = CONFIG.invidious_companion.sample
|
|
||||||
return env.redirect "#{invidious_companion.public_url}/api/manifest/dash/id/#{id}?#{env.params.query}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# Since some implementations create playlists based on resolution regardless of different codecs,
|
# Since some implementations create playlists based on resolution regardless of different codecs,
|
||||||
# we can opt to only add a source to a representation if it has a unique height within that representation
|
# we can opt to only add a source to a representation if it has a unique height within that representation
|
||||||
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
unique_res = env.params.query["unique_res"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||||
|
@ -186,8 +181,9 @@ module Invidious::Routes::API::Manifest
|
||||||
manifest = response.body
|
manifest = response.body
|
||||||
|
|
||||||
if local
|
if local
|
||||||
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
|
manifest = manifest.gsub(/^https:\/\/[^\n"]*/m) do |match|
|
||||||
path = URI.parse(match).path
|
uri = URI.parse(match)
|
||||||
|
path = uri.path
|
||||||
|
|
||||||
path = path.lchop("/videoplayback/")
|
path = path.lchop("/videoplayback/")
|
||||||
path = path.rchop("/")
|
path = path.rchop("/")
|
||||||
|
@ -216,20 +212,14 @@ module Invidious::Routes::API::Manifest
|
||||||
raw_params["fvip"] = fvip["fvip"]
|
raw_params["fvip"] = fvip["fvip"]
|
||||||
end
|
end
|
||||||
|
|
||||||
raw_params["local"] = "true"
|
raw_params["host"] = uri.host.not_nil!
|
||||||
|
|
||||||
proxy = Invidious::HttpServer::Utils.get_external_proxy
|
proxy = Invidious::HttpServer::Utils.get_external_proxy
|
||||||
|
|
||||||
if CONFIG.https_only
|
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
|
|
||||||
if !proxy.empty?
|
if !proxy.empty?
|
||||||
"#{proxy}/videoplayback?#{raw_params}"
|
"#{proxy}/videoplayback?#{raw_params}"
|
||||||
else
|
else
|
||||||
"#{scheme}#{env.request.headers["Host"]}/videoplayback?#{raw_params}"
|
"#{HOST_URL}/videoplayback?#{raw_params}"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -253,12 +243,7 @@ module Invidious::Routes::API::Manifest
|
||||||
manifest = response.body
|
manifest = response.body
|
||||||
|
|
||||||
if local
|
if local
|
||||||
if CONFIG.https_only
|
manifest = manifest.gsub("https://www.youtube.com", HOST_URL)
|
||||||
scheme = "https://"
|
|
||||||
else
|
|
||||||
scheme = "http://"
|
|
||||||
end
|
|
||||||
manifest = manifest.gsub("https://www.youtube.com", "#{scheme}#{env.request.headers["Host"]}")
|
|
||||||
manifest = manifest.gsub("index.m3u8", "index.m3u8?local=true")
|
manifest = manifest.gsub("index.m3u8", "index.m3u8?local=true")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -86,7 +86,7 @@ module Invidious::Routes::API::V1::Authenticated
|
||||||
return error_json(400, "Invalid video id.")
|
return error_json(400, "Invalid video id.")
|
||||||
end
|
end
|
||||||
|
|
||||||
Invidious::Database::Users.mark_watched(user, id)
|
# Invidious::Database::Users.mark_watched(user, id)
|
||||||
env.response.status_code = 204
|
env.response.status_code = 204
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -226,7 +226,7 @@ module Invidious::Routes::API::V1::Authenticated
|
||||||
end
|
end
|
||||||
|
|
||||||
playlist = create_playlist(title, privacy, user)
|
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
|
env.response.status_code = 201
|
||||||
{
|
{
|
||||||
"title" => title,
|
"title" => title,
|
||||||
|
@ -482,7 +482,7 @@ module Invidious::Routes::API::V1::Authenticated
|
||||||
env.response.content_type = "text/event-stream"
|
env.response.content_type = "text/event-stream"
|
||||||
|
|
||||||
raw_topics = env.params.body["topics"]? || env.params.query["topics"]?
|
raw_topics = env.params.body["topics"]? || env.params.query["topics"]?
|
||||||
topics = raw_topics.try &.split(",").uniq!.first(1000)
|
topics = raw_topics.try &.split(",").uniq.first(1000)
|
||||||
topics ||= [] of String
|
topics ||= [] of String
|
||||||
|
|
||||||
create_notification_stream(env, topics, CONNECTION_CHANNEL)
|
create_notification_stream(env, topics, CONNECTION_CHANNEL)
|
||||||
|
|
|
@ -197,7 +197,6 @@ module Invidious::Routes::API::V1::Channels
|
||||||
get_channel()
|
get_channel()
|
||||||
|
|
||||||
# Retrieve continuation from URL parameters
|
# Retrieve continuation from URL parameters
|
||||||
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
|
|
||||||
continuation = env.params.query["continuation"]?
|
continuation = env.params.query["continuation"]?
|
||||||
|
|
||||||
if channel.is_age_gated
|
if channel.is_age_gated
|
||||||
|
@ -212,7 +211,7 @@ module Invidious::Routes::API::V1::Channels
|
||||||
else
|
else
|
||||||
begin
|
begin
|
||||||
videos, next_continuation = Channel::Tabs.get_shorts(
|
videos, next_continuation = Channel::Tabs.get_shorts(
|
||||||
channel, continuation: continuation, sort_by: sort_by
|
channel, continuation: continuation
|
||||||
)
|
)
|
||||||
rescue ex
|
rescue ex
|
||||||
return error_json(500, ex)
|
return error_json(500, ex)
|
||||||
|
|
|
@ -31,7 +31,9 @@ module Invidious::Routes::API::V1::Search
|
||||||
query = env.params.query["q"]? || ""
|
query = env.params.query["q"]? || ""
|
||||||
|
|
||||||
begin
|
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"
|
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
|
response = client.get(url).body
|
||||||
|
|
|
@ -263,60 +263,61 @@ module Invidious::Routes::API::V1::Videos
|
||||||
|
|
||||||
annotations = ""
|
annotations = ""
|
||||||
|
|
||||||
case source
|
# case source
|
||||||
when "archive"
|
# when "archive"
|
||||||
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
|
# if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
|
||||||
annotations = cached_annotation.annotations
|
# annotations = cached_annotation.annotations
|
||||||
else
|
# else
|
||||||
index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
|
# index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
|
||||||
|
|
||||||
# IA doesn't handle leading hyphens,
|
# # IA doesn't handle leading hyphens,
|
||||||
# so we use https://archive.org/details/youtubeannotations_64
|
# # so we use https://archive.org/details/youtubeannotations_64
|
||||||
if index == "62"
|
# if index == "62"
|
||||||
index = "64"
|
# index = "64"
|
||||||
id = id.sub(/^-/, 'A')
|
# id = id.sub(/^-/, 'A')
|
||||||
end
|
# end
|
||||||
|
|
||||||
file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
|
# file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
|
||||||
|
|
||||||
location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
# location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
||||||
|
|
||||||
if !location.headers["Location"]?
|
# if !location.headers["Location"]?
|
||||||
env.response.status_code = location.status_code
|
# env.response.status_code = location.status_code
|
||||||
end
|
# end
|
||||||
|
|
||||||
response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"]))
|
# response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"]))
|
||||||
|
|
||||||
if response.body.empty?
|
# if response.body.empty?
|
||||||
haltf env, 404
|
# haltf env, 404
|
||||||
end
|
# end
|
||||||
|
|
||||||
if response.status_code != 200
|
# if response.status_code != 200
|
||||||
haltf env, response.status_code
|
# haltf env, response.status_code
|
||||||
end
|
# end
|
||||||
|
|
||||||
annotations = response.body
|
# annotations = response.body
|
||||||
|
|
||||||
cache_annotation(id, annotations)
|
# cache_annotation(id, annotations)
|
||||||
end
|
# end
|
||||||
else # "youtube"
|
# else # "youtube"
|
||||||
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
|
# response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
|
||||||
|
|
||||||
if response.status_code != 200
|
# if response.status_code != 200
|
||||||
haltf env, response.status_code
|
# haltf env, response.status_code
|
||||||
end
|
# end
|
||||||
|
|
||||||
annotations = response.body
|
# annotations = response.body
|
||||||
end
|
# end
|
||||||
|
|
||||||
etag = sha256(annotations)[0, 16]
|
# etag = sha256(annotations)[0, 16]
|
||||||
if env.request.headers["If-None-Match"]?.try &.== etag
|
# if env.request.headers["If-None-Match"]?.try &.== etag
|
||||||
haltf env, 304
|
# haltf env, 304
|
||||||
else
|
# else
|
||||||
env.response.headers["ETag"] = etag
|
# env.response.headers["ETag"] = etag
|
||||||
|
# annotations
|
||||||
|
# end
|
||||||
annotations
|
annotations
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def self.comments(env)
|
def self.comments(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
|
|
@ -20,25 +20,12 @@ module Invidious::Routes::BeforeAll
|
||||||
env.response.headers["X-XSS-Protection"] = "1; mode=block"
|
env.response.headers["X-XSS-Protection"] = "1; mode=block"
|
||||||
env.response.headers["X-Content-Type-Options"] = "nosniff"
|
env.response.headers["X-Content-Type-Options"] = "nosniff"
|
||||||
|
|
||||||
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
|
# Allow media resources to be loaded from google servers
|
||||||
# TODO: check if *.youtube.com can be removed
|
# TODO: check if *.youtube.com can be removed
|
||||||
if CONFIG.disabled?("local") || !preferences.local
|
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
|
end
|
||||||
|
|
||||||
# Only allow the pages at /embed/* to be embedded
|
# Only allow the pages at /embed/* to be embedded
|
||||||
|
@ -56,9 +43,9 @@ module Invidious::Routes::BeforeAll
|
||||||
"style-src 'self' 'unsafe-inline'",
|
"style-src 'self' 'unsafe-inline'",
|
||||||
"img-src 'self' data:",
|
"img-src 'self' data:",
|
||||||
"font-src 'self' data:",
|
"font-src 'self' data:",
|
||||||
"connect-src 'self'" + extra_connect_csp,
|
"connect-src 'self'" + EXT_VIDEOP_LIST,
|
||||||
"manifest-src 'self'",
|
"manifest-src 'self'",
|
||||||
"media-src 'self' blob:" + extra_media_csp,
|
"media-src 'self' blob:" + extra_media_csp + EXT_VIDEOP_LIST,
|
||||||
"child-src 'self' blob:",
|
"child-src 'self' blob:",
|
||||||
"frame-src 'self'",
|
"frame-src 'self'",
|
||||||
"frame-ancestors " + frame_ancestors,
|
"frame-ancestors " + frame_ancestors,
|
||||||
|
|
|
@ -416,22 +416,18 @@ module Invidious::Routes::Feeds
|
||||||
author = entry.xpath_node("default:author/default:name", namespaces).not_nil!.content
|
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)
|
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)
|
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
|
begin
|
||||||
video_ = get_video(id, force_refresh: true)
|
video = get_video(id, force_refresh: true)
|
||||||
rescue
|
rescue
|
||||||
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
|
next # skip this video since it raised an exception (e.g. it is a scheduled live event)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
if CONFIG.enable_user_notifications
|
if CONFIG.enable_user_notifications
|
||||||
# Deliver notifications to `/api/v1/auth/notifications`
|
# Deliver notifications to `/api/v1/auth/notifications`
|
||||||
payload = {
|
payload = {
|
||||||
"topic" => ucid,
|
"topic" => video.ucid,
|
||||||
"videoId" => id,
|
"videoId" => video.id,
|
||||||
"published" => published.to_unix,
|
"published" => published.to_unix,
|
||||||
}.to_json
|
}.to_json
|
||||||
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
|
PG_DB.exec("NOTIFY notifications, E'#{payload}'")
|
||||||
|
@ -439,15 +435,15 @@ module Invidious::Routes::Feeds
|
||||||
|
|
||||||
video = ChannelVideo.new({
|
video = ChannelVideo.new({
|
||||||
id: id,
|
id: id,
|
||||||
title: title,
|
title: video.title,
|
||||||
published: published,
|
published: published,
|
||||||
updated: updated,
|
updated: updated,
|
||||||
ucid: ucid,
|
ucid: video.ucid,
|
||||||
author: author,
|
author: author,
|
||||||
length_seconds: video_.try &.length_seconds || 0,
|
length_seconds: video.length_seconds,
|
||||||
live_now: video_.try &.live_now || false,
|
live_now: video.live_now,
|
||||||
premiere_timestamp: video_.try &.premiere_timestamp || nil,
|
premiere_timestamp: video.premiere_timestamp,
|
||||||
views: video_.try &.views || nil,
|
views: video.views,
|
||||||
})
|
})
|
||||||
|
|
||||||
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
|
was_insert = Invidious::Database::ChannelVideos.insert(video, with_premiere_timestamp: true)
|
||||||
|
|
|
@ -64,8 +64,6 @@ module Invidious::Routes::Login
|
||||||
# TOR or I2P address
|
# TOR or I2P address
|
||||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
|
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
|
else
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||||
end
|
end
|
||||||
|
@ -172,8 +170,6 @@ module Invidious::Routes::Login
|
||||||
# TOR or I2P address
|
# TOR or I2P address
|
||||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
|
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
|
else
|
||||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||||
end
|
end
|
||||||
|
|
|
@ -86,6 +86,12 @@ module Invidious::Routes::PreferencesRoute
|
||||||
show_nick ||= "off"
|
show_nick ||= "off"
|
||||||
show_nick = show_nick == "on"
|
show_nick = show_nick == "on"
|
||||||
|
|
||||||
|
po_token = env.params.body["po_token"]?.try &.as(String)
|
||||||
|
po_token ||= CONFIG.default_user_preferences.po_token
|
||||||
|
|
||||||
|
visitor_data = env.params.body["visitor_data"]?.try &.as(String)
|
||||||
|
visitor_data ||= CONFIG.default_user_preferences.visitor_data
|
||||||
|
|
||||||
comments = [] of String
|
comments = [] of String
|
||||||
2.times do |i|
|
2.times do |i|
|
||||||
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
|
comments << (env.params.body["comments[#{i}]"]?.try &.as(String) || CONFIG.default_user_preferences.comments[i])
|
||||||
|
@ -180,6 +186,8 @@ module Invidious::Routes::PreferencesRoute
|
||||||
vr_mode: vr_mode,
|
vr_mode: vr_mode,
|
||||||
show_nick: show_nick,
|
show_nick: show_nick,
|
||||||
save_player_pos: save_player_pos,
|
save_player_pos: save_player_pos,
|
||||||
|
po_token: po_token,
|
||||||
|
visitor_data: visitor_data,
|
||||||
}.to_json)
|
}.to_json)
|
||||||
|
|
||||||
if user = env.get? "user"
|
if user = env.get? "user"
|
||||||
|
@ -228,8 +236,6 @@ module Invidious::Routes::PreferencesRoute
|
||||||
# TOR or I2P address
|
# TOR or I2P address
|
||||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
|
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
|
else
|
||||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
||||||
end
|
end
|
||||||
|
@ -271,8 +277,6 @@ module Invidious::Routes::PreferencesRoute
|
||||||
# TOR or I2P address
|
# TOR or I2P address
|
||||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
|
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
|
else
|
||||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,7 +47,11 @@ module Invidious::Routes::VideoPlayback
|
||||||
headers["Range"] = "bytes=#{range_for_head}"
|
headers["Range"] = "bytes=#{range_for_head}"
|
||||||
end
|
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) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36"
|
||||||
|
|
||||||
|
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||||
response = HTTP::Client::Response.new(500)
|
response = HTTP::Client::Response.new(500)
|
||||||
error = ""
|
error = ""
|
||||||
5.times do
|
5.times do
|
||||||
|
@ -62,7 +66,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
if new_host != host
|
if new_host != host
|
||||||
host = new_host
|
host = new_host
|
||||||
client.close
|
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
|
end
|
||||||
|
|
||||||
url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
||||||
|
@ -76,7 +80,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
fvip = "3"
|
fvip = "3"
|
||||||
|
|
||||||
host = "https://r#{fvip}---#{mn}.googlevideo.com"
|
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
|
rescue ex
|
||||||
error = ex.message
|
error = ex.message
|
||||||
end
|
end
|
||||||
|
@ -201,7 +205,7 @@ module Invidious::Routes::VideoPlayback
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
client.close
|
client.close
|
||||||
client = make_client(URI.parse(host), region, force_resolve: true)
|
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -258,11 +262,6 @@ module Invidious::Routes::VideoPlayback
|
||||||
# YouTube /videoplayback links expire after 6 hours,
|
# YouTube /videoplayback links expire after 6 hours,
|
||||||
# so we have a mechanism here to redirect to the latest version
|
# so we have a mechanism here to redirect to the latest version
|
||||||
def self.latest_version(env)
|
def self.latest_version(env)
|
||||||
if CONFIG.invidious_companion.present?
|
|
||||||
invidious_companion = CONFIG.invidious_companion.sample
|
|
||||||
return env.redirect "#{invidious_companion.public_url}/latest_version?#{env.params.query}"
|
|
||||||
end
|
|
||||||
|
|
||||||
id = env.params.query["id"]?
|
id = env.params.query["id"]?
|
||||||
itag = env.params.query["itag"]?.try &.to_i?
|
itag = env.params.query["itag"]?.try &.to_i?
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,14 @@
|
||||||
module Invidious::Routes::Watch
|
module Invidious::Routes::Watch
|
||||||
def self.handle(env)
|
def self.handle(env)
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
|
if !CONFIG.ignore_user_tokens
|
||||||
|
user_po_token = env.get("preferences").as(Preferences).po_token
|
||||||
|
user_visitor_data = env.get("preferences").as(Preferences).visitor_data
|
||||||
|
else
|
||||||
|
user_po_token = ""
|
||||||
|
user_visitor_data = ""
|
||||||
|
end
|
||||||
|
|
||||||
region = env.params.query["region"]?
|
region = env.params.query["region"]?
|
||||||
|
|
||||||
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
|
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
|
||||||
|
@ -52,7 +60,7 @@ module Invidious::Routes::Watch
|
||||||
env.params.query.delete_all("listen")
|
env.params.query.delete_all("listen")
|
||||||
|
|
||||||
begin
|
begin
|
||||||
video = get_video(id, region: params.region)
|
video = get_video(id, region: params.region, po_token: user_po_token, visitor_data: user_visitor_data)
|
||||||
rescue ex : NotFoundException
|
rescue ex : NotFoundException
|
||||||
LOGGER.error("get_video not found: #{id} : #{ex.message}")
|
LOGGER.error("get_video not found: #{id} : #{ex.message}")
|
||||||
return error_template(404, ex)
|
return error_template(404, ex)
|
||||||
|
@ -68,8 +76,14 @@ module Invidious::Routes::Watch
|
||||||
end
|
end
|
||||||
env.params.query.delete_all("iv_load_policy")
|
env.params.query.delete_all("iv_load_policy")
|
||||||
|
|
||||||
|
history_details = JSON::Any.new({
|
||||||
|
"id" => JSON::Any.new(id),
|
||||||
|
"title" => JSON::Any.new(video.title),
|
||||||
|
"author" => JSON::Any.new(video.author)
|
||||||
|
})
|
||||||
|
|
||||||
if watched && preferences.watch_history
|
if watched && preferences.watch_history
|
||||||
Invidious::Database::Users.mark_watched(user.as(User), id)
|
Invidious::Database::Users.mark_watched(user.as(User), history_details)
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.enable_user_notifications && notifications && notifications.includes? id
|
if CONFIG.enable_user_notifications && notifications && notifications.includes? id
|
||||||
|
@ -278,9 +292,9 @@ module Invidious::Routes::Watch
|
||||||
|
|
||||||
case action
|
case action
|
||||||
when "action_mark_watched"
|
when "action_mark_watched"
|
||||||
Invidious::Database::Users.mark_watched(user, id)
|
# Invidious::Database::Users.mark_watched(user, history_details)
|
||||||
when "action_mark_unwatched"
|
when "action_mark_unwatched"
|
||||||
Invidious::Database::Users.mark_unwatched(user, id)
|
# Invidious::Database::Users.mark_unwatched(user, id)
|
||||||
else
|
else
|
||||||
return error_json(400, "Unsupported action #{action}")
|
return error_json(400, "Unsupported action #{action}")
|
||||||
end
|
end
|
||||||
|
@ -347,18 +361,14 @@ module Invidious::Routes::Watch
|
||||||
env.params.query["label"] = URI.decode_www_form(label.as_s)
|
env.params.query["label"] = URI.decode_www_form(label.as_s)
|
||||||
|
|
||||||
return Invidious::Routes::API::V1::Videos.captions(env)
|
return Invidious::Routes::API::V1::Videos.captions(env)
|
||||||
elsif itag = download_widget["itag"]?.try &.as_i.to_s
|
elsif itag = download_widget["itag"]?.try &.as_i
|
||||||
# URL params specific to /latest_version
|
# URL params specific to /latest_version
|
||||||
env.params.query["id"] = video_id
|
env.params.query["id"] = video_id
|
||||||
|
env.params.query["itag"] = itag.to_s
|
||||||
env.params.query["title"] = filename
|
env.params.query["title"] = filename
|
||||||
env.params.query["local"] = "true"
|
env.params.query["local"] = "true"
|
||||||
|
|
||||||
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)
|
return Invidious::Routes::VideoPlayback.latest_version(env)
|
||||||
end
|
|
||||||
else
|
else
|
||||||
return error_template(400, "Invalid label or itag")
|
return error_template(400, "Invalid label or itag")
|
||||||
end
|
end
|
||||||
|
|
|
@ -76,6 +76,7 @@ module Invidious::Routing
|
||||||
post "/authorize_token", Routes::Account, :post_authorize_token
|
post "/authorize_token", Routes::Account, :post_authorize_token
|
||||||
get "/token_manager", Routes::Account, :token_manager
|
get "/token_manager", Routes::Account, :token_manager
|
||||||
post "/token_ajax", Routes::Account, :token_ajax
|
post "/token_ajax", Routes::Account, :token_ajax
|
||||||
|
get "/generate_tokens", Routes::Account, :generate_tokens
|
||||||
post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription
|
post "/subscription_ajax", Routes::Subscriptions, :toggle_subscription
|
||||||
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
|
get "/subscription_manager", Routes::Subscriptions, :subscription_manager
|
||||||
end
|
end
|
||||||
|
@ -243,16 +244,17 @@ module Invidious::Routing
|
||||||
|
|
||||||
# Channels
|
# Channels
|
||||||
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home
|
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/shorts", {{namespace}}::Channels, :shorts
|
||||||
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
|
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
|
||||||
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
|
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
|
||||||
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
|
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
|
||||||
get "/api/v1/channels/:ucid/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/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
|
# Posts
|
||||||
get "/api/v1/post/:id", {{namespace}}::Channels, :post
|
get "/api/v1/post/:id", {{namespace}}::Channels, :post
|
||||||
|
@ -270,6 +272,11 @@ module Invidious::Routing
|
||||||
|
|
||||||
# Authenticated
|
# 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
|
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences
|
||||||
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences
|
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences
|
||||||
|
|
||||||
|
|
|
@ -57,6 +57,10 @@ struct Preferences
|
||||||
property volume : Int32 = CONFIG.default_user_preferences.volume
|
property volume : Int32 = CONFIG.default_user_preferences.volume
|
||||||
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
|
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
|
||||||
|
|
||||||
|
@[YAML::Field(converter: Preferences::ProcessString)]
|
||||||
|
property po_token : String = ""
|
||||||
|
property visitor_data : String = ""
|
||||||
|
|
||||||
module BoolToString
|
module BoolToString
|
||||||
def self.to_json(value : String, json : JSON::Builder)
|
def self.to_json(value : String, json : JSON::Builder)
|
||||||
json.string value
|
json.string value
|
||||||
|
|
|
@ -15,7 +15,7 @@ struct Video
|
||||||
# NOTE: don't forget to bump this number if any change is made to
|
# NOTE: don't forget to bump this number if any change is made to
|
||||||
# the `params` structure in videos/parser.cr!!!
|
# the `params` structure in videos/parser.cr!!!
|
||||||
#
|
#
|
||||||
SCHEMA_VERSION = 3
|
SCHEMA_VERSION = 2
|
||||||
|
|
||||||
property id : String
|
property id : String
|
||||||
|
|
||||||
|
@ -192,10 +192,6 @@ struct Video
|
||||||
}
|
}
|
||||||
end
|
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
|
# Macros defining getters/setters for various types of data
|
||||||
|
|
||||||
private macro getset_string(name)
|
private macro getset_string(name)
|
||||||
|
@ -298,7 +294,7 @@ struct Video
|
||||||
predicate_bool upcoming, isUpcoming
|
predicate_bool upcoming, isUpcoming
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_video(id, refresh = true, region = nil, force_refresh = false)
|
def get_video(id, refresh = true, region = nil, force_refresh = false, po_token = "", visitor_data = "")
|
||||||
if (video = Invidious::Database::Videos.select(id)) && !region
|
if (video = Invidious::Database::Videos.select(id)) && !region
|
||||||
# If record was last updated over 10 minutes ago, or video has since premiered,
|
# If record was last updated over 10 minutes ago, or video has since premiered,
|
||||||
# refresh (expire param in response lasts for 6 hours)
|
# refresh (expire param in response lasts for 6 hours)
|
||||||
|
@ -308,7 +304,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
|
||||||
force_refresh ||
|
force_refresh ||
|
||||||
video.schema_version != Video::SCHEMA_VERSION # cache control
|
video.schema_version != Video::SCHEMA_VERSION # cache control
|
||||||
begin
|
begin
|
||||||
video = fetch_video(id, region)
|
video = fetch_video(id, region, po_token, visitor_data)
|
||||||
Invidious::Database::Videos.insert(video)
|
Invidious::Database::Videos.insert(video)
|
||||||
rescue ex
|
rescue ex
|
||||||
Invidious::Database::Videos.delete(id)
|
Invidious::Database::Videos.delete(id)
|
||||||
|
@ -316,7 +312,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
video = fetch_video(id, region)
|
video = fetch_video(id, region, po_token, visitor_data)
|
||||||
Invidious::Database::Videos.insert(video) if !region
|
Invidious::Database::Videos.insert(video) if !region
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -324,11 +320,11 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
|
||||||
rescue DB::Error
|
rescue DB::Error
|
||||||
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
|
# Avoid common `DB::PoolRetryAttemptsExceeded` error and friends
|
||||||
# Note: All DB errors inherit from `DB::Error`
|
# Note: All DB errors inherit from `DB::Error`
|
||||||
return fetch_video(id, region)
|
return fetch_video(id, region, po_token, visitor_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch_video(id, region)
|
def fetch_video(id, region, po_token, visitor_data)
|
||||||
info = extract_video_info(video_id: id)
|
info = extract_video_info(video_id: id, user_po_token: po_token, user_visitor_data: visitor_data)
|
||||||
|
|
||||||
if reason = info["reason"]?
|
if reason = info["reason"]?
|
||||||
if reason == "Video unavailable"
|
if reason == "Video unavailable"
|
||||||
|
|
|
@ -50,12 +50,17 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_video_info(video_id : String)
|
def extract_video_info(video_id : String, user_po_token, user_visitor_data)
|
||||||
# Init client config for the API
|
# Init client config for the API
|
||||||
client_config = YoutubeAPI::ClientConfig.new
|
client_config = YoutubeAPI::ClientConfig.new
|
||||||
|
|
||||||
|
redis_po_token, redis_visitor_data = Tokens.get_tokens
|
||||||
|
|
||||||
|
po_token = (user_po_token if !user_po_token.empty?) || redis_po_token || CONFIG.po_token
|
||||||
|
visitor_data = (user_visitor_data if !user_visitor_data.empty?) || redis_visitor_data || CONFIG.visitor_data
|
||||||
|
|
||||||
# Fetch data from the player endpoint
|
# Fetch data from the player endpoint
|
||||||
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
|
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
|
||||||
|
|
||||||
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
||||||
|
|
||||||
|
@ -100,18 +105,26 @@ def extract_video_info(video_id : String)
|
||||||
params = parse_video_info(video_id, player_response)
|
params = parse_video_info(video_id, player_response)
|
||||||
params["reason"] = JSON::Any.new(reason) if reason
|
params["reason"] = JSON::Any.new(reason) if reason
|
||||||
|
|
||||||
if CONFIG.invidious_companion.present?
|
|
||||||
new_player_response = nil
|
new_player_response = nil
|
||||||
|
|
||||||
# Don't use Android test suite client if po_token is passed because po_token doesn't
|
# Don't use Android client if po_token is passed because po_token doesn't
|
||||||
# work for Android test suite client.
|
# work for Android client.
|
||||||
if reason.nil? && CONFIG.po_token.nil?
|
if reason.nil? && po_token.nil?
|
||||||
# Fetch the video streams using an Android client in order to get the
|
# Fetch the video streams using an Android client in order to get the
|
||||||
# decrypted URLs and maybe fix throttling issues (#2194). See the
|
# decrypted URLs and maybe fix throttling issues (#2194). See the
|
||||||
# following issue for an explanation about decrypted URLs:
|
# following issue for an explanation about decrypted URLs:
|
||||||
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
|
# https://github.com/TeamNewPipe/NewPipeExtractor/issues/562
|
||||||
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
|
client_config.client_type = YoutubeAPI::ClientType::AndroidTestSuite
|
||||||
new_player_response = try_fetch_streaming_data(video_id, client_config)
|
new_player_response = try_fetch_streaming_data(video_id, client_config, po_token, visitor_data)
|
||||||
|
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 po_token.nil? && reason || 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, po_token, visitor_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Replace player response and reset reason
|
# Replace player response and reset reason
|
||||||
|
@ -123,9 +136,8 @@ def extract_video_info(video_id : String)
|
||||||
player_response = new_player_response
|
player_response = new_player_response
|
||||||
params.delete("reason")
|
params.delete("reason")
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
{"captions", "playabilityStatus", "playerConfig", "storyboards", "invidiousCompanion"}.each do |f|
|
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
|
||||||
params[f] = player_response[f] if player_response[f]?
|
params[f] = player_response[f] if player_response[f]?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -133,7 +145,7 @@ def extract_video_info(video_id : String)
|
||||||
if streaming_data = player_response["streamingData"]?
|
if streaming_data = player_response["streamingData"]?
|
||||||
%w[formats adaptiveFormats].each do |key|
|
%w[formats adaptiveFormats].each do |key|
|
||||||
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
||||||
format.as_h["url"] = JSON::Any.new(convert_url(format))
|
format.as_h["url"] = JSON::Any.new(convert_url(format, po_token))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -146,9 +158,9 @@ def extract_video_info(video_id : String)
|
||||||
return params
|
return params
|
||||||
end
|
end
|
||||||
|
|
||||||
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
|
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig, po_token, visitor_data) : Hash(String, JSON::Any)?
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
||||||
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)
|
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config, po_token: po_token, visitor_data: visitor_data)
|
||||||
|
|
||||||
playability_status = response["playabilityStatus"]["status"]
|
playability_status = response["playabilityStatus"]["status"]
|
||||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
||||||
|
@ -217,17 +229,8 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
|
||||||
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
|
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
|
||||||
.try { |t| Time.parse_rfc3339(t.as_s) }
|
.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")
|
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
|
||||||
.try &.as_bool
|
.try &.as_bool || false
|
||||||
live_now ||= video_details.dig?("isLive").try &.as_bool || false
|
|
||||||
|
|
||||||
post_live_dvr = video_details.dig?("isPostLiveDvr")
|
post_live_dvr = video_details.dig?("isPostLiveDvr")
|
||||||
.try &.as_bool || false
|
.try &.as_bool || false
|
||||||
|
@ -457,7 +460,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
|
||||||
return params
|
return params
|
||||||
end
|
end
|
||||||
|
|
||||||
private def convert_url(fmt)
|
private def convert_url(fmt, po_token)
|
||||||
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
||||||
sp = cfr["sp"]
|
sp = cfr["sp"]
|
||||||
url = URI.parse(cfr["url"])
|
url = URI.parse(cfr["url"])
|
||||||
|
@ -475,7 +478,9 @@ private def convert_url(fmt)
|
||||||
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
|
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
|
||||||
params["n"] = n if n
|
params["n"] = n if n
|
||||||
|
|
||||||
if token = CONFIG.po_token
|
if !po_token.nil?
|
||||||
|
params["pot"] = po_token
|
||||||
|
elsif token = CONFIG.po_token
|
||||||
params["pot"] = token
|
params["pot"] = token
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
ucid = channel.ucid
|
ucid = channel.ucid
|
||||||
author = HTML.escape(channel.author)
|
author = HTML.escape(channel.author)
|
||||||
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
|
channel_profile_pic = URI.parse(channel.author_thumbnail).request_target
|
||||||
host = env.request.headers["Host"]
|
|
||||||
|
|
||||||
relative_url =
|
relative_url =
|
||||||
case selected_tab
|
case selected_tab
|
||||||
|
@ -29,15 +28,15 @@
|
||||||
<%- if selected_tab.videos? -%>
|
<%- if selected_tab.videos? -%>
|
||||||
<meta name="description" content="<%= channel.description %>">
|
<meta name="description" content="<%= channel.description %>">
|
||||||
<meta property="og:site_name" content="Invidious">
|
<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: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 property="og:description" content="<%= channel.description %>">
|
||||||
<meta name="twitter:card" content="summary">
|
<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:title" content="<%= author %>">
|
||||||
<meta name="twitter:description" content="<%= channel.description %>">
|
<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 %>" />
|
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/channel/<%= ucid %>" />
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
|
|
||||||
|
|
|
@ -103,15 +103,12 @@
|
||||||
if item.is_a?(PlaylistVideo)
|
if item.is_a?(PlaylistVideo)
|
||||||
link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}"
|
link_url = "/watch?v=#{item.id}&list=#{item.plid}&index=#{item.index}"
|
||||||
endpoint_params = "?v=#{item.id}&list=#{item.plid}"
|
endpoint_params = "?v=#{item.id}&list=#{item.plid}"
|
||||||
embed_id = item.id
|
|
||||||
elsif item.is_a?(MixVideo)
|
elsif item.is_a?(MixVideo)
|
||||||
link_url = "/watch?v=#{item.id}&list=#{item.rdid}"
|
link_url = "/watch?v=#{item.id}&list=#{item.rdid}"
|
||||||
endpoint_params = "?v=#{item.id}&list=#{item.rdid}"
|
endpoint_params = "?v=#{item.id}&list=#{item.rdid}"
|
||||||
embed_id = item.id
|
|
||||||
else
|
else
|
||||||
link_url = "/watch?v=#{item.id}"
|
link_url = "/watch?v=#{item.id}"
|
||||||
endpoint_params = "?v=#{item.id}"
|
endpoint_params = "?v=#{item.id}"
|
||||||
embed_id = item.id
|
|
||||||
end
|
end
|
||||||
-%>
|
-%>
|
||||||
|
|
||||||
|
|
|
@ -22,8 +22,6 @@
|
||||||
audio_streams.each_with_index do |fmt, i|
|
audio_streams.each_with_index do |fmt, i|
|
||||||
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
||||||
src_url += "&local=true" if params.local
|
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"]
|
bitrate = fmt["bitrate"]
|
||||||
mimetype = HTML.escape(fmt["mimeType"].as_s)
|
mimetype = HTML.escape(fmt["mimeType"].as_s)
|
||||||
|
@ -36,12 +34,8 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% else %>
|
<% else %>
|
||||||
<% if params.quality == "dash"
|
<% if params.quality == "dash" %>
|
||||||
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1"
|
<source src="/api/manifest/dash/id/<%= video.id %>?local=true&unique_res=1" type='application/dash+xml' label="dash">
|
||||||
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">
|
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%
|
<%
|
||||||
|
@ -50,8 +44,6 @@
|
||||||
fmt_stream.each_with_index do |fmt, i|
|
fmt_stream.each_with_index do |fmt, i|
|
||||||
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
||||||
src_url += "&local=true" if params.local
|
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"]
|
quality = fmt["quality"]
|
||||||
mimetype = HTML.escape(fmt["mimeType"].as_s)
|
mimetype = HTML.escape(fmt["mimeType"].as_s)
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<div class="flex-right flexible">
|
<div class="flex-right flexible">
|
||||||
<div class="icon-buttons">
|
<div class="icon-buttons">
|
||||||
<a title="<%=translate(locale, "videoinfo_youTube_embed_link")%>" rel="noreferrer noopener" href="https://www.youtube.com/embed/<%=embed_id%>">
|
|
||||||
<i class="icon ion-md-open"></i>
|
|
||||||
</a>
|
|
||||||
<a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>">
|
<a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>">
|
||||||
<i class="icon ion-logo-youtube"></i>
|
<i class="icon ion-logo-youtube"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<%
|
<%
|
||||||
locale = env.get("preferences").as(Preferences).locale
|
locale = env.get("preferences").as(Preferences).locale
|
||||||
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
||||||
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value || env.request.headers["Host"]
|
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value
|
||||||
current_external_videoplayback_proxy = Invidious::HttpServer::Utils.get_external_proxy()
|
|
||||||
%>
|
%>
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="<%= locale %>">
|
<html lang="<%= locale %>">
|
||||||
|
@ -107,7 +106,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% if !CONFIG.backends.empty? %>
|
<% if !CONFIG.backends.empty? %>
|
||||||
<% if !CONFIG.backend_domains.includes?(env.request.headers["Host"]) %>
|
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
<b>Switch Backend:</b>
|
<b>Switch Backend:</b>
|
||||||
<% CONFIG.backends.each do | backend | %>
|
<% CONFIG.backends.each do | backend | %>
|
||||||
|
@ -130,7 +128,6 @@
|
||||||
<% end %>
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<% if CONFIG.banner %>
|
<% if CONFIG.banner %>
|
||||||
<div class="h-box">
|
<div class="h-box">
|
||||||
|
@ -314,9 +311,6 @@
|
||||||
<hr/>
|
<hr/>
|
||||||
<div class="footer-footer">
|
<div class="footer-footer">
|
||||||
<div class="box">You are currently using Backend: <%= current_backend %></div>
|
<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">
|
<span class="left">
|
||||||
<% if CONFIG.modified_source_code_url %>
|
<% if CONFIG.modified_source_code_url %>
|
||||||
<%= translate(locale, "footer_current_version_modified") %>
|
<%= translate(locale, "footer_current_version_modified") %>
|
||||||
|
|
|
@ -126,6 +126,24 @@
|
||||||
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
|
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<% if !CONFIG.ignore_user_tokens %>
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="po_token"><%= translate(locale, "preferences_po_token") %></label>
|
||||||
|
<input name="po_token" id="po_token" type="text" value="<%= preferences.po_token %>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<label for="visitor_data"><%= translate(locale, "preferences_visitor_data") %></label>
|
||||||
|
<input name="visitor_data" id="visitor_data" type="text" value="<%= preferences.visitor_data %>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% if env.get?("user") %>
|
||||||
|
<div class="pure-control-group">
|
||||||
|
<a href="/generate_tokens?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Generate po_token and visitor_data for your account") %></a>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
<legend><%= translate(locale, "preferences_category_visual") %></legend>
|
<legend><%= translate(locale, "preferences_category_visual") %></legend>
|
||||||
|
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
|
|
15
src/invidious/views/user/tokens.ecr
Normal file
15
src/invidious/views/user/tokens.ecr
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<% content_for "header" do %>
|
||||||
|
<title><%= translate(locale, "Invidious token generator") %> - Invidious</title>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<div class="pure-g">
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-3-5">
|
||||||
|
<div class="h-box">
|
||||||
|
<p>po_token and visitor_data successfully generated!</p>
|
||||||
|
<p>po_token: <%= po_token %></p>
|
||||||
|
<p>visitor_data: <%= visitor_data %></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="pure-u-1 pure-u-lg-1-5"></div>
|
||||||
|
</div>
|
|
@ -1,7 +1,6 @@
|
||||||
<% ucid = video.ucid %>
|
<% ucid = video.ucid %>
|
||||||
<% title = HTML.escape(video.title) %>
|
<% title = HTML.escape(video.title) %>
|
||||||
<% author = HTML.escape(video.author) %>
|
<% author = HTML.escape(video.author) %>
|
||||||
<% host = env.request.headers["Host"] %>
|
|
||||||
|
|
||||||
|
|
||||||
<% content_for "header" do %>
|
<% content_for "header" do %>
|
||||||
|
@ -9,9 +8,9 @@
|
||||||
<meta name="description" content="<%= HTML.escape(video.short_description) %>">
|
<meta name="description" content="<%= HTML.escape(video.short_description) %>">
|
||||||
<meta name="keywords" content="<%= video.keywords.join(",") %>">
|
<meta name="keywords" content="<%= video.keywords.join(",") %>">
|
||||||
<meta property="og:site_name" content="<%= author %> | Invidious">
|
<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: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:description" content="<%= HTML.escape(video.short_description) %>">
|
||||||
<meta property="og:type" content="video.other">
|
<meta property="og:type" content="video.other">
|
||||||
<!-- This shouldn't be empty, ever. -->
|
<!-- This shouldn't be empty, ever. -->
|
||||||
|
@ -22,11 +21,11 @@
|
||||||
<meta property="og:video:width" content="640">
|
<meta property="og:video:width" content="640">
|
||||||
<meta property="og:video:height" content="360">
|
<meta property="og:video:height" content="360">
|
||||||
<meta name="twitter:card" content="player">
|
<meta name="twitter:card" content="player">
|
||||||
<meta name="twitter:url" content="<%= host %>/watch?v=<%= video.id %>">
|
<meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
|
||||||
<meta name="twitter:title" content="<%= title %>">
|
<meta name="twitter:title" content="<%= title %>">
|
||||||
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
|
<meta name="twitter:description" content="<%= HTML.escape(video.short_description) %>">
|
||||||
<meta name="twitter:image" content="<%= host %>/vi/<%= video.id %>/maxres.jpg">
|
<meta name="twitter:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
|
||||||
<meta name="twitter:player" content="<%= host %>/embed/<%= video.id %>">
|
<meta name="twitter:player" content="<%= HOST_URL %>/embed/<%= video.id %>">
|
||||||
<meta name="twitter:player:width" content="1280">
|
<meta name="twitter:player:width" content="1280">
|
||||||
<meta name="twitter:player:height" content="720">
|
<meta name="twitter:player:height" content="720">
|
||||||
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">
|
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">
|
||||||
|
|
|
@ -22,8 +22,12 @@ struct YoutubeConnectionPool
|
||||||
response = yield conn
|
response = yield conn
|
||||||
rescue ex
|
rescue ex
|
||||||
conn.close
|
conn.close
|
||||||
conn = make_client(url, force_resolve: true)
|
|
||||||
|
|
||||||
|
conn = HTTP::Client.new(url)
|
||||||
|
conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
||||||
|
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
|
response = yield conn
|
||||||
ensure
|
ensure
|
||||||
pool.release(conn)
|
pool.release(conn)
|
||||||
|
@ -33,15 +37,12 @@ struct YoutubeConnectionPool
|
||||||
end
|
end
|
||||||
|
|
||||||
private def build_pool
|
private def build_pool
|
||||||
options = DB::Pool::Options.new(
|
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
|
||||||
initial_pool_size: 0,
|
conn = HTTP::Client.new(url)
|
||||||
max_pool_size: capacity,
|
conn.family = CONFIG.force_resolve
|
||||||
max_idle_pool_size: capacity,
|
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
||||||
checkout_timeout: timeout
|
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||||
)
|
conn
|
||||||
|
|
||||||
DB::Pool(HTTP::Client).new(options) do
|
|
||||||
next make_client(url, force_resolve: true)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -61,25 +62,23 @@ def add_yt_headers(request)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false, use_http_proxy : Bool = true)
|
def make_client(url : URI, region = nil, force_resolve : Bool = false)
|
||||||
client = HTTP::Client.new(url)
|
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
|
# Force the usage of a specific configured IP Family
|
||||||
if force_resolve
|
if force_resolve
|
||||||
client.family = CONFIG.force_resolve
|
client.family = CONFIG.force_resolve
|
||||||
client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC
|
|
||||||
end
|
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.read_timeout = 10.seconds
|
||||||
client.connect_timeout = 10.seconds
|
client.connect_timeout = 10.seconds
|
||||||
|
|
||||||
return client
|
return client
|
||||||
end
|
end
|
||||||
|
|
||||||
def make_client(url : URI, region = nil, force_resolve : Bool = false, use_http_proxy : Bool = true, &)
|
def make_client(url : URI, region = nil, force_resolve : Bool = false, &)
|
||||||
client = make_client(url, region, force_resolve: force_resolve, use_http_proxy: use_http_proxy)
|
client = make_client(url, region, force_resolve)
|
||||||
begin
|
begin
|
||||||
yield client
|
yield client
|
||||||
ensure
|
ensure
|
||||||
|
|
|
@ -21,7 +21,6 @@ private ITEM_PARSERS = {
|
||||||
Parsers::ItemSectionRendererParser,
|
Parsers::ItemSectionRendererParser,
|
||||||
Parsers::ContinuationItemRendererParser,
|
Parsers::ContinuationItemRendererParser,
|
||||||
Parsers::HashtagRendererParser,
|
Parsers::HashtagRendererParser,
|
||||||
Parsers::LockupViewModelParser,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private alias InitialData = Hash(String, JSON::Any)
|
private alias InitialData = Hash(String, JSON::Any)
|
||||||
|
@ -591,9 +590,8 @@ private module Parsers
|
||||||
# Parses an InnerTube lockupViewModel into a SearchPlaylist.
|
# Parses an InnerTube lockupViewModel into a SearchPlaylist.
|
||||||
# Returns nil when the given object is not a lockupViewModel.
|
# Returns nil when the given object is not a lockupViewModel.
|
||||||
#
|
#
|
||||||
# This structure is present since November 2024 on the "podcasts" and
|
# This structure is present since November 2024 on the "podcasts" tab of the
|
||||||
# "playlists" tabs of the channel page. It is usually encapsulated in either
|
# channel page. It is usually (always?) encapsulated in a richItemRenderer.
|
||||||
# a richItemRenderer or a richGridRenderer.
|
|
||||||
#
|
#
|
||||||
module LockupViewModelParser
|
module LockupViewModelParser
|
||||||
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||||
|
@ -610,7 +608,7 @@ private module Parsers
|
||||||
"primaryThumbnail", "thumbnailViewModel"
|
"primaryThumbnail", "thumbnailViewModel"
|
||||||
)
|
)
|
||||||
|
|
||||||
thumbnail = thumbnail_view_model.dig("image", "sources", 0, "url").as_s
|
thumbnail = thumbnail_view_model.dig("image", "sources", 1, "url").as_s
|
||||||
|
|
||||||
# This complicated sequences tries to extract the following data structure:
|
# This complicated sequences tries to extract the following data structure:
|
||||||
# "overlays": [{
|
# "overlays": [{
|
||||||
|
@ -623,15 +621,10 @@ private module Parsers
|
||||||
# }]
|
# }]
|
||||||
# }
|
# }
|
||||||
# }]
|
# }]
|
||||||
#
|
|
||||||
# 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
|
video_count = thumbnail_view_model.dig("overlays").as_a
|
||||||
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
|
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
|
||||||
.flatten
|
.flatten
|
||||||
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try { |node|
|
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try &.as_s.ends_with?("episodes"))
|
||||||
{"episodes", "videos"}.any? { |str| node.as_s.ends_with?(str) }
|
|
||||||
})
|
|
||||||
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)
|
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)
|
||||||
|
|
||||||
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
|
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
module YoutubeAPI
|
module YoutubeAPI
|
||||||
|
@@visitor_data : String = ""
|
||||||
|
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
|
# For Android versions, see https://en.wikipedia.org/wiki/Android_version_history
|
||||||
|
@ -320,7 +322,9 @@ module YoutubeAPI
|
||||||
client_context["client"]["platform"] = platform
|
client_context["client"]["platform"] = platform
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.visitor_data.is_a?(String)
|
if !@@visitor_data.empty?
|
||||||
|
client_context["client"]["visitorData"] = @@visitor_data
|
||||||
|
elsif CONFIG.visitor_data.is_a?(String)
|
||||||
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
|
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -456,7 +460,12 @@ module YoutubeAPI
|
||||||
*, # Force the following parameters to be passed by name
|
*, # Force the following parameters to be passed by name
|
||||||
params : String,
|
params : String,
|
||||||
client_config : ClientConfig | Nil = nil,
|
client_config : ClientConfig | Nil = nil,
|
||||||
|
po_token : String | Nil,
|
||||||
|
visitor_data : String | Nil,
|
||||||
)
|
)
|
||||||
|
if visitor_data
|
||||||
|
@@visitor_data = visitor_data
|
||||||
|
end
|
||||||
# Playback context, separate because it can be different between clients
|
# Playback context, separate because it can be different between clients
|
||||||
playback_ctx = {
|
playback_ctx = {
|
||||||
"html5Preference" => "HTML5_PREF_WANTS",
|
"html5Preference" => "HTML5_PREF_WANTS",
|
||||||
|
@ -482,7 +491,7 @@ module YoutubeAPI
|
||||||
"contentPlaybackContext" => playback_ctx,
|
"contentPlaybackContext" => playback_ctx,
|
||||||
},
|
},
|
||||||
"serviceIntegrityDimensions" => {
|
"serviceIntegrityDimensions" => {
|
||||||
"poToken" => CONFIG.po_token,
|
"poToken" => po_token || CONFIG.po_token,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -491,12 +500,8 @@ module YoutubeAPI
|
||||||
data["params"] = params
|
data["params"] = params
|
||||||
end
|
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)
|
return self._post_json("/youtubei/v1/player", data, client_config)
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
####################################################################
|
####################################################################
|
||||||
# resolve_url(url, client_config?)
|
# resolve_url(url, client_config?)
|
||||||
|
@ -620,7 +625,9 @@ module YoutubeAPI
|
||||||
headers["User-Agent"] = user_agent
|
headers["User-Agent"] = user_agent
|
||||||
end
|
end
|
||||||
|
|
||||||
if CONFIG.visitor_data.is_a?(String)
|
if !@@visitor_data.empty?
|
||||||
|
headers["X-Goog-Visitor-Id"] = @@visitor_data
|
||||||
|
elsif CONFIG.visitor_data.is_a?(String)
|
||||||
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
|
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -632,11 +639,6 @@ module YoutubeAPI
|
||||||
# Send the POST request
|
# Send the POST request
|
||||||
body = YT_POOL.client() do |client|
|
body = YT_POOL.client() do |client|
|
||||||
client.post(url, headers: headers, body: data.to_json) do |response|
|
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"]?)
|
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -661,51 +663,6 @@ module YoutubeAPI
|
||||||
return initial_data
|
return initial_data
|
||||||
end
|
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)
|
# _decompress(body_io, headers)
|
||||||
#
|
#
|
||||||
|
@ -724,7 +681,7 @@ module YoutubeAPI
|
||||||
# Multiple encodings can be combined, and are listed in the order
|
# Multiple encodings can be combined, and are listed in the order
|
||||||
# in which they were applied. E.g: "deflate, gzip" means that the
|
# in which they were applied. E.g: "deflate, gzip" means that the
|
||||||
# content must be first "gunzipped", then "defated".
|
# content must be first "gunzipped", then "defated".
|
||||||
encodings.split(',').reverse!.each do |enc|
|
encodings.split(',').reverse.each do |enc|
|
||||||
case enc.strip(' ')
|
case enc.strip(' ')
|
||||||
when "gzip"
|
when "gzip"
|
||||||
body_io = Compress::Gzip::Reader.new(body_io, sync_close: true)
|
body_io = Compress::Gzip::Reader.new(body_io, sync_close: true)
|
||||||
|
|
Loading…
Add table
Reference in a new issue