1
0
Fork 0
forked from loeade/invidious

Compare commits

...

92 commits

Author SHA1 Message Date
a4ee00c28d Merge branch 'master' of https://git.xn--la-fka.de/loeade/invidious 2025-02-12 12:05:34 +01:00
syeopite
164d764d55
API: Add a 'published' video parameter for related videos (#4149) 2025-01-22 11:38:12 -08:00
syeopite
4a31da4000
User: Ensure IO is properly closed when importing NewPipe subscriptions (#4346) 2025-01-22 11:36:58 -08:00
syeopite
831017f403
Frontend: Carry over audio-only mode in playlist links (#4784) 2025-01-22 11:35:33 -08:00
syeopite
52daafe047
Videos: Fix missing host parameter on playback URLs when local=true (#4992) 2025-01-22 11:34:46 -08:00
syeopite
dca130ca6f
Routes: Clean ajax actions handlers (#5036) 2025-01-22 11:33:51 -08:00
syeopite
086c6209ab
Remove stdlib override for proxy initialization (#5065) 2025-01-22 11:33:20 -08:00
syeopite
0d398c9d1a
API: Add support for author thumbnails in search api for videos (#5072) 2025-01-22 11:32:21 -08:00
syeopite
dc38bcdf17
Kemal: Skip route if response was closed by handlers (#5073) 2025-01-22 11:30:45 -08:00
syeopite
d5442d45bc
API: Fix video thumbnails in mixes (#5116) 2025-01-22 11:29:12 -08:00
syeopite
d4f0560e80
CI: Drop support for versions prior to 1.12 and add 1.15.0 (#5148) 2025-01-22 11:28:38 -08:00
syeopite
eae3c42dab
Videos: Set language for dash audio streams and sort (#5149) 2025-01-22 11:25:39 -08:00
syeopite
c0131d8646
Warn when any top-level config is "CHANGE_ME!!" (#5150) 2025-01-22 11:16:24 -08:00
syeopite
21fd717701
Comment out http_proxy in example config (#5151)
The http_proxy section was not commented out in the example config
causing Invidious to error out unless an HTTP proxy was configured.

This problem affects new manual installs in which the example config
is copied to create the actual config Invidious uses
2025-01-22 11:11:42 -08:00
syeopite
8ee73aa0c1
Remove formatter check on container workflows (#5153) 2025-01-22 19:07:24 +00:00
Giuliano Macedo
6e3ec10d76
feat(manifset): improved adaptationset label 2025-01-22 11:01:37 -08:00
GTechAlpha
d95ae7e6a5
Add audio track info to dash manifest, if present
- language id
  - language display name
  - main/default track
Sort audio formats so that main/default is first (for clients not using dash)

* Note: this should be a non-breaking change; if audio track info is not availablle, the behavior does not change from current
2025-01-22 11:01:37 -08:00
syeopite
d36f372bd1
CI: Add support for 1.15.0 2025-01-22 10:34:24 -08:00
syeopite
58c65e921f
CI: Drop support for versions prior to 1.12.0 2025-01-22 10:34:24 -08:00
syeopite
5d9ed95ffd
Warn when any top-level config is "CHANGE_ME!!" 2025-01-22 10:34:04 -08:00
syeopite
033e42a981
Comment out http_proxy in example config 2025-01-22 10:33:34 -08:00
syeopite
bfa6da2474
Make Invidious compliant to Crystal 1.15 formatting rules (#5014) 2025-01-22 18:32:35 +00:00
syeopite
097b4f0433
CI: Use separate shards cache for lint step
Ameba could be built with an older version of Crystal that follows
a different set of formatting rules than the latest version causing
the Lint/Formatting rule to fail when in actuality the code is actually
compliant with the formatting rules in the latest version of Crystal
2025-01-20 16:39:33 -08:00
syeopite
e1378702af
Apply upcoming formatting rules from Crystal 1.15 2025-01-20 16:15:13 -08:00
Émilien (perso)
b13f77b5af
Update bug report issue message 2025-01-09 14:21:28 +01:00
Brahim Hadriche
047ead8080 Fix video thumbnails in mixes 2024-12-16 16:54:04 -05:00
ChunkyProgrammer
bba1769f4b Use a find instead of an each loop 2024-11-17 13:12:56 -05:00
ChunkyProgrammer
6b0e4e6817 Put temp.delete inside ensure block 2024-11-17 13:12:56 -05:00
ChunkyProgrammer
6abee5de99 Ensure IO is properly closed when importing NewPipe subscriptions 2024-11-17 13:12:56 -05:00
Samantaz Fox
9892604758
Prepare for next release 2024-11-10 21:40:32 +01:00
Samantaz Fox
5d2dd40bc3
Release v2.20241110.0 2024-11-10 21:35:03 +01:00
Samantaz Fox
699d53ad41
Update shard.yml metadata (#5066)
Changes are mostly based off of the Crystal compiler's own shard.yml

Remember to bump the version attribute when creating a release!!!
2024-11-10 21:03:13 +01:00
Samantaz Fox
3ac8978e96
VideoProxy: Handle 302 redirects in chunked section 2024-11-10 18:15:24 +01:00
Samantaz Fox
e7a93fcc18
API: Replace any URL in HLS manifests 2024-11-10 18:13:30 +01:00
Samantaz Fox
aa33d9b7ec
Videos: Fix missing host parameter on playback URLs when local=true 2024-11-10 18:13:30 +01:00
Samantaz Fox
2150264d84
Update CHANGELOG.md 2024-11-10 18:00:26 +01:00
Samantaz Fox
d42561d74a
API: Add "sort_by" parameter to channels/shorts endpoint (#5071)
Small follow up to PR 5059

No related issue
2024-11-10 17:50:00 +01:00
Samantaz Fox
7092bb8855
Docker: Install tzdata in Dockerfile (#5070)
Should close 5067
2024-11-10 17:48:18 +01:00
Samantaz Fox
d7c35e6e3d
Videos: Stop using TVHTML5_SIMPLY_EMBEDDED_PLAYER (#5063)
The age restriction bypass does not work anymore with this client.
See: https://github.com/iv-org/invidious/issues/2189#issuecomment-2437740627

Related to 2189
2024-11-10 17:45:58 +01:00
Samantaz Fox
bc86fb8a82
Routing: Deprecate old channel API routes (#5045)
Deprecate the following routes:

* /api/v1/channels/videos/:ucid
* /api/v1/channels/latest/:ucid
* /api/v1/channels/playlists/:ucid
* /api/v1/channels/community/:ucid
* /api/v1/channels/search/:ucid

in favor of:

* /api/v1/channels/:ucid/videos
* /api/v1/channels/:ucid/latest
* /api/v1/channels/:ucid/playlists
* /api/v1/channels/:ucid/community
* /api/v1/channels/:ucid/search

No related issue
2024-11-10 17:44:45 +01:00
Samantaz Fox
ec82c2f539
Videos: use WEB client instead of WEB CREATOR (#4984)
Use the WEB client when a potoken is configured, otherwise try with Android
test suite if there is no potoken configured.

This PR reverts some of the changes made in 4928

Related to 4734
2024-11-10 17:41:54 +01:00
Samantaz Fox
4b363e32fa
Parsers: Fix parsing live_now and premiere_timestamp (#4934)
This pull request fixes the parsing for the 'live_now' and 'premiere_timestamp'
variables so that they work without the 'microformat' data being present.

Related to 4929
2024-11-10 17:36:49 +01:00
syeopite
7a15318fbc
Skip route if resp got closed by before handlers 2024-11-10 05:45:06 +00:00
ChunkyProgrammer
5fa87cc27c Add support for author thumbnails in search api for videos 2024-11-09 22:31:41 -05:00
Brahim Hadriche
d2123b4682 Sort channel shorts API 2024-11-09 17:49:06 -05:00
Émilien (perso)
0f8f32bca8 remove explicit usage of WEB 2024-11-09 22:21:09 +01:00
Emilien
f3e93ca83d revert back to www.youtube.com when client_config.screen embed 2024-11-09 22:21:09 +01:00
Emilien
82b1506ccc remove usage of WebEmbeddedPlayer 2024-11-09 22:21:09 +01:00
Emilien
b9ad9bd723 use WEB when po_token + android test suite when no po_token 2024-11-09 22:21:09 +01:00
syeopite
8bf7e02978
Change authors section to reflect current state 2024-11-09 13:04:10 -08:00
Samantaz Fox
1a49e798c8
Docker: Install tzdata in Dockerfile 2024-11-09 21:52:06 +01:00
syeopite
9d54cf903e
Update shard.yml metadata 2024-11-08 15:54:37 -08:00
syeopite
1333fed26c
Remove stdlib override for proxy initialization
HTTP Proxy is now initialized in the make_client function
2024-11-08 15:28:12 -08:00
Émilien (perso)
09ccea1d31
remove usage of TVHTML5_SIMPLY_EMBEDDED_PLAYER 2024-11-08 22:01:23 +01:00
Samantaz Fox
6da18ddc41
Routing: Also remove outdated comment about notification routes 2024-10-31 11:52:09 +01:00
Samantaz Fox
cdf93b29e6
Routing: Remove deprecated /api/v1/channels/.../:ucid routes 2024-10-31 11:51:33 +01:00
RadoslavL
eed14d08a8
Update src/invidious/jsonify/api_v1/video_json.cr
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2024-10-31 09:59:06 +02:00
Samantaz Fox
b0c7dd9771
HTML: Replace hidden 'action' input with query parameter
The server side can only handle parameters passed as URL query
parameters and not inside the request body
2024-10-29 22:14:27 +01:00
Samantaz Fox
dbdf2ad23a
Routes: Simplify actions in watch_ajax 2024-10-29 18:27:53 +01:00
Samantaz Fox
dbd96c77e4
Routes: Simplify actions in token_ajax 2024-10-29 18:21:58 +01:00
Samantaz Fox
e453a2a682
Routes: Simplify actions in subscription_ajax 2024-10-29 18:16:52 +01:00
Samantaz Fox
7e4b3b182a
Routes: Simplify actions in playlist_ajax 2024-10-29 18:09:50 +01:00
absidue
b2a83991d1 Fix parsing live_now and premiere_timestamp 2024-09-20 18:46:00 +02:00
Samantaz Fox
9d91ac3b88
Use snake case for all variables 2024-08-26 20:17:45 +00:00
RadoslavL
b526f48120 Changed Unix time to Rfc3339 time and removed NaN message 2024-08-16 23:57:49 +03:00
RadoslavL
e8cd631b2d Formatting 2024-08-16 14:13:05 +03:00
RadoslavL
69ff6def5f Removed useless variable 2024-08-16 14:11:28 +03:00
RadoslavL
26dc9dc99c Solution 2024-08-16 14:08:04 +03:00
RadoslavL
2d6b46c926 Fixed a really easy mistake 2024-08-16 14:05:13 +03:00
RadoslavL
cab02d4959 Corrected usage of publishedText variable throughout the code 2024-08-16 13:54:27 +03:00
Krystof Pistek
5f590dda80
Carry over audio-only mode in playlist links 2024-08-07 20:58:08 +02:00
RadoslavL
7b7197cde8 retrigger checks 2024-04-22 16:26:49 +03:00
RadoslavL
6861148290 Moved code around and fixed a problem 2023-11-24 11:24:56 +02:00
RadoslavL
03f9962a47 This should work 2023-11-14 10:00:18 +02:00
RadoslavL
d098e5ae9b I hope it works at this point 2023-11-14 09:58:37 +02:00
RadoslavL
4c486634e2 Another attempt at fixing the issue 2023-11-14 09:56:06 +02:00
RadoslavL
3bced4e12b Fixed another issue 2023-11-14 09:51:12 +02:00
RadoslavL
0d22af6564 Moved methods around 2023-11-14 09:47:16 +02:00
RadoslavL
2a6a32e667 Fixed an issue 2023-11-14 09:43:52 +02:00
RadoslavL
50da6cf3e7
Organize the code better
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2023-11-12 20:52:11 +02:00
RadoslavL
7388e4ca72
Add translation to the publishedText parameter
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2023-11-12 20:51:33 +02:00
RadoslavL
be216fff94 Added the text version of the published parameter 2023-11-12 08:37:13 +02:00
RadoslavL
a0d24190b8 Made published be an optional parameter 2023-11-08 19:09:16 +02:00
RadoslavL
76369eb599 Removed unused attribute 2023-11-08 10:18:29 +02:00
RadoslavL
6236cea33e
Changed some variable names
Co-authored-by: syeopite <70992037+syeopite@users.noreply.github.com>
2023-11-08 10:13:16 +02:00
RadoslavL
fa59f41f7b Fixed an issue 2023-10-11 09:12:27 +03:00
RadoslavL
20ca1ebcc0 Used the decode_date function instead 2023-10-11 09:08:23 +03:00
RadoslavL
b0b4f09b3a Seperated the code in a function 2023-10-09 12:26:38 +03:00
RadoslavL
48af0af9d5 Added minutes as well 2023-10-09 12:18:50 +03:00
RadoslavL
f9460e31bc Fixed an issue 2023-10-09 12:09:03 +03:00
RadoslavL
b7a252b096 Removed need for more API calls by parsing the publishedTimeText string 2023-10-09 12:00:37 +03:00
RadoslavL
6b929da0e1 Added a 'published' video parameter 2023-10-07 16:57:47 +03:00
48 changed files with 308 additions and 282 deletions

View file

@ -10,8 +10,10 @@ assignees: ''
<!--
BEFORE TRYING TO REPORT A BUG:
* Read the FAQ!
* Use the search function to check if there is already an issue open for your problem!
* Read the FAQ: https://docs.invidious.io/faq/!
* Use the search function to check if there is already an issue open for your problem: https://github.com/search?q=repo%3Aiv-org%2Finvidious+replace+me+with+your+bug&type=issues!
MAKE SURE TO FOLLOW THE TWO STEPS ABOVE BEFORE REPORTING A BUG. A BUG THAT ALREADY EXIST WILL IMMEDIATELY CLOSED.
If you want to suggest a new feature please use "Feature request" instead
If you want to suggest an enhancement to an existing feature please use "Enhancement" instead

View file

@ -23,19 +23,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.2
with:
crystal: 1.12.2
- name: Run lint
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:

View file

@ -14,19 +14,6 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Install Crystal
uses: crystal-lang/install-crystal@v1.8.2
with:
crystal: 1.12.2
- name: Run lint
run: |
if ! crystal tool format --check; then
crystal tool format
git diff
exit 1
fi
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:

View file

@ -38,11 +38,10 @@ jobs:
matrix:
stable: [true]
crystal:
- 1.10.1
- 1.11.2
- 1.12.1
- 1.13.2
- 1.14.0
- 1.15.0
include:
- crystal: nightly
stable: false
@ -136,6 +135,7 @@ jobs:
submodules: true
- name: Install Crystal
id: lint_step_install_crystal
uses: crystal-lang/install-crystal@v1.8.0
with:
crystal: latest
@ -146,7 +146,7 @@ jobs:
path: |
./lib
./bin
key: shards-${{ hashFiles('shard.lock') }}
key: shards-${{ hashFiles('shard.lock') }}-${{ steps.lint_step_install_crystal.outputs.crystal }}
- name: Install Shards
run: |

View file

@ -3,8 +3,84 @@
## vX.Y.0 (future)
## v2.20241110.0
### Wrap-up
This release is most importantly here to fix to the annoying "Youtube API returned error 400"
error that prevented all channel pages from loading.
If you're updating from the previous release, it provides no improvements on the ability to play
videos. If updating from a commit in-between release, it removes the "Please sign in" error caused
by a previous attempt at restoring video playback on large instances.
In the preferences, a new option allows for control of video preload. When enabled, this option
tells the browser to load the video as soon as the page is loaded (this used to be the default).
When disabled, the video starts loading only when the "play" button is pressed.
New interface languages available: Bulgarian, Welsh and Lombard
New dependency required: `tzdata`.
An HTTP proxy can be configured directly in Invidious, if needed. \
**NOTE:** In that case, it is recommended to comment out `force_resolve`.
### New features & important changes
#### For users
* Channels: Fix "Youtube API returned error 400" error preventing channel pages from loading
* Channels: Shorts can now be sorted by "newest", "oldest" and "popular"
* Preferences: Addition of the new "preload" option
* New interface languages available: Bulgarian, Welsh and Lombard
* Added "Filipino (auto-generated)" to the list of caption languages available
* Lots of new translations from Weblate
#### For instance owners
* Allow the configuration of an HTTP proxy to talk to Youtube
* Invidious tries to reconnect to `inv_sig_helper` if the socket is closed
* The instance list is downloaded in the background to improve redirection speed
* New `colorize_logs` option makes each log level a different color
#### For developpers
* `/api/v1/channels/{id}/shorts` now supports the `sort-by` parameter with the following values:
`newest`, `oldest` and `popular`
* Older `/api/v1/channels/xyz/{id}` (tab name before UCID) were removed
* API/Search: New video metadata available: `isNew`, `is4k`, `is8k`, `isVr180`, `isVr360`,
`is3d` and `hasCaptions`
### Bugs fixed
#### User-side
* Channels: The second page of shorts now loads as expected
* Channels: Fixed intermittent empty "playlists" tab
* Search: Fixed `youtu.be` URLs not being properly redirected to the watch page
* Fixed `DB::MappingException` error on the subscriptions feed (due to missing `tzdata` in docker)
* Switching to another instance is much faster
* Fixed an "invalid byte sequence" error when subscribing to a playlist
* Videos: Playback URLs were sometimes broken when cached and `inv_sig_helper` was used
#### For instance owners
* Fix `force_resolve` being ignored in some cases
#### API
* API/Videos: Fixed `live_now` and `premiere_timestamp` sometimes not having the right values
### Full list of pull requests merged since the last release (newest first)
* API: Add "sort_by" parameter to channels/shorts endpoint ([#5071], thanks @iBicha)
* Docker: Install tzdata in Dockerfile ([#5070], by @SamantazFox)
* Videos: Stop using TVHTML5_SIMPLY_EMBEDDED_PLAYER ([#5063], thanks @unixfox)
* Routing: Deprecate old channel API routes ([#5045], by @SamantazFox)
* Videos: use WEB client instead of WEB CREATOR ([#4984], thanks @unixfox)
* Parsers: Fix parsing live_now and premiere_timestamp ([#4934], thanks @absidue)
* Stale bot updates ([#5060], thanks @syeopite)
* Channels: Fix "Youtube API returned error 400" ([#5059], by @SamantazFox)
* Channels: Fix for live videos ([#5027], thanks @iBicha)
@ -52,15 +128,21 @@
[#4928]: https://github.com/iv-org/invidious/pull/4928
[#4930]: https://github.com/iv-org/invidious/pull/4930
[#4931]: https://github.com/iv-org/invidious/pull/4931
[#4934]: https://github.com/iv-org/invidious/pull/4934
[#4942]: https://github.com/iv-org/invidious/pull/4942
[#4984]: https://github.com/iv-org/invidious/pull/4984
[#4991]: https://github.com/iv-org/invidious/pull/4991
[#4993]: https://github.com/iv-org/invidious/pull/4993
[#4995]: https://github.com/iv-org/invidious/pull/4995
[#5027]: https://github.com/iv-org/invidious/pull/5027
[#5034]: https://github.com/iv-org/invidious/pull/5034
[#5045]: https://github.com/iv-org/invidious/pull/5045
[#5046]: https://github.com/iv-org/invidious/pull/5046
[#5059]: https://github.com/iv-org/invidious/pull/5059
[#5060]: https://github.com/iv-org/invidious/pull/5060
[#5063]: https://github.com/iv-org/invidious/pull/5063
[#5070]: https://github.com/iv-org/invidious/pull/5070
[#5071]: https://github.com/iv-org/invidious/pull/5071
## v2.20240825.2 (2024-08-26)

View file

@ -91,7 +91,7 @@
var count = document.getElementById('count');
count.textContent--;
var url = '/token_ajax?action_revoke_token=1&redirect=false' +
var url = '/token_ajax?action=revoke_token&redirect=false' +
'&referer=' + encodeURIComponent(location.href) +
'&session=' + target.getAttribute('data-session');
@ -111,7 +111,7 @@
var count = document.getElementById('count');
count.textContent--;
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
'&referer=' + encodeURIComponent(location.href) +
'&c=' + target.getAttribute('data-ucid');

View file

@ -6,7 +6,7 @@ function add_playlist_video(target) {
var select = target.parentNode.children[0].children[1];
var option = select.children[select.selectedIndex];
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
var url = '/playlist_ajax?action=add_video&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&playlist_id=' + option.getAttribute('data-plid');
@ -21,7 +21,7 @@ function add_playlist_item(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
var url = '/playlist_ajax?action=add_video&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&playlist_id=' + target.getAttribute('data-plid');
@ -36,7 +36,7 @@ function remove_playlist_item(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
var url = '/playlist_ajax?action=remove_video&redirect=false' +
'&set_video_id=' + target.getAttribute('data-index') +
'&playlist_id=' + target.getAttribute('data-plid');

View file

@ -16,7 +16,7 @@ function subscribe() {
subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
var url = '/subscription_ajax?action=create_subscription_to_channel&redirect=false' +
'&c=' + subscribe_data.ucid;
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
@ -32,7 +32,7 @@ function unsubscribe() {
subscribe_button.onclick = subscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
var url = '/subscription_ajax?action=remove_subscriptions&redirect=false' +
'&c=' + subscribe_data.ucid;
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {

View file

@ -67,6 +67,10 @@ function get_playlist(plid) {
'&format=html&hl=' + video_data.preferences.locale;
}
if (video_data.params.listen) {
plid_url += '&listen=1'
}
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
on200: function (response) {
playlist.innerHTML = response.playlistHtml;

View file

@ -6,7 +6,7 @@ function mark_watched(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
var url = '/watch_ajax?action_mark_watched=1&redirect=false' +
var url = '/watch_ajax?action=mark_watched&redirect=false' +
'&id=' + target.getAttribute('data-id');
helpers.xhr('POST', url, {payload: payload}, {
@ -22,7 +22,7 @@ function mark_unwatched(target) {
var count = document.getElementById('count');
count.textContent--;
var url = '/watch_ajax?action_mark_unwatched=1&redirect=false' +
var url = '/watch_ajax?action=mark_unwatched&redirect=false' +
'&id=' + target.getAttribute('data-id');
helpers.xhr('POST', url, {payload: payload}, {

View file

@ -178,11 +178,11 @@ https_only: false
##
## If unset, then no HTTP proxy will be used.
##
http_proxy:
user:
password:
host:
port:
#http_proxy:
# user:
# password:
# host:
# port:
##

View file

@ -1,4 +1,4 @@
FROM crystallang/crystal:1.12.1-alpine AS builder
FROM crystallang/crystal:1.12.2-alpine AS builder
RUN apk add --no-cache sqlite-static yaml-static
@ -32,8 +32,8 @@ RUN if [[ "${release}" == 1 ]] ; then \
--link-flags "-lxml2 -llzma"; \
fi
FROM alpine:3.18
RUN apk add --no-cache rsvg-convert ttf-opensans tini
FROM alpine:3.20
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata
WORKDIR /invidious
RUN addgroup -g 1000 -S invidious && \
adduser -u 1000 -S invidious -G invidious

View file

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

View file

@ -1,13 +1,12 @@
name: invidious
version: 0.20.1
version: 2.20241110.0-dev
authors:
- Omar Roth <omarroth@protonmail.com>
- Invidious team
- Invidious team <contact@invidious.io>
- Contributors!
targets:
invidious:
main: src/invidious.cr
description: |
Invidious is an alternative front-end to YouTube
dependencies:
pg:
@ -40,6 +39,10 @@ development_dependencies:
github: crystal-ameba/ameba
version: ~> 1.6.1
crystal: ">= 1.0.0, < 2.0.0"
crystal: ">= 1.10.0, < 2.0.0"
license: AGPLv3
repository: https://github.com/iv-org/invidious
homepage: https://invidious.io
documentation: https://docs.invidious.io

View file

@ -184,6 +184,9 @@ class Config
config = Config.from_yaml(config_yaml)
# Update config from env vars (upcased and prefixed with "INVIDIOUS_")
#
# Also checks if any top-level config options are set to "CHANGE_ME!!"
# TODO: Support non-top-level config options such as the ones in DBConfig
{% for ivar in Config.instance_vars %}
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
@ -220,6 +223,12 @@ class Config
exit(1)
end
end
# Warn when any config attribute is set to "CHANGE_ME!!"
if config.{{ivar.id}} == "CHANGE_ME!!"
puts "Config: The value of '#{ {{ivar.stringify}} }' needs to be changed!!"
exit(1)
end
{% end %}
# HMAC_key is mandatory
@ -227,9 +236,6 @@ class Config
if config.hmac_key.empty?
puts "Config: 'hmac_key' is required/can't be empty"
exit(1)
elsif config.hmac_key == "CHANGE_ME!!"
puts "Config: The value of 'hmac_key' needs to be changed!!"
exit(1)
end
# Build database_url from db.* if it's not set directly

View file

@ -13,7 +13,7 @@ module Invidious::Frontend::WatchPage
@full_videos,
@video_streams,
@audio_streams,
@captions
@captions,
)
end
end

View file

@ -18,40 +18,6 @@ end
class HTTP::Client
property family : Socket::Family = Socket::Family::UNSPEC
# Override stdlib to automatically initialize proxy if configured
#
# Accurate as of crystal 1.12.1
def initialize(@host : String, port = nil, tls : TLSContext = nil)
check_host_only(@host)
{% if flag?(:without_openssl) %}
if tls
raise "HTTP::Client TLS is disabled because `-D without_openssl` was passed at compile time"
end
@tls = nil
{% else %}
@tls = case tls
when true
OpenSSL::SSL::Context::Client.new
when OpenSSL::SSL::Context::Client
tls
when false, nil
nil
end
{% end %}
@port = (port || (@tls ? 443 : 80)).to_i
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
end
def initialize(@io : IO, @host = "", @port = 80)
@reconnect = false
self.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
end
private def io
io = @io
return io if io

View file

@ -130,7 +130,7 @@ def error_json_helper(
env : HTTP::Server::Context,
status_code : Int32,
exception : Exception,
additional_fields : Hash(String, Object) | Nil = nil
additional_fields : Hash(String, Object) | Nil = nil,
)
if exception.is_a?(InfoException)
return error_json_helper(env, status_code, exception.message || "", additional_fields)
@ -152,7 +152,7 @@ def error_json_helper(
env : HTTP::Server::Context,
status_code : Int32,
message : String,
additional_fields : Hash(String, Object) | Nil = nil
additional_fields : Hash(String, Object) | Nil = nil,
)
env.response.content_type = "application/json"
env.response.status_code = status_code

View file

@ -27,6 +27,7 @@ class Kemal::RouteHandler
# Processes the route if it's a match. Otherwise renders 404.
private def process_request(context)
raise Kemal::Exceptions::RouteNotFound.new(context) unless context.route_found?
return if context.response.closed?
content = context.route.handler.call(context)
if !Kemal.config.error_handlers.empty? && Kemal.config.error_handlers.has_key?(context.response.status_code) && exclude_match?(context)

View file

@ -24,6 +24,7 @@ struct SearchVideo
property length_seconds : Int32
property premiere_timestamp : Time?
property author_verified : Bool
property author_thumbnail : String?
property badges : VideoBadges
def to_xml(auto_generated, query_params, xml : XML::Builder)
@ -88,6 +89,24 @@ struct SearchVideo
json.field "authorUrl", "/channel/#{self.ucid}"
json.field "authorVerified", self.author_verified
author_thumbnail = self.author_thumbnail
if author_thumbnail
json.field "authorThumbnails" do
json.array do
qualities = {32, 48, 76, 100, 176, 512}
qualities.each do |quality|
json.object do
json.field "url", author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", quality
end
end
end
end
end
json.field "videoThumbnails" do
Invidious::JSONify::APIv1.thumbnails(json, self.id)
end
@ -223,7 +242,7 @@ struct SearchChannel
qualities.each do |quality|
json.object do
json.field "url", self.author_thumbnail.gsub(/=\d+/, "=s#{quality}")
json.field "url", self.author_thumbnail.gsub(/=s\d+/, "=s#{quality}")
json.field "width", quality
json.field "height", quality
end

View file

@ -267,6 +267,12 @@ 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"]?.nil?
json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
else
json.field "publishedText", ""
end
end
end
end

View file

@ -81,7 +81,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
})
end
def template_mix(mix)
def template_mix(mix, listen)
html = <<-END_HTML
<h3>
<a href="/mix?list=#{mix["mixId"]}">
@ -95,7 +95,7 @@ def template_mix(mix)
mix["videos"].as_a.each do |video|
html += <<-END_HTML
<li class="pure-menu-item">
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}">
<a href="/watch?v=#{video["videoId"]}&list=#{mix["mixId"]}#{listen ? "&listen=1" : ""}">
<div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>

View file

@ -505,7 +505,7 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
return videos
end
def template_playlist(playlist)
def template_playlist(playlist, listen)
html = <<-END_HTML
<h3>
<a href="/playlist?list=#{playlist["playlistId"]}">
@ -519,7 +519,7 @@ def template_playlist(playlist)
playlist["videos"].as_a.each do |video|
html += <<-END_HTML
<li class="pure-menu-item" id="#{video["videoId"]}">
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}">
<a href="/watch?v=#{video["videoId"]}&list=#{playlist["playlistId"]}&index=#{video["index"]}#{listen ? "&listen=1" : ""}">
<div class="thumbnail">
<img loading="lazy" class="thumbnail" src="/vi/#{video["videoId"]}/mqdefault.jpg" alt="" />
<p class="length">#{recode_length_seconds(video["lengthSeconds"].as_i)}</p>

View file

@ -328,17 +328,9 @@ module Invidious::Routes::Account
end
end
if env.params.query["action_revoke_token"]?
action = "action_revoke_token"
else
return env.redirect referer
end
session = env.params.query["session"]?
session ||= ""
case action
when .starts_with? "action_revoke_token"
case action = env.params.query["action"]?
when "revoke_token"
session = env.params.query["session"]
Invidious::Database::SessionIDs.delete(sid: session, email: user.email)
else
return error_json(400, "Unsupported action #{action}")

View file

@ -27,28 +27,21 @@ module Invidious::Routes::API::Manifest
haltf env, status_code: response.status_code
end
manifest = response.body
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>")
url = url.rchop("</BaseURL>")
if local
uri = URI.parse(url)
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
end
# Proxy URLs for video playback on invidious.
# Other API clients can get the original URLs by omiting `local=true`.
manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
"<BaseURL>#{url}</BaseURL>"
end
return manifest
end
adaptive_fmts = video.adaptive_fmts
# Ditto, only proxify URLs if `local=true` is used
if local
adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
video.adaptive_fmts.each do |fmt|
fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
end
end
@ -70,17 +63,23 @@ module Invidious::Routes::API::Manifest
# OTF streams aren't supported yet (See https://github.com/TeamNewPipe/NewPipe/issues/2415)
next if !(fmt.has_key?("indexRange") && fmt.has_key?("initRange"))
audio_track = fmt["audioTrack"]?.try &.as_h? || {} of String => JSON::Any
lang = audio_track["id"]?.try &.as_s.split('.')[0] || "und"
is_default = audio_track.has_key?("audioIsDefault") ? audio_track["audioIsDefault"].as_bool : i == 0
displayname = audio_track["displayName"]?.try &.as_s || "Unknown"
bitrate = fmt["bitrate"]
# Different representations of the same audio should be groupped into one AdaptationSet.
# However, most players don't support auto quality switching, so we have to trick them
# into providing a quality selector.
# See https://github.com/iv-org/invidious/issues/3074 for more details.
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: fmt["bitrate"].to_s + "k") do
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: "#{displayname} [#{bitrate}k]", lang: lang) do
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
bandwidth = fmt["bitrate"].as_i
itag = fmt["itag"].as_i
url = fmt["url"].as_s
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: i == 0 ? "main" : "alternate")
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: is_default ? "main" : "alternate")
xml.element("Representation", id: fmt["itag"], codecs: codecs, bandwidth: bandwidth) do
xml.element("AudioChannelConfiguration", schemeIdUri: "urn:mpeg:dash:23003:3:audio_channel_configuration:2011",
@ -177,8 +176,9 @@ module Invidious::Routes::API::Manifest
manifest = response.body
if local
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
path = URI.parse(match).path
manifest = manifest.gsub(/https:\/\/[^\n"]*/m) do |match|
uri = URI.parse(match)
path = uri.path
path = path.lchop("/videoplayback/")
path = path.rchop("/")
@ -207,7 +207,7 @@ module Invidious::Routes::API::Manifest
raw_params["fvip"] = fvip["fvip"]
end
raw_params["local"] = "true"
raw_params["host"] = uri.host.not_nil!
"#{HOST_URL}/videoplayback?#{raw_params}"
end

View file

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

View file

@ -42,6 +42,9 @@ module Invidious::Routes::API::V1::Misc
format = env.params.query["format"]?
format ||= "json"
listen_param = env.params.query["listen"]?
listen = (listen_param == "true" || listen_param == "1")
if plid.starts_with? "RD"
return env.redirect "/api/v1/mixes/#{plid}"
end
@ -85,7 +88,7 @@ module Invidious::Routes::API::V1::Misc
end
if format == "html"
playlist_html = template_playlist(json_response)
playlist_html = template_playlist(json_response, listen)
index, next_video = json_response["videos"].as_a.skip(1 + lookback).select { |video| !video["author"].as_s.empty? }[0]?.try { |v| {v["index"], v["videoId"]} } || {nil, nil}
response = {
@ -111,6 +114,9 @@ module Invidious::Routes::API::V1::Misc
format = env.params.query["format"]?
format ||= "json"
listen_param = env.params.query["listen"]?
listen = (listen_param == "true" || listen_param == "1")
begin
mix = fetch_mix(rdid, continuation, locale: locale)
@ -141,9 +147,7 @@ module Invidious::Routes::API::V1::Misc
json.field "authorUrl", "/channel/#{video.ucid}"
json.field "videoThumbnails" do
json.array do
Invidious::JSONify::APIv1.thumbnails(json, video.id)
end
Invidious::JSONify::APIv1.thumbnails(json, video.id)
end
json.field "index", video.index
@ -157,7 +161,7 @@ module Invidious::Routes::API::V1::Misc
if format == "html"
response = JSON.parse(response)
playlist_html = template_mix(response)
playlist_html = template_mix(response, listen)
next_video = response["videos"].as_a.select { |video| !video["author"].as_s.empty? }[0]?.try &.["videoId"]
response = {

View file

@ -157,10 +157,12 @@ module Invidious::Routes::Embed
adaptive_fmts = video.adaptive_fmts
if params.local
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
end
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
video_streams = video.video_streams
audio_streams = video.audio_streams

View file

@ -194,6 +194,7 @@ module Invidious::Routes::Feeds
length_seconds: 0,
premiere_timestamp: nil,
author_verified: false,
author_thumbnail: nil,
badges: VideoBadges::None,
})
end

View file

@ -304,23 +304,6 @@ module Invidious::Routes::Playlists
end
end
if env.params.query["action_create_playlist"]?
action = "action_create_playlist"
elsif env.params.query["action_delete_playlist"]?
action = "action_delete_playlist"
elsif env.params.query["action_edit_playlist"]?
action = "action_edit_playlist"
elsif env.params.query["action_add_video"]?
action = "action_add_video"
video_id = env.params.query["video_id"]
elsif env.params.query["action_remove_video"]?
action = "action_remove_video"
elsif env.params.query["action_move_video_before"]?
action = "action_move_video_before"
else
return env.redirect referer
end
begin
playlist_id = env.params.query["playlist_id"]
playlist = get_playlist(playlist_id).as(InvidiousPlaylist)
@ -335,12 +318,8 @@ module Invidious::Routes::Playlists
end
end
email = user.email
case action
when "action_edit_playlist"
# TODO: Playlist stub
when "action_add_video"
case action = env.params.query["action"]?
when "add_video"
if playlist.index.size >= CONFIG.playlist_length_limit
if redirect
return error_template(400, "Playlist cannot have more than #{CONFIG.playlist_length_limit} videos")
@ -377,12 +356,14 @@ module Invidious::Routes::Playlists
Invidious::Database::PlaylistVideos.insert(playlist_video)
Invidious::Database::Playlists.update_video_added(playlist_id, playlist_video.index)
when "action_remove_video"
when "remove_video"
index = env.params.query["set_video_id"]
Invidious::Database::PlaylistVideos.delete(index)
Invidious::Database::Playlists.update_video_removed(playlist_id, index)
when "action_move_video_before"
when "move_video_before"
# TODO: Playlist stub
when nil
return error_json(400, "Missing action")
else
return error_json(400, "Unsupported action #{action}")
end

View file

@ -32,24 +32,16 @@ module Invidious::Routes::Subscriptions
end
end
if env.params.query["action_create_subscription_to_channel"]?.try &.to_i?.try &.== 1
action = "action_create_subscription_to_channel"
elsif env.params.query["action_remove_subscriptions"]?.try &.to_i?.try &.== 1
action = "action_remove_subscriptions"
else
return env.redirect referer
end
channel_id = env.params.query["c"]?
channel_id ||= ""
case action
when "action_create_subscription_to_channel"
case action = env.params.query["action"]?
when "create_subscription_to_channel"
if !user.subscriptions.includes? channel_id
get_channel(channel_id)
Invidious::Database::Users.subscribe_channel(user, channel_id)
end
when "action_remove_subscriptions"
when "remove_subscriptions"
Invidious::Database::Users.unsubscribe_channel(user, channel_id)
else
return error_json(400, "Unsupported action #{action}")

View file

@ -164,10 +164,13 @@ module Invidious::Routes::VideoPlayback
env.response.headers["Access-Control-Allow-Origin"] = "*"
if location = resp.headers["Location"]?
location = URI.parse(location)
location = "#{location.request_target}&host=#{location.host}#{region ? "&region=#{region}" : ""}"
url = Invidious::HttpServer::Utils.proxy_video_url(location, region: region)
env.redirect location
if title = query_params["title"]?
url = "#{url}&title=#{URI.encode_www_form(title)}"
end
env.redirect url
break
end

View file

@ -121,10 +121,12 @@ module Invidious::Routes::Watch
adaptive_fmts = video.adaptive_fmts
if params.local
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
end
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
video_streams = video.video_streams
audio_streams = video.audio_streams
@ -241,18 +243,10 @@ module Invidious::Routes::Watch
end
end
if env.params.query["action_mark_watched"]?
action = "action_mark_watched"
elsif env.params.query["action_mark_unwatched"]?
action = "action_mark_unwatched"
else
return env.redirect referer
end
case action
when "action_mark_watched"
case action = env.params.query["action"]?
when "mark_watched"
Invidious::Database::Users.mark_watched(user, id)
when "action_mark_unwatched"
when "mark_unwatched"
Invidious::Database::Users.mark_unwatched(user, id)
else
return error_json(400, "Unsupported action #{action}")

View file

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

View file

@ -75,7 +75,7 @@ module Invidious::Search
@type : Type = Type::All,
@duration : Duration = Duration::None,
@features : Features = Features::None,
@sort : Sort = Sort::Relevance
@sort : Sort = Sort::Relevance,
)
end

View file

@ -47,7 +47,7 @@ module Invidious::Search
def initialize(
params : HTTP::Params,
@type : Type = Type::Regular,
@region : String? = nil
@region : String? = nil,
)
# Get the raw search query string (common to all search types). In
# Regular search mode, also look for the `search_query` URL parameter

View file

@ -290,42 +290,39 @@ struct Invidious::User
end
def from_newpipe(user : User, body : String) : Bool
io = IO::Memory.new(body)
Compress::Zip::File.open(IO::Memory.new(body), true) do |file|
entry = file.entries.find { |file_entry| file_entry.filename == "newpipe.db" }
return false if entry.nil?
entry.open do |file_io|
# Ensure max size of 4MB
io_sized = IO::Sized.new(file_io, 0x400000)
Compress::Zip::File.open(io) do |file|
file.entries.each do |entry|
entry.open do |file_io|
# Ensure max size of 4MB
io_sized = IO::Sized.new(file_io, 0x400000)
begin
temp = File.tempfile(".db") do |tempfile|
begin
File.write(tempfile.path, io_sized.gets_to_end)
rescue
return false
end
next if entry.filename != "newpipe.db"
DB.open("sqlite3://" + tempfile.path) do |db|
user.watched += db.query_all("SELECT url FROM streams", as: String)
.map(&.lchop("https://www.youtube.com/watch?v="))
tempfile = File.tempfile(".db")
user.watched.uniq!
Invidious::Database::Users.update_watch_history(user)
begin
File.write(tempfile.path, io_sized.gets_to_end)
rescue
return false
user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String)
.map(&.lchop("https://www.youtube.com/channel/"))
user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions)
Invidious::Database::Users.update_subscriptions(user)
end
end
db = DB.open("sqlite3://" + tempfile.path)
user.watched += db.query_all("SELECT url FROM streams", as: String)
.map(&.lchop("https://www.youtube.com/watch?v="))
user.watched.uniq!
Invidious::Database::Users.update_watch_history(user)
user.subscriptions += db.query_all("SELECT url FROM subscriptions", as: String)
.map(&.lchop("https://www.youtube.com/channel/"))
user.subscriptions.uniq!
user.subscriptions = get_batch_channels(user.subscriptions)
Invidious::Database::Users.update_subscriptions(user)
db.close
tempfile.delete
ensure
temp.delete if !temp.nil?
end
end
end

View file

@ -106,7 +106,7 @@ struct Video
if formats = info.dig?("streamingData", "adaptiveFormats")
return formats
.as_a.map(&.as_h)
.sort_by! { |f| f["width"]?.try &.as_i || 0 }
.sort_by! { |f| f["width"]?.try &.as_i || f["audioTrack"]?.try { |a| a["audioIsDefault"]?.try { |v| v.as_bool ? -1 : 0 } } || 0 }
else
return [] of Hash(String, JSON::Any)
end

View file

@ -36,6 +36,13 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
if published_time_text = related["publishedTimeText"]?
decoded_time = decode_date(published_time_text["simpleText"].to_s)
published = decoded_time.to_rfc3339.to_s
else
published = nil
end
# TODO: when refactoring video types, make a struct for related videos
# or reuse an existing type, if that fits.
return {
@ -47,16 +54,13 @@ def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
"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 || ""),
}
end
def extract_video_info(video_id : String)
# Init client config for the API
client_config = YoutubeAPI::ClientConfig.new
# Use the WEB_CREATOR when po_token is configured because it fully only works on this client
if CONFIG.po_token
client_config.client_type = YoutubeAPI::ClientType::WebCreator
end
# Fetch data from the player endpoint
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
@ -106,15 +110,8 @@ def extract_video_info(video_id : String)
new_player_response = nil
# Second try in case WEB_CREATOR doesn't work with po_token.
# Only trigger if reason found and po_token configured.
if reason && CONFIG.po_token
client_config.client_type = YoutubeAPI::ClientType::WebEmbeddedPlayer
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# Don't use Android client if po_token is passed because po_token doesn't
# work for Android client.
# Don't use Android test suite client if po_token is passed because po_token doesn't
# work for Android test suite client.
if reason.nil? && CONFIG.po_token.nil?
# Fetch the video streams using an Android client in order to get the
# decrypted URLs and maybe fix throttling issues (#2194). See the
@ -124,14 +121,6 @@ def extract_video_info(video_id : String)
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# Last hope
# Only trigger if reason found or didn't work wth Android client.
# TvHtml5ScreenEmbed now requires sig helper for it to work but doesn't work with po_token.
if reason && CONFIG.po_token.nil?
client_config.client_type = YoutubeAPI::ClientType::TvHtml5ScreenEmbed
new_player_response = try_fetch_streaming_data(video_id, client_config)
end
# Replace player response and reset reason
if !new_player_response.nil?
# Preserve captions & storyboard data before replacement
@ -235,8 +224,17 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
premiere_timestamp = microformat.dig?("liveBroadcastDetails", "startTimestamp")
.try { |t| Time.parse_rfc3339(t.as_s) }
premiere_timestamp ||= player_response.dig?(
"playabilityStatus", "liveStreamability",
"liveStreamabilityRenderer", "offlineSlate",
"liveStreamOfflineSlateRenderer", "scheduledStartTime"
)
.try &.as_s.to_i64
.try { |t| Time.unix(t) }
live_now = microformat.dig?("liveBroadcastDetails", "isLiveNow")
.try &.as_bool || false
.try &.as_bool
live_now ||= video_details.dig?("isLive").try &.as_bool || false
post_live_dvr = video_details.dig?("isPostLiveDvr")
.try &.as_bool || false

View file

@ -20,7 +20,7 @@ module Invidious::Videos
def initialize(
*, @url, @width, @height, @count, @interval,
@rows, @columns, @images_count
@rows, @columns, @images_count,
)
authority = /(i\d?).ytimg.com/.match!(@url.host.not_nil!)[1]?

View file

@ -128,7 +128,7 @@
<div class="top-left-overlay">
<%- if env.get? "show_watched" -%>
<form data-onsubmit="return_false" action="/watch_ajax?action_mark_watched=1&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
<form data-onsubmit="return_false" action="/watch_ajax?action=mark_watched&id=<%= item.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button type="submit" class="pure-button pure-button-secondary low-profile"
data-onclick="mark_watched" data-id="<%= item.id %>">
@ -138,14 +138,14 @@
<%- end -%>
<%- if plid_form = env.get?("add_playlist_items") -%>
<%- form_parameters = "action_add_video=1&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
<%- form_parameters = "action=add_video&video_id=#{item.id}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
<form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button type="submit" class="pure-button pure-button-secondary low-profile"
data-onclick="add_playlist_item" data-id="<%= item.id %>" data-plid="<%= plid_form %>"><i class="icon ion-md-add"></i></button>
</form>
<%- elsif item.is_a?(PlaylistVideo) && (plid_form = env.get?("remove_playlist_items")) -%>
<%- form_parameters = "action_remove_video=1&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
<%- form_parameters = "action=remove_video&set_video_id=#{item.index}&playlist_id=#{plid_form}&referer=#{env.get("current_page")}" -%>
<form data-onsubmit="return_false" action="/playlist_ajax?<%= form_parameters %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button type="submit" class="pure-button pure-button-secondary low-profile"

View file

@ -1,13 +1,13 @@
<% if user %>
<% if subscriptions.includes? ucid %>
<form action="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<form action="/subscription_ajax?action=remove_subscriptions&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="unsubscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
</button>
</form>
<% else %>
<form action="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<form action="/subscription_ajax?action=create_subscription_to_channel&c=<%= ucid %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<button data-type="subscribe" id="subscribe" class="pure-button pure-button-primary">
<b><input style="all:unset" type="submit" value="<%= translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>

View file

@ -37,7 +37,7 @@
</a>
<div class="top-left-overlay"><div class="watched">
<form data-onsubmit="return_false" action="/watch_ajax?action_mark_unwatched=1&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
<form data-onsubmit="return_false" action="/watch_ajax?action=mark_unwatched&id=<%= item %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<button type="submit" class="pure-button pure-button-secondary low-profile"
data-onclick="mark_unwatched" data-id="<%= item %>"><i class="icon ion-md-trash"></i></button>

View file

@ -37,7 +37,7 @@
<div class="pure-u-2-5"></div>
<div class="pure-u-1-5" style="text-align:right">
<h3 style="padding-right:0.5em">
<form data-onsubmit="return_false" action="/subscription_ajax?action_remove_subscriptions=1&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
<form data-onsubmit="return_false" action="/subscription_ajax?action=remove_subscriptions&c=<%= channel.id %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= translate(locale, "unsubscribe") %>">
</form>

View file

@ -29,7 +29,7 @@
</div>
<div class="pure-u-1-5" style="text-align:right">
<h3 style="padding-right:0.5em">
<form data-onsubmit="return_false" action="/token_ajax?action_revoke_token=1&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
<form data-onsubmit="return_false" action="/token_ajax?action=revoke_token&session=<%= token[:session] %>&referer=<%= env.get("current_page") %>" method="post">
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
<input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= translate(locale, "revoke") %>">
</form>

View file

@ -159,7 +159,7 @@ we're going to need to do it here in order to allow for translations.
<% if user %>
<% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %>
<% if !playlists.empty? %>
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax" method="post" target="_blank">
<form data-onsubmit="return_false" class="pure-form pure-form-stacked" action="/playlist_ajax?action=add_video" method="post" target="_blank">
<div class="pure-control-group">
<label for="playlist_id"><%= translate(locale, "Add to playlist: ") %></label>
<select style="width:100%" name="playlist_id" id="playlist_id">
@ -170,7 +170,6 @@ we're going to need to do it here in order to allow for translations.
</div>
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
<input type="hidden" name="action_add_video" value="1">
<input type="hidden" name="video_id" value="<%= video.id %>">
<button data-onclick="add_playlist_video" data-id="<%= video.id %>" type="submit" class="pure-button pure-button-primary">
<b><%= translate(locale, "Add to playlist") %></b>

View file

@ -67,6 +67,8 @@ private module Parsers
author_id = author_fallback.id
end
author_thumbnail = item_contents.dig?("channelThumbnailSupportedRenderers", "channelThumbnailWithLinkRenderer", "thumbnail", "thumbnails", 0, "url").try &.as_s
author_verified = has_verified_badge?(item_contents["ownerBadges"]?)
# For live videos (and possibly recently premiered videos) there is no published information.
@ -148,6 +150,7 @@ private module Parsers
length_seconds: length_seconds,
premiere_timestamp: premiere_timestamp,
author_verified: author_verified,
author_thumbnail: author_thumbnail,
badges: badges,
})
end
@ -579,6 +582,7 @@ private module Parsers
length_seconds: duration,
premiere_timestamp: Time.unix(0),
author_verified: false,
author_thumbnail: nil,
badges: VideoBadges::None,
})
end
@ -708,6 +712,7 @@ private module Parsers
length_seconds: duration,
premiere_timestamp: Time.unix(0),
author_verified: false,
author_thumbnail: nil,
badges: VideoBadges::None,
})
end
@ -1024,7 +1029,7 @@ end
def extract_items(
initial_data : InitialData,
author_fallback : String? = nil,
author_id_fallback : String? = nil
author_id_fallback : String? = nil,
) : {Array(SearchItem), String?}
items = [] of SearchItem
continuation = nil

View file

@ -211,7 +211,7 @@ module YoutubeAPI
def initialize(
*,
@client_type = ClientType::Web,
@region = "US"
@region = "US",
)
end
@ -300,9 +300,8 @@ module YoutubeAPI
end
if client_config.screen == "EMBED"
# embedUrl https://www.google.com allow loading almost all video that are configured not embeddable
client_context["thirdParty"] = {
"embedUrl" => "https://www.google.com/",
"embedUrl" => "https://www.youtube.com/embed/#{video_id}",
} of String => String | Int64
end
@ -371,7 +370,7 @@ module YoutubeAPI
browse_id : String,
*, # Force the following parameters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil
client_config : ClientConfig | Nil = nil,
)
# JSON Request data, required by the API
data = {
@ -465,7 +464,7 @@ module YoutubeAPI
video_id : String,
*, # Force the following parameters to be passed by name
params : String,
client_config : ClientConfig | Nil = nil
client_config : ClientConfig | Nil = nil,
)
# Playback context, separate because it can be different between clients
playback_ctx = {
@ -558,7 +557,7 @@ module YoutubeAPI
def search(
search_query : String,
params : String,
client_config : ClientConfig | Nil = nil
client_config : ClientConfig | Nil = nil,
)
# JSON Request data, required by the API
data = {
@ -584,7 +583,7 @@ module YoutubeAPI
def get_transcript(
params : String,
client_config : ClientConfig | Nil = nil
client_config : ClientConfig | Nil = nil,
) : Hash(String, JSON::Any)
data = {
"context" => self.make_context(client_config),
@ -606,7 +605,7 @@ module YoutubeAPI
def _post_json(
endpoint : String,
data : Hash,
client_config : ClientConfig | Nil
client_config : ClientConfig | Nil,
) : Hash(String, JSON::Any)
# Use the default client config if nil is passed
client_config ||= DEFAULT_CLIENT_CONFIG