Compare commits

..

65 Commits

Author SHA1 Message Date
Fijxu
860fef64ff Dockerfile: Switch to 84codes crystal compiler container image 2025-09-13 23:28:44 -03:00
Fijxu
ba02a4cdf5 Prevent player microformat from being overwritten by the next microformat (#5453)
Some checks are pending
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Waiting to run
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Waiting to run
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Waiting to run
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Waiting to run
Invidious CI / lint (push) Waiting to run
* Prevent player microformat from being overwritten by the next microformat

Closes https://github.com/iv-org/invidious/issues/5443

The player microformat is what we need to get the published date,
premiere timestamp, allowed regions and more information of the video.

Youtube introduced a new `microformat.microformatDataRenderer` in the
next endpoint which overwrote the player microformat
`microformat.playerMicroformatRenderer` when merged

* Update src/invidious/videos/parser.cr

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2025-09-08 17:16:22 -03:00
Emilien
cf2dfbb75d chore: remove debug 2025-09-08 21:34:47 +02:00
Emilien
21c13bba9d chore: use api captions from companion when available 2025-09-08 21:34:47 +02:00
syeopite
5e9d51c06e Refactor FilteredCompressHandler to inherit from stdlib
This changes its behavior to align with the stdlib variant in that
compression is now delayed till the moment that the server begins to
send a response.

This allows the handler to avoid compressing empty responses,and
safeguards against any double compression of content that may occur
if another handler decides to compressi ts response.

This does however come at the drawback(?) of it now removing
`content-length` headers on requests if it exists; since compression
makes the value inaccurate anyway.

See: https://github.com/crystal-lang/crystal/pull/9625
2025-09-08 21:34:47 +02:00
Emilien
1653dd629e fix formatting 2025-09-08 21:34:47 +02:00
Emilien
cba2adc6ef fix csp + progress proxy + allow omit public_url 2025-09-08 21:34:47 +02:00
Emilien
42b955d713 chore: add the suggestions 2025-09-08 21:34:47 +02:00
Emilien
324a416fd4 initial support for base_url with invidious companion + proxy invidious_companion 2025-09-08 21:34:47 +02:00
Fijxu
89c8b1b901 CI: fix wrong if statement for build-docker job (#5442)
Some checks failed
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
2025-09-02 16:57:29 +02:00
syeopite
fd8dc93569 Show message when connection to the database is not possible (#5346)
Some checks failed
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
2025-08-23 04:04:06 -07:00
syeopite
67f93e55d8 Fix "ex" variable collision in invidious.cr
The exception handling for database connections results in an
`ex` variable which Ameba sees as overshadowing the `ex` used by the
`ex` block arg used to define the HTTP status code 500 handler below.

Although this is a non-issue since the db connection exception handling
will cause Invidious to exit, Ameba's nature as a static checker means
that it isn't aware of this.

The simplest fix without a dirty ameba ignore comment is to rename `ex`
within the Kemal handler block below, since `ex` within a begin rescue
block is a Crystal convention that will also cause Ameba to raise when
not adhered to.
2025-08-23 03:35:59 -07:00
syeopite
f35f529adc Videos: Fix missing .id to retrieve first playlist video ID (#5366) 2025-08-23 03:30:00 -07:00
syeopite
b32b077a80 Player: Persist caption settings (#5417) 2025-08-23 03:29:07 -07:00
syeopite
6badb80082 Channels: Fix fetching channel playlists (#5418) 2025-08-23 03:26:49 -07:00
syeopite
15099ac1dd Frontend: Fix notification count of TRUE (#5391) 2025-08-23 03:26:11 -07:00
syeopite
adc83f1c09 Documentation: Fix typo (effet -> effect) (#5369) 2025-08-23 03:23:42 -07:00
syeopite
41e0e77d33 HTML: Add Missing Noreferrers (#5368) 2025-08-23 03:23:05 -07:00
syeopite
9ebc76462f Channels: Fix fetching of individual community posts (#5361) 2025-08-23 03:20:04 -07:00
syeopite
0308acb624 Videos: Add fallback to TvSimply client (#5345) 2025-08-23 03:18:41 -07:00
syeopite
cac2397494 YTAPI: Add TvSimply client (#5344) 2025-08-23 03:17:28 -07:00
syeopite
cf640d808e YtAPI: Bump client versions (#5325) 2025-08-23 03:16:55 -07:00
syeopite
80ec027c8f CI: Fix docker ci job not checking if Invidious starts successfully or not (#5306) 2025-08-23 03:16:32 -07:00
syeopite
6f5f0dceca CI: Use public ARM64 Github actions runners for ARM64 builds (#5305) 2025-08-23 03:16:05 -07:00
syeopite
a8ab7b61f7 Player: Add keyboard shortcuts to configure captions (#5188) 2025-08-23 03:15:28 -07:00
Kristian Vos
dd8086e6d9 fix: fetching channel playlists returned 500 error 2025-08-13 15:43:54 +02:00
Eugene Pakhomov
875d8e7e41 Persist caption settings 2025-08-13 14:39:58 +03:00
dependabot[bot]
1ae0f45b0e Bump actions/checkout from 4 to 5 (#5415)
Some checks failed
Build and release container directly from master / release (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / build-docker (push) Has been cancelled
Invidious CI / build-docker-arm64 (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 15:06:16 +02:00
fieryhenry
3335bc8c38 Get a count of 0 if STORAGE_KEY_NOTIF_COUNT is not present in storage
Not sure if this is necessary as I think it should always be present in storage, but just in case it isn't
2025-07-18 19:07:41 +00:00
fieryhenry
a84bb1d22e Fix TRUE number of notifications
`update_ticker_count` used to use STORAGE_KEY_STREAM to get the number of notifications which is a boolean value, now it uses STORAGE_KEY_NOTIF_COUNT which is an integer
2025-07-18 19:02:50 +00:00
epicsam123
24252b836c add back semicolon 2025-06-30 22:38:30 -04:00
Nami Sunami
227c041b86 fix(config.example.yml): Fix typo (effet -> effect) 2025-06-28 11:38:31 +02:00
ChunkyProgrammer
803311713d make sort_by code more legible 2025-06-27 11:38:08 -04:00
epicsam123
64ac3b5203 add missing noreferrers 2025-06-26 18:40:06 -04:00
Samantaz Fox
b0c9f87fbe Fix missing .id to retrieve first playlist video ID
This was missed in the review of PR 5196
2025-06-26 19:09:52 +00:00
ChunkyProgrammer
f8febbe2b2 format changes 2025-06-25 23:53:07 -04:00
ChunkyProgrammer
436f955e0f update fetch_community_post_comments protobuf to match currently used protobuf, add sort_by option 2025-06-25 23:34:30 -04:00
ChunkyProgrammer
4155f15bf7 update resolve_url api to better support new post endpoint 2025-06-25 23:33:28 -04:00
ChunkyProgrammer
b9171d9dab Update protobuf for individual community post 2025-06-25 22:35:16 -04:00
ChunkyProgrammer
f3f6937ffc Fix community tab not loading 2025-06-25 22:22:30 -04:00
Fijxu
8723fdca06 Update src/invidious.cr
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2025-06-21 12:02:32 -04:00
Fijxu
d51e1cb051 remove fallback to TV client 2025-06-15 17:45:53 -04:00
Fijxu
cf0a68bd77 store adaptiveFormats data into a variable 2025-06-15 17:43:07 -04:00
Fijxu
8cd9d53fb1 show message when connection to the database is not possible 2025-06-12 18:44:01 -04:00
Fijxu
01cdb384e0 add suggestions from syeopite 2025-06-12 17:25:19 -04:00
Fijxu
b1e7e0c45e replace url by signatureCipher if url is not present 2025-06-12 16:18:01 -04:00
Fijxu
0c96e0977f check for signatureCipher too 2025-06-12 16:07:58 -04:00
Fijxu
37be513e14 Add fallback to TvSimply client 2025-06-12 01:25:59 -04:00
Fijxu
09d342b84d Update src/invidious/yt_backend/youtube_api.cr
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2025-05-22 17:55:46 -04:00
Fijxu
3a8d4f333f update IOS_APP_VERSION 2025-05-22 17:17:01 -04:00
Fijxu
97354adf0f Update src/invidious/yt_backend/youtube_api.cr
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2025-05-22 17:15:45 -04:00
Fijxu
6497e1c418 YtAPI: Bump client versions 2025-05-22 16:06:13 -04:00
epicsam123
f9472e4e4b revert format 2025-05-19 22:34:59 -04:00
Fijxu
cc643f209a CI: Fix build-docker job not checking if Invidious starts successfully or not 2025-05-15 19:57:46 -04:00
Fijxu
381074fce1 CI: Replace Dockerfile path depending of the os used 2025-05-15 19:38:21 -04:00
Fijxu
033a44fab5 CI: Also use matrix.docker_compose_file for Run Docker step 2025-05-15 17:58:24 -04:00
Fijxu
a3375e512e CI: Add name attribute to build-docker job 2025-05-15 17:43:03 -04:00
Fijxu
1d664c759f CI: Use matrix for build-docker on ci.yml 2025-05-15 16:33:03 -04:00
Fijxu
94f0a7a9d2 CI: remove --build-arg
Dockerfile and Dockerfile.arm64 already build Invidious without release mode if
`release` argument is not present.
2025-05-15 15:31:17 -04:00
Fijxu
1d2f4b6813 CI: fix typo on comment about the os used on the ARM64 builder 2025-05-15 15:29:24 -04:00
Fijxu
cef0097a30 CI: fix typo on matrix platforms 2025-05-15 15:28:14 -04:00
Fijxu
bef2d7b6b5 CI: Use public ARM64 Github actions runners for ARM64 builds.
Currently, Invidious uses QEMU to build it's ARM64 Invidious image,
which is slow (since we are basically using a virtual machine).

This helps with the speed of building ARM64 binaries for Invidious
on each release/commit.

More information about the public ARM64 runners here:
https://github.com/orgs/community/discussions/148648

CI: Use ARM64 compose file for build-docker-arm64
2025-05-15 01:49:17 -04:00
epicsam123
e67a30b124 formatting 2025-03-20 10:29:26 -04:00
epicsam123
bc3b3f6d69 updated caption features to use videojs interface 2025-03-20 10:09:43 -04:00
epicsam123
73bf956af5 captions: provide "w", "o", "-", "+" keydowns for player from YT 2025-02-19 21:08:45 -05:00
30 changed files with 384 additions and 229 deletions

View File

@@ -17,16 +17,26 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-latest strategy:
matrix:
include:
- os: ubuntu-latest
platform: linux/amd64
name: "AMD64"
dockerfile: "docker/Dockerfile"
tag_suffix: ""
# GitHub doesn't have a ubuntu-latest-arm runner
- os: ubuntu-24.04-arm
platform: linux/arm64/v8
name: "ARM64"
dockerfile: "docker/Dockerfile.arm64"
tag_suffix: "-arm64"
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -43,45 +53,22 @@ jobs:
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: quay.io/invidious/invidious images: quay.io/invidious/invidious
flavor: |
suffix=${{ matrix.tag_suffix }}
tags: | tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }} type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: | labels: |
quay.expires-after=12w quay.expires-after=12w
- name: Build and push Docker AMD64 image for Push Event - name: Build and push Docker ${{ matrix.name }} image for Push Event
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: docker/Dockerfile file: ${{ matrix.dockerfile }}
platforms: linux/amd64 platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
"release=1" "release=1"
- name: Docker meta
id: meta-arm64
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
flavor: |
suffix=-arm64
tags: |
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
type=raw,value=master,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
labels: |
quay.expires-after=12w
- name: Build and push Docker ARM64 image for Push Event
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
labels: ${{ steps.meta-arm64.outputs.labels }}
push: true
tags: ${{ steps.meta-arm64.outputs.tags }}
build-args: |
"release=1"

View File

@@ -8,16 +8,26 @@ on:
jobs: jobs:
release: release:
runs-on: ubuntu-latest strategy:
matrix:
include:
- os: ubuntu-latest
platform: linux/amd64
name: "AMD64"
dockerfile: "docker/Dockerfile"
tag_suffix: ""
# GitHub doesn't have a ubuntu-latest-arm runner
- os: ubuntu-24.04-arm
platform: linux/arm64/v8
name: "ARM64"
dockerfile: "docker/Dockerfile.arm64"
tag_suffix: "-arm64"
runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v5
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -36,46 +46,21 @@ jobs:
images: quay.io/invidious/invidious images: quay.io/invidious/invidious
flavor: | flavor: |
latest=false latest=false
suffix=${{ matrix.tag_suffix }}
tags: | tags: |
type=semver,pattern={{version}} type=semver,pattern={{version}}
type=raw,value=latest type=raw,value=latest
labels: | labels: |
quay.expires-after=12w quay.expires-after=12w
- name: Build and push Docker AMD64 image for Push Event - name: Build and push Docker ${{ matrix.name }} image for Push Event
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: . context: .
file: docker/Dockerfile file: ${{ matrix.dockerfile }}
platforms: linux/amd64 platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
push: true push: true
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
build-args: | build-args: |
"release=1" "release=1"
- name: Docker meta
id: meta-arm64
uses: docker/metadata-action@v5
with:
images: quay.io/invidious/invidious
flavor: |
latest=false
suffix=-arm64
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
labels: |
quay.expires-after=12w
- name: Build and push Docker ARM64 image for Push Event
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
labels: ${{ steps.meta-arm64.outputs.labels }}
push: true
tags: ${{ steps.meta-arm64.outputs.tags }}
build-args: |
"release=1"

View File

@@ -48,7 +48,7 @@ jobs:
stable: false stable: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
submodules: true submodules: true
@@ -83,46 +83,43 @@ jobs:
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
build-docker: build-docker:
strategy:
matrix:
include:
- os: ubuntu-latest
name: "AMD64"
# GitHub doesn't have a ubuntu-latest-arm runner
- os: ubuntu-24.04-arm
name: "ARM64"
runs-on: ubuntu-latest name: Test ${{ matrix.name }} Docker build
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
- name: Use ARM64 Dockerfile if ARM64
if: ${{ matrix.name == 'ARM64' }}
run: sed -i 's/Dockerfile/Dockerfile.arm64/' docker-compose.yml
- name: Build Docker - name: Build Docker
run: docker compose build --build-arg release=0 run: docker compose build
- name: Change hmac_key on docker-compose.yml
run: sed -i '/hmac_key/s/CHANGE_ME!!/docker-build-hmac-key/' docker-compose.yml
- name: Run Docker - name: Run Docker
run: docker compose up -d run: docker compose up -d
- name: Test Docker - name: Test Docker
run: while curl -Isf http://localhost:3000; do sleep 1; done id: test
run: curl -If http://localhost:3000 --retry 5 --retry-delay 1 --retry-all-errors
build-docker-arm64: - name: Print Invidious container logs
# Tells Github Actions to always run this step regardless of whether the previous step has failed
runs-on: ubuntu-latest # Without this expression this step would simply be skipped when the previous step fails.
if: success() || steps.test.conclusion == 'failure'
steps: run: docker compose logs
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker ARM64 image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.arm64
platforms: linux/arm64/v8
build-args: release=0
- name: Test Docker
run: while curl -Isf http://localhost:3000; do sleep 1; done
lint: lint:
@@ -131,7 +128,7 @@ jobs:
continue-on-error: true continue-on-error: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v5
with: with:
submodules: true submodules: true

View File

@@ -86,6 +86,7 @@ ul.vjs-menu-content::-webkit-scrollbar {
background-color: rgba(0, 0, 0, 0.75) !important; background-color: rgba(0, 0, 0, 0.75) !important;
border-radius: 9px !important; border-radius: 9px !important;
padding: 5px !important; padding: 5px !important;
line-height: 1.5 !important;
} }
.vjs-play-control, .vjs-play-control,

View File

@@ -77,7 +77,7 @@ function create_notification_stream(subscriptions) {
function update_ticker_count() { function update_ticker_count() {
var notification_ticker = document.getElementById('notification_ticker'); var notification_ticker = document.getElementById('notification_ticker');
const notification_count = helpers.storage.get(STORAGE_KEY_STREAM); const notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0;
if (notification_count > 0) { if (notification_count > 0) {
notification_ticker.innerHTML = notification_ticker.innerHTML =
'<span id="notification_count">' + notification_count + '</span> <i class="icon ion-ios-notifications"></i>'; '<span id="notification_count">' + notification_count + '</span> <i class="icon ion-ios-notifications"></i>';

View File

@@ -5,6 +5,10 @@ var video_data = JSON.parse(document.getElementById('video_data').textContent);
var options = { var options = {
liveui: true, liveui: true,
playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0], playbackRates: [0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0],
fontPercent: [0.5, 0.75, 1.25, 1.5, 1.75, 2, 3, 4],
windowOpacity: ['0', '0.5', '1'],
textOpacity: ['0.5', '1'],
persistTextTrackSettings: true,
controlBar: { controlBar: {
children: [ children: [
'playToggle', 'playToggle',
@@ -180,7 +184,7 @@ var shareOptions = {
}; };
if (location.pathname.startsWith('/embed/')) { if (location.pathname.startsWith('/embed/')) {
var overlay_content = '<h1><a rel="noopener" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>'; var overlay_content = '<h1><a rel="noopener noreferrer" target="_blank" href="' + location.origin + '/watch?v=' + video_data.id + '">' + player_data.title + '</a></h1>';
player.overlay({ player.overlay({
overlays: [ overlays: [
{ start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'}, { start: 'loadstart', content: overlay_content, end: 'playing', align: 'top'},
@@ -450,7 +454,7 @@ if (!video_data.params.listen && video_data.params.annotations) {
if (target === 'current') { if (target === 'current') {
location.href = path; location.href = path;
} else if (target === 'new') { } else if (target === 'new') {
open(path, '_blank'); open(path, '_blank', 'noopener,noreferrer');
} }
}); });
@@ -585,6 +589,13 @@ const toggle_captions = (function () {
}; };
})(); })();
// For real-time updates to captions (if currently showing)
function update_captions() {
if (document.body.querySelector('.vjs-text-track-cue')) {
toggle_captions(); toggle_captions();
}
}
function toggle_fullscreen() { function toggle_fullscreen() {
player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen(); player.isFullscreen() ? player.exitFullscreen() : player.requestFullscreen();
} }
@@ -597,6 +608,34 @@ function increase_playback_rate(steps) {
player.playbackRate(options.playbackRates[newIndex]); player.playbackRate(options.playbackRates[newIndex]);
} }
function increase_caption_size(steps) {
const maxIndex = options.fontPercent.length - 1;
const fontPercent = player.textTrackSettings.getValues().fontPercent || 1.25;
const curIndex = options.fontPercent.indexOf(fontPercent);
let newIndex = curIndex + steps;
newIndex = helpers.clamp(newIndex, 0, maxIndex);
player.textTrackSettings.setValues({ fontPercent: options.fontPercent[newIndex] });
update_captions();
}
function toggle_caption_window() {
const numOptions = options.windowOpacity.length;
const windowOpacity = player.textTrackSettings.getValues().windowOpacity || '0';
const curIndex = options.windowOpacity.indexOf(windowOpacity);
const newIndex = (curIndex + 1) % numOptions;
player.textTrackSettings.setValues({ windowOpacity: options.windowOpacity[newIndex] });
update_captions();
}
function toggle_caption_opacity() {
const numOptions = options.textOpacity.length;
const textOpacity = player.textTrackSettings.getValues().textOpacity || '1';
const curIndex = options.textOpacity.indexOf(textOpacity);
const newIndex = (curIndex + 1) % numOptions;
player.textTrackSettings.setValues({ textOpacity: options.textOpacity[newIndex] });
update_captions();
}
addEventListener('keydown', function (e) { addEventListener('keydown', function (e) {
if (e.target.tagName.toLowerCase() === 'input') { if (e.target.tagName.toLowerCase() === 'input') {
// Ignore input when focus is on certain elements, e.g. form fields. // Ignore input when focus is on certain elements, e.g. form fields.
@@ -693,6 +732,12 @@ addEventListener('keydown', function (e) {
case '>': action = increase_playback_rate.bind(this, 1); break; case '>': action = increase_playback_rate.bind(this, 1); break;
case '<': action = increase_playback_rate.bind(this, -1); break; case '<': action = increase_playback_rate.bind(this, -1); break;
case '=': action = increase_caption_size.bind(this, 1); break;
case '-': action = increase_caption_size.bind(this, -1); break;
case 'w': action = toggle_caption_window; break;
case 'o': action = toggle_caption_opacity; break;
default: default:
console.info('Unhandled key down event: %s:', decoratedKey, e); console.info('Unhandled key down event: %s:', decoratedKey, e);
break; break;

View File

@@ -141,7 +141,7 @@ function get_reddit_comments() {
</b> \ </b> \
</p> \ </p> \
<b> \ <b> \
<a rel="noopener" target="_blank" href="https://reddit.com{permalink}">{redditPermalinkText}</a> \ <a rel="noopener noreferrer" target="_blank" href="https://reddit.com{permalink}">{redditPermalinkText}</a> \
</b> \ </b> \
</div> \ </div> \
<div>{contentHtml}</div> \ <div>{contentHtml}</div> \

View File

@@ -75,17 +75,25 @@ db:
## If you are using a reverse proxy then you will probably need to ## 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. ## 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). ## Also apply when used from an external IP address (without a domain).
## Examples: https://MYINVIDIOUSDOMAIN or http://192.168.1.100:8282 ## Examples: https://MYINVIDIOUSDOMAIN/companion or http://192.168.1.100:8282/companion
## ##
## Both parameter can have identical URL when Invidious is hosted in ## Both parameter can have identical URL when Invidious is hosted in
## an internal network or at home or locally (localhost). ## an internal network or at home or locally (localhost).
## ##
## NOTE: If public_url is omitted, Invidious will use its built-in proxy
## to route companion requests through /companion, which is useful for
## simple setups where companion runs on the same network. When using
## the built-in proxy, CSP headers are not modified since requests
## stay within the same domain.
##
## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>" ## Accepted values: "http(s)://<IP-HOSTNAME>:<Port>"
## Default: <none> ## Default: <none>
## ##
#invidious_companion: #invidious_companion:
# - private_url: "http://localhost:8282" # - private_url: "http://localhost:8282/companion"
# public_url: "http://localhost:8282" # public_url: "http://localhost:8282/companion"
# # Example with built-in proxy (omit public_url):
# # - private_url: "http://localhost:8282/companion"
## ##
## API key for Invidious companion, used for securing the communication ## API key for Invidious companion, used for securing the communication
@@ -865,7 +873,7 @@ default_user_preferences:
## ##
## Default dash video quality. ## Default dash video quality.
## ##
## Note: this setting only takes effet if the ## Note: this setting only takes effect if the
## 'quality' parameter is set to "dash". ## 'quality' parameter is set to "dash".
## ##
## Accepted values: ## Accepted values:

View File

@@ -14,6 +14,10 @@ services:
restart: unless-stopped restart: unless-stopped
ports: ports:
- "127.0.0.1:3000:3000" - "127.0.0.1:3000:3000"
depends_on:
invidious-db:
condition: service_healthy
restart: true
environment: environment:
# Please read the following file for a comprehensive list of all available # Please read the following file for a comprehensive list of all available
# configuration options and their associated syntax: # configuration options and their associated syntax:

View File

@@ -1,4 +1,4 @@
FROM crystallang/crystal:1.16.3-alpine AS builder FROM 84codes/crystal:1.16.3-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static RUN apk add --no-cache sqlite-static yaml-static

View File

@@ -60,7 +60,13 @@ alias IV = Invidious
CONFIG = Config.load CONFIG = Config.load
HMAC_KEY = CONFIG.hmac_key HMAC_KEY = CONFIG.hmac_key
PG_DB = DB.open CONFIG.database_url PG_DB = begin
DB.open CONFIG.database_url
rescue ex
puts "Failed to connect to PostgreSQL database: #{ex.cause.try &.message}"
puts "Check your 'config.yml' database settings or PostgreSQL settings."
exit(1)
end
ARCHIVE_URL = URI.parse("https://archive.org") ARCHIVE_URL = URI.parse("https://archive.org")
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com") PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
REDDIT_URL = URI.parse("https://www.reddit.com") REDDIT_URL = URI.parse("https://www.reddit.com")
@@ -221,8 +227,8 @@ error 404 do |env|
Invidious::Routes::ErrorRoutes.error_404(env) Invidious::Routes::ErrorRoutes.error_404(env)
end end
error 500 do |env, ex| error 500 do |env, exception|
error_template(500, ex) error_template(500, exception)
end end
static_headers do |env| static_headers do |env|

View File

@@ -3,8 +3,8 @@ private IMAGE_QUALITIES = {320, 560, 640, 1280, 2000}
# TODO: Add "sort_by" # TODO: Add "sort_by"
def fetch_channel_community(ucid, cursor, locale, format, thin_mode) def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
if cursor.nil? if cursor.nil?
# Egljb21tdW5pdHk%3D is the protobuf object to load "community" # EgVwb3N0c_IGBAoCSgA%3D is the protobuf object to load "posts"
initial_data = YoutubeAPI.browse(ucid, params: "Egljb21tdW5pdHk%3D") initial_data = YoutubeAPI.browse(ucid, params: "EgVwb3N0c_IGBAoCSgA%3D")
items = [] of JSON::Any items = [] of JSON::Any
extract_items(initial_data) do |item| extract_items(initial_data) do |item|
@@ -24,15 +24,21 @@ def fetch_channel_community(ucid, cursor, locale, format, thin_mode)
return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode) return extract_channel_community(items, ucid: ucid, locale: locale, format: format, thin_mode: thin_mode)
end end
def decode_ucid_from_post_protobuf(params)
decoded_protobuf = params.try { |i| URI.decode_www_form(i) }
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
return decoded_protobuf.try(&.["56:0:embedded"]["2:0:string"].as_s)
end
def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode) def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
object = { object = {
"2:string" => "community", "56:embedded" => {
"25:embedded" => { "2:string" => ucid,
"22:string" => post_id.to_s, "3:string" => post_id.to_s,
}, "11:string" => ucid,
"45:embedded" => {
"2:varint" => 1_i64,
"3:varint" => 1_i64,
}, },
} }
params = object.try { |i| Protodec::Any.cast_json(i) } params = object.try { |i| Protodec::Any.cast_json(i) }
@@ -40,7 +46,7 @@ def fetch_channel_community_post(ucid, post_id, locale, format, thin_mode)
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }
.try { |i| URI.encode_www_form(i) } .try { |i| URI.encode_www_form(i) }
initial_data = YoutubeAPI.browse(ucid, params: params) initial_data = YoutubeAPI.browse("FEpost_detail", params: params)
items = [] of JSON::Any items = [] of JSON::Any
extract_items(initial_data) do |item| extract_items(initial_data) do |item|

View File

@@ -6,19 +6,19 @@ def fetch_channel_playlists(ucid, author, continuation, sort_by)
case sort_by case sort_by
when "last", "last_added" when "last", "last_added"
# Equivalent to "&sort=lad" # Equivalent to "&sort=lad"
# {"2:string": "playlists", "3:varint": 4, "4:varint": 1, "6:varint": 1} # {"2:string": "playlists", "3:varint": 4, "4:varint": 1, "6:varint": 1, "110:embedded": {"1:embedded": {"8:string": ""}}}
"EglwbGF5bGlzdHMYBCABMAE%3D" "EglwbGF5bGlzdHMYBCABMAHyBgQKAkIA"
when "oldest", "oldest_created" when "oldest", "oldest_created"
# formerly "&sort=da" # formerly "&sort=da"
# Not available anymore :c or maybe ?? # Not available anymore :c or maybe ??
# {"2:string": "playlists", "3:varint": 2, "4:varint": 1, "6:varint": 1} # {"2:string": "playlists", "3:varint": 2, "4:varint": 1, "6:varint": 1, "110:embedded": {"1:embedded": {"8:string": ""}}}
"EglwbGF5bGlzdHMYAiABMAE%3D" "EglwbGF5bGlzdHMYAiABMAHyBgQKAkIA"
# {"2:string": "playlists", "3:varint": 1, "4:varint": 1, "6:varint": 1} # {"2:string": "playlists", "3:varint": 1, "4:varint": 1, "6:varint": 1}
# "EglwbGF5bGlzdHMYASABMAE%3D" # "EglwbGF5bGlzdHMYASABMAE%3D"
when "newest", "newest_created" when "newest", "newest_created"
# Formerly "&sort=dd" # Formerly "&sort=dd"
# {"2:string": "playlists", "3:varint": 3, "4:varint": 1, "6:varint": 1} # {"2:string": "playlists", "3:varint": 3, "4:varint": 1, "6:varint": 1, "110:embedded": {"1:embedded": {"8:string": ""}}}
"EglwbGF5bGlzdHMYAyABMAE%3D" "EglwbGF5bGlzdHMYAyABMAHyBgQKAkIA"
end end
initial_data = YoutubeAPI.browse(ucid, params: params || "") initial_data = YoutubeAPI.browse(ucid, params: params || "")

View File

@@ -16,23 +16,27 @@ module Invidious::Comments
return parse_youtube(id, response, format, locale, thin_mode, sort_by) return parse_youtube(id, response, format, locale, thin_mode, sort_by)
end end
def fetch_community_post_comments(ucid, post_id) def fetch_community_post_comments(ucid, post_id, sort_by = "top")
case sort_by
when "top"
sort_by_val = 0_i64
when "new", "newest"
sort_by_val = 1_i64
else # top
sort_by_val = 0_i64
end
object = { object = {
"2:string" => "community", "2:string" => "posts",
"25:embedded" => {
"22:string" => post_id,
},
"45:embedded" => {
"2:varint" => 1_i64,
"3:varint" => 1_i64,
},
"53:embedded" => { "53:embedded" => {
"4:embedded" => { "4:embedded" => {
"6:varint" => 0_i64, "6:varint" => sort_by_val,
"27:varint" => 1_i64, "15:varint" => 2_i64,
"25:varint" => 0_i64,
"29:string" => post_id, "29:string" => post_id,
"30:string" => ucid, "30:string" => ucid,
}, },
"7:varint" => 0_i64,
"8:string" => "comments-section", "8:string" => "comments-section",
}, },
} }
@@ -43,7 +47,7 @@ module Invidious::Comments
object2 = { object2 = {
"80226972:embedded" => { "80226972:embedded" => {
"2:string" => ucid, "2:string" => "FEcomment_post_detail_page_web_top_level",
"3:string" => object_parsed, "3:string" => object_parsed,
}, },
} }
@@ -320,6 +324,15 @@ module Invidious::Comments
end end
def produce_continuation(video_id, cursor = "", sort_by = "top") def produce_continuation(video_id, cursor = "", sort_by = "top")
case sort_by
when "top"
sort_by_val = 0_i64
when "new", "newest"
sort_by_val = 1_i64
else # top
sort_by_val = 0_i64
end
object = { object = {
"2:embedded" => { "2:embedded" => {
"2:string" => video_id, "2:string" => video_id,
@@ -340,21 +353,12 @@ module Invidious::Comments
"1:string" => cursor, "1:string" => cursor,
"4:embedded" => { "4:embedded" => {
"4:string" => video_id, "4:string" => video_id,
"6:varint" => 0_i64, "6:varint" => sort_by_val,
}, },
"5:varint" => 20_i64, "5:varint" => 20_i64,
}, },
} }
case sort_by
when "top"
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64
when "new", "newest"
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 1_i64
else # top
object["6:embedded"].as(Hash)["4:embedded"].as(Hash)["6:varint"] = 0_i64
end
continuation = object.try { |i| Protodec::Any.cast_json(i) } continuation = object.try { |i| Protodec::Any.cast_json(i) }
.try { |i| Protodec::Any.from_json(i) } .try { |i| Protodec::Any.from_json(i) }
.try { |i| Base64.urlsafe_encode(i) } .try { |i| Base64.urlsafe_encode(i) }

View File

@@ -82,6 +82,9 @@ class Config
@[YAML::Field(converter: Preferences::URIConverter)] @[YAML::Field(converter: Preferences::URIConverter)]
property public_url : URI = URI.parse("") property public_url : URI = URI.parse("")
# Indicates if this companion instance uses the built-in proxy
property builtin_proxy : Bool = false
end 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)
@@ -271,6 +274,14 @@ class Config
puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters." puts "Config: The value of 'invidious_companion_key' needs to be a size of 16 characters."
exit(1) exit(1)
end end
# Set public_url to built-in proxy path when omitted
config.invidious_companion.each do |companion|
if companion.public_url.to_s.empty?
companion.public_url = URI.parse("/companion")
companion.builtin_proxy = true
end
end
elsif config.signature_server elsif config.signature_server
puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/") puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/")
else else

View File

@@ -34,7 +34,7 @@ module Invidious::Frontend::WatchPage
str << " class=\"pure-form pure-form-stacked\"" str << " class=\"pure-form pure-form-stacked\""
str << " action='#{url}'" str << " action='#{url}'"
str << " method='post'" str << " method='post'"
str << " rel='noopener'" str << " rel='noopener noreferrer'"
str << " target='_blank'>" str << " target='_blank'>"
str << '\n' str << '\n'

View File

@@ -61,28 +61,13 @@ class Kemal::ExceptionHandler
end end
end end
class FilteredCompressHandler < Kemal::Handler class FilteredCompressHandler < HTTP::CompressHandler
exclude ["/videoplayback", "/videoplayback/*", "/vi/*", "/sb/*", "/ggpht/*", "/api/v1/auth/notifications"] exclude ["/videoplayback", "/videoplayback/*", "/vi/*", "/sb/*", "/ggpht/*", "/api/v1/auth/notifications"]
exclude ["/api/v1/auth/notifications", "/data_control"], "POST" exclude ["/api/v1/auth/notifications", "/data_control"], "POST"
def call(env) def call(context)
return call_next env if exclude_match? env return call_next context if exclude_match? context
super
{% if flag?(:without_zlib) %}
call_next env
{% else %}
request_headers = env.request.headers
if request_headers.includes_word?("Accept-Encoding", "gzip")
env.response.headers["Content-Encoding"] = "gzip"
env.response.output = Compress::Gzip::Writer.new(env.response.output, sync_close: true)
elsif request_headers.includes_word?("Accept-Encoding", "deflate")
env.response.headers["Content-Encoding"] = "deflate"
env.response.output = Compress::Deflate::Writer.new(env.response.output, sync_close: true)
end
call_next env
{% end %}
end end
end end

View File

@@ -436,7 +436,7 @@ module Invidious::Routes::API::V1::Channels
if ucid.nil? if ucid.nil?
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}") response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
return error_json(400, "Invalid post ID") if response["error"]? return error_json(400, "Invalid post ID") if response["error"]?
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s ucid = decode_ucid_from_post_protobuf(response.dig("endpoint", "browseEndpoint", "params").as_s)
else else
ucid = ucid.to_s ucid = ucid.to_s
end end
@@ -460,13 +460,15 @@ module Invidious::Routes::API::V1::Channels
format = env.params.query["format"]? format = env.params.query["format"]?
format ||= "json" format ||= "json"
sort_by = env.params.query["sort_by"]?.try &.downcase
sort_by ||= "top"
continuation = env.params.query["continuation"]? continuation = env.params.query["continuation"]?
case continuation case continuation
when nil, "" when nil, ""
ucid = env.params.query["ucid"] ucid = env.params.query["ucid"]
comments = Comments.fetch_community_post_comments(ucid, id) comments = Comments.fetch_community_post_comments(ucid, id, sort_by: sort_by)
else else
comments = YoutubeAPI.browse(continuation: continuation) comments = YoutubeAPI.browse(continuation: continuation)
end end

View File

@@ -190,15 +190,30 @@ module Invidious::Routes::API::V1::Misc
sub_endpoint = endpoint["watchEndpoint"]? || endpoint["browseEndpoint"]? || endpoint sub_endpoint = endpoint["watchEndpoint"]? || endpoint["browseEndpoint"]? || endpoint
params = sub_endpoint.try &.dig?("params") params = sub_endpoint.try &.dig?("params")
if sub_endpoint["browseId"]?.try &.as_s == "FEpost_detail"
decoded_protobuf = params.try &.as_s.try { |i| URI.decode_www_form(i) }
.try { |i| Base64.decode(i) }
.try { |i| IO::Memory.new(i) }
.try { |i| Protodec::Any.parse(i) }
ucid = decoded_protobuf.try(&.["56:0:embedded"]["2:0:string"].as_s)
post_id = decoded_protobuf.try(&.["56:0:embedded"]["3:1:string"].as_s)
else
ucid = sub_endpoint["browseId"]? if sub_endpoint["browseId"]? && sub_endpoint["browseId"]?.try &.as_s.starts_with? "UC"
post_id = nil
end
rescue ex rescue ex
return error_json(500, ex) return error_json(500, ex)
end end
JSON.build do |json| JSON.build do |json|
json.object do json.object do
json.field "ucid", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]? json.field "browseId", sub_endpoint["browseId"].as_s if sub_endpoint["browseId"]?
json.field "ucid", ucid if ucid != nil
json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]? json.field "videoId", sub_endpoint["videoId"].as_s if sub_endpoint["videoId"]?
json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]? json.field "playlistId", sub_endpoint["playlistId"].as_s if sub_endpoint["playlistId"]?
json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]? json.field "startTimeSeconds", sub_endpoint["startTimeSeconds"].as_i if sub_endpoint["startTimeSeconds"]?
json.field "postId", post_id if post_id != nil
json.field "params", params.try &.as_s json.field "params", params.try &.as_s
json.field "pageType", page_type json.field "pageType", page_type
end end

View File

@@ -63,6 +63,7 @@ module Invidious::Routes::BeforeAll
"/videoplayback", "/videoplayback",
"/latest_version", "/latest_version",
"/download", "/download",
"/companion/",
}.any? { |r| env.request.resource.starts_with? r } }.any? { |r| env.request.resource.starts_with? r }
if env.request.cookies.has_key? "SID" if env.request.cookies.has_key? "SID"

View File

@@ -284,7 +284,7 @@ module Invidious::Routes::Channels
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}") response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
return error_template(400, "Invalid post ID") if response["error"]? return error_template(400, "Invalid post ID") if response["error"]?
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s ucid = decode_ucid_from_post_protobuf(response.dig("endpoint", "browseEndpoint", "params").as_s)
post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode) post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
end end

View File

@@ -0,0 +1,43 @@
module Invidious::Routes::Companion
# /companion
def self.get_companion(env)
url = env.request.path
if env.request.query
url += "?#{env.request.query}"
end
begin
COMPANION_POOL.client do |wrapper|
wrapper.client.get(url, env.request.headers) do |resp|
return self.proxy_companion(env, resp)
end
end
rescue ex
end
end
def self.options_companion(env)
url = env.request.path
if env.request.query
url += "?#{env.request.query}"
end
begin
COMPANION_POOL.client do |wrapper|
wrapper.client.options(url, env.request.headers) do |resp|
return self.proxy_companion(env, resp)
end
end
rescue ex
end
end
private def self.proxy_companion(env, response)
env.response.status_code = response.status_code
response.headers.each do |key, value|
env.response.headers[key] = value
end
return IO.copy response.body_io, env.response
end
end

View File

@@ -20,7 +20,7 @@ module Invidious::Routes::Embed
return error_template(500, ex) return error_template(500, ex)
end end
url = "/embed/#{first_playlist_video}?#{env.params.query}" url = "/embed/#{first_playlist_video.id}?#{env.params.query}"
if env.params.query.size > 0 if env.params.query.size > 0
url += "?#{env.params.query}" url += "?#{env.params.query}"
@@ -209,10 +209,17 @@ module Invidious::Routes::Embed
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] = invidious_companion_urls = CONFIG.invidious_companion.reject(&.builtin_proxy).map do |companion|
env.response.headers["Content-Security-Policy"] uri =
.gsub("media-src", "media-src #{invidious_companion.public_url}") "#{companion.public_url.scheme}://#{companion.public_url.host}#{companion.public_url.port ? ":#{companion.public_url.port}" : ""}"
.gsub("connect-src", "connect-src #{invidious_companion.public_url}") end.join(" ")
if !invidious_companion_urls.empty?
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion_urls}")
.gsub("connect-src", "connect-src #{invidious_companion_urls}")
end
end end
rendered "embed" rendered "embed"

View File

@@ -194,10 +194,17 @@ module Invidious::Routes::Watch
if CONFIG.invidious_companion.present? if CONFIG.invidious_companion.present?
invidious_companion = CONFIG.invidious_companion.sample invidious_companion = CONFIG.invidious_companion.sample
env.response.headers["Content-Security-Policy"] = invidious_companion_urls = CONFIG.invidious_companion.reject(&.builtin_proxy).map do |companion|
env.response.headers["Content-Security-Policy"] uri =
.gsub("media-src", "media-src #{invidious_companion.public_url}") "#{companion.public_url.scheme}://#{companion.public_url.host}#{companion.public_url.port ? ":#{companion.public_url.port}" : ""}"
.gsub("connect-src", "connect-src #{invidious_companion.public_url}") end.join(" ")
if !invidious_companion_urls.empty?
env.response.headers["Content-Security-Policy"] =
env.response.headers["Content-Security-Policy"]
.gsub("media-src", "media-src #{invidious_companion_urls}")
.gsub("connect-src", "connect-src #{invidious_companion_urls}")
end
end end
templated "watch" templated "watch"

View File

@@ -46,6 +46,7 @@ module Invidious::Routing
self.register_api_v1_routes self.register_api_v1_routes
self.register_api_manifest_routes self.register_api_manifest_routes
self.register_video_playback_routes self.register_video_playback_routes
self.register_companion_routes
end end
# ------------------- # -------------------
@@ -188,7 +189,7 @@ module Invidious::Routing
end end
# ------------------- # -------------------
# Media proxy routes # Proxy routes
# ------------------- # -------------------
def register_api_manifest_routes def register_api_manifest_routes
@@ -223,6 +224,13 @@ module Invidious::Routing
get "/vi/:id/:name", Routes::Images, :thumbnails get "/vi/:id/:name", Routes::Images, :thumbnails
end end
def register_companion_routes
if CONFIG.invidious_companion.present?
get "/companion/*", Routes::Companion, :get_companion
options "/companion/*", Routes::Companion, :options_companion
end
end
# ------------------- # -------------------
# API routes # API routes
# ------------------- # -------------------

View File

@@ -102,6 +102,9 @@ def extract_video_info(video_id : String)
# Don't fetch the next endpoint if the video is unavailable. # Don't fetch the next endpoint if the video is unavailable.
if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
next_response = YoutubeAPI.next({"videoId": video_id, "params": ""}) next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
# Remove the microformat returned by the /next endpoint on some videos
# to prevent player_response microformat from being overwritten.
next_response.delete("microformat")
player_response = player_response.merge(next_response) player_response = player_response.merge(next_response)
end end
@@ -111,16 +114,17 @@ def extract_video_info(video_id : String)
if !CONFIG.invidious_companion.present? if !CONFIG.invidious_companion.present?
if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil? if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.") LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
players_fallback = {YoutubeAPI::ClientType::TvHtml5, YoutubeAPI::ClientType::WebMobile} players_fallback = {YoutubeAPI::ClientType::TvSimply, YoutubeAPI::ClientType::WebMobile}
players_fallback.each do |player_fallback| players_fallback.each do |player_fallback|
client_config.client_type = player_fallback client_config.client_type = player_fallback
next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config)) next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config))
if player_fallback_response.dig?("streamingData", "adaptiveFormats", 0, "url") adaptive_formats = player_fallback_response.dig?("streamingData", "adaptiveFormats")
if adaptive_formats && (adaptive_formats.dig?(0, "url") || adaptive_formats.dig?(0, "signatureCipher"))
streaming_data = player_response["streamingData"].as_h streaming_data = player_response["streamingData"].as_h
streaming_data["adaptiveFormats"] = player_fallback_response["streamingData"]["adaptiveFormats"] streaming_data["adaptiveFormats"] = adaptive_formats
player_response["streamingData"] = JSON::Any.new(streaming_data) player_response["streamingData"] = JSON::Any.new(streaming_data)
break break
end end
@@ -146,7 +150,11 @@ 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 = format.as_h
if format["url"]?.nil?
format["url"] = format["signatureCipher"]
end
format["url"] = JSON::Any.new(convert_url(format))
end end
end end

View File

@@ -65,12 +65,18 @@
<% end %> <% end %>
<% end %> <% end %>
<% preferred_captions.each do |caption| %> <% preferred_captions.each do |caption|
<track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>"> api_captions_url = "/api/v1/captions/"
api_captions_url = invidious_companion.public_url.to_s + api_captions_url if (invidious_companion)
%>
<track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>">
<% end %> <% end %>
<% captions.each do |caption| %> <% captions.each do |caption|
<track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>"> api_captions_url = "/api/v1/captions/"
api_captions_url = invidious_companion.public_url.to_s + api_captions_url if (invidious_companion)
%>
<track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>">
<% end %> <% end %>
<% end %> <% end %>
</video> </video>

View File

@@ -14,7 +14,7 @@
<div class="pure-control-group"> <div class="pure-control-group">
<label for="import_youtube"> <label for="import_youtube">
<a rel="noopener" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md"> <a rel="noopener noreferrer" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md">
<%= translate(locale, "Import YouTube subscriptions") %> <%= translate(locale, "Import YouTube subscriptions") %>
</a> </a>
</label> </label>

View File

@@ -46,8 +46,27 @@ struct YoutubeConnectionPool
end end
end end
# Packages a `HTTP::Client` to an Invidious companion instance alongside the configuration for that instance.
#
# This is used as the resource for the `CompanionPool` as to allow the ability to
# proxy the requests to Invidious companion from Invidious directly.
# Instead of setting up routes in a reverse proxy.
struct CompanionWrapper
property client : HTTP::Client
property companion : Config::CompanionConfig
def initialize(companion : Config::CompanionConfig)
@companion = companion
@client = make_client(companion.private_url, use_http_proxy: false)
end
def close
@client.close
end
end
struct CompanionConnectionPool struct CompanionConnectionPool
property pool : DB::Pool(HTTP::Client) property pool : DB::Pool(CompanionWrapper)
def initialize(capacity = 5, timeout = 5.0) def initialize(capacity = 5, timeout = 5.0)
options = DB::Pool::Options.new( options = DB::Pool::Options.new(
@@ -57,26 +76,28 @@ struct CompanionConnectionPool
checkout_timeout: timeout checkout_timeout: timeout
) )
@pool = DB::Pool(HTTP::Client).new(options) do @pool = DB::Pool(CompanionWrapper).new(options) do
companion = CONFIG.invidious_companion.sample companion = CONFIG.invidious_companion.sample
next make_client(companion.private_url, use_http_proxy: false) make_client(companion.private_url, use_http_proxy: false)
CompanionWrapper.new(companion: companion)
end end
end end
def client(&) def client(&)
conn = pool.checkout wrapper = pool.checkout
begin begin
response = yield conn response = yield wrapper
rescue ex rescue ex
conn.close wrapper.close
companion = CONFIG.invidious_companion.sample companion = CONFIG.invidious_companion.sample
conn = make_client(companion.private_url, use_http_proxy: false) make_client(companion.private_url, use_http_proxy: false)
wrapper = CompanionWrapper.new(companion: companion)
response = yield conn response = yield wrapper
ensure ensure
pool.release(conn) pool.release(wrapper)
end end
response response

View File

@@ -6,10 +6,10 @@ module YoutubeAPI
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
private ANDROID_APP_VERSION = "19.32.34" private ANDROID_APP_VERSION = "19.35.36"
private ANDROID_VERSION = "12" private ANDROID_VERSION = "13"
private ANDROID_USER_AGENT = "com.google.android.youtube/#{ANDROID_APP_VERSION} (Linux; U; Android #{ANDROID_VERSION}; US) gzip" private ANDROID_USER_AGENT = "com.google.android.youtube/#{ANDROID_APP_VERSION} (Linux; U; Android #{ANDROID_VERSION}; en_US; SM-S908E Build/TP1A.220624.014) gzip"
private ANDROID_SDK_VERSION = 31_i64 private ANDROID_SDK_VERSION = 33_i64
private ANDROID_TS_APP_VERSION = "1.9" private ANDROID_TS_APP_VERSION = "1.9"
private ANDROID_TS_USER_AGENT = "com.google.android.youtube/1.9 (Linux; U; Android 12; US) gzip" private ANDROID_TS_USER_AGENT = "com.google.android.youtube/1.9 (Linux; U; Android 12; US) gzip"
@@ -17,9 +17,9 @@ module YoutubeAPI
# For Apple device names, see https://gist.github.com/adamawolf/3048717 # For Apple device names, see https://gist.github.com/adamawolf/3048717
# For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases, # For iOS versions, see https://en.wikipedia.org/wiki/IOS_version_history#Releases,
# then go to the dedicated article of the major version you want. # then go to the dedicated article of the major version you want.
private IOS_APP_VERSION = "19.32.8" private IOS_APP_VERSION = "20.11.6"
private IOS_USER_AGENT = "com.google.ios.youtube/#{IOS_APP_VERSION} (iPhone14,5; U; CPU iOS 17_6 like Mac OS X;)" private IOS_USER_AGENT = "com.google.ios.youtube/#{IOS_APP_VERSION} (iPhone14,5; U; CPU iOS 18_5 like Mac OS X;)"
private IOS_VERSION = "17.6.1.21G93" # Major.Minor.Patch.Build private IOS_VERSION = "18.5.0.22F76" # Major.Minor.Patch.Build
private WINDOWS_VERSION = "10.0" private WINDOWS_VERSION = "10.0"
@@ -50,7 +50,7 @@ module YoutubeAPI
ClientType::Web => { ClientType::Web => {
name: "WEB", name: "WEB",
name_proto: "1", name_proto: "1",
version: "2.20240814.00.00", version: "2.20250222.10.00",
screen: "WATCH_FULL_SCREEN", screen: "WATCH_FULL_SCREEN",
os_name: "Windows", os_name: "Windows",
os_version: WINDOWS_VERSION, os_version: WINDOWS_VERSION,
@@ -59,7 +59,7 @@ module YoutubeAPI
ClientType::WebEmbeddedPlayer => { ClientType::WebEmbeddedPlayer => {
name: "WEB_EMBEDDED_PLAYER", name: "WEB_EMBEDDED_PLAYER",
name_proto: "56", name_proto: "56",
version: "1.20240812.01.00", version: "1.20250219.01.00",
screen: "EMBED", screen: "EMBED",
os_name: "Windows", os_name: "Windows",
os_version: WINDOWS_VERSION, os_version: WINDOWS_VERSION,
@@ -68,7 +68,7 @@ module YoutubeAPI
ClientType::WebMobile => { ClientType::WebMobile => {
name: "MWEB", name: "MWEB",
name_proto: "2", name_proto: "2",
version: "2.20240813.02.00", version: "2.20250224.01.00",
os_name: "Android", os_name: "Android",
os_version: ANDROID_VERSION, os_version: ANDROID_VERSION,
platform: "MOBILE", platform: "MOBILE",
@@ -76,7 +76,7 @@ module YoutubeAPI
ClientType::WebScreenEmbed => { ClientType::WebScreenEmbed => {
name: "WEB", name: "WEB",
name_proto: "1", name_proto: "1",
version: "2.20240814.00.00", version: "2.20250222.10.00",
screen: "EMBED", screen: "EMBED",
os_name: "Windows", os_name: "Windows",
os_version: WINDOWS_VERSION, os_version: WINDOWS_VERSION,
@@ -85,7 +85,7 @@ module YoutubeAPI
ClientType::WebCreator => { ClientType::WebCreator => {
name: "WEB_CREATOR", name: "WEB_CREATOR",
name_proto: "62", name_proto: "62",
version: "1.20240918.03.00", version: "1.20241203.01.00",
os_name: "Windows", os_name: "Windows",
os_version: WINDOWS_VERSION, os_version: WINDOWS_VERSION,
platform: "DESKTOP", platform: "DESKTOP",
@@ -171,7 +171,7 @@ module YoutubeAPI
ClientType::TvHtml5 => { ClientType::TvHtml5 => {
name: "TVHTML5", name: "TVHTML5",
name_proto: "7", name_proto: "7",
version: "7.20240813.07.00", version: "7.20250219.14.00",
}, },
ClientType::TvHtml5ScreenEmbed => { ClientType::TvHtml5ScreenEmbed => {
name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER", name: "TVHTML5_SIMPLY_EMBEDDED_PLAYER",
@@ -701,22 +701,20 @@ module YoutubeAPI
# Send the POST request # Send the POST request
begin begin
response = COMPANION_POOL.client &.post(endpoint, headers: headers, body: data.to_json) response_body = Hash(String, JSON::Any).new
body = response.body
if (response.status_code != 200) COMPANION_POOL.client do |wrapper|
raise Exception.new( companion_base_url = wrapper.companion.private_url.path
"Error while communicating with Invidious companion: \
status code: #{response.status_code} and body: #{body.dump}" wrapper.client.post("#{companion_base_url}#{endpoint}", headers: headers, body: data.to_json) do |response|
) response_body = JSON.parse(response.body_io).as_h
end
end end
return response_body
rescue ex rescue ex
raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found")) raise InfoException.new("Error while communicating with Invidious companion: " + (ex.message || "no extra info found"))
end end
# Convert result to Hash
initial_data = JSON.parse(body).as_h
return initial_data
end end
#################################################################### ####################################################################