mirror of
https://github.com/iv-org/invidious.git
synced 2024-11-23 05:33:07 +05:30
Compare commits
65 Commits
2a648b0fbf
...
9b995d6dec
Author | SHA1 | Date | |
---|---|---|---|
|
9b995d6dec | ||
|
42d588b2d5 | ||
|
2c072ab91d | ||
|
ad089214af | ||
|
0c414adbeb | ||
|
71da6c2fec | ||
|
9892604758 | ||
|
5d2dd40bc3 | ||
|
699d53ad41 | ||
|
2150264d84 | ||
|
d42561d74a | ||
|
7092bb8855 | ||
|
d7c35e6e3d | ||
|
bc86fb8a82 | ||
|
ec82c2f539 | ||
|
4b363e32fa | ||
|
d2123b4682 | ||
|
0f8f32bca8 | ||
|
f3e93ca83d | ||
|
82b1506ccc | ||
|
b9ad9bd723 | ||
|
8bf7e02978 | ||
|
1a49e798c8 | ||
|
9d54cf903e | ||
|
b173d4acf2 | ||
|
43d5efd9da | ||
|
1480e0089f | ||
|
a5fb78bba5 | ||
|
09f5485889 | ||
|
a760b69cb6 | ||
|
4f7a18a630 | ||
|
42da2547e3 | ||
|
09ccea1d31 | ||
|
2a19dbb1fe | ||
|
6dd662a5b8 | ||
|
301aeffa78 | ||
|
d27a5e7fae | ||
|
afc5b27d83 | ||
|
1a5047aad9 | ||
|
ce910b5269 | ||
|
78f18b257c | ||
|
3196182d4d | ||
|
82248fad02 | ||
|
cbc546f032 | ||
|
792d0d5f6d | ||
|
ac6e796c73 | ||
|
75c5881c55 | ||
|
6da18ddc41 | ||
|
cdf93b29e6 | ||
|
c243d08afb | ||
|
711d52d47f | ||
|
ee72809282 | ||
|
d2edd4b63f | ||
|
17b525f2a6 | ||
|
b2a83991d1 | ||
|
d77afdcf00 | ||
|
f8ec312328 | ||
|
6e39b9b303 | ||
|
46c58bd84c | ||
|
7521902e88 | ||
|
bd48af825c | ||
|
ee89db49ba | ||
|
3af6681869 | ||
|
1124dd645d | ||
|
c24ed85110 |
33
.github/workflows/ci.yml
vendored
33
.github/workflows/ci.yml
vendored
@ -54,7 +54,7 @@ jobs:
|
||||
|
||||
- name: Install required APT packages
|
||||
run: |
|
||||
sudo apt install -y libsqlite3-dev
|
||||
sudo apt install -y libsqlite3-dev
|
||||
shell: bash
|
||||
|
||||
- name: Install Crystal
|
||||
@ -65,7 +65,9 @@ jobs:
|
||||
- name: Cache Shards
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ./lib
|
||||
path: |
|
||||
./lib
|
||||
./bin
|
||||
key: shards-${{ hashFiles('shard.lock') }}
|
||||
|
||||
- name: Install Shards
|
||||
@ -77,14 +79,6 @@ jobs:
|
||||
- name: Run tests
|
||||
run: crystal spec
|
||||
|
||||
- name: Run lint
|
||||
run: |
|
||||
if ! crystal tool format --check; then
|
||||
crystal tool format
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
|
||||
|
||||
@ -130,8 +124,12 @@ jobs:
|
||||
- name: Test Docker
|
||||
run: while curl -Isf http://localhost:3000; do sleep 1; done
|
||||
|
||||
ameba_lint:
|
||||
lint:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@ -151,7 +149,18 @@ jobs:
|
||||
key: shards-${{ hashFiles('shard.lock') }}
|
||||
|
||||
- name: Install Shards
|
||||
run: shards install
|
||||
run: |
|
||||
if ! shards check; then
|
||||
shards install
|
||||
fi
|
||||
|
||||
- name: Check Crystal formatter compliance
|
||||
run: |
|
||||
if ! crystal tool format --check; then
|
||||
crystal tool format
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Run Ameba linter
|
||||
run: bin/ameba
|
||||
|
13
.github/workflows/stale.yml
vendored
13
.github/workflows/stale.yml
vendored
@ -13,14 +13,11 @@ jobs:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 365
|
||||
days-before-pr-stale: 90
|
||||
days-before-close: 30
|
||||
exempt-pr-labels: blocked,exempt-stale
|
||||
days-before-stale: 730
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 60
|
||||
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
||||
stale-pr-message: 'This pull request has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely abandoned or outdated. If you think this pull request is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
||||
stale-issue-label: "stale"
|
||||
stale-pr-label: "stale"
|
||||
ascending: true
|
||||
# Never mark feature requests/enhancements as stale
|
||||
exempt-issue-labels: "feature-request,enhancement,exempt-stale"
|
||||
# Exempt the following types of issues from being staled
|
||||
exempt-issue-labels: "feature-request,enhancement,discussion,exempt-stale"
|
||||
|
98
CHANGELOG.md
98
CHANGELOG.md
@ -3,8 +3,92 @@
|
||||
## 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)
|
||||
* Locales: Add Bulgarian, Welsh and Lombard to the list ([#5046], by @SamantazFox)
|
||||
* Shards: Update database dependencies ([#5034], by @SamantazFox)
|
||||
* Logger: Add color support for different log levels ([#4931], thanks @Fijxu)
|
||||
* Fix named arg syntax when passing force_resolve ([#4754], thanks @syeopite)
|
||||
* Use make_client instead of calling HTTP::Client ([#4709], thanks @syeopite)
|
||||
* Add "Filipino (auto-generated)" to the list of caption languages ([#4995], by @SamantazFox)
|
||||
* Makefile: Add MT option to enable the 'preview_mt' flag ([#4993], by @SamantazFox)
|
||||
* SigHelper: Reconnect to signature helper ([#4991], thanks @Fijxu)
|
||||
@ -31,7 +115,9 @@
|
||||
[#4270]: https://github.com/iv-org/invidious/pull/4270
|
||||
[#4326]: https://github.com/iv-org/invidious/pull/4326
|
||||
[#4652]: https://github.com/iv-org/invidious/pull/4652
|
||||
[#4709]: https://github.com/iv-org/invidious/pull/4709
|
||||
[#4750]: https://github.com/iv-org/invidious/pull/4750
|
||||
[#4754]: https://github.com/iv-org/invidious/pull/4754
|
||||
[#4850]: https://github.com/iv-org/invidious/pull/4850
|
||||
[#4862]: https://github.com/iv-org/invidious/pull/4862
|
||||
[#4863]: https://github.com/iv-org/invidious/pull/4863
|
||||
@ -41,10 +127,22 @@
|
||||
[#4923]: https://github.com/iv-org/invidious/pull/4923
|
||||
[#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)
|
||||
|
@ -10,6 +10,14 @@ String.prototype.supplant = function (o) {
|
||||
});
|
||||
};
|
||||
|
||||
function updateReplyLinks() {
|
||||
document.querySelectorAll("a[href^='/comment_viewer']").forEach(function (replyLink) {
|
||||
replyLink.setAttribute("href", "javascript:void(0)");
|
||||
replyLink.removeAttribute("target");
|
||||
});
|
||||
}
|
||||
updateReplyLinks();
|
||||
|
||||
function toggle_comments(event) {
|
||||
var target = event.target;
|
||||
var body = target.parentNode.parentNode.parentNode.children[1];
|
||||
@ -104,6 +112,7 @@ function get_youtube_comments() {
|
||||
})
|
||||
});
|
||||
comments.innerHTML = commentInnerHtml;
|
||||
updateReplyLinks();
|
||||
comments.children[0].children[0].children[0].onclick = toggle_comments;
|
||||
if (video_data.support_reddit) {
|
||||
comments.children[0].children[1].children[0].onclick = swap_comments;
|
||||
@ -143,6 +152,7 @@ function get_youtube_replies(target, load_more, load_replies) {
|
||||
body = body.parentNode.parentNode;
|
||||
body.removeChild(body.lastElementChild);
|
||||
body.insertAdjacentHTML('beforeend', response.contentHtml);
|
||||
updateReplyLinks();
|
||||
} else {
|
||||
body.removeChild(body.lastElementChild);
|
||||
|
||||
@ -161,6 +171,7 @@ function get_youtube_replies(target, load_more, load_replies) {
|
||||
|
||||
body.appendChild(p);
|
||||
body.appendChild(div);
|
||||
updateReplyLinks();
|
||||
}
|
||||
},
|
||||
onNon200: function (xhr) {
|
||||
|
@ -1,37 +1,20 @@
|
||||
'use strict';
|
||||
var community_data = JSON.parse(document.getElementById('community_data').textContent);
|
||||
|
||||
function hide_youtube_replies(event) {
|
||||
var target = event.target;
|
||||
// first page of community posts are loaded without javascript so we need to update the Load more button
|
||||
var initialLoadMore = document.querySelector('a[data-onclick="get_youtube_replies"]');
|
||||
initialLoadMore.setAttribute('href', 'javascript:void(0);');
|
||||
initialLoadMore.removeAttribute('target');
|
||||
|
||||
var sub_text = target.getAttribute('data-inner-text');
|
||||
var inner_text = target.getAttribute('data-sub-text');
|
||||
|
||||
var body = target.parentNode.parentNode.children[1];
|
||||
body.style.display = 'none';
|
||||
|
||||
target.innerHTML = sub_text;
|
||||
target.onclick = show_youtube_replies;
|
||||
target.setAttribute('data-inner-text', inner_text);
|
||||
target.setAttribute('data-sub-text', sub_text);
|
||||
function updateReplyLinks() {
|
||||
document.querySelectorAll("a[href^='/comment_viewer']").forEach(function (replyLink) {
|
||||
replyLink.setAttribute("href", "javascript:void(0)");
|
||||
replyLink.removeAttribute("target");
|
||||
});
|
||||
}
|
||||
updateReplyLinks();
|
||||
|
||||
function show_youtube_replies(event) {
|
||||
var target = event.target;
|
||||
|
||||
var sub_text = target.getAttribute('data-inner-text');
|
||||
var inner_text = target.getAttribute('data-sub-text');
|
||||
|
||||
var body = target.parentNode.parentNode.children[1];
|
||||
body.style.display = '';
|
||||
|
||||
target.innerHTML = sub_text;
|
||||
target.onclick = hide_youtube_replies;
|
||||
target.setAttribute('data-inner-text', inner_text);
|
||||
target.setAttribute('data-sub-text', sub_text);
|
||||
}
|
||||
|
||||
function get_youtube_replies(target, load_more) {
|
||||
function get_youtube_replies(target) {
|
||||
var continuation = target.getAttribute('data-continuation');
|
||||
|
||||
var body = target.parentNode.parentNode;
|
||||
@ -47,29 +30,10 @@ function get_youtube_replies(target, load_more) {
|
||||
|
||||
helpers.xhr('GET', url, {}, {
|
||||
on200: function (response) {
|
||||
if (load_more) {
|
||||
body = body.parentNode.parentNode;
|
||||
body.removeChild(body.lastElementChild);
|
||||
body.innerHTML += response.contentHtml;
|
||||
} else {
|
||||
body.removeChild(body.lastElementChild);
|
||||
|
||||
var p = document.createElement('p');
|
||||
var a = document.createElement('a');
|
||||
p.appendChild(a);
|
||||
|
||||
a.href = 'javascript:void(0)';
|
||||
a.onclick = hide_youtube_replies;
|
||||
a.setAttribute('data-sub-text', community_data.hide_replies_text);
|
||||
a.setAttribute('data-inner-text', community_data.show_replies_text);
|
||||
a.textContent = community_data.hide_replies_text;
|
||||
|
||||
var div = document.createElement('div');
|
||||
div.innerHTML = response.contentHtml;
|
||||
|
||||
body.appendChild(p);
|
||||
body.appendChild(div);
|
||||
}
|
||||
body = body.parentNode.parentNode;
|
||||
body.removeChild(body.lastElementChild);
|
||||
body.insertAdjacentHTML('beforeend', response.contentHtml);
|
||||
updateReplyLinks();
|
||||
},
|
||||
onNon200: function (xhr) {
|
||||
body.innerHTML = fallback;
|
||||
|
@ -233,6 +233,17 @@ http_proxy:
|
||||
##
|
||||
#log_level: Info
|
||||
|
||||
##
|
||||
## Enables colors in logs. Useful for debugging purposes
|
||||
## This is overridden if "-k" or "--colorize"
|
||||
## are passed on the command line.
|
||||
## Colors are also disabled if the environment variable
|
||||
## NO_COLOR is present and has any value
|
||||
##
|
||||
## Accepted values: true, false
|
||||
## Default: true
|
||||
##
|
||||
#colorize_logs: false
|
||||
|
||||
# -----------------------------
|
||||
# Features
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -14,7 +14,7 @@ shards:
|
||||
|
||||
db:
|
||||
git: https://github.com/crystal-lang/crystal-db.git
|
||||
version: 0.10.1
|
||||
version: 0.13.1
|
||||
|
||||
exception_page:
|
||||
git: https://github.com/crystal-loot/exception_page.git
|
||||
@ -34,7 +34,7 @@ shards:
|
||||
|
||||
pg:
|
||||
git: https://github.com/will/crystal-pg.git
|
||||
version: 0.24.0
|
||||
version: 0.28.0
|
||||
|
||||
protodec:
|
||||
git: https://github.com/iv-org/protodec.git
|
||||
@ -50,5 +50,5 @@ shards:
|
||||
|
||||
sqlite3:
|
||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||
version: 0.18.0
|
||||
version: 0.21.0
|
||||
|
||||
|
21
shard.yml
21
shard.yml
@ -1,21 +1,20 @@
|
||||
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:
|
||||
github: will/crystal-pg
|
||||
version: ~> 0.24.0
|
||||
version: ~> 0.28.0
|
||||
sqlite3:
|
||||
github: crystal-lang/crystal-sqlite3
|
||||
version: ~> 0.18.0
|
||||
version: ~> 0.21.0
|
||||
kemal:
|
||||
github: kemalcr/kemal
|
||||
version: ~> 1.1.2
|
||||
@ -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
|
||||
|
@ -122,6 +122,9 @@ Kemal.config.extra_options do |parser|
|
||||
parser.on("-l LEVEL", "--log-level=LEVEL", "Log level, one of #{LogLevel.values} (default: #{CONFIG.log_level})") do |log_level|
|
||||
CONFIG.log_level = LogLevel.parse(log_level)
|
||||
end
|
||||
parser.on("-k", "--colorize", "Colorize logs") do
|
||||
CONFIG.colorize_logs = true
|
||||
end
|
||||
parser.on("-v", "--version", "Print version") do
|
||||
puts SOFTWARE.to_pretty_json
|
||||
exit
|
||||
@ -138,7 +141,7 @@ if CONFIG.output.upcase != "STDOUT"
|
||||
FileUtils.mkdir_p(File.dirname(CONFIG.output))
|
||||
end
|
||||
OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mode: "a")
|
||||
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level)
|
||||
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level, CONFIG.colorize_logs)
|
||||
|
||||
# Check table integrity
|
||||
Invidious::Database.check_integrity(CONFIG)
|
||||
|
@ -279,7 +279,7 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
|
||||
|
||||
if format == "html"
|
||||
response = JSON.parse(response)
|
||||
content_html = IV::Frontend::Comments.template_youtube(response, locale, thin_mode)
|
||||
content_html = IV::Frontend::Comments.template_youtube(response, locale, thin_mode, ucid, "community")
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
|
@ -1,78 +1,3 @@
|
||||
def produce_channel_content_continuation(ucid, content_type, page = 1, auto_generated = nil, sort_by = "newest", v2 = false)
|
||||
object_inner_2 = {
|
||||
"2:0:embedded" => {
|
||||
"1:0:varint" => 0_i64,
|
||||
},
|
||||
"5:varint" => 50_i64,
|
||||
"6:varint" => 1_i64,
|
||||
"7:varint" => (page * 30).to_i64,
|
||||
"9:varint" => 1_i64,
|
||||
"10:varint" => 0_i64,
|
||||
}
|
||||
|
||||
object_inner_2_encoded = object_inner_2
|
||||
.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
content_type_numerical =
|
||||
case content_type
|
||||
when "videos" then 15
|
||||
when "livestreams" then 14
|
||||
else 15 # Fallback to "videos"
|
||||
end
|
||||
|
||||
sort_by_numerical =
|
||||
case sort_by
|
||||
when "newest" then 1_i64
|
||||
when "popular" then 2_i64
|
||||
when "oldest" then 4_i64
|
||||
else 1_i64 # Fallback to "newest"
|
||||
end
|
||||
|
||||
object_inner_1 = {
|
||||
"110:embedded" => {
|
||||
"3:embedded" => {
|
||||
"#{content_type_numerical}:embedded" => {
|
||||
"1:embedded" => {
|
||||
"1:string" => object_inner_2_encoded,
|
||||
},
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"3:varint" => sort_by_numerical,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
object_inner_1_encoded = object_inner_1
|
||||
.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
object = {
|
||||
"80226972:embedded" => {
|
||||
"2:string" => ucid,
|
||||
"3:string" => object_inner_1_encoded,
|
||||
"35:string" => "browse-feed#{ucid}videos102",
|
||||
},
|
||||
}
|
||||
|
||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
return continuation
|
||||
end
|
||||
|
||||
def make_initial_content_ctoken(ucid, content_type, sort_by) : String
|
||||
return produce_channel_content_continuation(ucid, content_type, sort_by: sort_by)
|
||||
end
|
||||
|
||||
module Invidious::Channel::Tabs
|
||||
extend self
|
||||
|
||||
@ -101,7 +26,7 @@ module Invidious::Channel::Tabs
|
||||
end
|
||||
|
||||
def get_videos(author : String, ucid : String, *, continuation : String? = nil, sort_by = "newest")
|
||||
continuation ||= make_initial_content_ctoken(ucid, "videos", sort_by)
|
||||
continuation ||= make_initial_videos_ctoken(ucid, sort_by)
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
|
||||
return extract_items(initial_data, author, ucid)
|
||||
@ -130,14 +55,10 @@ module Invidious::Channel::Tabs
|
||||
# Shorts
|
||||
# -------------------
|
||||
|
||||
def get_shorts(channel : AboutChannel, continuation : String? = nil)
|
||||
if continuation.nil?
|
||||
# EgZzaG9ydHPyBgUKA5oBAA%3D%3D is the protobuf object to load "shorts"
|
||||
# TODO: try to extract the continuation tokens that allows other sorting options
|
||||
initial_data = YoutubeAPI.browse(channel.ucid, params: "EgZzaG9ydHPyBgUKA5oBAA%3D%3D")
|
||||
else
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
end
|
||||
def get_shorts(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
|
||||
continuation ||= make_initial_shorts_ctoken(channel.ucid, sort_by)
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
|
||||
return extract_items(initial_data, channel.author, channel.ucid)
|
||||
end
|
||||
|
||||
@ -145,9 +66,8 @@ module Invidious::Channel::Tabs
|
||||
# Livestreams
|
||||
# -------------------
|
||||
|
||||
def get_livestreams(channel : AboutChannel, continuation : String? = nil, sort_by = "newest")
|
||||
continuation ||= make_initial_content_ctoken(channel.ucid, "livestreams", sort_by)
|
||||
|
||||
def get_livestreams(channel : AboutChannel, *, continuation : String? = nil, sort_by = "newest")
|
||||
continuation ||= make_initial_livestreams_ctoken(channel.ucid, sort_by)
|
||||
initial_data = YoutubeAPI.browse(continuation: continuation)
|
||||
|
||||
return extract_items(initial_data, channel.author, channel.ucid)
|
||||
@ -171,4 +91,102 @@ module Invidious::Channel::Tabs
|
||||
|
||||
return items, next_continuation
|
||||
end
|
||||
|
||||
# -------------------
|
||||
# C-tokens
|
||||
# -------------------
|
||||
|
||||
private def sort_options_videos_short(sort_by : String)
|
||||
case sort_by
|
||||
when "newest" then return 4_i64
|
||||
when "popular" then return 2_i64
|
||||
when "oldest" then return 5_i64
|
||||
else return 4_i64 # Fallback to "newest"
|
||||
end
|
||||
end
|
||||
|
||||
# Generate the initial "continuation token" to get the first page of the
|
||||
# "videos" tab. The following page requires the ctoken provided in that
|
||||
# first page, and so on.
|
||||
private def make_initial_videos_ctoken(ucid : String, sort_by = "newest")
|
||||
object = {
|
||||
"15:embedded" => {
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"4:varint" => sort_options_videos_short(sort_by),
|
||||
},
|
||||
}
|
||||
|
||||
return channel_ctoken_wrap(ucid, object)
|
||||
end
|
||||
|
||||
# Generate the initial "continuation token" to get the first page of the
|
||||
# "shorts" tab. The following page requires the ctoken provided in that
|
||||
# first page, and so on.
|
||||
private def make_initial_shorts_ctoken(ucid : String, sort_by = "newest")
|
||||
object = {
|
||||
"10:embedded" => {
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"4:varint" => sort_options_videos_short(sort_by),
|
||||
},
|
||||
}
|
||||
|
||||
return channel_ctoken_wrap(ucid, object)
|
||||
end
|
||||
|
||||
# Generate the initial "continuation token" to get the first page of the
|
||||
# "livestreams" tab. The following page requires the ctoken provided in that
|
||||
# first page, and so on.
|
||||
private def make_initial_livestreams_ctoken(ucid : String, sort_by = "newest")
|
||||
sort_by_numerical =
|
||||
case sort_by
|
||||
when "newest" then 12_i64
|
||||
when "popular" then 14_i64
|
||||
when "oldest" then 13_i64
|
||||
else 12_i64 # Fallback to "newest"
|
||||
end
|
||||
|
||||
object = {
|
||||
"14:embedded" => {
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"5:varint" => sort_by_numerical,
|
||||
},
|
||||
}
|
||||
|
||||
return channel_ctoken_wrap(ucid, object)
|
||||
end
|
||||
|
||||
# The protobuf structure common between videos/shorts/livestreams
|
||||
private def channel_ctoken_wrap(ucid : String, object)
|
||||
object_inner = {
|
||||
"110:embedded" => {
|
||||
"3:embedded" => object,
|
||||
},
|
||||
}
|
||||
|
||||
object_inner_encoded = object_inner
|
||||
.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
object = {
|
||||
"80226972:embedded" => {
|
||||
"2:string" => ucid,
|
||||
"3:string" => object_inner_encoded,
|
||||
},
|
||||
}
|
||||
|
||||
continuation = object.try { |i| Protodec::Any.cast_json(i) }
|
||||
.try { |i| Protodec::Any.from_json(i) }
|
||||
.try { |i| Base64.urlsafe_encode(i) }
|
||||
.try { |i| URI.encode_www_form(i) }
|
||||
|
||||
return continuation
|
||||
end
|
||||
end
|
||||
|
@ -57,7 +57,7 @@ module Invidious::Comments
|
||||
return initial_data
|
||||
end
|
||||
|
||||
def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", is_post = false)
|
||||
def parse_youtube(id, response, format, locale, thin_mode, sort_by = "top", type = "video", ucid = nil)
|
||||
contents = nil
|
||||
|
||||
if on_response_received_endpoints = response["onResponseReceivedEndpoints"]?
|
||||
@ -115,7 +115,11 @@ module Invidious::Comments
|
||||
json.field "commentCount", comment_count
|
||||
end
|
||||
|
||||
if is_post
|
||||
if !ucid.nil?
|
||||
json.field "authorId", ucid
|
||||
end
|
||||
|
||||
if type == "post"
|
||||
json.field "postId", id
|
||||
else
|
||||
json.field "videoId", id
|
||||
@ -302,7 +306,8 @@ module Invidious::Comments
|
||||
|
||||
if format == "html"
|
||||
response = JSON.parse(response)
|
||||
content_html = Frontend::Comments.template_youtube(response, locale, thin_mode)
|
||||
content_html = Frontend::Comments.template_youtube(response, locale, thin_mode, id, type)
|
||||
|
||||
response = JSON.build do |json|
|
||||
json.object do
|
||||
json.field "contentHtml", content_html
|
||||
|
@ -78,6 +78,8 @@ class Config
|
||||
property output : String = "STDOUT"
|
||||
# Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
|
||||
property log_level : LogLevel = LogLevel::Info
|
||||
# Enables colors in logs. Useful for debugging purposes
|
||||
property colorize_logs : Bool = false
|
||||
# Database configuration with separate parameters (username, hostname, etc)
|
||||
property db : DBConfig? = nil
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
module Invidious::Frontend::Comments
|
||||
extend self
|
||||
|
||||
def template_youtube(comments, locale, thin_mode, is_replies = false)
|
||||
def template_youtube(comments, locale, thin_mode, id, type = "video", is_replies = false)
|
||||
String.build do |html|
|
||||
root = comments["comments"].as_a
|
||||
root.each do |child|
|
||||
@ -13,17 +13,17 @@ module Invidious::Frontend::Comments
|
||||
)
|
||||
|
||||
replies_html = <<-END_HTML
|
||||
<div id="replies" class="pure-g">
|
||||
<div class="pure-g replies">
|
||||
<div class="pure-u-1-24"></div>
|
||||
<div class="pure-u-23-24">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
|
||||
<a target="_blank" href="/comment_viewer?continuation=#{child["replies"]["continuation"]}&id=#{id}&type=#{type}" data-continuation="#{child["replies"]["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-replies>#{replies_count_text}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
elsif comments["authorId"]? && !comments["singlePost"]?
|
||||
elsif comments["authorId"]? && !comments["singlePost"]? && type != "post"
|
||||
# for posts we should display a link to the post
|
||||
replies_count_text = translate_count(locale,
|
||||
"comments_view_x_replies",
|
||||
@ -147,7 +147,12 @@ module Invidious::Frontend::Comments
|
||||
|
|
||||
END_HTML
|
||||
|
||||
if comments["videoId"]?
|
||||
if type == "post" && !comments["singlePost"]?
|
||||
html << <<-END_HTML
|
||||
<a href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{id}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
elsif comments["videoId"]?
|
||||
html << <<-END_HTML
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
@ -196,7 +201,7 @@ module Invidious::Frontend::Comments
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
||||
<a target="_blank" href="/comment_viewer?continuation=#{comments["continuation"]}&id=#{id}&type=#{type}" data-continuation="#{comments["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,8 +1,22 @@
|
||||
# Languages requiring a better level of translation (at least 20%)
|
||||
# to be added to the list below:
|
||||
#
|
||||
# "af" => "", # Afrikaans
|
||||
# "az" => "", # Azerbaijani
|
||||
# "be" => "", # Belarusian
|
||||
# "bn_BD" => "", # Bengali (Bangladesh)
|
||||
# "ia" => "", # Interlingua
|
||||
# "or" => "", # Odia
|
||||
# "tk" => "", # Turkmen
|
||||
# "tok => "", # Toki Pona
|
||||
#
|
||||
LOCALES_LIST = {
|
||||
"ar" => "العربية", # Arabic
|
||||
"bg" => "български", # Bulgarian
|
||||
"bn" => "বাংলা", # Bengali
|
||||
"ca" => "Català", # Catalan
|
||||
"cs" => "Čeština", # Czech
|
||||
"cy" => "Cymraeg", # Welsh
|
||||
"da" => "Dansk", # Danish
|
||||
"de" => "Deutsch", # German
|
||||
"el" => "Ελληνικά", # Greek
|
||||
@ -23,6 +37,7 @@ LOCALES_LIST = {
|
||||
"it" => "Italiano", # Italian
|
||||
"ja" => "日本語", # Japanese
|
||||
"ko" => "한국어", # Korean
|
||||
"lmo" => "Lombard", # Lombard
|
||||
"lt" => "Lietuvių", # Lithuanian
|
||||
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
|
||||
"nl" => "Nederlands", # Dutch
|
||||
|
@ -1,3 +1,5 @@
|
||||
require "colorize"
|
||||
|
||||
enum LogLevel
|
||||
All = 0
|
||||
Trace = 1
|
||||
@ -10,7 +12,9 @@ enum LogLevel
|
||||
end
|
||||
|
||||
class Invidious::LogHandler < Kemal::BaseLogHandler
|
||||
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug)
|
||||
def initialize(@io : IO = STDOUT, @level = LogLevel::Debug, use_color : Bool = true)
|
||||
Colorize.enabled = use_color
|
||||
Colorize.on_tty_only!
|
||||
end
|
||||
|
||||
def call(context : HTTP::Server::Context)
|
||||
@ -39,10 +43,22 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
|
||||
@io.flush
|
||||
end
|
||||
|
||||
def color(level)
|
||||
case level
|
||||
when LogLevel::Trace then :cyan
|
||||
when LogLevel::Debug then :green
|
||||
when LogLevel::Info then :white
|
||||
when LogLevel::Warn then :yellow
|
||||
when LogLevel::Error then :red
|
||||
when LogLevel::Fatal then :magenta
|
||||
else :default
|
||||
end
|
||||
end
|
||||
|
||||
{% for level in %w(trace debug info warn error fatal) %}
|
||||
def {{level.id}}(message : String)
|
||||
if LogLevel::{{level.id.capitalize}} >= @level
|
||||
puts("#{Time.utc} [{{level.id}}] #{message}")
|
||||
puts("#{Time.utc} [{{level.id}}] #{message}".colorize(color(LogLevel::{{level.id.capitalize}})))
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
|
@ -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)
|
||||
@ -394,7 +395,7 @@ module Invidious::Routes::API::V1::Channels
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
id = env.params.url["id"].to_s
|
||||
id = URI.encode_www_form(env.params.url["id"].to_s)
|
||||
ucid = env.params.query["ucid"]?
|
||||
|
||||
thin_mode = env.params.query["thin_mode"]?
|
||||
@ -406,9 +407,9 @@ module Invidious::Routes::API::V1::Channels
|
||||
if ucid.nil?
|
||||
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
||||
return error_json(400, "Invalid post ID") if response["error"]?
|
||||
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
|
||||
ucid = URI.encode_www_form(response.dig("endpoint", "browseEndpoint", "browseId").as_s)
|
||||
else
|
||||
ucid = ucid.to_s
|
||||
ucid = URI.encode_www_form(ucid.to_s)
|
||||
end
|
||||
|
||||
begin
|
||||
@ -423,7 +424,7 @@ module Invidious::Routes::API::V1::Channels
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
id = env.params.url["id"]
|
||||
id = URI.encode_www_form(env.params.url["id"])
|
||||
|
||||
thin_mode = env.params.query["thin_mode"]?
|
||||
thin_mode = thin_mode == "true"
|
||||
@ -432,15 +433,23 @@ module Invidious::Routes::API::V1::Channels
|
||||
format ||= "json"
|
||||
|
||||
continuation = env.params.query["continuation"]?
|
||||
ucid = env.params.query["ucid"]?
|
||||
|
||||
if ucid.nil?
|
||||
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
||||
return error_json(400, "Invalid post ID") if response["error"]?
|
||||
ucid = URI.encode_www_form(response.dig("endpoint", "browseEndpoint", "browseId").as_s)
|
||||
else
|
||||
ucid = URI.encode_www_form(ucid.to_s)
|
||||
end
|
||||
|
||||
case continuation
|
||||
when nil, ""
|
||||
ucid = env.params.query["ucid"]
|
||||
comments = Comments.fetch_community_post_comments(ucid, id)
|
||||
else
|
||||
comments = YoutubeAPI.browse(continuation: continuation)
|
||||
end
|
||||
return Comments.parse_youtube(id, comments, format, locale, thin_mode, is_post: true)
|
||||
return Comments.parse_youtube(id, comments, format, locale, thin_mode, type: "post", ucid: ucid)
|
||||
end
|
||||
|
||||
def self.channels(env)
|
||||
|
@ -31,9 +31,7 @@ module Invidious::Routes::API::V1::Search
|
||||
query = env.params.query["q"]? || ""
|
||||
|
||||
begin
|
||||
client = HTTP::Client.new("suggestqueries-clients6.youtube.com")
|
||||
client.before_request { |r| add_yt_headers(r) }
|
||||
|
||||
client = make_client(URI.parse("https://suggestqueries-clients6.youtube.com"), force_youtube_headers: true)
|
||||
url = "/complete/search?client=youtube&hl=en&gl=#{region}&q=#{URI.encode_www_form(query)}&gs_ri=youtube&ds=yt"
|
||||
|
||||
response = client.get(url).body
|
||||
|
@ -392,7 +392,7 @@ module Invidious::Routes::API::V1::Videos
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
clip_id = env.params.url["id"]
|
||||
clip_id = URI.encode_www_form(env.params.url["id"])
|
||||
region = env.params.query["region"]?
|
||||
proxy = {"1", "true"}.any? &.== env.params.query["local"]?
|
||||
|
||||
|
@ -20,10 +20,11 @@ module Invidious::Routes::Channels
|
||||
sort_by = env.params.query["sort_by"]?.try &.downcase
|
||||
|
||||
if channel.auto_generated
|
||||
sort_by ||= "last"
|
||||
sort_options = {"last", "oldest", "newest"}
|
||||
|
||||
items, next_continuation = fetch_channel_playlists(
|
||||
channel.ucid, channel.author, continuation, (sort_by || "last")
|
||||
channel.ucid, channel.author, continuation, sort_by
|
||||
)
|
||||
|
||||
items.uniq! do |item|
|
||||
@ -49,9 +50,11 @@ module Invidious::Routes::Channels
|
||||
end
|
||||
next_continuation = nil
|
||||
else
|
||||
sort_by ||= "newest"
|
||||
sort_options = {"newest", "oldest", "popular"}
|
||||
items, next_continuation = Channel::Tabs.get_videos(
|
||||
channel, continuation: continuation, sort_by: (sort_by || "newest")
|
||||
|
||||
items, next_continuation = Channel::Tabs.get_60_videos(
|
||||
channel, continuation: continuation, sort_by: sort_by
|
||||
)
|
||||
end
|
||||
end
|
||||
@ -82,13 +85,12 @@ module Invidious::Routes::Channels
|
||||
end
|
||||
next_continuation = nil
|
||||
else
|
||||
# TODO: support sort option for shorts
|
||||
sort_by = ""
|
||||
sort_options = [] of String
|
||||
sort_by = env.params.query["sort_by"]?.try &.downcase || "newest"
|
||||
sort_options = {"newest", "oldest", "popular"}
|
||||
|
||||
# Fetch items and continuation token
|
||||
items, next_continuation = Channel::Tabs.get_shorts(
|
||||
channel, continuation: continuation
|
||||
channel, continuation: continuation, sort_by: sort_by
|
||||
)
|
||||
end
|
||||
|
||||
@ -237,7 +239,7 @@ module Invidious::Routes::Channels
|
||||
|
||||
def self.post(env)
|
||||
# /post/{postId}
|
||||
id = env.params.url["id"]
|
||||
id = URI.encode_www_form(env.params.url["id"])
|
||||
ucid = env.params.query["ucid"]?
|
||||
|
||||
prefs = env.get("preferences").as(Preferences)
|
||||
@ -253,14 +255,14 @@ module Invidious::Routes::Channels
|
||||
nojs = nojs == "1"
|
||||
|
||||
if !ucid.nil?
|
||||
ucid = ucid.to_s
|
||||
ucid = URI.encode_www_form(ucid.to_s)
|
||||
post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
|
||||
else
|
||||
# resolve the url to get the author's UCID
|
||||
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
||||
return error_template(400, "Invalid post ID") if response["error"]?
|
||||
|
||||
ucid = response.dig("endpoint", "browseEndpoint", "browseId").as_s
|
||||
ucid = URI.encode_www_form(response.dig("endpoint", "browseEndpoint", "browseId").as_s)
|
||||
post_response = fetch_channel_community_post(ucid, id, locale, "json", thin_mode)
|
||||
end
|
||||
|
||||
@ -268,7 +270,7 @@ module Invidious::Routes::Channels
|
||||
|
||||
if nojs
|
||||
comments = Comments.fetch_community_post_comments(ucid, id)
|
||||
comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, is_post: true))["contentHtml"]
|
||||
comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, type: "post", ucid: ucid))["contentHtml"]
|
||||
end
|
||||
templated "post"
|
||||
end
|
||||
|
@ -42,7 +42,7 @@ module Invidious::Routes::VideoPlayback
|
||||
headers["Range"] = "bytes=#{range_for_head}"
|
||||
end
|
||||
|
||||
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||
client = make_client(URI.parse(host), region, force_resolve: true)
|
||||
response = HTTP::Client::Response.new(500)
|
||||
error = ""
|
||||
5.times do
|
||||
@ -57,7 +57,7 @@ module Invidious::Routes::VideoPlayback
|
||||
if new_host != host
|
||||
host = new_host
|
||||
client.close
|
||||
client = make_client(URI.parse(new_host), region, force_resolve = true)
|
||||
client = make_client(URI.parse(new_host), region, force_resolve: true)
|
||||
end
|
||||
|
||||
url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
||||
@ -71,7 +71,7 @@ module Invidious::Routes::VideoPlayback
|
||||
fvip = "3"
|
||||
|
||||
host = "https://r#{fvip}---#{mn}.googlevideo.com"
|
||||
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||
client = make_client(URI.parse(host), region, force_resolve: true)
|
||||
rescue ex
|
||||
error = ex.message
|
||||
end
|
||||
@ -196,7 +196,7 @@ module Invidious::Routes::VideoPlayback
|
||||
break
|
||||
else
|
||||
client.close
|
||||
client = make_client(URI.parse(host), region, force_resolve = true)
|
||||
client = make_client(URI.parse(host), region, force_resolve: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -332,4 +332,53 @@ module Invidious::Routes::Watch
|
||||
return error_template(400, "Invalid label or itag")
|
||||
end
|
||||
end
|
||||
|
||||
# used for fetching replies/ fetching more comments when js is disabled.
|
||||
def self.comments(env)
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
region = env.params.query["region"]?
|
||||
|
||||
id = URI.encode_www_form(env.params.query["id"])
|
||||
continuation = env.params.query["continuation"]?
|
||||
|
||||
source = env.params.query["source"]? || "youtube"
|
||||
|
||||
thin_mode = env.params.query["thin_mode"]? == "true"
|
||||
comment_type = env.params.query["type"]? || "video"
|
||||
|
||||
parent_comment = nil
|
||||
if comment_type == "community"
|
||||
# community posts
|
||||
comment_html = JSON.parse(fetch_channel_community(id, continuation, locale, "html", thin_mode))["contentHtml"]
|
||||
elsif comment_type == "post"
|
||||
# replies to a community post
|
||||
ucid = env.params.query["ucid"]?
|
||||
if ucid.nil?
|
||||
response = YoutubeAPI.resolve_url("https://www.youtube.com/post/#{id}")
|
||||
return error_json(400, "Invalid post ID") if response["error"]?
|
||||
ucid = URI.encode_www_form(response.dig("endpoint", "browseEndpoint", "browseId").as_s)
|
||||
else
|
||||
ucid = URI.encode_www_form(ucid.to_s)
|
||||
end
|
||||
case continuation
|
||||
when nil, ""
|
||||
comments = Comments.fetch_community_post_comments(ucid, id)
|
||||
else
|
||||
comments = YoutubeAPI.browse(continuation: continuation)
|
||||
end
|
||||
comment_html = JSON.parse(Comments.parse_youtube(id, comments, "html", locale, thin_mode, type: "post", ucid: ucid))["contentHtml"]
|
||||
else
|
||||
# video comments
|
||||
if source == "youtube"
|
||||
comment_html = JSON.parse(Comments.fetch_youtube(id, continuation, "html", locale, thin_mode, region))["contentHtml"]
|
||||
elsif source == "reddit"
|
||||
comments, reddit_thread = Comments.fetch_reddit(id)
|
||||
comment_html = Frontend::Comments.template_reddit(comments, locale)
|
||||
|
||||
comment_html = Comments.fill_links(comment_html, "https", "www.reddit.com")
|
||||
comment_html = Comments.replace_links(comment_html)
|
||||
end
|
||||
end
|
||||
templated "comments_no_js"
|
||||
end
|
||||
end
|
||||
|
@ -170,6 +170,8 @@ module Invidious::Routing
|
||||
|
||||
get "/embed/", Routes::Embed, :redirect
|
||||
get "/embed/:id", Routes::Embed, :show
|
||||
# currently only for fetching continuations when js is disabled.
|
||||
get "/comment_viewer", Routes::Watch, :comments
|
||||
end
|
||||
|
||||
def register_yt_playlist_routes
|
||||
@ -243,17 +245,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 +272,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
|
||||
|
||||
|
@ -53,10 +53,6 @@ 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 +102,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 +113,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 +216,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
|
||||
|
8
src/invidious/views/comments_no_js.ecr
Normal file
8
src/invidious/views/comments_no_js.ecr
Normal file
@ -0,0 +1,8 @@
|
||||
<% content_for "header" do %>
|
||||
<title>Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<!-- basic comments page for people with js disabled. -->
|
||||
<div class="comments post-comments">
|
||||
<%= comment_html %>
|
||||
</div>
|
@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="h-box pure-g comments" id="comments">
|
||||
<%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode) %>
|
||||
<%= IV::Frontend::Comments.template_youtube(items.not_nil!, locale, thin_mode, ucid, "community") %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<div>
|
||||
<div id="post" class="comments post-comments">
|
||||
<%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode) %>
|
||||
<%= IV::Frontend::Comments.template_youtube(post_response.not_nil!, locale, thin_mode, id, "post") %>
|
||||
</div>
|
||||
|
||||
<% if nojs %>
|
||||
|
@ -22,12 +22,8 @@ struct YoutubeConnectionPool
|
||||
response = yield conn
|
||||
rescue ex
|
||||
conn.close
|
||||
conn = make_client(url, force_resolve: true)
|
||||
|
||||
conn = HTTP::Client.new(url)
|
||||
conn.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
||||
conn.family = CONFIG.force_resolve
|
||||
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
||||
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||
response = yield conn
|
||||
ensure
|
||||
pool.release(conn)
|
||||
@ -37,12 +33,15 @@ struct YoutubeConnectionPool
|
||||
end
|
||||
|
||||
private def build_pool
|
||||
DB::Pool(HTTP::Client).new(initial_pool_size: 0, max_pool_size: capacity, max_idle_pool_size: capacity, checkout_timeout: timeout) do
|
||||
conn = HTTP::Client.new(url)
|
||||
conn.family = CONFIG.force_resolve
|
||||
conn.family = Socket::Family::INET if conn.family == Socket::Family::UNSPEC
|
||||
conn.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||
conn
|
||||
options = DB::Pool::Options.new(
|
||||
initial_pool_size: 0,
|
||||
max_pool_size: capacity,
|
||||
max_idle_pool_size: capacity,
|
||||
checkout_timeout: timeout
|
||||
)
|
||||
|
||||
DB::Pool(HTTP::Client).new(options) do
|
||||
next make_client(url, force_resolve: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -62,15 +61,17 @@ def add_yt_headers(request)
|
||||
end
|
||||
end
|
||||
|
||||
def make_client(url : URI, region = nil, force_resolve : Bool = false)
|
||||
def make_client(url : URI, region = nil, force_resolve : Bool = false, force_youtube_headers : Bool = false)
|
||||
client = HTTP::Client.new(url)
|
||||
client.proxy = make_configured_http_proxy_client() if CONFIG.http_proxy
|
||||
|
||||
# Force the usage of a specific configured IP Family
|
||||
if force_resolve
|
||||
client.family = CONFIG.force_resolve
|
||||
client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC
|
||||
end
|
||||
|
||||
client.before_request { |r| add_yt_headers(r) } if url.host == "www.youtube.com"
|
||||
client.before_request { |r| add_yt_headers(r) } if url.host.try &.ends_with?("youtube.com") || force_youtube_headers
|
||||
client.read_timeout = 10.seconds
|
||||
client.connect_timeout = 10.seconds
|
||||
|
||||
@ -78,7 +79,7 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false)
|
||||
end
|
||||
|
||||
def make_client(url : URI, region = nil, force_resolve : Bool = false, &)
|
||||
client = make_client(url, region, force_resolve)
|
||||
client = make_client(url, region, force_resolve: force_resolve)
|
||||
begin
|
||||
yield client
|
||||
ensure
|
||||
|
@ -21,6 +21,7 @@ private ITEM_PARSERS = {
|
||||
Parsers::ItemSectionRendererParser,
|
||||
Parsers::ContinuationItemRendererParser,
|
||||
Parsers::HashtagRendererParser,
|
||||
Parsers::LockupViewModelParser,
|
||||
}
|
||||
|
||||
private alias InitialData = Hash(String, JSON::Any)
|
||||
@ -467,9 +468,9 @@ private module Parsers
|
||||
# Parses an InnerTube richItemRenderer into a SearchVideo.
|
||||
# Returns nil when the given object isn't a RichItemRenderer
|
||||
#
|
||||
# A richItemRenderer seems to be a simple wrapper for a videoRenderer, used
|
||||
# by the result page for hashtags and for the podcast tab on channels.
|
||||
# It is located inside a continuationItems container for hashtags.
|
||||
# A richItemRenderer seems to be a simple wrapper for a various other types,
|
||||
# used on the hashtags result page and the channel podcast tab. It is located
|
||||
# itself inside a richGridRenderer container.
|
||||
#
|
||||
module RichItemRendererParser
|
||||
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||
@ -482,6 +483,8 @@ private module Parsers
|
||||
child = VideoRendererParser.process(item_contents, author_fallback)
|
||||
child ||= ReelItemRendererParser.process(item_contents, author_fallback)
|
||||
child ||= PlaylistRendererParser.process(item_contents, author_fallback)
|
||||
child ||= LockupViewModelParser.process(item_contents, author_fallback)
|
||||
child ||= ShortsLockupViewModelParser.process(item_contents, author_fallback)
|
||||
return child
|
||||
end
|
||||
|
||||
@ -496,6 +499,9 @@ private module Parsers
|
||||
# reelItemRenderer items are used in the new (2022) channel layout,
|
||||
# in the "shorts" tab.
|
||||
#
|
||||
# NOTE: As of 10/2024, it might have been fully replaced by shortsLockupViewModel
|
||||
# TODO: Confirm that hypothesis
|
||||
#
|
||||
module ReelItemRendererParser
|
||||
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||
if item_contents = item["reelItemRenderer"]?
|
||||
@ -582,6 +588,135 @@ private module Parsers
|
||||
end
|
||||
end
|
||||
|
||||
# Parses an InnerTube lockupViewModel into a SearchPlaylist.
|
||||
# Returns nil when the given object is not a lockupViewModel.
|
||||
#
|
||||
# This structure is present since November 2024 on the "podcasts" and
|
||||
# "playlists" tabs of the channel page. It is usually encapsulated in either
|
||||
# a richItemRenderer or a richGridRenderer.
|
||||
#
|
||||
module LockupViewModelParser
|
||||
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||
if item_contents = item["lockupViewModel"]?
|
||||
return self.parse(item_contents, author_fallback)
|
||||
end
|
||||
end
|
||||
|
||||
private def self.parse(item_contents, author_fallback)
|
||||
playlist_id = item_contents["contentId"].as_s
|
||||
|
||||
thumbnail_view_model = item_contents.dig(
|
||||
"contentImage", "collectionThumbnailViewModel",
|
||||
"primaryThumbnail", "thumbnailViewModel"
|
||||
)
|
||||
|
||||
thumbnail = thumbnail_view_model.dig("image", "sources", 0, "url").as_s
|
||||
|
||||
# This complicated sequences tries to extract the following data structure:
|
||||
# "overlays": [{
|
||||
# "thumbnailOverlayBadgeViewModel": {
|
||||
# "thumbnailBadges": [{
|
||||
# "thumbnailBadgeViewModel": {
|
||||
# "text": "430 episodes",
|
||||
# "badgeStyle": "THUMBNAIL_OVERLAY_BADGE_STYLE_DEFAULT"
|
||||
# }
|
||||
# }]
|
||||
# }
|
||||
# }]
|
||||
#
|
||||
# NOTE: this simplistic `.to_i` conversion might not work on larger
|
||||
# playlists and hasn't been tested.
|
||||
video_count = thumbnail_view_model.dig("overlays").as_a
|
||||
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
|
||||
.flatten
|
||||
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try { |node|
|
||||
{"episodes", "videos"}.any? { |str| node.as_s.ends_with?(str) }
|
||||
})
|
||||
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)
|
||||
|
||||
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
|
||||
title = metadata.dig("title", "content").as_s
|
||||
|
||||
# TODO: Retrieve "updated" info from metadata parts
|
||||
# rows = metadata.dig("metadata", "contentMetadataViewModel", "metadataRows").as_a
|
||||
# parts_text = rows.map(&.dig?("metadataParts", "text", "content").try &.as_s)
|
||||
# One of these parts should contain a string like: "Updated 2 days ago"
|
||||
|
||||
# TODO: Maybe add a button to access the first video of the playlist?
|
||||
# item_contents.dig("rendererContext", "commandContext", "onTap", "innertubeCommand", "watchEndpoint")
|
||||
# Available fields: "videoId", "playlistId", "params"
|
||||
|
||||
return SearchPlaylist.new({
|
||||
title: title,
|
||||
id: playlist_id,
|
||||
author: author_fallback.name,
|
||||
ucid: author_fallback.id,
|
||||
video_count: video_count || -1,
|
||||
videos: [] of SearchPlaylistVideo,
|
||||
thumbnail: thumbnail,
|
||||
author_verified: false,
|
||||
})
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
end
|
||||
end
|
||||
|
||||
# Parses an InnerTube shortsLockupViewModel into a SearchVideo.
|
||||
# Returns nil when the given object is not a shortsLockupViewModel.
|
||||
#
|
||||
# This structure is present since around October 2024 on the "shorts" tab of
|
||||
# the channel page and likely replaces the reelItemRenderer structure. It is
|
||||
# usually (always?) encapsulated in a richItemRenderer.
|
||||
#
|
||||
module ShortsLockupViewModelParser
|
||||
def self.process(item : JSON::Any, author_fallback : AuthorFallback)
|
||||
if item_contents = item["shortsLockupViewModel"]?
|
||||
return self.parse(item_contents, author_fallback)
|
||||
end
|
||||
end
|
||||
|
||||
private def self.parse(item_contents, author_fallback)
|
||||
# TODO: Maybe add support for "oardefault.jpg" thumbnails?
|
||||
# thumbnail = item_contents.dig("thumbnail", "sources", 0, "url").as_s
|
||||
# Gives: https://i.ytimg.com/vi/{video_id}/oardefault.jpg?...
|
||||
|
||||
video_id = item_contents.dig(
|
||||
"onTap", "innertubeCommand", "reelWatchEndpoint", "videoId"
|
||||
).as_s
|
||||
|
||||
title = item_contents.dig("overlayMetadata", "primaryText", "content").as_s
|
||||
|
||||
view_count = short_text_to_number(
|
||||
item_contents.dig("overlayMetadata", "secondaryText", "content").as_s
|
||||
)
|
||||
|
||||
# Approximate to one minute, as "shorts" generally don't exceed that.
|
||||
# NOTE: The actual duration is not provided by Youtube anymore.
|
||||
# TODO: Maybe use -1 as an error value and handle that on the frontend?
|
||||
duration = 60_i32
|
||||
|
||||
SearchVideo.new({
|
||||
title: title,
|
||||
id: video_id,
|
||||
author: author_fallback.name,
|
||||
ucid: author_fallback.id,
|
||||
published: Time.unix(0),
|
||||
views: view_count,
|
||||
description_html: "",
|
||||
length_seconds: duration,
|
||||
premiere_timestamp: Time.unix(0),
|
||||
author_verified: false,
|
||||
badges: VideoBadges::None,
|
||||
})
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
end
|
||||
end
|
||||
|
||||
# Parses an InnerTube continuationItemRenderer into a Continuation.
|
||||
# Returns nil when the given object isn't a continuationItemRenderer.
|
||||
#
|
||||
|
@ -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
|
||||
|
||||
@ -638,6 +637,11 @@ module YoutubeAPI
|
||||
# Send the POST request
|
||||
body = YT_POOL.client() do |client|
|
||||
client.post(url, headers: headers, body: data.to_json) do |response|
|
||||
if response.status_code != 200
|
||||
raise InfoException.new("Error: non 200 status code. Youtube API returned \
|
||||
status code #{response.status_code}. See <a href=\"https://docs.invidious.io/youtube-errors-explained/\"> \
|
||||
https://docs.invidious.io/youtube-errors-explained/</a> for troubleshooting.")
|
||||
end
|
||||
self._decompress(response.body_io, response.headers["Content-Encoding"]?)
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user