├── .dockerignore
├── .github
├── dependabot.yml
└── workflows
│ ├── deno-check.yaml
│ └── docker-build-push.yaml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── compile.env
├── config
└── config.example.toml
├── deno.json
├── deno.lock
├── docker-compose.yaml
├── grafana_dashboard.json
└── src
├── lib
├── helpers
│ ├── config.ts
│ ├── encodeRFC5987ValueChars.ts
│ ├── encryptQuery.ts
│ ├── getFetchClient.ts
│ ├── metrics.ts
│ ├── verifyRequest.ts
│ ├── youtubePlayerHandling.ts
│ ├── youtubePlayerReq.ts
│ └── youtubeTranscriptsHandling.ts
├── jobs
│ ├── potoken.ts
│ └── worker.ts
└── types
│ └── HonoVariables.ts
├── main.ts
├── routes
├── health.ts
├── index.ts
├── invidious_routes
│ ├── captions.ts
│ ├── dashManifest.ts
│ ├── download.ts
│ └── latestVersion.ts
├── metrics.ts
├── videoPlaybackProxy.ts
└── youtube_api_routes
│ └── player.ts
└── tests
├── dashManifest.ts
├── deps.ts
├── latestVersion.ts
├── main_test.ts
└── youtubePlayer.ts
/.dockerignore:
--------------------------------------------------------------------------------
1 | invidious_companion
2 | test_things/
3 | config/local.toml
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "docker"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | - package-ecosystem: github-actions
8 | directory: /
9 | schedule:
10 | interval: "weekly"
11 |
--------------------------------------------------------------------------------
/.github/workflows/deno-check.yaml:
--------------------------------------------------------------------------------
1 | name: Testing
2 |
3 | on:
4 | pull_request:
5 | branches: [master]
6 |
7 | jobs:
8 | testing:
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - name: Setup repo
13 | uses: actions/checkout@v4
14 |
15 | - name: Setup Deno
16 | uses: denoland/setup-deno@v2
17 | with:
18 | deno-version: v2.x
19 |
20 | - name: Verify formatting
21 | run: deno fmt --check src/**
22 |
23 | - name: Verify typing
24 | run: deno check src/**
25 |
26 | - name: Run linter
27 | run: deno lint
28 |
29 | - name: Install and run tor
30 | uses: tor-actions/setup-tor@main
31 | with:
32 | daemon: true
33 | port: 9150
34 |
35 | - name: Run tests x4 times (IP may be banned)
36 | uses: nick-fields/retry@v3
37 | with:
38 | timeout_minutes: 5
39 | max_attempts: 4
40 | command: |
41 | pkill -HUP tor
42 | curl -s --socks5 127.0.0.1:9150 https://check.torproject.org/api/ip; echo
43 | rm -rf /var/tmp/youtubei.js
44 | PROXY=socks5://127.0.0.1:9150 deno task test
45 |
--------------------------------------------------------------------------------
/.github/workflows/docker-build-push.yaml:
--------------------------------------------------------------------------------
1 | name: Build and Push Docker Image
2 |
3 | # Define when this workflow will run
4 | on:
5 | push:
6 | branches:
7 | - master # Trigger on pushes to master branch
8 | tags:
9 | - '[0-9]+.[0-9]+.[0-9]+' # Trigger on semantic version tags
10 | paths-ignore:
11 | - '.gitignore'
12 | - 'LICENSE'
13 | - 'README.md'
14 | - 'docker-compose.yml'
15 | workflow_dispatch: # Allow manual triggering of the workflow
16 |
17 | # Define environment variables used throughout the workflow
18 | env:
19 | REGISTRY: quay.io
20 | IMAGE_NAME: invidious/invidious-companion
21 |
22 | jobs:
23 | build-and-push:
24 | runs-on: ubuntu-latest
25 |
26 | steps:
27 | # Step 1: Check out the repository code
28 | - name: Checkout code
29 | uses: actions/checkout@v4
30 |
31 | # Step 2: Set up QEMU for multi-architecture builds
32 | - name: Set up QEMU
33 | uses: docker/setup-qemu-action@v3
34 |
35 | # Step 3: Set up Docker Buildx for enhanced build capabilities
36 | - name: Set up Docker Buildx
37 | uses: docker/setup-buildx-action@v3
38 |
39 | # Step 4: Authenticate with Quay.io registry
40 | - name: Log in to Quay.io
41 | uses: docker/login-action@v3
42 | with:
43 | registry: ${{ env.REGISTRY }}
44 | username: ${{ secrets.QUAY_USERNAME }}
45 | password: ${{ secrets.QUAY_PASSWORD }}
46 |
47 | # Step 5: Extract metadata for Docker image tagging and labeling
48 | - name: Extract metadata for Docker
49 | id: meta
50 | uses: docker/metadata-action@v5
51 | with:
52 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
53 | # Define tagging strategy
54 | tags: |
55 | type=semver,pattern={{version}}
56 | type=semver,pattern={{major}}.{{minor}}
57 | type=semver,pattern={{major}}
58 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
59 | type=sha,prefix={{branch}}-
60 | # Define labels
61 | labels: |
62 | quay.expires-after=12w
63 |
64 | # Step 6: Build and push the Docker image
65 | - name: Build and push Docker image
66 | uses: docker/build-push-action@v6
67 | with:
68 | context: .
69 | push: true
70 | platforms: linux/amd64,linux/arm64 # Build for multiple architectures
71 | tags: ${{ steps.meta.outputs.tags }}
72 | labels: ${{ steps.meta.outputs.labels }}
73 | cache-from: type=gha
74 | cache-to: type=gha,mode=max
75 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.orig
2 | *.pyc
3 | *.swp
4 | .env
5 |
6 | /.cargo_home/
7 | /.idea/
8 | /.vs/
9 | /.vscode/
10 | gclient_config.py_entries
11 | /target/
12 | /std/hash/_wasm/target
13 | /tests/wpt/runner/manifest.json
14 | /third_party/
15 | /tests/napi/node_modules
16 | /tests/napi/build
17 | /tests/napi/third_party_tests/node_modules
18 |
19 | # MacOS generated files
20 | .DS_Store
21 | .DS_Store?
22 |
23 | # Flamegraphs
24 | /flamebench*.svg
25 | /flamegraph*.svg
26 |
27 | # WPT generated cert files
28 | /tests/wpt/runner/certs/index.txt*
29 | /tests/wpt/runner/certs/serial*
30 |
31 | /ext/websocket/autobahn/reports
32 |
33 | # JUnit files produced by deno test --junit
34 | junit.xml
35 |
36 | # Jupyter files
37 | .ipynb_checkpoints/
38 | Untitled*.ipynb
39 |
40 | invidious_companion
41 | test_things/
42 | config/config.toml
43 | .cache/
44 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | # check=error=true
3 |
4 | # Default values for versions
5 | ARG THC_VERSION='0.36.0' \
6 | TINI_VERSION='0.19.0'
7 |
8 | # Default values for variables that change less often
9 | ARG DENO_DIR='/deno-dir' \
10 | GH_BASE_URL='https://github.com' \
11 | HOST='0.0.0.0' \
12 | PORT='8282'
13 |
14 |
15 | # we can use these aliases and let dependabot remain simple
16 | # inspired by:
17 | # https://github.com/dependabot/dependabot-core/issues/2057#issuecomment-1351660410
18 | FROM alpine:3.22 AS dependabot-alpine
19 | FROM debian:12-slim AS dependabot-debian
20 |
21 | # Retrieve the deno binary from the repository
22 | FROM denoland/deno:bin-2.3.5 AS deno-bin
23 |
24 |
25 | # Stage for creating the non-privileged user
26 | FROM dependabot-alpine AS user-stage
27 |
28 | RUN adduser -u 10001 -S appuser
29 |
30 | # Stage for downloading files using curl from Debian
31 | FROM dependabot-debian AS debian-curl
32 | RUN DEBIAN_FRONTEND='noninteractive' && export DEBIAN_FRONTEND && \
33 | apt-get update && apt-get install -y curl
34 |
35 | # Download tiny-health-checker from GitHub
36 | FROM debian-curl AS thc-download
37 | ARG GH_BASE_URL THC_VERSION
38 | RUN arch="$(uname -m)" && \
39 | gh_url() { printf -- "${GH_BASE_URL}/%s/releases/download/%s/%s\n" "$@" ; } && \
40 | URL="$(gh_url dmikusa/tiny-health-checker v${THC_VERSION} thc-${arch}-unknown-linux-musl)" && \
41 | curl -fsSL --output /thc "${URL}" && chmod -v 00555 /thc
42 |
43 | # Cache the thc binary as a layer
44 | FROM scratch AS thc-bin
45 | ARG THC_VERSION
46 | ENV THC_VERSION="${THC_VERSION}"
47 | COPY --from=thc-download /thc /thc
48 |
49 | # Download tini from GitHub
50 | FROM debian-curl AS tini-download
51 | ARG GH_BASE_URL TINI_VERSION
52 | RUN arch="$(dpkg --print-architecture)" && \
53 | gh_url() { printf -- "${GH_BASE_URL}/%s/releases/download/%s/%s\n" "$@" ; } && \
54 | URL="$(gh_url krallin/tini v${TINI_VERSION} tini-${arch})" && \
55 | curl -fsSL --output /tini "${URL}" && chmod -v 00555 /tini
56 |
57 | # Cache the tini binary as a layer
58 | FROM scratch AS tini-bin
59 | ARG TINI_VERSION
60 | ENV TINI_VERSION="${TINI_VERSION}"
61 | COPY --from=tini-download /tini /tini
62 |
63 | # Stage for using git from Debian
64 | FROM dependabot-debian AS debian-git
65 | RUN DEBIAN_FRONTEND='noninteractive' && export DEBIAN_FRONTEND && \
66 | apt-get update && apt-get install -y git
67 |
68 | # Stage for using deno on Debian
69 | FROM debian-git AS debian-deno
70 |
71 | # cache dir for youtube.js library
72 | RUN mkdir -v -p /var/tmp/youtubei.js
73 |
74 | ARG DENO_DIR
75 | RUN useradd --uid 1993 --user-group deno \
76 | && mkdir -v "${DENO_DIR}" \
77 | && chown deno:deno "${DENO_DIR}"
78 |
79 | ENV DENO_DIR="${DENO_DIR}" \
80 | DENO_INSTALL_ROOT='/usr/local'
81 |
82 | COPY --from=deno-bin /deno /usr/bin/deno
83 |
84 | # Create a builder using deno on Debian
85 | FROM debian-deno AS builder
86 |
87 | WORKDIR /app
88 |
89 | COPY deno.lock ./
90 | COPY deno.json ./
91 |
92 | COPY ./src/ ./src/
93 |
94 | # To let the `deno task compile` know the current commit on which
95 | # Invidious companion is being built, similar to how Invidious does it.
96 | # Dependencies are cached in ${DENO_DIR} for our deno builder
97 | RUN --mount=type=bind,rw,source=.git,target=/app/.git \
98 | --mount=type=cache,target="${DENO_DIR}" \
99 | deno task compile
100 |
101 | FROM gcr.io/distroless/cc AS app
102 |
103 | # Copy group file for the non-privileged user from the user-stage
104 | COPY --from=user-stage /etc/group /etc/group
105 |
106 | # Copy passwd file for the non-privileged user from the user-stage
107 | COPY --from=user-stage /etc/passwd /etc/passwd
108 |
109 | COPY --from=thc-bin /thc /thc
110 | COPY --from=tini-bin /tini /tini
111 |
112 | # Copy cache directory and set correct permissions
113 | COPY --from=builder --chown=appuser:nogroup /var/tmp/youtubei.js /var/tmp/youtubei.js
114 |
115 | # Set the working directory
116 | WORKDIR /app
117 |
118 | COPY --from=builder /app/invidious_companion ./
119 |
120 | ARG HOST PORT THC_VERSION TINI_VERSION
121 | EXPOSE "${PORT}/tcp"
122 | ENV HOST="${HOST}" \
123 | PORT="${PORT}" \
124 | THC_PORT="${PORT}" \
125 | THC_PATH='/healthz' \
126 | THC_VERSION="${THC_VERSION}" \
127 | TINI_VERSION="${TINI_VERSION}"
128 |
129 | COPY ./config/ ./config/
130 |
131 | # Switch to non-privileged user
132 | USER appuser
133 |
134 | ENTRYPOINT ["/tini", "--", "/app/invidious_companion"]
135 |
136 | HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=5 CMD ["/thc"]
137 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Invidious companion
2 |
3 | Companion for Invidious which handle all the video stream retrieval from YouTube servers.
4 |
5 | ## Requirements
6 |
7 | - [deno](https://docs.deno.com/runtime/)
8 |
9 | ## Documentation
10 | - Installation guide: https://docs.invidious.io/companion-installation/
11 | - Extra documentation for Invidious companion: https://github.com/iv-org/invidious-companion/wiki
12 |
13 | ## Run Locally (development)
14 |
15 | ```
16 | SERVER_SECRET_KEY=CHANGEME deno task dev
17 | ```
18 |
19 | ## Available tasks using deno
20 |
21 | - `deno task dev`: Launch Invidious companion in debug mode
22 | - `deno task compile`: Compile the project to a single file.
23 | - `deno task test`: Test all the tests for Invidious companion
24 | - `deno task format`: Format all the .ts files in the project.
25 |
--------------------------------------------------------------------------------
/compile.env:
--------------------------------------------------------------------------------
1 | DENO_COMPILED=true
2 |
--------------------------------------------------------------------------------
/config/config.example.toml:
--------------------------------------------------------------------------------
1 | #####
2 | # The configuration options listed below are able to be enabled as needed.
3 | # The values in this example are the defaults. Some values can alternatively
4 | # be set using an environment variable.
5 | #
6 | # In order to enable an option, make sure you uncomment both the option
7 | # and the block header for the section it belongs to. Any other commented
8 | # options will continue to use default values.
9 | # See https://toml.io/en/ for details on the configuration format.
10 | #####
11 |
12 | # [server]
13 | # port = 8282 # env variable: PORT
14 | # host = "127.0.0.1" # env variable: HOST
15 | # # secret key needs to be exactly 16 characters long
16 | # secret_key = "CHANGE_ME" # env variable: SERVER_SECRET_KEY
17 | # verify_requests = false # env variable: SERVER_VERIFY_REQUESTS
18 | # encrypt_query_params = false # env variable: SERVER_ENCRYPT_QUERY_PARAMS
19 | # enable_metrics = false # env variable: SERVER_ENABLE_METRICS
20 |
21 | # [cache]
22 | # enabled = true # env variable: CACHE_ENABLED
23 | # # will get cached in /var/tmp/youtubei.js if you specify /var/tmp
24 | # # you need to change the --allow-write from deno run too
25 | # directory = "/var/tmp" # env variable: CACHE_DIRECTORY
26 |
27 | # [networking]
28 | # #proxy = "" # env variable: PROXY
29 | # # Enable YouTube new video format UMP
30 |
31 | # [networking.videoplayback]
32 | # ump = false # env variable: NETWORKING_VIDEOPLAYBACK_UMP
33 | # # size of chunks to request from google servers for rate limiting reductions
34 | # video_fetch_chunk_size_mb = 5 # env variable: NETWORKING_VIDEOPLAYBACK_VIDEO_FETCH_CHUNK_SIZE_MB
35 |
36 | ###
37 | # Network call timeouts when talking to YouTube.
38 | # Needed in order to ensure Deno closes hanging connections
39 | ###
40 | # [networking.fetch]
41 | # timeout_ms = 30000 # env variable: NETWORKING_FETCH_TIMEOUT_MS
42 |
43 | ###
44 | # Network call retries when talking to YouTube, using
45 | # https://docs.deno.com/examples/exponential_backoff/
46 | ###
47 | # [networking.fetch.retry]
48 | # # enable retries on calls to YouTube
49 | # enabled = false # env variable: NETWORKING_FETCH_RETRY_ENABLED
50 | # # max number of times to retry
51 | # times = 1 # env variable: NETWORKING_FETCH_RETRY_TIMES
52 | # # minimum wait after first call (ms)
53 | # initial_debounce = 0 # env variable: NETWORKING_FETCH_RETRY_INITIAL_DEBOUNCE
54 | # # how much to back off after each retry (multiplier of initial_debounce)
55 | # debounce_multiplier = 0 # env variable: NETWORKING_FETCH_RETRY_DEBOUNCE_MULTIPLIER
56 |
57 | # [jobs]
58 |
59 | # [jobs.youtube_session]
60 | # # whether to generate PO tokens
61 | # po_token_enabled = true # env variable: JOBS_YOUTUBE_SESSION_PO_TOKEN_ENABLED
62 | # # frequency of PO token refresh in cron format
63 | # frequency = "*/5 * * * *" # env variable: JOBS_YOUTUBE_SESSION_FREQUENCY
64 |
65 | # [youtube_session]
66 | # oauth_enabled = false # env variable: YOUTUBE_SESSION_OAUTH_ENABLED
67 | # cookies = "" # env variable: YOUTUBE_SESSION_COOKIES
68 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "dev": "deno run --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-sys=hostname --allow-read --allow-write=/var/tmp/youtubei.js --watch src/main.ts",
4 | "compile": "deno compile --include ./src/lib/helpers/youtubePlayerReq.ts --include ./src/lib/helpers/getFetchClient.ts --output invidious_companion --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-read --allow-sys=hostname --allow-write=/var/tmp/youtubei.js src/main.ts --_version_date=\"$(git log -1 --format=%ci | awk '{print $1}' | sed s/-/./g)\" --_version_commit=\"$(git rev-list HEAD --max-count=1 --abbrev-commit)\"",
5 | "test": "deno test --allow-import=github.com:443,jsr.io:443,cdn.jsdelivr.net:443,esm.sh:443,deno.land:443 --allow-net --allow-env --allow-sys=hostname --allow-read --allow-write=/var/tmp/youtubei.js",
6 | "format": "deno fmt src/**"
7 | },
8 | "imports": {
9 | "@std/cli": "jsr:@std/cli@^1.0.17",
10 | "hono": "jsr:@hono/hono@4.7.4",
11 | "@std/toml": "jsr:@std/toml@1.0.2",
12 | "prom-client": "https://esm.sh/prom-client@15.1.3",
13 | "youtubei.js": "https://cdn.jsdelivr.net/gh/LuanRT/YouTube.js@v13.4.0-deno/deno.ts",
14 | "youtubei.js/Utils": "https://cdn.jsdelivr.net/gh/LuanRT/YouTube.js@v13.4.0-deno/deno/src/utils/Utils.ts",
15 | "youtubei.js/NavigationEndpoint": "https://cdn.jsdelivr.net/gh/LuanRT/YouTube.js@v13.4.0-deno/deno/src/parser/classes/NavigationEndpoint.ts",
16 | "youtubei.js/PlayerCaptionsTracklist": "https://cdn.jsdelivr.net/gh/LuanRT/YouTube.js@v13.4.0-deno/deno/src/parser/classes/PlayerCaptionsTracklist.ts",
17 | "jsdom": "npm:jsdom@26.0.0",
18 | "bgutils": "https://esm.sh/bgutils-js@3.2.0",
19 | "estree": "https://esm.sh/@types/estree@1.0.6",
20 | "youtubePlayerReq": "./src/lib/helpers/youtubePlayerReq.ts",
21 | "getFetchClient": "./src/lib/helpers/getFetchClient.ts",
22 | "googlevideo": "jsr:@luanrt/googlevideo@2.0.0",
23 | "jsr:@luanrt/jintr": "jsr:@luanrt/jintr@3.3.1",
24 | "crypto/": "https://deno.land/x/crypto@v0.11.0/",
25 | "@std/encoding/base64": "jsr:@std/encoding@1.0.7/base64",
26 | "@std/async": "jsr:@std/async@1.0.11",
27 | "@std/fs": "jsr:@std/fs@1.0.14",
28 | "@std/path": "jsr:@std/path@1.0.8",
29 | "brotli": "https://deno.land/x/brotli@0.1.7/mod.ts",
30 | "zod": "https://deno.land/x/zod@v3.24.2/mod.ts"
31 | },
32 | "unstable": [
33 | "cron",
34 | "kv",
35 | "http",
36 | "temporal"
37 | ],
38 | "fmt": {
39 | "indentWidth": 4
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | invidious_companion:
4 | build:
5 | context: .
6 | dockerfile: Dockerfile
7 | # image: quay.io/invidious/invidious-companion:latest
8 | ports:
9 | - 127.0.0.1:8282:8282
10 | restart: unless-stopped
11 | cap_drop:
12 | - ALL
13 | read_only: true
14 | user: 10001:10001
15 | # cache for youtube library
16 | volumes:
17 | - /var/tmp/youtubei.js:/var/tmp/youtubei.js:rw
18 | security_opt:
19 | - no-new-privileges:true
20 |
--------------------------------------------------------------------------------
/grafana_dashboard.json:
--------------------------------------------------------------------------------
1 | {
2 | "annotations": {
3 | "list": [
4 | {
5 | "builtIn": 1,
6 | "datasource": {
7 | "type": "grafana",
8 | "uid": "-- Grafana --"
9 | },
10 | "enable": true,
11 | "hide": true,
12 | "iconColor": "rgba(0, 211, 255, 1)",
13 | "name": "Annotations & Alerts",
14 | "type": "dashboard"
15 | }
16 | ]
17 | },
18 | "editable": true,
19 | "fiscalYearStartMonth": 0,
20 | "graphTooltip": 0,
21 | "id": 38,
22 | "links": [],
23 | "panels": [
24 | {
25 | "collapsed": true,
26 | "gridPos": {
27 | "h": 1,
28 | "w": 24,
29 | "x": 0,
30 | "y": 0
31 | },
32 | "id": 18,
33 | "panels": [
34 | {
35 | "datasource": {
36 | "type": "prometheus",
37 | "uid": "bdy7ccu1ntm2oa"
38 | },
39 | "fieldConfig": {
40 | "defaults": {
41 | "color": {
42 | "mode": "palette-classic"
43 | },
44 | "custom": {
45 | "axisBorderShow": false,
46 | "axisCenteredZero": false,
47 | "axisColorMode": "text",
48 | "axisLabel": "",
49 | "axisPlacement": "auto",
50 | "barAlignment": 0,
51 | "barWidthFactor": 0.6,
52 | "drawStyle": "line",
53 | "fillOpacity": 0,
54 | "gradientMode": "none",
55 | "hideFrom": {
56 | "legend": false,
57 | "tooltip": false,
58 | "viz": false
59 | },
60 | "insertNulls": false,
61 | "lineInterpolation": "linear",
62 | "lineWidth": 1,
63 | "pointSize": 5,
64 | "scaleDistribution": {
65 | "type": "linear"
66 | },
67 | "showPoints": "auto",
68 | "spanNulls": true,
69 | "stacking": {
70 | "group": "A",
71 | "mode": "none"
72 | },
73 | "thresholdsStyle": {
74 | "mode": "off"
75 | }
76 | },
77 | "mappings": [],
78 | "thresholds": {
79 | "mode": "absolute",
80 | "steps": [
81 | {
82 | "color": "green",
83 | "value": null
84 | },
85 | {
86 | "color": "red",
87 | "value": 80
88 | }
89 | ]
90 | }
91 | },
92 | "overrides": []
93 | },
94 | "gridPos": {
95 | "h": 10,
96 | "w": 12,
97 | "x": 0,
98 | "y": 1
99 | },
100 | "id": 12,
101 | "options": {
102 | "legend": {
103 | "calcs": [],
104 | "displayMode": "list",
105 | "placement": "bottom",
106 | "showLegend": true
107 | },
108 | "tooltip": {
109 | "hideZeros": false,
110 | "mode": "single",
111 | "sort": "none"
112 | }
113 | },
114 | "pluginVersion": "11.5.1",
115 | "targets": [
116 | {
117 | "datasource": {
118 | "type": "prometheus",
119 | "uid": "bdy7ccu1ntm2oa"
120 | },
121 | "disableTextWrap": false,
122 | "editorMode": "builder",
123 | "expr": "rate(invidious_companion_innertube_successful_request_total[$__rate_interval])",
124 | "fullMetaSearch": false,
125 | "includeNullMetadata": false,
126 | "legendFormat": "{{instance}}",
127 | "range": true,
128 | "refId": "A",
129 | "useBackend": false
130 | }
131 | ],
132 | "title": "Successful Requests Rate",
133 | "type": "timeseries"
134 | },
135 | {
136 | "datasource": {
137 | "type": "prometheus",
138 | "uid": "bdy7ccu1ntm2oa"
139 | },
140 | "fieldConfig": {
141 | "defaults": {
142 | "color": {
143 | "mode": "palette-classic"
144 | },
145 | "custom": {
146 | "axisBorderShow": false,
147 | "axisCenteredZero": false,
148 | "axisColorMode": "text",
149 | "axisLabel": "",
150 | "axisPlacement": "auto",
151 | "barAlignment": 0,
152 | "barWidthFactor": 0.6,
153 | "drawStyle": "line",
154 | "fillOpacity": 0,
155 | "gradientMode": "none",
156 | "hideFrom": {
157 | "legend": false,
158 | "tooltip": false,
159 | "viz": false
160 | },
161 | "insertNulls": false,
162 | "lineInterpolation": "linear",
163 | "lineWidth": 1,
164 | "pointSize": 5,
165 | "scaleDistribution": {
166 | "type": "linear"
167 | },
168 | "showPoints": "auto",
169 | "spanNulls": true,
170 | "stacking": {
171 | "group": "A",
172 | "mode": "none"
173 | },
174 | "thresholdsStyle": {
175 | "mode": "off"
176 | }
177 | },
178 | "mappings": [],
179 | "thresholds": {
180 | "mode": "absolute",
181 | "steps": [
182 | {
183 | "color": "green",
184 | "value": null
185 | },
186 | {
187 | "color": "red",
188 | "value": 80
189 | }
190 | ]
191 | }
192 | },
193 | "overrides": []
194 | },
195 | "gridPos": {
196 | "h": 10,
197 | "w": 12,
198 | "x": 12,
199 | "y": 1
200 | },
201 | "id": 13,
202 | "options": {
203 | "legend": {
204 | "calcs": [],
205 | "displayMode": "list",
206 | "placement": "bottom",
207 | "showLegend": true
208 | },
209 | "tooltip": {
210 | "hideZeros": false,
211 | "mode": "single",
212 | "sort": "none"
213 | }
214 | },
215 | "pluginVersion": "11.5.1",
216 | "targets": [
217 | {
218 | "datasource": {
219 | "type": "prometheus",
220 | "uid": "bdy7ccu1ntm2oa"
221 | },
222 | "disableTextWrap": false,
223 | "editorMode": "builder",
224 | "expr": "rate(invidious_companion_innertube_failed_request_total[$__rate_interval])",
225 | "fullMetaSearch": false,
226 | "includeNullMetadata": false,
227 | "legendFormat": "{{instance}}",
228 | "range": true,
229 | "refId": "A",
230 | "useBackend": false
231 | }
232 | ],
233 | "title": "Failed Requests Rate",
234 | "type": "timeseries"
235 | }
236 | ],
237 | "title": "Requests",
238 | "type": "row"
239 | },
240 | {
241 | "collapsed": true,
242 | "gridPos": {
243 | "h": 1,
244 | "w": 24,
245 | "x": 0,
246 | "y": 1
247 | },
248 | "id": 19,
249 | "panels": [
250 | {
251 | "datasource": {
252 | "type": "prometheus",
253 | "uid": "bdy7ccu1ntm2oa"
254 | },
255 | "fieldConfig": {
256 | "defaults": {
257 | "color": {
258 | "mode": "palette-classic"
259 | },
260 | "custom": {
261 | "axisBorderShow": false,
262 | "axisCenteredZero": false,
263 | "axisColorMode": "text",
264 | "axisLabel": "",
265 | "axisPlacement": "auto",
266 | "barAlignment": 0,
267 | "barWidthFactor": 0.6,
268 | "drawStyle": "line",
269 | "fillOpacity": 0,
270 | "gradientMode": "none",
271 | "hideFrom": {
272 | "legend": false,
273 | "tooltip": false,
274 | "viz": false
275 | },
276 | "insertNulls": false,
277 | "lineInterpolation": "linear",
278 | "lineStyle": {
279 | "fill": "solid"
280 | },
281 | "lineWidth": 1,
282 | "pointSize": 5,
283 | "scaleDistribution": {
284 | "type": "linear"
285 | },
286 | "showPoints": "auto",
287 | "spanNulls": true,
288 | "stacking": {
289 | "group": "A",
290 | "mode": "none"
291 | },
292 | "thresholdsStyle": {
293 | "mode": "off"
294 | }
295 | },
296 | "mappings": [],
297 | "thresholds": {
298 | "mode": "absolute",
299 | "steps": [
300 | {
301 | "color": "green",
302 | "value": null
303 | },
304 | {
305 | "color": "red",
306 | "value": 80
307 | }
308 | ]
309 | }
310 | },
311 | "overrides": []
312 | },
313 | "gridPos": {
314 | "h": 10,
315 | "w": 12,
316 | "x": 0,
317 | "y": 2
318 | },
319 | "id": 15,
320 | "options": {
321 | "legend": {
322 | "calcs": [],
323 | "displayMode": "list",
324 | "placement": "bottom",
325 | "showLegend": true
326 | },
327 | "tooltip": {
328 | "hideZeros": false,
329 | "mode": "single",
330 | "sort": "none"
331 | }
332 | },
333 | "pluginVersion": "11.5.1",
334 | "targets": [
335 | {
336 | "datasource": {
337 | "type": "prometheus",
338 | "uid": "bdy7ccu1ntm2oa"
339 | },
340 | "disableTextWrap": false,
341 | "editorMode": "builder",
342 | "expr": "rate(invidious_companion_innertube_error_status_unknown_total[$__rate_interval])",
343 | "fullMetaSearch": false,
344 | "includeNullMetadata": false,
345 | "legendFormat": "{{instance}}",
346 | "range": true,
347 | "refId": "A",
348 | "useBackend": false
349 | }
350 | ],
351 | "title": "Unknown Status Rate",
352 | "type": "timeseries"
353 | },
354 | {
355 | "datasource": {
356 | "type": "prometheus",
357 | "uid": "bdy7ccu1ntm2oa"
358 | },
359 | "fieldConfig": {
360 | "defaults": {
361 | "color": {
362 | "mode": "palette-classic"
363 | },
364 | "custom": {
365 | "axisBorderShow": false,
366 | "axisCenteredZero": false,
367 | "axisColorMode": "text",
368 | "axisLabel": "",
369 | "axisPlacement": "auto",
370 | "barAlignment": 0,
371 | "barWidthFactor": 0.6,
372 | "drawStyle": "line",
373 | "fillOpacity": 0,
374 | "gradientMode": "none",
375 | "hideFrom": {
376 | "legend": false,
377 | "tooltip": false,
378 | "viz": false
379 | },
380 | "insertNulls": false,
381 | "lineInterpolation": "linear",
382 | "lineStyle": {
383 | "fill": "solid"
384 | },
385 | "lineWidth": 1,
386 | "pointSize": 5,
387 | "scaleDistribution": {
388 | "type": "linear"
389 | },
390 | "showPoints": "auto",
391 | "spanNulls": true,
392 | "stacking": {
393 | "group": "A",
394 | "mode": "none"
395 | },
396 | "thresholdsStyle": {
397 | "mode": "off"
398 | }
399 | },
400 | "mappings": [],
401 | "thresholds": {
402 | "mode": "absolute",
403 | "steps": [
404 | {
405 | "color": "green",
406 | "value": null
407 | },
408 | {
409 | "color": "red",
410 | "value": 80
411 | }
412 | ]
413 | }
414 | },
415 | "overrides": []
416 | },
417 | "gridPos": {
418 | "h": 10,
419 | "w": 12,
420 | "x": 12,
421 | "y": 2
422 | },
423 | "id": 22,
424 | "options": {
425 | "legend": {
426 | "calcs": [],
427 | "displayMode": "list",
428 | "placement": "bottom",
429 | "showLegend": true
430 | },
431 | "tooltip": {
432 | "hideZeros": false,
433 | "mode": "single",
434 | "sort": "none"
435 | }
436 | },
437 | "pluginVersion": "11.5.1",
438 | "targets": [
439 | {
440 | "datasource": {
441 | "type": "prometheus",
442 | "uid": "bdy7ccu1ntm2oa"
443 | },
444 | "disableTextWrap": false,
445 | "editorMode": "code",
446 | "expr": "rate(invidious_companion_innertube_error_status_loginRequired_total[$__rate_interval])",
447 | "fullMetaSearch": false,
448 | "includeNullMetadata": false,
449 | "legendFormat": "{{instance}}",
450 | "range": true,
451 | "refId": "A",
452 | "useBackend": false
453 | }
454 | ],
455 | "title": "\"LOGIN_REQUIRED\" Rate",
456 | "type": "timeseries"
457 | }
458 | ],
459 | "title": "Status",
460 | "type": "row"
461 | },
462 | {
463 | "collapsed": true,
464 | "gridPos": {
465 | "h": 1,
466 | "w": 24,
467 | "x": 0,
468 | "y": 2
469 | },
470 | "id": 8,
471 | "panels": [
472 | {
473 | "datasource": {
474 | "type": "prometheus",
475 | "uid": "bdy7ccu1ntm2oa"
476 | },
477 | "fieldConfig": {
478 | "defaults": {
479 | "color": {
480 | "mode": "palette-classic"
481 | },
482 | "custom": {
483 | "axisBorderShow": false,
484 | "axisCenteredZero": false,
485 | "axisColorMode": "text",
486 | "axisLabel": "",
487 | "axisPlacement": "auto",
488 | "barAlignment": 0,
489 | "barWidthFactor": 0.6,
490 | "drawStyle": "line",
491 | "fillOpacity": 0,
492 | "gradientMode": "none",
493 | "hideFrom": {
494 | "legend": false,
495 | "tooltip": false,
496 | "viz": false
497 | },
498 | "insertNulls": false,
499 | "lineInterpolation": "linear",
500 | "lineStyle": {
501 | "fill": "solid"
502 | },
503 | "lineWidth": 1,
504 | "pointSize": 5,
505 | "scaleDistribution": {
506 | "type": "linear"
507 | },
508 | "showPoints": "auto",
509 | "spanNulls": true,
510 | "stacking": {
511 | "group": "A",
512 | "mode": "none"
513 | },
514 | "thresholdsStyle": {
515 | "mode": "off"
516 | }
517 | },
518 | "mappings": [],
519 | "thresholds": {
520 | "mode": "absolute",
521 | "steps": [
522 | {
523 | "color": "green",
524 | "value": null
525 | },
526 | {
527 | "color": "red",
528 | "value": 80
529 | }
530 | ]
531 | }
532 | },
533 | "overrides": []
534 | },
535 | "gridPos": {
536 | "h": 10,
537 | "w": 12,
538 | "x": 0,
539 | "y": 23
540 | },
541 | "id": 14,
542 | "options": {
543 | "legend": {
544 | "calcs": [],
545 | "displayMode": "list",
546 | "placement": "bottom",
547 | "showLegend": true
548 | },
549 | "tooltip": {
550 | "hideZeros": false,
551 | "mode": "single",
552 | "sort": "none"
553 | }
554 | },
555 | "pluginVersion": "11.5.1",
556 | "targets": [
557 | {
558 | "datasource": {
559 | "type": "prometheus",
560 | "uid": "bdy7ccu1ntm2oa"
561 | },
562 | "disableTextWrap": false,
563 | "editorMode": "builder",
564 | "expr": "rate(invidious_companion_innertube_error_reason_unknown_total[$__rate_interval])",
565 | "fullMetaSearch": false,
566 | "includeNullMetadata": false,
567 | "legendFormat": "{{instance}}",
568 | "range": true,
569 | "refId": "A",
570 | "useBackend": false
571 | }
572 | ],
573 | "title": "Unknown Reason Rate",
574 | "type": "timeseries"
575 | },
576 | {
577 | "datasource": {
578 | "type": "prometheus",
579 | "uid": "bdy7ccu1ntm2oa"
580 | },
581 | "fieldConfig": {
582 | "defaults": {
583 | "color": {
584 | "mode": "palette-classic"
585 | },
586 | "custom": {
587 | "axisBorderShow": false,
588 | "axisCenteredZero": false,
589 | "axisColorMode": "text",
590 | "axisLabel": "",
591 | "axisPlacement": "auto",
592 | "barAlignment": 0,
593 | "barWidthFactor": 0.6,
594 | "drawStyle": "line",
595 | "fillOpacity": 0,
596 | "gradientMode": "none",
597 | "hideFrom": {
598 | "legend": false,
599 | "tooltip": false,
600 | "viz": false
601 | },
602 | "insertNulls": false,
603 | "lineInterpolation": "linear",
604 | "lineStyle": {
605 | "fill": "solid"
606 | },
607 | "lineWidth": 1,
608 | "pointSize": 5,
609 | "scaleDistribution": {
610 | "type": "linear"
611 | },
612 | "showPoints": "auto",
613 | "spanNulls": true,
614 | "stacking": {
615 | "group": "A",
616 | "mode": "none"
617 | },
618 | "thresholdsStyle": {
619 | "mode": "off"
620 | }
621 | },
622 | "mappings": [],
623 | "thresholds": {
624 | "mode": "absolute",
625 | "steps": [
626 | {
627 | "color": "green",
628 | "value": null
629 | },
630 | {
631 | "color": "red",
632 | "value": 80
633 | }
634 | ]
635 | }
636 | },
637 | "overrides": []
638 | },
639 | "gridPos": {
640 | "h": 10,
641 | "w": 12,
642 | "x": 12,
643 | "y": 23
644 | },
645 | "id": 4,
646 | "options": {
647 | "legend": {
648 | "calcs": [],
649 | "displayMode": "list",
650 | "placement": "bottom",
651 | "showLegend": true
652 | },
653 | "tooltip": {
654 | "hideZeros": false,
655 | "mode": "single",
656 | "sort": "none"
657 | }
658 | },
659 | "pluginVersion": "11.5.1",
660 | "targets": [
661 | {
662 | "datasource": {
663 | "type": "prometheus",
664 | "uid": "bdy7ccu1ntm2oa"
665 | },
666 | "disableTextWrap": false,
667 | "editorMode": "builder",
668 | "expr": "rate(invidious_companion_innertube_error_reason_SignIn_total[$__rate_interval])",
669 | "fullMetaSearch": false,
670 | "includeNullMetadata": false,
671 | "legendFormat": "{{instance}}",
672 | "range": true,
673 | "refId": "A",
674 | "useBackend": false
675 | }
676 | ],
677 | "title": "\"Sign in to confirm you’re not a bot.\" Rate",
678 | "type": "timeseries"
679 | }
680 | ],
681 | "title": "Reasons",
682 | "type": "row"
683 | },
684 | {
685 | "collapsed": true,
686 | "gridPos": {
687 | "h": 1,
688 | "w": 24,
689 | "x": 0,
690 | "y": 3
691 | },
692 | "id": 9,
693 | "panels": [
694 | {
695 | "datasource": {
696 | "type": "prometheus",
697 | "uid": "bdy7ccu1ntm2oa"
698 | },
699 | "fieldConfig": {
700 | "defaults": {
701 | "color": {
702 | "mode": "palette-classic"
703 | },
704 | "custom": {
705 | "axisBorderShow": false,
706 | "axisCenteredZero": false,
707 | "axisColorMode": "text",
708 | "axisLabel": "",
709 | "axisPlacement": "auto",
710 | "barAlignment": 0,
711 | "barWidthFactor": 0.6,
712 | "drawStyle": "line",
713 | "fillOpacity": 0,
714 | "gradientMode": "none",
715 | "hideFrom": {
716 | "legend": false,
717 | "tooltip": false,
718 | "viz": false
719 | },
720 | "insertNulls": false,
721 | "lineInterpolation": "linear",
722 | "lineWidth": 1,
723 | "pointSize": 5,
724 | "scaleDistribution": {
725 | "type": "linear"
726 | },
727 | "showPoints": "auto",
728 | "spanNulls": true,
729 | "stacking": {
730 | "group": "A",
731 | "mode": "none"
732 | },
733 | "thresholdsStyle": {
734 | "mode": "off"
735 | }
736 | },
737 | "mappings": [],
738 | "thresholds": {
739 | "mode": "absolute",
740 | "steps": [
741 | {
742 | "color": "green",
743 | "value": null
744 | },
745 | {
746 | "color": "red",
747 | "value": 80
748 | }
749 | ]
750 | }
751 | },
752 | "overrides": []
753 | },
754 | "gridPos": {
755 | "h": 10,
756 | "w": 12,
757 | "x": 0,
758 | "y": 34
759 | },
760 | "id": 21,
761 | "options": {
762 | "legend": {
763 | "calcs": [],
764 | "displayMode": "list",
765 | "placement": "bottom",
766 | "showLegend": true
767 | },
768 | "tooltip": {
769 | "hideZeros": false,
770 | "mode": "single",
771 | "sort": "none"
772 | }
773 | },
774 | "pluginVersion": "11.5.1",
775 | "targets": [
776 | {
777 | "datasource": {
778 | "type": "prometheus",
779 | "uid": "bdy7ccu1ntm2oa"
780 | },
781 | "disableTextWrap": false,
782 | "editorMode": "code",
783 | "expr": "rate(invidious_companion_innertube_error_subreason_unknown_total[$__rate_interval])",
784 | "fullMetaSearch": false,
785 | "includeNullMetadata": false,
786 | "legendFormat": "{{instance}}",
787 | "range": true,
788 | "refId": "A",
789 | "useBackend": false
790 | }
791 | ],
792 | "title": "Unknown Subreason Rate",
793 | "type": "timeseries"
794 | },
795 | {
796 | "datasource": {
797 | "type": "prometheus",
798 | "uid": "bdy7ccu1ntm2oa"
799 | },
800 | "fieldConfig": {
801 | "defaults": {
802 | "color": {
803 | "mode": "palette-classic"
804 | },
805 | "custom": {
806 | "axisBorderShow": false,
807 | "axisCenteredZero": false,
808 | "axisColorMode": "text",
809 | "axisLabel": "",
810 | "axisPlacement": "auto",
811 | "barAlignment": 0,
812 | "barWidthFactor": 0.6,
813 | "drawStyle": "line",
814 | "fillOpacity": 0,
815 | "gradientMode": "none",
816 | "hideFrom": {
817 | "legend": false,
818 | "tooltip": false,
819 | "viz": false
820 | },
821 | "insertNulls": false,
822 | "lineInterpolation": "linear",
823 | "lineWidth": 1,
824 | "pointSize": 5,
825 | "scaleDistribution": {
826 | "type": "linear"
827 | },
828 | "showPoints": "auto",
829 | "spanNulls": true,
830 | "stacking": {
831 | "group": "A",
832 | "mode": "none"
833 | },
834 | "thresholdsStyle": {
835 | "mode": "off"
836 | }
837 | },
838 | "mappings": [],
839 | "thresholds": {
840 | "mode": "absolute",
841 | "steps": [
842 | {
843 | "color": "green",
844 | "value": null
845 | },
846 | {
847 | "color": "red",
848 | "value": 80
849 | }
850 | ]
851 | }
852 | },
853 | "overrides": []
854 | },
855 | "gridPos": {
856 | "h": 10,
857 | "w": 12,
858 | "x": 12,
859 | "y": 34
860 | },
861 | "id": 6,
862 | "options": {
863 | "legend": {
864 | "calcs": [],
865 | "displayMode": "list",
866 | "placement": "bottom",
867 | "showLegend": true
868 | },
869 | "tooltip": {
870 | "hideZeros": false,
871 | "mode": "single",
872 | "sort": "none"
873 | }
874 | },
875 | "pluginVersion": "11.5.1",
876 | "targets": [
877 | {
878 | "datasource": {
879 | "type": "prometheus",
880 | "uid": "bdy7ccu1ntm2oa"
881 | },
882 | "disableTextWrap": false,
883 | "editorMode": "builder",
884 | "expr": "rate(invidious_companion_innertube_error_subreason_ProtectCommunity_total[$__rate_interval])",
885 | "fullMetaSearch": false,
886 | "includeNullMetadata": false,
887 | "legendFormat": "{{instance}}",
888 | "range": true,
889 | "refId": "A",
890 | "useBackend": false
891 | }
892 | ],
893 | "title": "\"This helps protect our community.\" Rate",
894 | "type": "timeseries"
895 | }
896 | ],
897 | "title": "Subreasons",
898 | "type": "row"
899 | },
900 | {
901 | "collapsed": true,
902 | "gridPos": {
903 | "h": 1,
904 | "w": 24,
905 | "x": 0,
906 | "y": 4
907 | },
908 | "id": 20,
909 | "panels": [
910 | {
911 | "datasource": {
912 | "type": "prometheus",
913 | "uid": "bdy7ccu1ntm2oa"
914 | },
915 | "fieldConfig": {
916 | "defaults": {
917 | "color": {
918 | "mode": "palette-classic"
919 | },
920 | "custom": {
921 | "axisBorderShow": false,
922 | "axisCenteredZero": false,
923 | "axisColorMode": "text",
924 | "axisLabel": "",
925 | "axisPlacement": "auto",
926 | "barAlignment": 0,
927 | "barWidthFactor": 0.6,
928 | "drawStyle": "line",
929 | "fillOpacity": 0,
930 | "gradientMode": "none",
931 | "hideFrom": {
932 | "legend": false,
933 | "tooltip": false,
934 | "viz": false
935 | },
936 | "insertNulls": false,
937 | "lineInterpolation": "linear",
938 | "lineStyle": {
939 | "fill": "solid"
940 | },
941 | "lineWidth": 1,
942 | "pointSize": 5,
943 | "scaleDistribution": {
944 | "type": "linear"
945 | },
946 | "showPoints": "auto",
947 | "spanNulls": true,
948 | "stacking": {
949 | "group": "A",
950 | "mode": "none"
951 | },
952 | "thresholdsStyle": {
953 | "mode": "off"
954 | }
955 | },
956 | "mappings": [],
957 | "thresholds": {
958 | "mode": "absolute",
959 | "steps": [
960 | {
961 | "color": "green",
962 | "value": null
963 | },
964 | {
965 | "color": "red",
966 | "value": 80
967 | }
968 | ]
969 | }
970 | },
971 | "overrides": []
972 | },
973 | "gridPos": {
974 | "h": 10,
975 | "w": 12,
976 | "x": 0,
977 | "y": 185
978 | },
979 | "id": 16,
980 | "options": {
981 | "legend": {
982 | "calcs": [],
983 | "displayMode": "list",
984 | "placement": "bottom",
985 | "showLegend": true
986 | },
987 | "tooltip": {
988 | "hideZeros": false,
989 | "mode": "single",
990 | "sort": "none"
991 | }
992 | },
993 | "pluginVersion": "11.5.1",
994 | "targets": [
995 | {
996 | "datasource": {
997 | "type": "prometheus",
998 | "uid": "bdy7ccu1ntm2oa"
999 | },
1000 | "disableTextWrap": false,
1001 | "editorMode": "builder",
1002 | "expr": "rate(invidious_companion_potoken_generation_failure_total[$__rate_interval])",
1003 | "fullMetaSearch": false,
1004 | "includeNullMetadata": false,
1005 | "legendFormat": "{{instance}}",
1006 | "range": true,
1007 | "refId": "A",
1008 | "useBackend": false
1009 | }
1010 | ],
1011 | "title": "poToken Generation Failure Rate",
1012 | "type": "timeseries"
1013 | }
1014 | ],
1015 | "title": "Jobs",
1016 | "type": "row"
1017 | }
1018 | ],
1019 | "preload": false,
1020 | "refresh": "5s",
1021 | "schemaVersion": 40,
1022 | "tags": [],
1023 | "templating": {
1024 | "list": []
1025 | },
1026 | "time": {
1027 | "from": "now-30m",
1028 | "to": "now"
1029 | },
1030 | "timepicker": {},
1031 | "timezone": "browser",
1032 | "title": "Invidious Companion",
1033 | "uid": "1-0-0",
1034 | "version": 20,
1035 | "weekStart": ""
1036 | }
--------------------------------------------------------------------------------
/src/lib/helpers/config.ts:
--------------------------------------------------------------------------------
1 | import { z, ZodError } from "zod";
2 | import { parse } from "@std/toml";
3 |
4 | export const ConfigSchema = z.object({
5 | server: z.object({
6 | port: z.number().default(Number(Deno.env.get("PORT")) || 8282),
7 | host: z.string().default(Deno.env.get("HOST") || "127.0.0.1"),
8 | secret_key: z.string().length(16).default(
9 | Deno.env.get("SERVER_SECRET_KEY") || "",
10 | ),
11 | verify_requests: z.boolean().default(
12 | Deno.env.get("SERVER_VERIFY_REQUESTS") === "true" || false,
13 | ),
14 | encrypt_query_params: z.boolean().default(
15 | Deno.env.get("SERVER_ENCRYPT_QUERY_PARAMS") === "true" || false,
16 | ),
17 | enable_metrics: z.boolean().default(
18 | Deno.env.get("SERVER_ENABLE_METRICS") === "true" || false,
19 | ),
20 | }).strict().default({}),
21 | cache: z.object({
22 | enabled: z.boolean().default(
23 | Deno.env.get("CACHE_ENABLED") === "false" ? false : true,
24 | ),
25 | directory: z.string().default(
26 | Deno.env.get("CACHE_DIRECTORY") || "/var/tmp",
27 | ),
28 | }).strict().default({}),
29 | networking: z.object({
30 | proxy: z.string().nullable().default(Deno.env.get("PROXY") || null),
31 | fetch: z.object({
32 | timeout_ms: z.number().default(
33 | Number(Deno.env.get("NETWORKING_FETCH_TIMEOUT_MS")) || 30_000,
34 | ),
35 | retry: z.object({
36 | enabled: z.boolean().default(
37 | Deno.env.get("NETWORKING_FETCH_RETRY_ENABLED") === "true" ||
38 | false,
39 | ),
40 | times: z.number().optional().default(
41 | Number(Deno.env.get("NETWORKING_FETCH_RETRY_TIMES")) || 1,
42 | ),
43 | initial_debounce: z.number().optional().default(
44 | Number(
45 | Deno.env.get("NETWORKING_FETCH_RETRY_INITIAL_DEBOUNCE"),
46 | ) || 0,
47 | ),
48 | debounce_multiplier: z.number().optional().default(
49 | Number(
50 | Deno.env.get(
51 | "NETWORKING_FETCH_RETRY_DEBOUNCE_MULTIPLIER",
52 | ),
53 | ) || 0,
54 | ),
55 | }).strict().default({}),
56 | }).strict().default({}),
57 | videoplayback: z.object({
58 | ump: z.boolean().default(
59 | Deno.env.get("NETWORKING_VIDEOPLAYBACK_UMP") === "true" ||
60 | false,
61 | ),
62 | video_fetch_chunk_size_mb: z.number().default(
63 | Number(
64 | Deno.env.get(
65 | "NETWORKING_VIDEOPLAYBACK_VIDEO_FETCH_CHUNK_SIZE_MB",
66 | ),
67 | ) || 5,
68 | ),
69 | }).strict().default({}),
70 | }).strict().default({}),
71 | jobs: z.object({
72 | youtube_session: z.object({
73 | po_token_enabled: z.boolean().default(
74 | Deno.env.get("JOBS_YOUTUBE_SESSION_PO_TOKEN_ENABLED") ===
75 | "false"
76 | ? false
77 | : true,
78 | ),
79 | frequency: z.string().default(
80 | Deno.env.get("JOBS_YOUTUBE_SESSION_FREQUENCY") || "*/5 * * * *",
81 | ),
82 | }).strict().default({}),
83 | }).strict().default({}),
84 | youtube_session: z.object({
85 | oauth_enabled: z.boolean().default(
86 | Deno.env.get("YOUTUBE_SESSION_OAUTH_ENABLED") === "true" || false,
87 | ),
88 | cookies: z.string().default(
89 | Deno.env.get("YOUTUBE_SESSION_COOKIES") || "",
90 | ),
91 | }).strict().default({}),
92 | }).strict();
93 |
94 | export type Config = z.infer;
95 |
96 | export async function parseConfig() {
97 | const configFileName = Deno.env.get("CONFIG_FILE") || "config/config.toml";
98 | const configFileContents = await Deno.readTextFile(configFileName).catch(
99 | () => null,
100 | );
101 | if (configFileContents) {
102 | console.log("[INFO] Using custom settings local file");
103 | } else {
104 | console.log(
105 | "[INFO] No local config file found, using default config",
106 | );
107 | }
108 |
109 | try {
110 | const rawConfig = configFileContents ? parse(configFileContents) : {};
111 | const validatedConfig = ConfigSchema.parse(rawConfig);
112 |
113 | console.log("Loaded Configuration", validatedConfig);
114 |
115 | return validatedConfig;
116 | } catch (err) {
117 | let errorMessage =
118 | "There is an error in your configuration, check your environment variables";
119 | if (configFileContents) {
120 | errorMessage +=
121 | ` or in your configuration file located at ${configFileName}`;
122 | }
123 | console.log(errorMessage);
124 | if (err instanceof ZodError) {
125 | console.log(err.issues);
126 | throw new Error("Failed to parse configuration file");
127 | }
128 | // rethrow error if not Zod
129 | throw err;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/lib/helpers/encodeRFC5987ValueChars.ts:
--------------------------------------------------------------------------------
1 | // Taken from
2 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_content-disposition_and_link_headers
3 | export function encodeRFC5987ValueChars(str: string) {
4 | return (
5 | encodeURIComponent(str)
6 | // The following creates the sequences %27 %28 %29 %2A (Note that
7 | // the valid encoding of "*" is %2A, which necessitates calling
8 | // toUpperCase() to properly encode). Although RFC3986 reserves "!",
9 | // RFC5987 does not, so we do not need to escape it.
10 | .replace(
11 | /['()*]/g,
12 | (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`,
13 | )
14 | // The following are not required for percent-encoding per RFC5987,
15 | // so we can allow for a little better readability over the wire: |`^
16 | .replace(
17 | /%(7C|60|5E)/g,
18 | (_str, hex) => String.fromCharCode(parseInt(hex, 16)),
19 | )
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/helpers/encryptQuery.ts:
--------------------------------------------------------------------------------
1 | import { decodeBase64, encodeBase64 } from "@std/encoding/base64";
2 | import { Aes } from "crypto/aes.ts";
3 | import { Ecb, Padding } from "crypto/block-modes.ts";
4 | import type { Config } from "./config.ts";
5 |
6 | export const encryptQuery = (
7 | queryParams: string,
8 | config: Config,
9 | ): string => {
10 | try {
11 | const cipher = new Ecb(
12 | Aes,
13 | new TextEncoder().encode(
14 | config.server.secret_key,
15 | ),
16 | Padding.PKCS7,
17 | );
18 |
19 | const encodedData = new TextEncoder().encode(
20 | queryParams,
21 | );
22 |
23 | const encryptedData = cipher.encrypt(encodedData);
24 |
25 | return encodeBase64(encryptedData);
26 | } catch (err) {
27 | console.error("[ERROR] Failed to encrypt query parameters:", err);
28 | return "";
29 | }
30 | };
31 |
32 | export const decryptQuery = (
33 | queryParams: string,
34 | config: Config,
35 | ): string => {
36 | try {
37 | const decipher = new Ecb(
38 | Aes,
39 | new TextEncoder().encode(config.server.secret_key),
40 | Padding.PKCS7,
41 | );
42 |
43 | const decryptedData = new TextDecoder().decode(
44 | decipher.decrypt(
45 | decodeBase64(
46 | queryParams,
47 | ),
48 | ),
49 | );
50 |
51 | return decryptedData;
52 | } catch (err) {
53 | console.error("[ERROR] Failed to decrypt query parameters:", err);
54 | return "";
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/src/lib/helpers/getFetchClient.ts:
--------------------------------------------------------------------------------
1 | import { retry, type RetryOptions } from "@std/async";
2 | import type { Config } from "./config.ts";
3 |
4 | type FetchInputParameter = Parameters[0];
5 | type FetchInitParameterWithClient =
6 | | RequestInit
7 | | RequestInit & { client: Deno.HttpClient };
8 | type FetchReturn = ReturnType;
9 |
10 | export const getFetchClient = (config: Config): {
11 | (
12 | input: FetchInputParameter,
13 | init?: FetchInitParameterWithClient,
14 | ): FetchReturn;
15 | } => {
16 | const proxyAddress = config.networking.proxy;
17 | if (proxyAddress) {
18 | return async (
19 | input: FetchInputParameter,
20 | init?: RequestInit,
21 | ) => {
22 | const client = Deno.createHttpClient({
23 | proxy: {
24 | url: proxyAddress,
25 | },
26 | });
27 | const fetchRes = await fetchShim(config, input, {
28 | client,
29 | headers: init?.headers,
30 | method: init?.method,
31 | body: init?.body,
32 | });
33 | return new Response(fetchRes.body, {
34 | status: fetchRes.status,
35 | headers: fetchRes.headers,
36 | });
37 | };
38 | }
39 |
40 | return (input: FetchInputParameter, init?: FetchInitParameterWithClient) =>
41 | fetchShim(config, input, init);
42 | };
43 |
44 | function fetchShim(
45 | config: Config,
46 | input: FetchInputParameter,
47 | init?: FetchInitParameterWithClient,
48 | ): FetchReturn {
49 | const fetchTimeout = config.networking.fetch?.timeout_ms;
50 | const fetchRetry = config.networking.fetch?.retry?.enabled;
51 | const fetchMaxAttempts = config.networking.fetch?.retry?.times;
52 | const fetchInitialDebounce = config.networking.fetch?.retry
53 | ?.initial_debounce;
54 | const fetchDebounceMultiplier = config.networking.fetch?.retry
55 | ?.debounce_multiplier;
56 | const retryOptions: RetryOptions = {
57 | maxAttempts: fetchMaxAttempts,
58 | minTimeout: fetchInitialDebounce,
59 | multiplier: fetchDebounceMultiplier,
60 | jitter: 0,
61 | };
62 |
63 | const callFetch = () =>
64 | fetch(input, {
65 | // only set the AbortSignal if the timeout is supplied in the config
66 | signal: fetchTimeout
67 | ? AbortSignal.timeout(Number(fetchTimeout))
68 | : null,
69 | ...(init || {}),
70 | });
71 | // if retry enabled, call retry with the fetch shim, otherwise pass the fetch shim back directly
72 | return fetchRetry ? retry(callFetch, retryOptions) : callFetch();
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib/helpers/metrics.ts:
--------------------------------------------------------------------------------
1 | import { IRawResponse } from "youtubei.js";
2 | import { Counter, Registry } from "prom-client";
3 |
4 | export class Metrics {
5 | private METRICS_PREFIX = "invidious_companion_";
6 | public register = new Registry();
7 |
8 | public createCounter(name: string, help?: string): Counter {
9 | return new Counter({
10 | name: `${this.METRICS_PREFIX}${name}`,
11 | help: help || "No help has been provided for this metric",
12 | registers: [this.register],
13 | });
14 | }
15 |
16 | public potokenGenerationFailure = this.createCounter(
17 | "potoken_generation_failure_total",
18 | "Number of times that the PoToken generation job has failed for whatever reason",
19 | );
20 |
21 | private innertubeErrorStatusLoginRequired = this.createCounter(
22 | "innertube_error_status_loginRequired_total",
23 | 'Number of times that the status "LOGIN_REQUIRED" has been returned by Innertube API',
24 | );
25 |
26 | private innertubeErrorStatusUnknown = this.createCounter(
27 | "innertube_error_status_unknown_total",
28 | "Number of times that an unknown status has been returned by Innertube API",
29 | );
30 |
31 | private innertubeErrorReasonSignIn = this.createCounter(
32 | "innertube_error_reason_SignIn_total",
33 | 'Number of times that the message "Sign in to confirm you’re not a bot." has been returned by Innertube API',
34 | );
35 |
36 | private innertubeErrorSubreasonProtectCommunity = this.createCounter(
37 | "innertube_error_subreason_ProtectCommunity_total",
38 | 'Number of times that the message "This helps protect our community." has been returned by Innertube API',
39 | );
40 |
41 | private innertubeErrorReasonUnknown = this.createCounter(
42 | "innertube_error_reason_unknown_total",
43 | "Number of times that an unknown reason has been returned by the Innertube API",
44 | );
45 |
46 | private innertubeErrorSubreasonUnknown = this.createCounter(
47 | "innertube_error_subreason_unknown_total",
48 | "Number of times that an unknown subreason has been returned by the Innertube API",
49 | );
50 |
51 | public innertubeSuccessfulRequest = this.createCounter(
52 | "innertube_successful_request_total",
53 | "Number successful requests made to the Innertube API",
54 | );
55 |
56 | private innertubeFailedRequest = this.createCounter(
57 | "innertube_failed_request_total",
58 | "Number failed requests made to the Innertube API for whatever reason",
59 | );
60 |
61 | private checkStatus(videoData: IRawResponse) {
62 | const status = videoData.playabilityStatus?.status;
63 |
64 | return {
65 | unplayable: status ===
66 | "UNPLAYABLE",
67 | contentCheckRequired: status ===
68 | "CONTENT_CHECK_REQUIRED",
69 | loginRequired: status === "LOGIN_REQUIRED",
70 | };
71 | }
72 |
73 | private checkReason(videoData: IRawResponse) {
74 | const reason = videoData.playabilityStatus?.reason;
75 |
76 | return {
77 | signInToConfirmAge: reason?.includes(
78 | "Sign in to confirm your age",
79 | ),
80 | SignInToConfirmBot: reason?.includes(
81 | "Sign in to confirm you’re not a bot",
82 | ),
83 | };
84 | }
85 |
86 | private checkSubreason(videoData: IRawResponse) {
87 | const subReason = videoData.playabilityStatus?.errorScreen
88 | ?.playerErrorMessageRenderer
89 | ?.subreason?.runs?.[0]?.text;
90 |
91 | return {
92 | thisHelpsProtectCommunity: subReason?.includes(
93 | "This helps protect our community",
94 | ),
95 | };
96 | }
97 |
98 | public checkInnertubeResponse(videoData: IRawResponse) {
99 | this.innertubeFailedRequest.inc();
100 | const status = this.checkStatus(videoData);
101 |
102 | if (status.contentCheckRequired || status.unplayable) return;
103 |
104 | if (status.loginRequired) {
105 | this.innertubeErrorStatusLoginRequired.inc();
106 | const reason = this.checkReason(videoData);
107 |
108 | if (reason.signInToConfirmAge) return;
109 |
110 | if (reason.SignInToConfirmBot) {
111 | this.innertubeErrorReasonSignIn.inc();
112 | const subReason = this.checkSubreason(videoData);
113 |
114 | if (subReason.thisHelpsProtectCommunity) {
115 | this.innertubeErrorSubreasonProtectCommunity.inc();
116 | } else {
117 | this.innertubeErrorSubreasonUnknown.inc();
118 | }
119 | } else {
120 | this.innertubeErrorReasonUnknown.inc();
121 | }
122 | } else {
123 | this.innertubeErrorStatusUnknown.inc();
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/lib/helpers/verifyRequest.ts:
--------------------------------------------------------------------------------
1 | import { decodeBase64 } from "@std/encoding/base64";
2 | import { Aes } from "crypto/aes.ts";
3 | import { Ecb, Padding } from "crypto/block-modes.ts";
4 | import type { Config } from "./config.ts";
5 |
6 | export const verifyRequest = (
7 | stringToCheck: string,
8 | videoId: string,
9 | config: Config,
10 | ): boolean => {
11 | try {
12 | const decipher = new Ecb(
13 | Aes,
14 | new TextEncoder().encode(config.server.secret_key),
15 | Padding.PKCS7,
16 | );
17 |
18 | const encryptedData = new TextDecoder().decode(
19 | decipher.decrypt(
20 | decodeBase64(
21 | stringToCheck.replace(/-/g, "+").replace(/_/g, "/"),
22 | ),
23 | ),
24 | );
25 | const [parsedTimestamp, parsedVideoId] = encryptedData.split("|");
26 | const parsedTimestampInt = parseInt(parsedTimestamp);
27 | const timestampNow = Math.round(+new Date() / 1000);
28 | if (parsedVideoId !== videoId) {
29 | return false;
30 | }
31 | // only allow ID to live for 6 hours
32 | if ((timestampNow + 6 * 60 * 60) - parsedTimestampInt < 0) {
33 | return false;
34 | }
35 | } catch (_) {
36 | return false;
37 | }
38 | return true;
39 | };
40 |
--------------------------------------------------------------------------------
/src/lib/helpers/youtubePlayerHandling.ts:
--------------------------------------------------------------------------------
1 | import { ApiResponse, Innertube, YT } from "youtubei.js";
2 | import { generateRandomString } from "youtubei.js/Utils";
3 | import { compress, decompress } from "brotli";
4 | import type { TokenMinter } from "../jobs/potoken.ts";
5 | import { Metrics } from "../helpers/metrics.ts";
6 | let youtubePlayerReqLocation = "youtubePlayerReq";
7 | if (Deno.env.get("YT_PLAYER_REQ_LOCATION")) {
8 | if (Deno.env.has("DENO_COMPILED")) {
9 | youtubePlayerReqLocation = Deno.mainModule.replace("src/main.ts", "") +
10 | Deno.env.get("YT_PLAYER_REQ_LOCATION");
11 | } else {
12 | youtubePlayerReqLocation = Deno.env.get(
13 | "YT_PLAYER_REQ_LOCATION",
14 | ) as string;
15 | }
16 | }
17 | const { youtubePlayerReq } = await import(youtubePlayerReqLocation);
18 |
19 | import type { Config } from "./config.ts";
20 |
21 | const kv = await Deno.openKv();
22 |
23 | export const youtubePlayerParsing = async ({
24 | innertubeClient,
25 | videoId,
26 | config,
27 | tokenMinter,
28 | metrics,
29 | overrideCache = false,
30 | }: {
31 | innertubeClient: Innertube;
32 | videoId: string;
33 | config: Config;
34 | tokenMinter: TokenMinter;
35 | metrics: Metrics | undefined;
36 | overrideCache?: boolean;
37 | }): Promise