Compare commits

..

40 Commits

Author SHA1 Message Date
Émilien (perso)
209f27bb32 Document supported proxy types in config.example.yml
Added note about supported proxy types in configuration.
2026-01-08 11:56:44 +01:00
Émilien (perso)
4b020414c9 doc: Update HTTP proxy configuration comments
Added information about proxy configuration for YouTube streams.
2026-01-08 10:30:25 +01:00
Fijxu
5f84a5b353 Generate companion check id one time and add missing companion check id on captions (#5575)
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.17.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.18.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
* Only generate companion check id one time

* Add missing check id for companion captions
2025-12-22 17:14:59 +01:00
Fijxu
9603f5151d Downgrade Crystal to 1.16.3 in OCI (#5577)
Some checks failed
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.17.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.18.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
* downgrade to 1.16.3

* Downgrade Alpine base image from 3.23 to 3.22

---------

Co-authored-by: Émilien (perso) <4016501+unixfox@users.noreply.github.com>
2025-12-22 11:19:13 +01:00
Fijxu
f7a31aa3de fix lint
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.17.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.18.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
2025-12-21 00:50:37 -03:00
Jeroen Boersma
dbbaf51f1f Allow downloading via companion (#5561)
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.17.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.18.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
* Allow downloading via companion

* post request where not proxied for the download companion which made
  it impossible to download with the companion enabled

* Re-apply Channel to Channels rename which was pulled in

* Update src/invidious/routes/companion.cr

* doc: better comments for each route

---------

Co-authored-by: Fijxu <fijxu@nadeko.net>
Co-authored-by: Émilien (perso) <4016501+unixfox@users.noreply.github.com>
2025-12-19 15:09:22 +01:00
Émilien (perso)
7a4b901846 chore: update crystal 1.18.2 + alpine 3.23 (#5574) 2025-12-19 15:08:07 +01:00
Fijxu
bf17d53068 Replace deprecated blocking property of Socket (#5538)
* Replace deprecated `blocking` property of `Socket`

This replaces the deprecated argument `blocking` and uses
`Socket.set_blocking(fd, value)` instead.

Fixes a warning in the compiler

https://github.com/crystal-lang/crystal/pull/16033

* Upgrade to upstream

* chore: only Socket.set_blocking for > 1.18

---------

Co-authored-by: Emilien <4016501+unixfox@users.noreply.github.com>
2025-12-19 14:59:42 +01:00
syeopite
1f5685ef92 Reduce indent in StaticAssetsHandler#serve_file 2025-12-19 12:35:00 +01:00
syeopite
21049518d6 Improve cache size check to be more performant
Summing the sizes of each cached file every time is very inefficient.
Instead we can simply store the cache size in an constant and increase
it everytime a file is added into the cache.
2025-12-19 12:35:00 +01:00
syeopite
7f9cfe1aa2 Refactor logic for updating temp files in tests 2025-12-19 12:35:00 +01:00
syeopite
89a0761a19 Fix Ameba Lint/UselessAssign 2025-12-19 12:35:00 +01:00
syeopite
7749ea1956 Isolate static assets handler spec from others
Running `crystal spec` without a file argument essentially produces one
big program that combines every single spec file, their imports, and
the files that those imports themselves depend on. Most of the types
within this combined program will get ignored by the compiler due to a
lack of any calls to them from the spec files.

But for some types, partially the HTTP module ones, using them within
the spec files will suddenly make the compiler enable a bunch of
previously ignored code. And those code will suddenly require the
presence of additional types, constants, etc. This not only make it
annoying for getting the specs working but also makes it difficult to
isolate behaviors for testing.

The `static_assets_handler_spec.cr` causes this issue and so will be
marked as an isolated spec for now. In the future all of the tests
should be organized into independent groupings similar to how the
Crystal compiler splits their tests into std, compiler, primitives and
interpreter.
2025-12-19 12:35:00 +01:00
syeopite
9e482b4807 Add specs for the new StaticAssetsHandler 2025-12-19 12:35:00 +01:00
syeopite
6fd1cb3585 Compare against 1.17.0-dev until full release 2025-12-19 12:35:00 +01:00
syeopite
ddfbed68f7 Simplify StaticAssetsHandler implementation
Overriding `#call` or patching out `serve_file_compressed` provides
only minimal benefits over the ease of maintenance granted by only
overriding what we need to for the caching behavior.
2025-12-19 12:35:00 +01:00
syeopite
d2be57a454 Replace Kemal::StaticFileHandler on Crystal < 1.17.0
Kemal's subclass of the stdlib `HTTP::StaticFileHandler` is not as
maintained as its parent, and so misses out on many enhancements and bug
fixes from upstream, which unfortunately also includes the patches for
security vulnerabilities...

Though this isn't necessarily Kemal's fault since the bulk of the stdlib
handler's logic was done in a single big method, making any changes hard
to maintain. This was fixed in Crystal 1.17.0 where the handler
was refactored into many private methods, making it easier for an
inheriting type to implement custom behaviors while still leveraging
much of the pre-existing code.

Since we don't actually use any of the Kemal specific features added by
`Kemal::StaticFileHandler`, there really isn't a reason to not just
create a new handler based upon the stdlib implementation instead which
will address the problems mentioned above.

This PR implements a new handler which inherits from the stdlib variant
and overrides the helper methods added in Crystal 1.17.0 to add the
caching behavior with minimal code changes. Since this new handler
depends on the code in Crystal 1.17.0, it will only be applied on
versions greater than or equal to 1.17.0. On older versions we'll
fallback to the current monkey patched `Kemal::StaticFileHandler`
2025-12-19 12:35:00 +01:00
Fijxu
eed8f25a3d dockerfile: compile openssl instead of using the one bundled on the crystal alpine image. (#5441)
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
* dockerfile: compile openssl instead of using the one bundled on the crystal alpine image.

* fix formatting

* CI: add --no-cache to openssl-builder

* CI: add Dockerfile.arm64 version

* add comment why we compile openssl ourselves

* fix wrong position of comment

* oopsie

* verify openssl checksums

* set nproc for openssl make

* use ARG for openssl sha256 checksum
2025-12-18 10:16:15 +01:00
dependabot[bot]
cf52a35366 Bump actions/cache from 4 to 5 (#5569)
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-15 22:49:01 +01:00
Fijxu
aba31a8e20 Set Kemal max_request_line_size to 16384 for large channel continuation query parameters. (#5566)
Some checks failed
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
* feat: Add configurable max_request_line_size to handle long URLs

This commit adds a new configuration option `max_request_line_size` that allows
users to increase the HTTP request line size limit. This is particularly useful
for handling very long continuation tokens that can cause 414 (URI Too Long) errors.

Changes:
- Add `max_request_line_size` property to Config class
- Configure Kemal server to use the custom limit if specified
- Document the option in config.example.yml with recommendations
- Add examples in docker-compose.yml for both YAML and env var configuration

The default behavior remains unchanged (8KB limit) unless explicitly configured.
This provides a solution for users experiencing 414 errors without affecting
existing installations.

* Hardcode max_request_line_size to 16384

---------

Co-authored-by: Sunghyun Kim <hello@sunghyun.me>
2025-12-15 08:21:55 +01:00
Sebastian Hädrich
994c25de2e Add link to GitHub release/tag/commit in footer (#4702)
Some checks failed
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
* Add link to GitHub release/tag/commit in footer

* Only show tag if there is one

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

---------

Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2025-12-14 19:30:52 -03:00
Fijxu
65463333f3 Display "Erroneous CAPTCHA" for invalid captchas (#5508)
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
2025-12-11 17:28:20 -03:00
Fijxu
ef2290c1fd Fix channel name overflow (#5553)
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
2025-12-06 20:20:42 -03:00
Fijxu
3944d2490c Fix trending page by leaving livestream and gaming trending pages (#5555)
The livestream trending page is now the default.

Adds `content_container = special_category_container["gridRenderer"]?` in the `CategoryRendererParser`
needed for the gaming trending page. The JSON structure of the gaming
trending page looked like this:

```json
"contents": {
 "twoColumnBrowseResultsRenderer": {
  "tabs": [
   {
    "tabRenderer": {
     "selected": true,
     "content": {
      "sectionListRenderer": {
       "contents": [
        {
         "itemSectionRenderer": {
          "contents": [
           {
            "shelfRenderer": {
             "title": {
              "runs": [
               {
                "text": "Trending videos"
               }
              ]
             },
             "content": {
              "gridRenderer": { // <- This was added to the CategoryRendererParser
               "items": [
                {
                 "gridVideoRenderer": {
                  "videoId": "sTWztaLjD20",
                  // More video data
                  // ...
                 }
                }
               ]
              }
             }
            }
           }
          ]
         }
        }
       ]
      }
     }
    }
   }
  ]
 }
}
```

Thanks to
ae2755bf71/extractor/src/main/java/org/schabi/newpipe/extractor/services/youtube/extractors/kiosk/YoutubeTrendingGamingVideosExtractor.java (L11-L13)
for the `browse_id` and `params` needed for the gaming trending page.
2025-12-06 20:19:38 -03:00
Fijxu
a7935bc378 fix: restore dmca_content functionality (#5228)
* fix: restore dmca_content functionality

This restores (or adds) the functionality of the `dmca_content` config
option that at this date, has been unused and makes no effect.

* only disable download widget for dmca video ids
2025-12-06 17:15:25 -03:00
Fijxu
07f3894a71 Remove signature helper completely from Invidious (#5550)
* Remove signature helper completely from Invidious

The official way to reproduce video with Invidious now is by using
Invidious Companion which uses Youtube.JS with a Javascript Interpreter
that can successfully decrypt youtube video URLs.

Sig helper has not been used for a long time, is beyond broken and no
one has plans to fix it and maintain it.

* Remove DECRYPT_FUNCTION and shrink player function

* remove `sp = cfr[sp]`

* Improve message
2025-12-06 16:50:59 -03:00
Fijxu
46a9c933be Fix community posts when there is a unavailable video in a post (#5549)
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Posts with a video that has been removed returned
`ProblematicTimelineItem` type which was not taken in account for
community posts.

Now community posts with a broken video will not display an embedded
video.
2025-12-04 12:00:58 -03:00
Fijxu
48765f759d chore: Update shard.yml to use SPDX license identifier (#5552) 2025-12-04 11:59:55 -03:00
Fijxu
35d1d499bc chore: Store preferences in a variable when reused and rename prefs to preferences (#5450)
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
A little code cleanup on places where `preferences` is used more than one time and rename `prefs` to `preferences` to maintain consistency.
2025-12-02 18:20:15 -03:00
Émilien (perso)
b2ecd8abc3 chore: update healthcheck for /api/v1/stats
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
since /api/v1/trending doesn't work anymore
2025-11-25 14:32:15 +01:00
dependabot[bot]
bb9c4a01a1 Bump actions/checkout from 5 to 6
Some checks failed
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-25 08:40:27 +01:00
dependabot[bot]
c250b9c0b1 Bump crystal-lang/install-crystal from 1.8.3 to 1.9.1
Some checks failed
Stale issue handler / stale (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
Bumps [crystal-lang/install-crystal](https://github.com/crystal-lang/install-crystal) from 1.8.3 to 1.9.1.
- [Release notes](https://github.com/crystal-lang/install-crystal/releases)
- [Commits](https://github.com/crystal-lang/install-crystal/compare/v1.8.3...v1.9.1)

---
updated-dependencies:
- dependency-name: crystal-lang/install-crystal
  dependency-version: 1.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-10 23:00:49 +01:00
shiny-comic
5cfe294063 Fix 0 view count on related videos section (#5446)
Some checks failed
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Invidious CI / build - crystal: 1.12.2, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.13.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.14.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.15.1, stable: true (push) Has been cancelled
Invidious CI / build - crystal: 1.16.3, stable: true (push) Has been cancelled
Invidious CI / build - crystal: nightly, stable: false (push) Has been cancelled
Invidious CI / Test AMD64 Docker build (push) Has been cancelled
Invidious CI / Test ARM64 Docker build (push) Has been cancelled
* Fix 0 view count on related videos

* Remove view_count variable since it's unused by Innertube

* Remove view_count from specs and API

---------

Co-authored-by: Fijxu <fijxu@nadeko.net>
2025-10-16 21:59:34 -03:00
Fijxu
0c13c4fab1 Prevent timestamp from being set for Livestreams on "Watch on Youtube" links (#5481)
Some checks are pending
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Waiting to run
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Waiting to run
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Waiting to run
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Waiting to run
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Waiting to run
Invidious CI / lint (push) Waiting to run
2025-10-16 17:32:01 -03:00
Fijxu
fdf0a25b9e Add Livestreams to trending page (#5480) 2025-10-16 17:31:48 -03:00
Fijxu
3226e17953 Fix button overflow (#5452) 2025-10-16 17:31:33 -03:00
dependabot[bot]
710b3f250b Bump crystal-lang/install-crystal from 1.8.2 to 1.8.3
Some checks failed
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
Bumps [crystal-lang/install-crystal](https://github.com/crystal-lang/install-crystal) from 1.8.2 to 1.8.3.
- [Release notes](https://github.com/crystal-lang/install-crystal/releases)
- [Commits](https://github.com/crystal-lang/install-crystal/compare/v1.8.2...v1.8.3)

---
updated-dependencies:
- dependency-name: crystal-lang/install-crystal
  dependency-version: 1.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-30 19:56:16 +02:00
Alex
42d34cd084 Removed specific section from hyperlink in config.cr
Some checks failed
Invidious CI / lint (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Has been cancelled
2025-09-24 18:54:27 +02:00
Alex
18a8490587 Fixed broken companion hyperlink 2025-09-24 18:54:27 +02:00
syeopite
325e013e0d Prepare for the next release
Some checks failed
Build and release container directly from master / release (docker/Dockerfile, AMD64, ubuntu-latest, linux/amd64, ) (push) Has been cancelled
Build and release container directly from master / release (docker/Dockerfile.arm64, ARM64, ubuntu-24.04-arm, linux/arm64/v8, -arm64) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (nightly, false) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.12.2, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.13.3, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.14.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.15.1, true) (push) Has been cancelled
Invidious CI / build - crystal: ${{ matrix.crystal }}, stable: ${{ matrix.stable }} (1.16.3, true) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (AMD64, ubuntu-latest) (push) Has been cancelled
Invidious CI / Test ${{ matrix.name }} Docker build (ARM64, ubuntu-24.04-arm) (push) Has been cancelled
Invidious CI / lint (push) Has been cancelled
2025-09-13 11:55:10 -07:00
48 changed files with 637 additions and 664 deletions

View File

@@ -36,7 +36,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -27,7 +27,7 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -38,17 +38,17 @@ jobs:
matrix:
stable: [true]
crystal:
- 1.12.2
- 1.13.3
- 1.14.1
- 1.15.1
- 1.16.3
- 1.17.1
- 1.18.2
include:
- crystal: nightly
stable: false
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: true
@@ -58,12 +58,12 @@ jobs:
shell: bash
- name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.2
uses: crystal-lang/install-crystal@v1.9.1
with:
crystal: ${{ matrix.crystal }}
- name: Cache Shards
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
./lib
@@ -96,7 +96,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Use ARM64 Dockerfile if ARM64
if: ${{ matrix.name == 'ARM64' }}
@@ -128,18 +128,18 @@ jobs:
continue-on-error: true
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
submodules: true
- name: Install Crystal
id: lint_step_install_crystal
uses: crystal-lang/install-crystal@v1.8.2
uses: crystal-lang/install-crystal@v1.9.1
with:
crystal: latest
- name: Cache Shards
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
./lib

View File

@@ -167,6 +167,7 @@ body a.pure-button-primary,
.pure-button-primary,
.pure-button-secondary {
white-space: normal;
border: 1px solid #a0a0a0;
border-radius: 3px;
margin: 0 .4em;
@@ -403,8 +404,9 @@ input[type="search"]::-webkit-search-cancel-button {
.video-card-row { margin: 15px 0; }
p.channel-name { margin: 0; }
p.channel-name { margin: 0; overflow-wrap: anywhere;}
p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
.channel-profile > .channel-name { overflow-wrap: anywhere;}
/*

View File

@@ -137,6 +137,7 @@ player.on('timeupdate', function () {
// YouTube links
if (!video_data.live_now) {
let elem_yt_watch = document.getElementById('link-yt-watch');
if (elem_yt_watch) {
let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url');
@@ -148,6 +149,7 @@ player.on('timeupdate', function () {
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
}
}
// Invidious links

View File

@@ -40,20 +40,6 @@ db:
##
#check_tables: false
##
## Path to an external signature resolver, used to emulate
## the Youtube client's Javascript. If no such server is
## available, some videos will not be playable.
##
## When this setting is commented out, no external
## resolver will be used.
##
## Accepted values: a path to a UNIX socket or "<IP>:<Port>"
## Default: <none>
##
#signature_server:
##
## Invidious companion is an external program
## for loading the video streams from YouTube servers.
@@ -237,8 +223,12 @@ https_only: false
##
## Configuration for using a HTTP proxy
##
## If unset, then no HTTP proxy will be used.
## Proxy type supported: HTTP, HTTPS
##
## This is not used for loading the video streams from YouTube servers (circumvent YouTube restrictions)
## Please instead configure the proxy in Invidious companion:
## https://github.com/iv-org/invidious-companion/blob/master/config/config.example.toml
##
#http_proxy:
# user:
@@ -259,19 +249,6 @@ https_only: false
##
# use_innertube_for_captions: false
##
## Send Google session informations. This is useful when Invidious is blocked
## by the message "This helps protect our community."
## See https://github.com/iv-org/invidious/issues/4734.
##
## Warning: These strings gives much more identifiable information to Google!
##
## Accepted values: String
## Default: <none>
##
# po_token: ""
# visitor_data: ""
# -----------------------------
# Logging
# -----------------------------

View File

@@ -36,7 +36,7 @@ services:
# statistics_enabled: false
hmac_key: "CHANGE_ME!!"
healthcheck:
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/stats || exit 1
interval: 30s
timeout: 5s
retries: 2

View File

@@ -1,6 +1,29 @@
FROM crystallang/crystal:1.16.3-alpine AS builder
# https://github.com/openssl/openssl/releases/tag/openssl-3.5.2
ARG OPENSSL_VERSION='3.5.2'
ARG OPENSSL_SHA256='c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec'
FROM crystallang/crystal:1.16.3-alpine AS dependabot-crystal
# We compile openssl ourselves due to a memory leak in how crystal interacts
# with openssl
# Reference: https://github.com/iv-org/invidious/issues/1438#issuecomment-3087636228
FROM dependabot-crystal AS openssl-builder
RUN apk add --no-cache curl perl linux-headers
WORKDIR /
ARG OPENSSL_VERSION
ARG OPENSSL_SHA256
RUN curl -Ls "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" --output openssl-${OPENSSL_VERSION}.tar.gz
RUN echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c
RUN tar -xzvf openssl-${OPENSSL_VERSION}.tar.gz
RUN cd openssl-${OPENSSL_VERSION} && ./Configure --openssldir=/etc/ssl && make -j$(nproc)
FROM dependabot-crystal AS builder
RUN apk add --no-cache sqlite-static yaml-static
RUN apk del openssl-dev openssl-libs-static
ARG release
@@ -21,18 +44,24 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
ARG OPENSSL_VERSION
COPY --from=openssl-builder /openssl-${OPENSSL_VERSION} /openssl-${OPENSSL_VERSION}
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \
crystal build ./src/invidious.cr \
--release \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
else \
PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \
crystal build ./src/invidious.cr \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:3.21
FROM alpine:3.23
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \

View File

@@ -1,6 +1,31 @@
FROM alpine:3.21 AS builder
RUN apk add --no-cache 'crystal=1.14.0-r0' shards sqlite-static yaml-static yaml-dev libxml2-static \
zlib-static openssl-libs-static openssl-dev musl-dev xz-static
# https://github.com/openssl/openssl/releases/tag/openssl-3.5.2
ARG OPENSSL_VERSION='3.5.2'
ARG OPENSSL_SHA256='c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec'
FROM alpine:3.22 AS dependabot-alpine
# We compile openssl ourselves due to a memory leak in how crystal interacts
# with openssl
# Reference: https://github.com/iv-org/invidious/issues/1438#issuecomment-3087636228
FROM dependabot-alpine AS openssl-builder
RUN apk add --no-cache curl perl linux-headers build-base
WORKDIR /
ARG OPENSSL_VERSION
ARG OPENSSL_SHA256
RUN curl -Ls "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" --output openssl-${OPENSSL_VERSION}.tar.gz
RUN echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c
RUN tar -xzvf openssl-${OPENSSL_VERSION}.tar.gz
RUN cd openssl-${OPENSSL_VERSION} && ./Configure --openssldir=/etc/ssl && make -j$(nproc)
FROM dependabot-alpine AS builder
RUN apk add --no-cache 'crystal=1.16.3-r0' shards \
sqlite-static yaml-static yaml-dev \
pcre2-static gc-static \
libxml2-static zlib-static \
openssl-libs-static openssl-dev musl-dev xz-static
ARG release
@@ -22,18 +47,23 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
RUN crystal spec --warnings all \
--link-flags "-lxml2 -llzma"
ARG OPENSSL_VERSION
COPY --from=openssl-builder /openssl-${OPENSSL_VERSION} /openssl-${OPENSSL_VERSION}
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \
crystal build ./src/invidious.cr \
--release \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
else \
PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \
crystal build ./src/invidious.cr \
--static --warnings all \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:3.21
FROM alpine:3.22
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \

View File

@@ -408,6 +408,7 @@
"Default": "Default",
"Music": "Music",
"Gaming": "Gaming",
"Livestreams": "Livestreams",
"News": "News",
"Movies": "Movies",
"Download": "Download",
@@ -504,5 +505,6 @@
"carousel_go_to": "Go to slide `x`",
"timeline_parse_error_placeholder_heading": "Unable to parse item",
"timeline_parse_error_placeholder_message": "Invidious encountered an error while trying to parse this item. For more information see below:",
"timeline_parse_error_show_technical_details": "Show technical details"
"timeline_parse_error_show_technical_details": "Show technical details",
"dmca_content": "This video cannot be downloaded on this instance due to a DMCA/copyright infringement letter sent to the instance administrator."
}

View File

@@ -1,5 +1,5 @@
name: invidious
version: 2.20250913.0
version: 2.20250913.0-dev
authors:
- Invidious team <contact@invidious.io>
@@ -38,7 +38,7 @@ development_dependencies:
crystal: ">= 1.10.0, < 2.0.0"
license: AGPLv3
license: AGPL-3.0-only
repository: https://github.com/iv-org/invidious
homepage: https://invidious.io

View File

@@ -0,0 +1 @@
Hello world

View File

@@ -0,0 +1,233 @@
# Due to the way that specs are handled this file cannot be run together with
# everything else without causing a compile time error that'll be incredibly
# annoying to resolve.
#
# TODO: Create different spec categories that can then be ran through make.
# An implementation of this can be seen with the tests for the Crystal compiler itself.
#
# For now run this with `crystal spec spec/http_server/handlers/static_assets_handler_spec.cr -Drunning_by_self`
{% skip_file if compare_versions(Crystal::VERSION, "1.17.0-dev") < 0 || !flag?(:running_by_self) %}
require "http"
require "spectator"
require "../../../src/invidious/http_server/static_assets_handler.cr"
private def get_static_assets_handler
return Invidious::HttpServer::StaticAssetsHandler.new "spec/http_server/handlers/static_assets_handler", directory_listing: false
end
# Slightly modified version of `handle` function from
#
# https://github.com/crystal-lang/crystal/blob/3f369d2c721e9462d9f6126cb0bcd4c6992f0225/spec/std/http/server/handlers/static_file_handler_spec.cr#L5
private def handle(request, handler : HTTP::Handler? = nil, decompress : Bool = false)
io = IO::Memory.new
response = HTTP::Server::Response.new(io)
context = HTTP::Server::Context.new(request, response)
if !handler
handler = get_static_assets_handler
get_static_assets_handler.call context
else
handler.call(context)
end
response.close
io.rewind
HTTP::Client::Response.from_io(io, decompress: decompress)
end
# Makes and yields a temporary file with the given prefix
private def make_temporary_file(prefix, contents = nil, &)
tempfile = File.tempfile(prefix, "static_assets_handler_spec", dir: "spec/http_server/handlers/static_assets_handler")
file_link = "/#{File.basename(tempfile.path)}"
yield tempfile, file_link
ensure
tempfile.try &.delete
end
# Changes the contents of the temporary file after yield
private def cycle_temporary_file_contents(temporary_file, initial, &)
temporary_file.rewind << initial
temporary_file.rewind.flush
yield
temporary_file.rewind << "something else"
temporary_file.rewind.flush
end
# Get relative file path to a file within the static_assets_handler folder
macro get_file_path(basename)
"spec/http_server/handlers/static_assets_handler/#{ {{basename}} }"
end
Spectator.describe StaticAssetsHandler do
it "Can serve a file" do
response = handle HTTP::Request.new("GET", "/test.txt")
expect(response.status_code).to eq(200)
expect(response.body).to eq(File.read(get_file_path("test.txt")))
end
it "Can serve cached file" do
make_temporary_file("cache_test") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "foo") do
expect(temporary_file.rewind.gets_to_end).to eq("foo")
# Should get cached by the first run
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq("foo")
end
# Temporary file is updated after `cycle_temporary_file_contents` is called
# but if the file is successfully cached then we'll only get the original
# contents.
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq("foo")
end
end
it "Adds cache headers" do
response = handle HTTP::Request.new("GET", "/test.txt")
expect(response.headers["cache_control"]).to eq("max-age=2629800")
end
context "Can handle range requests" do
it "Can serve range request" do
headers = HTTP::Headers{"Range" => "bytes=0-2"}
response = handle HTTP::Request.new("GET", "/test.txt", headers)
expect(response.status_code).to eq(206)
expect(response.headers["Content-Range"]?).to eq "bytes 0-2/11"
expect(response.body).to eq "Hel"
end
it "Will cache entire file even if doing partial requests" do
make_temporary_file("range_cache") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "Hello world") do
handle HTTP::Request.new("GET", file_link, HTTP::Headers{"Range" => "bytes=0-2"})
end
# Second request shouldn't have changed
headers = HTTP::Headers{"Range" => "bytes=3-8"}
response = handle HTTP::Request.new("GET", file_link, headers)
expect(response.status_code).to eq(206)
expect(response.body).to eq "lo wor"
end
end
end
context "Is able to support compression" do
def decompressed(string : String)
decompressed = Compress::Gzip::Reader.open(IO::Memory.new(string)) do |gzip|
gzip.gets_to_end
end
return expect(decompressed)
end
it "For full file requests" do
handler = HTTP::CompressHandler.new
handler.next = get_static_assets_handler()
make_temporary_file("check decompression handler") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "Hello world") do
response = handle HTTP::Request.new("GET", file_link, headers: HTTP::Headers{"Accept-Encoding" => "gzip"}), handler: handler
expect(response.headers["Content-Encoding"]).to eq("gzip")
decompressed(response.body).to eq("Hello world")
end
# Are cached requests working?
response = handle HTTP::Request.new("GET", file_link, headers: HTTP::Headers{"Accept-Encoding" => "gzip"}), handler: handler
expect(response.headers["Content-Encoding"]).to eq("gzip")
decompressed(response.body).to eq("Hello world")
# Able to retrieve non gzipped file?
response = handle HTTP::Request.new("GET", file_link), handler: handler
expect(response.body).to eq("Hello world")
expect(response.headers).to_not have_key("Content-Encoding")
end
end
# Inspired by the equivalent tests from upstream
it "For partial file requests" do
handler = HTTP::CompressHandler.new
handler.next = get_static_assets_handler()
make_temporary_file("check_decompression_handler_on_partial_requests") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "Hello world this is a very long string") do
range_response_results = {
"10-20/38" => "d this is a",
"0-0/38" => "H",
"5-9/38" => " worl",
}
range_request_header_value = {"10-20", "5-9", "0-0"}.join(',')
range_response_header_value = range_response_results.keys
response = handle HTTP::Request.new("GET", file_link, headers: HTTP::Headers{"Range" => "bytes=#{range_request_header_value}", "Accept-Encoding" => "gzip"}), handler: handler
expect(response.headers["Content-Encoding"]).to eq("gzip")
# Decompress response
response = HTTP::Client::Response.new(
status: response.status,
headers: response.headers,
body_io: Compress::Gzip::Reader.new(IO::Memory.new(response.body)),
)
count = 0
MIME::Multipart.parse(response) do |headers, part|
part_range = headers["Content-Range"][6..]
expect(part_range).to be_within(range_response_header_value)
expect(part.gets_to_end).to eq(range_response_results[part_range])
count += 1
end
expect(count).to eq(3)
end
# Is the file cached?
temporary_file << "Something else"
temporary_file.flush.rewind
response = handle HTTP::Request.new("GET", file_link, headers: HTTP::Headers{"Accept-Encoding" => "gzip"}), handler: handler
decompressed(response.body).to eq("Hello world this is a very long string")
end
end
end
it "Will not cache additional files if the cache limit is reached" do
5.times do |times|
data = "a" * 1_000_000
make_temporary_file("test cache size limit #{times}") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, data) do
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq(data)
end
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq(data)
end
end
# Cache should be 5 mb so no more files will be cached.
make_temporary_file("test cache size limit uncached") do |temporary_file, file_link|
cycle_temporary_file_contents(temporary_file, "a") do
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to eq("a")
end
response = handle HTTP::Request.new("GET", file_link)
expect(response.status_code).to eq(200)
expect(response.body).to_not eq("a")
end
end
after_each { Invidious::HttpServer::StaticAssetsHandler.clear_cache }
end

View File

@@ -52,7 +52,6 @@ Spectator.describe "parse_video_info" do
expect(info["relatedVideos"][0]["title"]).to eq("$1 vs $250,000,000 Private Island!")
expect(info["relatedVideos"][0]["author"]).to eq("MrBeast")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCX6OQ3DkcsbYNE6H8uQQuVA")
expect(info["relatedVideos"][0]["view_count"]).to eq("230617484")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("230M")
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")
@@ -138,7 +137,6 @@ Spectator.describe "parse_video_info" do
expect(info["relatedVideos"][0]["title"]).to eq("Chris Rea - The Road To Hell 1989 Full Version")
expect(info["relatedVideos"][0]["author"]).to eq("NEA ZIXNH")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCYMEOGcvav3gCgImK2J07CQ")
expect(info["relatedVideos"][0]["view_count"]).to eq("53298661")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("53M")
expect(info["relatedVideos"][0]["author_verified"]).to eq("false")

View File

@@ -75,7 +75,6 @@ Spectator.describe "parse_video_info" do
expect(info["relatedVideos"][0]["id"]).to eq("j7jPzzjbVuk")
expect(info["relatedVideos"][0]["author"]).to eq("Democracy Now!")
expect(info["relatedVideos"][0]["ucid"]).to eq("UCzuqE7-t13O4NIDYJfakrhw")
expect(info["relatedVideos"][0]["view_count"]).to eq("7576")
expect(info["relatedVideos"][0]["short_view_count"]).to eq("7.5K")
expect(info["relatedVideos"][0]["author_verified"]).to eq("true")

View File

@@ -1,3 +1,24 @@
{% if compare_versions(Crystal::VERSION, "1.17.0-dev") >= 0 %}
# Strip StaticFileHandler from the binary
#
# This allows us to compile on 1.17.0 as the compiler won't try to
# semantically check the outdated upstream code.
class Kemal::Config
private def setup_static_file_handler
end
end
# Nullify `Kemal::StaticFileHandler`
#
# Needed until the next release of Kemal after 1.7
class Kemal::StaticFileHandler < HTTP::StaticFileHandler
def call(context : HTTP::Server::Context)
end
end
{% skip_file %}
{% end %}
# Since systems have a limit on number of open files (`ulimit -a`),
# we serve them from memory to avoid 'Too many open files' without needing
# to modify ulimit.

View File

@@ -84,6 +84,7 @@ HTTP_CHUNK_SIZE = 10485760 # ~10MB
CURRENT_BRANCH = {{ "#{`git branch | sed -n '/* /s///p'`.strip}" }}
CURRENT_COMMIT = {{ "#{`git rev-list HEAD --max-count=1 --abbrev-commit`.strip}" }}
CURRENT_VERSION = {{ "#{`git log -1 --format=%ci | awk '{print $1}' | sed s/-/./g`.strip}" }}
CURRENT_TAG = {{ "#{`git tag --points-at HEAD`.strip}" }}
# This is used to determine the `?v=` on the end of file URLs (for cache busting). We
# only need to expire modified assets, so we can use this to find the last commit that changes
@@ -170,15 +171,6 @@ Invidious::Database.check_integrity(CONFIG)
{% puts "\nDone checking player dependencies, now compiling Invidious...\n" %}
{% end %}
# Misc
DECRYPT_FUNCTION =
if sig_helper_address = CONFIG.signature_server.presence
IV::DecryptFunction.new(sig_helper_address)
else
nil
end
# Start jobs
if CONFIG.channel_threads > 0
@@ -231,19 +223,25 @@ error 500 do |env, exception|
error_template(500, exception)
end
static_headers do |env|
env.response.headers.add("Cache-Control", "max-age=2629800")
end
# Init Kemal
public_folder "assets"
Kemal.config.powered_by_header = false
add_handler FilteredCompressHandler.new
add_handler APIHandler.new
add_handler AuthHandler.new
add_handler DenyFrame.new
{% if compare_versions(Crystal::VERSION, "1.17.0-dev") >= 0 %}
Kemal.config.serve_static = false
add_handler Invidious::HttpServer::StaticAssetsHandler.new("assets", directory_listing: false)
{% else %}
public_folder "assets"
static_headers do |env|
env.response.headers.add("Cache-Control", "max-age=2629800")
end
{% end %}
add_context_storage_type(Array(String))
add_context_storage_type(Preferences)
add_context_storage_type(Invidious::User)
@@ -258,6 +256,8 @@ Kemal.config.app_name = "Invidious"
{% end %}
Kemal.run do |config|
config.server.not_nil!.max_request_line_size = 16384
if socket_binding = CONFIG.socket_binding
File.delete?(socket_binding.path)
# Create a socket and set its desired permissions

View File

@@ -143,7 +143,7 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
case attachment.as_h
when .has_key?("videoRenderer")
parse_item(attachment)
.as(SearchVideo)
.as(SearchVideo | ProblematicTimelineItem)
.to_json(locale, json)
when .has_key?("backstageImageRenderer")
json.object do

View File

@@ -153,9 +153,6 @@ class Config
@[YAML::Field(converter: Preferences::FamilyConverter)]
property force_resolve : Socket::Family = Socket::Family::UNSPEC
# External signature solver server socket (either a path to a UNIX domain socket or "<IP>:<Port>")
property signature_server : String? = nil
# Port to listen for connections (overridden by command line argument)
property port : Int32 = 3000
# Host to bind (overridden by command line argument)
@@ -170,11 +167,6 @@ class Config
# Use Innertube's transcripts API instead of timedtext for closed captions
property use_innertube_for_captions : Bool = false
# visitor data ID for Google session
property visitor_data : String? = nil
# poToken for passing bot attestation
property po_token : String? = nil
# Invidious companion
property invidious_companion : Array(CompanionConfig) = [] of CompanionConfig
@@ -262,11 +254,7 @@ class Config
{% end %}
if config.invidious_companion.present?
# invidious_companion and signature_server can't work together
if config.signature_server
puts "Config: You can not run inv_sig_helper and invidious_companion at the same time."
exit(1)
elsif config.invidious_companion_key.empty?
if config.invidious_companion_key.empty?
puts "Config: Please configure a key if you are using invidious companion."
exit(1)
elsif config.invidious_companion_key == "CHANGE_ME!!"
@@ -284,10 +272,8 @@ class Config
companion.builtin_proxy = true
end
end
elsif config.signature_server
puts("WARNING: inv-sig-helper is deprecated. Please switch to Invidious companion: https://docs.invidious.io/companion-installation/")
else
puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/companion-installation/")
puts("WARNING: Invidious companion is required to view and playback videos. For more information see https://docs.invidious.io/installation/")
end
# HMAC_key is mandatory

View File

@@ -2,9 +2,9 @@ module Invidious::Frontend::Misc
extend self
def redirect_url(env : HTTP::Server::Context)
prefs = env.get("preferences").as(Preferences)
preferences = env.get("preferences").as(Preferences)
if prefs.automatic_instance_redirect
if preferences.automatic_instance_redirect
current_page = env.get?("current_page").as(String)
return "/redirect?referer=#{current_page}"
else

View File

@@ -23,6 +23,10 @@ module Invidious::Frontend::WatchPage
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
end
if CONFIG.dmca_content.includes?(video.id)
return "<p id=\"download\">#{translate(locale, "dmca_content")}</p>"
end
url = "/download"
if (CONFIG.invidious_companion.present?)
invidious_companion = CONFIG.invidious_companion.sample

View File

@@ -3,7 +3,19 @@
# IPv6 addresses.
#
class TCPSocket
def initialize(host, port, dns_timeout = nil, connect_timeout = nil, blocking = false, family = Socket::Family::UNSPEC)
{% if compare_versions(Crystal::VERSION, "1.18.0-dev") >= 0 %}
def initialize(host : String, port, dns_timeout = nil, connect_timeout = nil, blocking = false, family = Socket::Family::UNSPEC)
Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo|
super(family: addrinfo.family, type: addrinfo.type, protocol: addrinfo.protocol)
Socket.set_blocking(self.fd, blocking)
connect(addrinfo, timeout: connect_timeout) do |error|
close
error
end
end
end
{% else %}
def initialize(host : String, port, dns_timeout = nil, connect_timeout = nil, blocking = false, family = Socket::Family::UNSPEC)
Addrinfo.tcp(host, port, timeout: dns_timeout, family: family) do |addrinfo|
super(addrinfo.family, addrinfo.type, addrinfo.protocol, blocking)
connect(addrinfo, timeout: connect_timeout) do |error|
@@ -12,6 +24,7 @@ class TCPSocket
end
end
end
{% end %}
end
# :ditto:

View File

@@ -1,349 +0,0 @@
require "uri"
require "socket"
require "socket/tcp_socket"
require "socket/unix_socket"
{% if flag?(:advanced_debug) %}
require "io/hexdump"
{% end %}
private alias NetworkEndian = IO::ByteFormat::NetworkEndian
module Invidious::SigHelper
enum UpdateStatus
Updated
UpdateNotRequired
Error
end
# -------------------
# Payload types
# -------------------
abstract struct Payload
end
struct StringPayload < Payload
getter string : String
def initialize(str : String)
raise Exception.new("SigHelper: String can't be empty") if str.empty?
@string = str
end
def self.from_bytes(slice : Bytes)
size = IO::ByteFormat::NetworkEndian.decode(UInt16, slice)
if size == 0 # Error code
raise Exception.new("SigHelper: Server encountered an error")
end
if (slice.bytesize - 2) != size
raise Exception.new("SigHelper: String size mismatch")
end
if str = String.new(slice[2..])
return self.new(str)
else
raise Exception.new("SigHelper: Can't read string from socket")
end
end
def to_io(io)
# `.to_u16` raises if there is an overflow during the conversion
io.write_bytes(@string.bytesize.to_u16, NetworkEndian)
io.write(@string.to_slice)
end
end
private enum Opcode
FORCE_UPDATE = 0
DECRYPT_N_SIGNATURE = 1
DECRYPT_SIGNATURE = 2
GET_SIGNATURE_TIMESTAMP = 3
GET_PLAYER_STATUS = 4
PLAYER_UPDATE_TIMESTAMP = 5
end
private record Request,
opcode : Opcode,
payload : Payload?
# ----------------------
# High-level functions
# ----------------------
class Client
@mux : Multiplexor
def initialize(uri_or_path)
@mux = Multiplexor.new(uri_or_path)
end
# Forces the server to re-fetch the YouTube player, and extract the necessary
# components from it (nsig function code, sig function code, signature timestamp).
def force_update : UpdateStatus
request = Request.new(Opcode::FORCE_UPDATE, nil)
value = send_request(request) do |bytes|
IO::ByteFormat::NetworkEndian.decode(UInt16, bytes)
end
case value
when 0x0000 then return UpdateStatus::Error
when 0xFFFF then return UpdateStatus::UpdateNotRequired
when 0xF44F then return UpdateStatus::Updated
else
code = value.nil? ? "nil" : value.to_s(base: 16)
raise Exception.new("SigHelper: Invalid status code received #{code}")
end
end
# Decrypt a provided n signature using the server's current nsig function
# code, and return the result (or an error).
def decrypt_n_param(n : String) : String?
request = Request.new(Opcode::DECRYPT_N_SIGNATURE, StringPayload.new(n))
n_dec = self.send_request(request) do |bytes|
StringPayload.from_bytes(bytes).string
end
return n_dec
end
# Decrypt a provided s signature using the server's current sig function
# code, and return the result (or an error).
def decrypt_sig(sig : String) : String?
request = Request.new(Opcode::DECRYPT_SIGNATURE, StringPayload.new(sig))
sig_dec = self.send_request(request) do |bytes|
StringPayload.from_bytes(bytes).string
end
return sig_dec
end
# Return the signature timestamp from the server's current player
def get_signature_timestamp : UInt64?
request = Request.new(Opcode::GET_SIGNATURE_TIMESTAMP, nil)
return self.send_request(request) do |bytes|
IO::ByteFormat::NetworkEndian.decode(UInt64, bytes)
end
end
# Return the current player's version
def get_player : UInt32?
request = Request.new(Opcode::GET_PLAYER_STATUS, nil)
return self.send_request(request) do |bytes|
has_player = (bytes[0] == 0xFF)
player_version = IO::ByteFormat::NetworkEndian.decode(UInt32, bytes[1..4])
has_player ? player_version : nil
end
end
# Return when the player was last updated
def get_player_timestamp : UInt64?
request = Request.new(Opcode::PLAYER_UPDATE_TIMESTAMP, nil)
return self.send_request(request) do |bytes|
IO::ByteFormat::NetworkEndian.decode(UInt64, bytes)
end
end
private def send_request(request : Request, &)
channel = @mux.send(request)
slice = channel.receive
return yield slice
rescue ex
LOGGER.debug("SigHelper: Error when sending a request")
LOGGER.trace(ex.inspect_with_backtrace)
return nil
end
end
# ---------------------
# Low level functions
# ---------------------
class Multiplexor
alias TransactionID = UInt32
record Transaction, channel = ::Channel(Bytes).new
@prng = Random.new
@mutex = Mutex.new
@queue = {} of TransactionID => Transaction
@conn : Connection
@uri_or_path : String
def initialize(@uri_or_path)
@conn = Connection.new(uri_or_path)
listen
end
def listen : Nil
raise "Socket is closed" if @conn.closed?
LOGGER.debug("SigHelper: Multiplexor listening")
spawn do
loop do
begin
receive_data
rescue ex
LOGGER.info("SigHelper: Connection to helper died with '#{ex.message}' trying to reconnect...")
# We close the socket because for some reason is not closed.
@conn.close
loop do
begin
@conn = Connection.new(@uri_or_path)
LOGGER.info("SigHelper: Reconnected to SigHelper!")
rescue ex
LOGGER.debug("SigHelper: Reconnection to helper unsuccessful with error '#{ex.message}'. Retrying")
sleep 500.milliseconds
next
end
break if !@conn.closed?
end
end
Fiber.yield
end
end
end
def send(request : Request)
transaction = Transaction.new
transaction_id = @prng.rand(TransactionID)
# Add transaction to queue
@mutex.synchronize do
# On a 32-bits random integer, this should never happen. Though, just in case, ...
if @queue[transaction_id]?
raise Exception.new("SigHelper: Duplicate transaction ID! You got a shiny pokemon!")
end
@queue[transaction_id] = transaction
end
write_packet(transaction_id, request)
return transaction.channel
end
def receive_data
transaction_id, slice = read_packet
@mutex.synchronize do
if transaction = @queue.delete(transaction_id)
# Remove transaction from queue and send data to the channel
transaction.channel.send(slice)
LOGGER.trace("SigHelper: Transaction unqueued and data sent to channel")
else
raise Exception.new("SigHelper: Received transaction was not in queue")
end
end
end
# Read a single packet from the socket
private def read_packet : {TransactionID, Bytes}
# Header
transaction_id = @conn.read_bytes(UInt32, NetworkEndian)
length = @conn.read_bytes(UInt32, NetworkEndian)
LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} / length #{length}")
if length > 67_000
raise Exception.new("SigHelper: Packet longer than expected (#{length})")
end
# Payload
slice = Bytes.new(length)
@conn.read(slice) if length > 0
LOGGER.trace("SigHelper: payload = #{slice}")
LOGGER.trace("SigHelper: Recv transaction 0x#{transaction_id.to_s(base: 16)} - Done")
return transaction_id, slice
end
# Write a single packet to the socket
private def write_packet(transaction_id : TransactionID, request : Request)
LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} / opcode #{request.opcode}")
io = IO::Memory.new(1024)
io.write_bytes(request.opcode.to_u8, NetworkEndian)
io.write_bytes(transaction_id, NetworkEndian)
if payload = request.payload
payload.to_io(io)
end
@conn.send(io)
@conn.flush
LOGGER.trace("SigHelper: Send transaction 0x#{transaction_id.to_s(base: 16)} - Done")
end
end
class Connection
@socket : UNIXSocket | TCPSocket
{% if flag?(:advanced_debug) %}
@io : IO::Hexdump
{% end %}
def initialize(host_or_path : String)
case host_or_path
when .starts_with?('/')
# Make sure that the file exists
if File.exists?(host_or_path)
@socket = UNIXSocket.new(host_or_path)
else
raise Exception.new("SigHelper: '#{host_or_path}' no such file")
end
when .starts_with?("tcp://")
uri = URI.parse(host_or_path)
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
else
uri = URI.parse("tcp://#{host_or_path}")
@socket = TCPSocket.new(uri.host.not_nil!, uri.port.not_nil!)
end
LOGGER.info("SigHelper: Using helper at '#{host_or_path}'")
{% if flag?(:advanced_debug) %}
@io = IO::Hexdump.new(@socket, output: STDERR, read: true, write: true)
{% end %}
@socket.sync = false
@socket.blocking = false
end
def closed? : Bool
return @socket.closed?
end
def close : Nil
@socket.close if !@socket.closed?
end
def flush(*args, **options)
@socket.flush(*args, **options)
end
def send(*args, **options)
@socket.send(*args, **options)
end
# Wrap IO functions, with added debug tooling if needed
{% for function in %w(read read_bytes write write_bytes) %}
def {{function.id}}(*args, **options)
{% if flag?(:advanced_debug) %}
@io.{{function.id}}(*args, **options)
{% else %}
@socket.{{function.id}}(*args, **options)
{% end %}
end
{% end %}
end
end

View File

@@ -1,53 +0,0 @@
require "http/params"
require "./sig_helper"
class Invidious::DecryptFunction
@last_update : Time = Time.utc - 42.days
def initialize(uri_or_path)
@client = SigHelper::Client.new(uri_or_path)
self.check_update
end
def check_update
# If we have updated in the last 5 minutes, do nothing
return if (Time.utc - @last_update) < 5.minutes
# Get the amount of time elapsed since when the player was updated, in the
# event where multiple invidious processes are run in parallel.
update_time_elapsed = (@client.get_player_timestamp || 301).seconds
if update_time_elapsed > 5.minutes
LOGGER.debug("Signature: Player might be outdated, updating")
@client.force_update
@last_update = Time.utc
end
end
def decrypt_nsig(n : String) : String?
self.check_update
return @client.decrypt_n_param(n)
rescue ex
LOGGER.debug(ex.message || "Signature: Unknown error")
LOGGER.trace(ex.inspect_with_backtrace)
return nil
end
def decrypt_signature(str : String) : String?
self.check_update
return @client.decrypt_sig(str)
rescue ex
LOGGER.debug(ex.message || "Signature: Unknown error")
LOGGER.trace(ex.inspect_with_backtrace)
return nil
end
def get_sts : UInt64?
self.check_update
return @client.get_signature_timestamp
rescue ex
LOGGER.debug(ex.message || "Signature: Unknown error")
LOGGER.trace(ex.inspect_with_backtrace)
return nil
end
end

View File

@@ -0,0 +1,120 @@
{% skip_file if compare_versions(Crystal::VERSION, "1.17.0-dev") < 0 %}
module Invidious::HttpServer
class StaticAssetsHandler < HTTP::StaticFileHandler
# In addition to storing the actual data of a file, it also implements the required
# getters needed for the object to imitate a `File::Stat` within `StaticFileHandler`.
#
# Since the `File::Stat` is created once in `#call` and then passed around to the
# rest of the class's methods, imitating the object allows us to only lookup
# the cache hash once for every request.
#
private record CachedFile, data : Bytes, size : Int64, modification_time : Time do
def directory?
false
end
def file?
true
end
end
CACHE_LIMIT = 5_000_000 # 5MB
@@current_cache_size = 0
@@cached_files = {} of Path => CachedFile
# Returns metadata for the requested file
#
# If the requested file is cached, a `CachedFile` is returned instead of a `File::Stat`.
# This represents the metadata info of a cached file and implements all the methods of `File::Stat` that
# is used by the `StaticAssetsHandler`.
#
# The `CachedFile` also stores the raw bytes of the cached file, and this method serves as the place where
# the cached file is retrieved if it exists. Though the data will only be read in `#serve_file`
private def file_info(expanded_path : Path)
file_path = @public_dir.join(expanded_path.to_kind(Path::Kind.native))
{@@cached_files[file_path]? || File.info?(file_path), file_path}
end
# Add "Cache-Control" header to the response
private def add_cache_headers(response_headers : HTTP::Headers, last_modified : Time) : Nil
super; response_headers["Cache-Control"] = "max-age=2629800"
end
# Serves and caches the file at the given path.
#
# This is an override of `serve_file` to allow serving a file from memory, and to cache it
# it as needed.
private def serve_file(context : HTTP::Server::Context, file_info, file_path : Path, original_file_path : Path, last_modified : Time)
context.response.content_type = MIME.from_filename(original_file_path.to_s, "application/octet-stream")
range_header = context.request.headers["Range"]?
# If the file is cached we can just directly serve it
if file_info.is_a? CachedFile
return dispatch_serve(context, file_info.data, file_info, range_header)
end
# Otherwise we'll need to read from disk and cache it
retrieve_bytes_from = IO::Memory.new
File.open(file_path) do |file|
# We cannot cache partial data so we'll rewind and read from the start
if range_header
dispatch_serve(context, file, file_info, range_header)
IO.copy(file.rewind, retrieve_bytes_from)
else
context.response.output = IO::MultiWriter.new(context.response.output, retrieve_bytes_from, sync_close: true)
dispatch_serve(context, file, file_info, range_header)
end
end
return flush_io_to_cache(retrieve_bytes_from, file_path, file_info)
end
# Writes file data to the cache
private def flush_io_to_cache(io, file_path, file_info)
if (@@current_cache_size += file_info.size) <= CACHE_LIMIT
@@cached_files[file_path] = CachedFile.new(io.to_slice, file_info.size, file_info.modification_time)
end
end
# Either send the file in full, or just fragments of it depending on the request
private def dispatch_serve(context, file, file_info, range_header)
if range_header
# an IO is needed for `serve_file_range`
file = file.is_a?(Bytes) ? IO::Memory.new(file, writeable: false) : file
serve_file_range(context, file, range_header, file_info)
else
context.response.headers["Accept-Ranges"] = "bytes"
serve_file_full(context, file, file_info)
end
end
# If we're serving the full file right away then there's no need for an IO at all.
private def serve_file_full(context : HTTP::Server::Context, file : Bytes, file_info)
context.response.status = :ok
context.response.content_length = file_info.size
context.response.write file
end
# Serves segments of a file based on the `Range header`
#
# An override of `serve_file_range` to allow using a generic IO rather than a `File`.
# Literally the same code as what we inherited but just with the `file` argument's type
# being set to `IO` rather than `File`
#
# Can be removed once https://github.com/crystal-lang/crystal/issues/15817 is fixed.
private def serve_file_range(context : HTTP::Server::Context, file : IO, range_header : String, file_info)
# Paste in the body of inherited serve_file_range
{{@type.superclass.methods.select(&.name.==("serve_file_range"))[0].body}}
end
# Clear cached files.
#
# This is only used in the specs to clear the cache before each handler test
def self.clear_cache
@@current_cache_size = 0
return @@cached_files.clear
end
end
end

View File

@@ -266,7 +266,6 @@ module Invidious::JSONify::APIv1
json.field "lengthSeconds", rv["length_seconds"]?.try &.to_i
json.field "viewCountText", rv["short_view_count"]?
json.field "viewCount", rv["view_count"]?.try &.empty? ? nil : rv["view_count"].to_i64
json.field "published", rv["published"]?
if rv["published"]?.try &.presence
json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))

View File

@@ -264,11 +264,11 @@ module Invidious::Routes::Channels
id = env.params.url["id"]
ucid = env.params.query["ucid"]?
prefs = env.get("preferences").as(Preferences)
preferences = env.get("preferences").as(Preferences)
locale = prefs.locale
locale = preferences.locale
thin_mode = env.params.query["thin_mode"]? || prefs.thin_mode
thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode
thin_mode = thin_mode == "true"
nojs = env.params.query["nojs"]?

View File

@@ -1,5 +1,5 @@
module Invidious::Routes::Companion
# /companion
# GET /companion
def self.get_companion(env)
url = env.request.path
if env.request.query
@@ -16,6 +16,23 @@ module Invidious::Routes::Companion
end
end
# POST /companion
def self.post_companion(env)
url = env.request.path
if env.request.query
url += "?#{env.request.query}"
end
begin
COMPANION_POOL.client do |wrapper|
wrapper.client.post(url, env.request.headers, env.request.body) do |resp|
return self.proxy_companion(env, resp)
end
end
rescue ex
end
end
def self.options_companion(env)
url = env.request.path
if env.request.query

View File

@@ -33,7 +33,8 @@ module Invidious::Routes::Embed
end
def self.show(env)
locale = env.get("preferences").as(Preferences).locale
preferences = env.get("preferences").as(Preferences)
locale = preferences.locale
id = env.params.url["id"]
plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
@@ -45,8 +46,6 @@ module Invidious::Routes::Embed
env.params.query.delete("playlist")
end
preferences = env.get("preferences").as(Preferences)
if id.includes?("%20") || id.includes?("+") || env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
id = env.params.url["id"].gsub("%20", "").delete("+")

View File

@@ -43,13 +43,14 @@ module Invidious::Routes::Feeds
end
def self.trending(env)
locale = env.get("preferences").as(Preferences).locale
preferences = env.get("preferences").as(Preferences)
locale = preferences.locale
trending_type = env.params.query["type"]?
trending_type ||= "Default"
region = env.params.query["region"]?
region ||= env.get("preferences").as(Preferences).region
region ||= preferences.region
begin
trending, plid = fetch_trending(trending_type, region, locale)

View File

@@ -98,6 +98,8 @@ module Invidious::Routes::Login
begin
validate_request(tokens[0], answer, env.request, HMAC_KEY, locale)
rescue ex : InfoException
return error_template(400, InfoException.new("Erroneous CAPTCHA"))
rescue ex
return error_template(400, ex)
end

View File

@@ -225,10 +225,10 @@ module Invidious::Routes::Playlists
end
def self.add_playlist_items_page(env)
prefs = env.get("preferences").as(Preferences)
locale = prefs.locale
preferences = env.get("preferences").as(Preferences)
locale = preferences.locale
region = env.params.query["region"]? || prefs.region
region = env.params.query["region"]? || preferences.region
user = env.get? "user"
sid = env.get? "sid"

View File

@@ -2,12 +2,11 @@
module Invidious::Routes::PreferencesRoute
def self.show(env)
locale = env.get("preferences").as(Preferences).locale
preferences = env.get("preferences").as(Preferences)
locale = preferences.locale
referer = get_referer(env)
preferences = env.get("preferences").as(Preferences)
templated "user/preferences"
end

View File

@@ -37,10 +37,10 @@ module Invidious::Routes::Search
end
def self.search(env)
prefs = env.get("preferences").as(Preferences)
locale = prefs.locale
preferences = env.get("preferences").as(Preferences)
locale = preferences.locale
region = env.params.query["region"]? || prefs.region
region = env.params.query["region"]? || preferences.region
query = Invidious::Search::Query.new(env.params.query, :regular, region)

View File

@@ -2,7 +2,8 @@
module Invidious::Routes::Watch
def self.handle(env)
locale = env.get("preferences").as(Preferences).locale
preferences = env.get("preferences").as(Preferences)
locale = preferences.locale
region = env.params.query["region"]?
if env.params.query.to_s.includes?("%20") || env.params.query.to_s.includes?("+")
@@ -38,8 +39,6 @@ module Invidious::Routes::Watch
nojs ||= "0"
nojs = nojs == "1"
preferences = env.get("preferences").as(Preferences)
user = env.get?("user").try &.as(User)
if user
subscriptions = user.subscriptions

View File

@@ -227,6 +227,7 @@ module Invidious::Routing
def register_companion_routes
if CONFIG.invidious_companion.present?
get "/companion/*", Routes::Companion, :get_companion
post "/companion/*", Routes::Companion, :post_companion
options "/companion/*", Routes::Companion, :options_companion
end
end

View File

@@ -4,19 +4,25 @@ def fetch_trending(trending_type, region, locale)
plid = nil
browse_id = ""
case trending_type.try &.downcase
when "music"
params = "4gINGgt5dG1hX2NoYXJ0cw%3D%3D"
when "gaming"
params = "4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D"
when "movies"
params = "4gIKGgh0cmFpbGVycw%3D%3D"
else # Default
params = ""
browse_id = "UCOpNcN46UbXVtpKMrmU4Abg"
params = "Egh0cmVuZGluZw%3D%3D"
when "livestreams"
browse_id = "UC4R8DWoMoI7CAwX8_LjQHig"
params = "EgdsaXZldGFikgEDCKEK"
else
# Livestreams is the default one as Youtube removed
# the aggregated trending page
# https://github.com/iv-org/invidious/issues/5397#issuecomment-3218928458
browse_id = "UC4R8DWoMoI7CAwX8_LjQHig"
params = "EgdsaXZldGFikgEDCKEK"
end
client_config = YoutubeAPI::ClientConfig.new(region: region)
initial_data = YoutubeAPI.browse("FEtrending", params: params, client_config: client_config)
initial_data = YoutubeAPI.browse(browse_id, params: params, client_config: client_config)
items, _ = extract_items(initial_data)

View File

@@ -326,6 +326,14 @@ end
def fetch_video(id, region)
info = extract_video_info(video_id: id)
if info.nil?
raise InfoException.new("Invidious companion is not available. \
Video playback cannot continue. \
If you are the administrator of this instance, install Invidious companion \
following the installation instructions \
<a href=\"https://docs.invidious.io/installation/\">https://docs.invidious.io/installation/</a>")
end
if reason = info["reason"]?
if reason == "Video unavailable"
raise NotFoundException.new(reason.as_s || "")

View File

@@ -25,11 +25,6 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
# "4,088,033 views", only available on compact renderer
# and when video is not a livestream
view_count = related.dig?("viewCountText", "simpleText")
.try &.as_s.gsub(/\D/, "")
short_view_count = related.try do |r|
HelperExtractors.get_short_view_count(r).to_s
end
@@ -51,7 +46,6 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
"author" => author || JSON::Any.new(""),
"ucid" => JSON::Any.new(ucid || ""),
"length_seconds" => JSON::Any.new(length || "0"),
"view_count" => JSON::Any.new(view_count || "0"),
"short_view_count" => JSON::Any.new(short_view_count || "0"),
"author_verified" => JSON::Any.new(author_verified),
"published" => JSON::Any.new(published || ""),
@@ -59,11 +53,12 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
end
def extract_video_info(video_id : String)
# Init client config for the API
client_config = YoutubeAPI::ClientConfig.new
# Fetch data from the player endpoint
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
player_response = YoutubeAPI.player(video_id: video_id)
if player_response.nil?
return nil
end
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
@@ -111,37 +106,6 @@ def extract_video_info(video_id : String)
params = parse_video_info(video_id, player_response)
params["reason"] = JSON::Any.new(reason) if reason
if !CONFIG.invidious_companion.present?
if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
LOGGER.warn("Missing URLs for adaptive formats, falling back to other YT clients.")
players_fallback = {YoutubeAPI::ClientType::TvSimply, YoutubeAPI::ClientType::WebMobile}
players_fallback.each do |player_fallback|
client_config.client_type = player_fallback
next if !(player_fallback_response = try_fetch_streaming_data(video_id, client_config))
adaptive_formats = player_fallback_response.dig?("streamingData", "adaptiveFormats")
if adaptive_formats && (adaptive_formats.dig?(0, "url") || adaptive_formats.dig?(0, "signatureCipher"))
streaming_data = player_response["streamingData"].as_h
streaming_data["adaptiveFormats"] = adaptive_formats
player_response["streamingData"] = JSON::Any.new(streaming_data)
break
end
rescue InfoException
next LOGGER.warn("Failed to fetch streams with #{player_fallback}")
end
end
# Seems like video page can still render even without playable streams.
# its better than nothing.
#
# # Were we able to find playable video streams?
# if player_response.dig?("streamingData", "adaptiveFormats", 0, "url").nil?
# # No :(
# end
end
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
params[f] = player_response[f] if player_response[f]?
end
@@ -169,7 +133,7 @@ end
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
response = YoutubeAPI.player(video_id: id, params: "2AMB", client_config: client_config)
response = YoutubeAPI.player(video_id: id)
playability_status = response["playabilityStatus"]["status"]
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
@@ -481,26 +445,15 @@ end
private def convert_url(fmt)
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
sp = cfr["sp"]
url = URI.parse(cfr["url"])
params = url.query_params
LOGGER.debug("convert_url: Decoding '#{cfr}'")
unsig = DECRYPT_FUNCTION.try &.decrypt_signature(cfr["s"])
params[sp] = unsig if unsig
else
url = URI.parse(fmt["url"].as_s)
params = url.query_params
end
n = DECRYPT_FUNCTION.try &.decrypt_nsig(params["n"])
params["n"] = n if n
if token = CONFIG.po_token
params["pot"] = token
end
url.query_params = params
LOGGER.trace("convert_url: new url is '#{url}'")

View File

@@ -12,7 +12,7 @@
<div class="pure-u-1-2 flex-left flexible">
<div class="channel-profile">
<img src="/ggpht<%= channel_profile_pic %>" alt="" />
<span><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
<span class="channel-name"><%= author %></span><% if !channel.verified.nil? && channel.verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %>
</div>
</div>

View File

@@ -1,3 +1,6 @@
<%
invidious_companion_check_id = invidious_companion_encrypt(video.id) if invidious_companion
%>
<video style="outline:none;width:100%;background-color:#000" playsinline poster="<%= thumbnail %>"
id="player" class="on-video_player video-js player-style-<%= params.player_style %>"
preload="<% if params.preload %>auto<% else %>none<% end %>"
@@ -23,7 +26,7 @@
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
"&check=#{invidious_companion_check_id}" if (invidious_companion)
bitrate = fmt["bitrate"]
mimetype = HTML.escape(fmt["mimeType"].as_s)
@@ -39,7 +42,7 @@
<% if params.quality == "dash"
src_url = "/api/manifest/dash/id/" + video.id + "?local=true&unique_res=1"
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
"&check=#{invidious_companion_check_id}" if (invidious_companion)
%>
<source src="<%= src_url %>" type='application/dash+xml' label="dash">
<% end %>
@@ -51,7 +54,7 @@
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
src_url += "&local=true" if params.local
src_url = invidious_companion.public_url.to_s + src_url +
"&check=#{invidious_companion_encrypt(video.id)}" if (invidious_companion)
"&check=#{invidious_companion_check_id}" if (invidious_companion)
quality = fmt["quality"]
mimetype = HTML.escape(fmt["mimeType"].as_s)
@@ -68,15 +71,17 @@
<% preferred_captions.each do |caption|
api_captions_url = "/api/v1/captions/"
api_captions_url = invidious_companion.public_url.to_s + api_captions_url if (invidious_companion)
api_captions_check_id = "&check=#{invidious_companion_check_id}"
%>
<track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>">
<track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %><%= api_captions_check_id %>" label="<%= caption.name %>">
<% end %>
<% captions.each do |caption|
api_captions_url = "/api/v1/captions/"
api_captions_url = invidious_companion.public_url.to_s + api_captions_url if (invidious_companion)
api_captions_check_id = "&check=#{invidious_companion_check_id}"
%>
<track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %>" label="<%= caption.name %>">
<track kind="captions" src="<%= api_captions_url %><%= video.id %>?label=<%= caption.name %><%= api_captions_check_id %>" label="<%= caption.name %>">
<% end %>
<% end %>
</video>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="<%= env.get("preferences").as(Preferences).locale %>">
<html lang="<%= preferences.locale %>">
<head>
<meta charset="utf-8">

View File

@@ -21,7 +21,7 @@
</div>
<div class="pure-u-1-3">
<div class="pure-g" style="text-align:right">
<% {"Default", "Music", "Gaming", "Movies"}.each do |option| %>
<% {"Livestreams", "Gaming"}.each do |option| %>
<div class="pure-u-1 pure-md-1-3">
<% if trending_type == option %>
<b><%= translate(locale, option) %></b>

View File

@@ -38,7 +38,7 @@
"params" => {
"comments": ["youtube"]
},
"preferences" => prefs,
"preferences" => preferences,
"base_url" => "/api/v1/post/#{URI.encode_www_form(id)}/comments",
"ucid" => ucid
}.to_pretty_json

View File

@@ -1,6 +1,7 @@
<%
locale = env.get("preferences").as(Preferences).locale
dark_mode = env.get("preferences").as(Preferences).dark_mode
preferences = env.get("preferences").as(Preferences)
locale = preferences.locale
dark_mode = preferences.dark_mode
%>
<!DOCTYPE html>
<html lang="<%= locale %>">
@@ -149,7 +150,24 @@
<i class="icon ion-ios-wallet"></i>
<a href="https://invidious.io/donate/"><%= translate(locale, "footer_donate_page") %></a>
</span>
<span><%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %></span>
<span>
<%= translate(locale, "Current version: ") %>
<% if CONFIG.modified_source_code_url %>
<a href="<%= CONFIG.modified_source_code_url %>/commit/<%= CURRENT_COMMIT %>"><%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %></a>
<% else %>
<a href="https://github.com/iv-org/invidious/commit/<%= CURRENT_COMMIT %>"><%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %></a>
<% end %>
@ <%= CURRENT_BRANCH %>
<% if CURRENT_TAG != "" %>
(
<% if CONFIG.modified_source_code_url %>
<a href="<%= CONFIG.modified_source_code_url %>/releases/tag/<%= CURRENT_TAG %>"><%= CURRENT_TAG %></a>
<% else %>
<a href="https://github.com/iv-org/invidious/releases/tag/<%= CURRENT_TAG %>"><%= CURRENT_TAG %></a>
<% end %>
)
<% end %>
</span>
</div>
</div>
</footer>

View File

@@ -65,7 +65,8 @@ we're going to need to do it here in order to allow for translations.
"vr" => video.vr?,
"projection_type" => video.projection_type,
"local_disabled" => CONFIG.disabled?("local"),
"support_reddit" => true
"support_reddit" => true,
"live_now" => video.live_now
}.to_pretty_json
%>
</script>
@@ -229,7 +230,7 @@ we're going to need to do it here in order to allow for translations.
<% if !video.author_thumbnail.empty? %>
<img src="/ggpht<%= URI.parse(video.author_thumbnail).request_target %>" alt="" />
<% end %>
<span id="channel-name"><%= author %><% if !video.author_verified.nil? && video.author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></span>
<span id="channel-name" class="channel-name"><%= author %><% if !video.author_verified.nil? && video.author_verified %>&nbsp;<i class="icon ion ion-md-checkmark-circle"></i><% end %></span>
</div>
</a>
</div>
@@ -354,9 +355,8 @@ we're going to need to do it here in order to allow for translations.
<div class="pure-u-10-24" style="text-align:right">
<b class="width:100%"><%=
views = rv["view_count"]?.try &.to_i?
views ||= rv["view_count_short"]?.try { |x| short_text_to_number(x) }
translate_count(locale, "generic_views_count", views || 0, NumberFormatting::Short)
views = short_text_to_number(rv["short_view_count"]? || "0")
translate_count(locale, "generic_views_count", views, NumberFormatting::Short)
%></b>
</div>
</h5>

View File

@@ -442,6 +442,7 @@ private module Parsers
if content_container = special_category_container["horizontalListRenderer"]?
elsif content_container = special_category_container["expandedShelfContentsRenderer"]?
elsif content_container = special_category_container["verticalListRenderer"]?
elsif content_container = special_category_container["gridRenderer"]?
else
# Anything else, such as `horizontalMovieListRenderer` is currently unsupported.
return

View File

@@ -199,10 +199,6 @@ module YoutubeAPI
# conf_1 = ClientConfig.new(region: "NO")
# YoutubeAPI::search("Kollektivet", params: "", client_config: conf_1)
#
# # Use the Android client to request video streams URLs
# conf_2 = ClientConfig.new(client_type: ClientType::Android)
# YoutubeAPI::player(video_id: "dQw4w9WgXcQ", client_config: conf_2)
#
#
struct ClientConfig
# Type of client to emulate.
@@ -335,10 +331,6 @@ module YoutubeAPI
client_context["client"]["platform"] = platform
end
if CONFIG.visitor_data.is_a?(String)
client_context["client"]["visitorData"] = CONFIG.visitor_data.as(String)
end
return client_context
end
@@ -455,61 +447,23 @@ module YoutubeAPI
end
####################################################################
# player(video_id, params, client_config?)
# player(video_id)
#
# Requests the youtubei/v1/player endpoint with the required headers
# and POST data in order to get a JSON reply.
# Requests the youtubei/v1/player Invidious Companion endpoint with
# the requested video ID.
#
# The requested data is a video ID (`v=` parameter), with some
# additional parameters, formatted as a base64 string.
# The requested data is a video ID (`v=` parameter).
#
# An optional ClientConfig parameter can be passed, too (see
# `struct ClientConfig` above for more details).
#
def player(
video_id : String,
*, # Force the following parameters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil,
)
# Playback context, separate because it can be different between clients
playback_ctx = {
"html5Preference" => "HTML5_PREF_WANTS",
"referer" => "https://www.youtube.com/watch?v=#{video_id}",
} of String => String | Int64
if {"WEB", "TVHTML5"}.any? { |s| client_config.name.starts_with? s }
if sts = DECRYPT_FUNCTION.try &.get_sts
playback_ctx["signatureTimestamp"] = sts.to_i64
end
end
# JSON Request data, required by the API
def player(video_id : String)
# JSON Request data, required by Invidious Companion
data = {
"contentCheckOk" => true,
"videoId" => video_id,
"context" => self.make_context(client_config, video_id),
"racyCheckOk" => true,
"user" => {
"lockedSafetyMode" => false,
},
"playbackContext" => {
"contentPlaybackContext" => playback_ctx,
},
"serviceIntegrityDimensions" => {
"poToken" => CONFIG.po_token,
},
}
# Append the additional parameters if those were provided
if params != ""
data["params"] = params
end
if CONFIG.invidious_companion.present?
return self._post_invidious_companion("/youtubei/v1/player", data)
else
return self._post_json("/youtubei/v1/player", data, client_config)
return nil
end
end
@@ -635,10 +589,6 @@ module YoutubeAPI
headers["User-Agent"] = user_agent
end
if CONFIG.visitor_data.is_a?(String)
headers["X-Goog-Visitor-Id"] = CONFIG.visitor_data.as(String)
end
# Logging
LOGGER.debug("YoutubeAPI: Using endpoint: \"#{endpoint}\"")
LOGGER.trace("YoutubeAPI: ClientConfig: #{client_config}")