mirror of
https://github.com/iv-org/invidious.git
synced 2026-06-10 18:53:30 +05:30
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63cb6db1a3 | |||
| e96ad036ca | |||
| 1a5a71b086 | |||
| 3a35552a66 | |||
| 8ef5ea03d4 | |||
| 86c425b43f | |||
| 4ae227ce91 | |||
| 8b183caa2a | |||
| edb3a0fc36 | |||
| 6659cbbbd8 | |||
| 99390d065d | |||
| e82ac674ae | |||
| 103f80e617 | |||
| 9ee39bc437 | |||
| 529fc8a8a3 | |||
| b4728b81dc | |||
| f914ce8040 | |||
| 57ba257233 | |||
| e012334975 | |||
| 85a078a580 | |||
| afea61bb8f | |||
| fd313e0107 | |||
| 0c600988ca | |||
| 264e7c24e9 | |||
| 9eda6e5bc4 | |||
| 73c749f13f | |||
| bc64cd9b67 | |||
| 54365c0e2a | |||
| 606467c693 | |||
| 749791cdf1 | |||
| d7361cbb9a | |||
| f07c9a7209 | |||
| cf9b6c4fcb | |||
| 21d0d1041a | |||
| fda8d1b528 | |||
| e7f8b15b21 | |||
| 60c31e3069 | |||
| 11db343cfb | |||
| 118d635650 | |||
| 29c29f7c8d | |||
| 067a426235 | |||
| ffd9f4b112 | |||
| cc7cb94095 | |||
| 0ee92e3298 | |||
| a3a97ccf07 | |||
| ce9494133d | |||
| e4beb00413 | |||
| 050032b188 | |||
| 471857ce8b | |||
| 7be6fbd75c | |||
| 84a699f7b7 | |||
| 864893f4c7 | |||
| ecbc21b067 | |||
| a9f812799c | |||
| 48be830544 | |||
| b521e3be6c | |||
| abb0aa436c | |||
| d51a7a44ad | |||
| 7e36cfb667 | |||
| d25cc9570c | |||
| 66c67f4c7a | |||
| 344bc2d8e9 |
@@ -29,7 +29,7 @@ jobs:
|
||||
- os: ubuntu-24.04-arm
|
||||
platform: linux/arm64/v8
|
||||
name: "ARM64"
|
||||
dockerfile: "docker/Dockerfile.arm64"
|
||||
dockerfile: "docker/Dockerfile"
|
||||
tag_suffix: "-arm64"
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -39,10 +39,10 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: quay.io/invidious/invidious
|
||||
flavor: |
|
||||
@@ -62,13 +62,34 @@ jobs:
|
||||
quay.expires-after=12w
|
||||
|
||||
- name: Build and push Docker ${{ matrix.name }} image for Push Event
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
"release=1"
|
||||
|
||||
combine-multiarch-images:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_PASSWORD }}
|
||||
|
||||
# https://github.com/marketplace/actions/docker-manifest-create-action
|
||||
- name: Create and push manifest
|
||||
uses: int128/docker-manifest-create-action@v2.21.0
|
||||
with:
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:master
|
||||
sources: |
|
||||
quay.io/invidious/invidious:master
|
||||
quay.io/invidious/invidious:master-arm64
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
- os: ubuntu-24.04-arm
|
||||
platform: linux/arm64/v8
|
||||
name: "ARM64"
|
||||
dockerfile: "docker/Dockerfile.arm64"
|
||||
dockerfile: "docker/Dockerfile"
|
||||
tag_suffix: "-arm64"
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
@@ -30,10 +30,10 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: quay.io/invidious/invidious
|
||||
flavor: |
|
||||
@@ -54,13 +54,34 @@ jobs:
|
||||
quay.expires-after=12w
|
||||
|
||||
- name: Build and push Docker ${{ matrix.name }} image for Push Event
|
||||
uses: docker/build-push-action@v6
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ${{ matrix.dockerfile }}
|
||||
file: ${{ matrix.dockerfile }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
build-args: |
|
||||
"release=1"
|
||||
|
||||
combine-multiarch-images:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Login to registry
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: quay.io
|
||||
username: ${{ secrets.QUAY_USERNAME }}
|
||||
password: ${{ secrets.QUAY_PASSWORD }}
|
||||
|
||||
# https://github.com/marketplace/actions/docker-manifest-create-action
|
||||
- name: Create and push manifest
|
||||
uses: int128/docker-manifest-create-action@v2.21.0
|
||||
with:
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:latest
|
||||
sources: |
|
||||
quay.io/invidious/invidious:latest
|
||||
quay.io/invidious/invidious:latest-arm64
|
||||
|
||||
@@ -43,6 +43,8 @@ jobs:
|
||||
- 1.16.3
|
||||
- 1.17.1
|
||||
- 1.18.2
|
||||
- 1.19.2
|
||||
- 1.20.2
|
||||
include:
|
||||
- crystal: nightly
|
||||
stable: false
|
||||
@@ -58,7 +60,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1.9.1
|
||||
uses: crystal-lang/install-crystal@v1.9.2
|
||||
with:
|
||||
crystal: ${{ matrix.crystal }}
|
||||
|
||||
@@ -80,7 +82,7 @@ jobs:
|
||||
run: crystal spec
|
||||
|
||||
- name: Build
|
||||
run: crystal build --warnings all --error-on-warnings --error-trace src/invidious.cr
|
||||
run: crystal build --warnings all --error-on-warnings --stats --time --progress --error-trace src/invidious.cr
|
||||
|
||||
build-docker:
|
||||
strategy:
|
||||
@@ -98,10 +100,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Use ARM64 Dockerfile if ARM64
|
||||
if: ${{ matrix.name == 'ARM64' }}
|
||||
run: sed -i 's/Dockerfile/Dockerfile.arm64/' docker-compose.yml
|
||||
|
||||
- name: Build Docker
|
||||
run: docker compose build
|
||||
|
||||
@@ -134,7 +132,7 @@ jobs:
|
||||
|
||||
- name: Install Crystal
|
||||
id: lint_step_install_crystal
|
||||
uses: crystal-lang/install-crystal@v1.9.1
|
||||
uses: crystal-lang/install-crystal@v1.9.2
|
||||
with:
|
||||
crystal: latest
|
||||
|
||||
|
||||
@@ -2,6 +2,96 @@
|
||||
|
||||
## vX.Y.0 (future)
|
||||
|
||||
## v2.20260207.0
|
||||
|
||||
### Wrap-up
|
||||
|
||||
This release hardens the Invidious companion pipeline and cleans up a long list of UI papercuts. Companion downloads now work end-to-end, CSP headers and check identifiers are generated once and reused, proxy responses strip stray headers, and the final traces of the legacy signature helper are gone so the helper can be rolled out safely.
|
||||
|
||||
Livestream navigation, playlists, and channel metadata also see overdue fixes: Trending once again lists livestreams, "Watch on YouTube" buttons stop jumping to arbitrary timestamps, playlist imports/API calls handle missing data, and channel pages now display creator pronouns and playlist thumbnails. Deployments benefit from compiling OpenSSL into docker images to mitigate a long-standing memory leak observed with Alpine-provided OpenSSL, Crystal pinned back to 1.16.3 for docker and OCI builds, a rewritten static file handler, clarified README/HTTP proxy/unix socket docs, and dozens of smaller cleanups.
|
||||
|
||||
### New features & important changes
|
||||
#### For Users
|
||||
- Livestream experiences are restored: Trending shows livestreams again, the gaming feed remains accessible, and "Watch on YouTube" links stop carrying stale timestamps (#5480, #5555, #5481)
|
||||
- Channel and playlist metadata is richer thanks to pronoun support, topic playlist thumbnails, and accurate related video counts (#5617, #5616, #5446)
|
||||
- Downloads get smoother because download actions are URL-safe and downloads can flow through Invidious companion when available (#5367, #5561)
|
||||
- Users see clearer feedback with Erroneous CAPTCHA messages, DMCA controls restored, and a footer link pointing at the current release (#5508, #5228, #4702)
|
||||
|
||||
#### For instance owners
|
||||
- Companion integration is sturdier: CSP is generated once, check identifiers persist, and the helper hyperlink is fixed (#5497, #5575, #5491)
|
||||
- Proxied images and videoplayback strip unwanted response headers (shared header-strip list) (#5595)
|
||||
- Runtime and packaging updates pin docker/OCI builds to Crystal 1.16.3, bring an optional Crystal 1.18.2 + Alpine 3.23 image, and compile OpenSSL from source to mitigate the memory leak seen with Alpine-provided OpenSSL (#5604, #5577, #5574, #5441)
|
||||
- Configuration docs saw polish with unix socket instructions, refreshed HTTP proxy comments, and corrected README commands (#5347, #5586, #5607)
|
||||
- Server stability improves via a larger `max_request_line_size` that is required to be able to access some next pages of Youtube channels videos and a rewritten static file handler (#5566, #5338)
|
||||
|
||||
#### For developers
|
||||
- Top-level constants moved into dedicated modules, preferences handling was cleaned up, and the legacy signature helper is finally removed (#5596, #5450, #5550)
|
||||
- Crystal API updates replaced the deprecated `Socket#blocking` property and restored the shard target plus SPDX license metadata (#5538, #5608, #5552)
|
||||
- CI/tooling stayed current with newer GitHub Actions, install-crystal releases, and cache/checkout bumps (#5569, #5544, #5530, #5499)
|
||||
|
||||
### Bugs fixed
|
||||
#### User-side
|
||||
- Playlist importer edge cases, playlist API author URLs, and channel continuation tokens now handle empty values without crashing (#4787, #5618, #5614)
|
||||
- Thin mode community posts, posts that reference unavailable videos, and DMCA content toggles work again (#5567, #5549, #5228)
|
||||
- UI cleanups prevent channel name/button overflow, show explicit Erroneous CAPTCHA errors, and keep livestream timestamps clean (#5553, #5452, #5508, #5481)
|
||||
- Trending feeds and related video counts regained accuracy alongside livestream/gaming categories (#5555, #5480, #5446)
|
||||
|
||||
#### For instance owners
|
||||
- Companion downloads, CSP reuse, and check id generation behave predictably even under load (#5561, #5497, #5575)
|
||||
- Proxy responses drop stray headers and HTTP proxy examples in the config were clarified (#5595, #5586)
|
||||
- Docker/OCI builds were pinned to stable Crystal releases with OpenSSL bundled to avoid memory leaks (#5604, #5577, #5441)
|
||||
|
||||
#### For developers
|
||||
- README commit instructions, shard targets, and unix socket docs were corrected (#5607, #5608, #5347)
|
||||
- Thin mode preference comparisons no longer convert unnecessary strings (#5568)
|
||||
- URL encoding fixes in the download widget and socket API updates prevent regressions when upgrading Crystal (#5367, #5538)
|
||||
|
||||
### Full list of pull requests merged since the last release (newest first)
|
||||
|
||||
* refactor: Move top level constants to it's own modules (https://github.com/iv-org/invidious/pull/5596, by @Fijxu)
|
||||
* pages/watch: URL encode 'action' in download widget (https://github.com/iv-org/invidious/pull/5367, by @SamantazFox)
|
||||
* Document use of unix sockets for `db` (https://github.com/iv-org/invidious/pull/5347, by @Fijxu)
|
||||
* Generate companion CSP only once to reuse it (https://github.com/iv-org/invidious/pull/5497, by @Fijxu)
|
||||
* Fix youtube CSV playlist importer (https://github.com/iv-org/invidious/pull/4787, by @ThatMatrix)
|
||||
* Playlist API: return empty author url if ucid is empty (https://github.com/iv-org/invidious/pull/5618, by @radmorecameron)
|
||||
* Channels: parse pronouns and display them on channel page (https://github.com/iv-org/invidious/pull/5617, by @radmorecameron)
|
||||
* playlist: parse playlist thumbnails for topic autogenerated playlists (https://github.com/iv-org/invidious/pull/5616, by @radmorecameron)
|
||||
* fix: add missing embedded protobuf message in continuation token for channel videos (https://github.com/iv-org/invidious/pull/5614, by @Fijxu)
|
||||
* Update shard.yml to include target that was removed in commit 9d54cf9 (https://github.com/iv-org/invidious/pull/5608, by @Harm133)
|
||||
* chore: Do not convert thin_mode preference to string to compare it in before_all (https://github.com/iv-org/invidious/pull/5568, by @Fijxu)
|
||||
* Fix thin_mode preference for channel community page (https://github.com/iv-org/invidious/pull/5567, by @Fijxu)
|
||||
* Fix commit command in README instructions, as per #5606 (https://github.com/iv-org/invidious/pull/5607, by @kirisakow)
|
||||
* Revert "Bump crystallang/crystal from 1.16.3-alpine to 1.19.0-alpine in /docker" (https://github.com/iv-org/invidious/pull/5604, by @unixfox)
|
||||
* Bump crystallang/crystal from 1.16.3-alpine to 1.19.0-alpine in /docker (https://github.com/iv-org/invidious/pull/5603, by @dependabot[bot])
|
||||
* doc: Update HTTP proxy configuration comments (https://github.com/iv-org/invidious/pull/5586, by @unixfox)
|
||||
* Strip unwanted headers from response headers in images and videoplayback (https://github.com/iv-org/invidious/pull/5595, by @Fijxu)
|
||||
* Generate companion check id one time and add missing companion check id on captions (https://github.com/iv-org/invidious/pull/5575, by @Fijxu)
|
||||
* Downgrade Crystal to 1.16.3 in OCI (https://github.com/iv-org/invidious/pull/5577, by @Fijxu)
|
||||
* Allow downloading via companion (https://github.com/iv-org/invidious/pull/5561, by @JeroenBoersma)
|
||||
* chore: crystal 1.8.2 + alpine 3.23 (https://github.com/iv-org/invidious/pull/5574, by @unixfox)
|
||||
* Replace deprecated `blocking` property of `Socket` (https://github.com/iv-org/invidious/pull/5538, by @Fijxu)
|
||||
* Replace `Kemal::StaticFileHandler` with direct subclass of stdlib `HTTP::StaticFileHandler` on Crystal >= 1.17.0 (https://github.com/iv-org/invidious/pull/5338, by @syeopite)
|
||||
* dockerfile: compile openssl instead of using the one bundled on the crystal alpine image. (https://github.com/iv-org/invidious/pull/5441, by @Fijxu)
|
||||
* Bump actions/cache from 4 to 5 (https://github.com/iv-org/invidious/pull/5569, by @dependabot[bot])
|
||||
* Set Kemal `max_request_line_size` to 16384 for large channel continuation query parameters. (https://github.com/iv-org/invidious/pull/5566, by @Fijxu)
|
||||
* Add link to GitHub release/tag/commit in footer (https://github.com/iv-org/invidious/pull/4702, by @shaedrich)
|
||||
* Display "Erroneous CAPTCHA" for invalid captchas (https://github.com/iv-org/invidious/pull/5508, by @Fijxu)
|
||||
* Fix channel name overflow (https://github.com/iv-org/invidious/pull/5553, by @Fijxu)
|
||||
* Fix trending page by leaving livestream and gaming trending pages (https://github.com/iv-org/invidious/pull/5555, by @Fijxu)
|
||||
* fix: restore dmca_content functionality (https://github.com/iv-org/invidious/pull/5228, by @Fijxu)
|
||||
* Remove signature helper completely from Invidious (https://github.com/iv-org/invidious/pull/5550, by @Fijxu)
|
||||
* Fix community posts when there is a unavailable video in a post (https://github.com/iv-org/invidious/pull/5549, by @Fijxu)
|
||||
* chore: Update shard.yml to use SPDX license identifier (https://github.com/iv-org/invidious/pull/5552, by @Fijxu)
|
||||
* Store `preferences` in a variable when reused and rename `prefs` to `preferences` (https://github.com/iv-org/invidious/pull/5450, by @Fijxu)
|
||||
* Bump actions/checkout from 5 to 6 (https://github.com/iv-org/invidious/pull/5544, by @dependabot[bot])
|
||||
* Bump crystal-lang/install-crystal from 1.8.3 to 1.9.1 (https://github.com/iv-org/invidious/pull/5530, by @dependabot[bot])
|
||||
* Fix 0 view count on related videos section (https://github.com/iv-org/invidious/pull/5446, by @shiny-comic)
|
||||
* Prevent timestamp from being set for Livestreams on "Watch on Youtube" links (https://github.com/iv-org/invidious/pull/5481, by @Fijxu)
|
||||
* Add Livestreams to trending page (https://github.com/iv-org/invidious/pull/5480, by @Fijxu)
|
||||
* Fix button overflow (https://github.com/iv-org/invidious/pull/5452, by @Fijxu)
|
||||
* Bump crystal-lang/install-crystal from 1.8.2 to 1.8.3 (https://github.com/iv-org/invidious/pull/5499, by @dependabot[bot])
|
||||
* Fixed broken companion hyperlink (https://github.com/iv-org/invidious/pull/5491, by @ndsvw)
|
||||
|
||||
## v2.20250913.0
|
||||
|
||||
### Wrap-up
|
||||
|
||||
@@ -129,7 +129,7 @@ You can read more here: https://docs.invidious.io/applications/
|
||||
1. Fork it ( https://github.com/iv-org/invidious/fork ).
|
||||
1. Create your feature branch (`git checkout -b my-new-feature`).
|
||||
1. Stage your files (`git add .`).
|
||||
1. Commit your changes (`git commit -am 'Add some feature'`).
|
||||
1. Commit your changes (`git commit -m 'Add some feature'`).
|
||||
1. Push to the branch (`git push origin my-new-feature`).
|
||||
1. Create a new pull request ( https://github.com/iv-org/invidious/compare ).
|
||||
|
||||
|
||||
+23
-1
@@ -75,6 +75,16 @@ body {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.channel-profile > .channel-name-pronouns {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.channel-profile > .channel-name-pronouns > .channel-pronouns {
|
||||
font-style: italic;
|
||||
font-size: .8em;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
body a.channel-owner {
|
||||
background-color: #008bec;
|
||||
color: #fff;
|
||||
@@ -406,7 +416,12 @@ input[type="search"]::-webkit-search-cancel-button {
|
||||
|
||||
p.channel-name { margin: 0; overflow-wrap: anywhere;}
|
||||
p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
|
||||
.channel-profile > .channel-name { overflow-wrap: anywhere;}
|
||||
|
||||
.channel-profile > .channel-name,
|
||||
.channel-profile > .channel-name-pronouns > .channel-name
|
||||
{
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
@@ -887,4 +902,11 @@ h1, h2, h3, h4, h5, p,
|
||||
.error-issue-template {
|
||||
padding: 20px;
|
||||
background: rgba(0, 0, 0, 0.12345);
|
||||
}
|
||||
|
||||
.preference-description {
|
||||
width: 250px;
|
||||
padding-left: 10px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -211,9 +211,9 @@ window.helpers = window.helpers || {
|
||||
helpers.storage.remove(key);
|
||||
}
|
||||
},
|
||||
set: function (key, value) {
|
||||
set: function (key, value) {
|
||||
let encoded_value = encodeURIComponent(JSON.stringify(value))
|
||||
localStorage.setItem(key, encoded_value);
|
||||
localStorage.setItem(key, encoded_value);
|
||||
},
|
||||
remove: function (key) { localStorage.removeItem(key); }
|
||||
};
|
||||
|
||||
+15
-8
@@ -104,14 +104,15 @@ if (video_data.params.quality === 'dash') {
|
||||
*
|
||||
* @param {String} url
|
||||
* @param {String} [base]
|
||||
* @param {'t' | 'start'} param
|
||||
* @returns {URL} urlWithTimeArg
|
||||
*/
|
||||
function addCurrentTimeToURL(url, base) {
|
||||
function addCurrentTimeToURL(url, base, param = 't') {
|
||||
var urlUsed = new URL(url, base);
|
||||
urlUsed.searchParams.delete('start');
|
||||
var currentTime = Math.ceil(player.currentTime());
|
||||
if (currentTime > 0)
|
||||
urlUsed.searchParams.set('t', currentTime);
|
||||
urlUsed.searchParams.set(param, currentTime);
|
||||
else if (urlUsed.searchParams.has('t'))
|
||||
urlUsed.searchParams.delete('t');
|
||||
return urlUsed;
|
||||
@@ -132,7 +133,7 @@ var timeupdate_last_ts = 5;
|
||||
player.on('timeupdate', function () {
|
||||
// Only update once every second
|
||||
let current_ts = Math.floor(player.currentTime());
|
||||
if (current_ts > timeupdate_last_ts) timeupdate_last_ts = current_ts;
|
||||
if (current_ts != timeupdate_last_ts) timeupdate_last_ts = current_ts;
|
||||
else return;
|
||||
|
||||
// YouTube links
|
||||
@@ -143,11 +144,11 @@ player.on('timeupdate', function () {
|
||||
let base_url_yt_watch = elem_yt_watch.getAttribute('data-base-url');
|
||||
elem_yt_watch.href = addCurrentTimeToURL(base_url_yt_watch);
|
||||
}
|
||||
|
||||
|
||||
let elem_yt_embed = document.getElementById('link-yt-embed');
|
||||
if (elem_yt_embed) {
|
||||
let base_url_yt_embed = elem_yt_embed.getAttribute('data-base-url');
|
||||
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed);
|
||||
elem_yt_embed.href = addCurrentTimeToURL(base_url_yt_embed, undefined, 'start');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,12 +161,18 @@ player.on('timeupdate', function () {
|
||||
let base_url_iv_embed = elem_iv_embed.getAttribute('data-base-url');
|
||||
elem_iv_embed.href = addCurrentTimeToURL(base_url_iv_embed, domain);
|
||||
}
|
||||
|
||||
|
||||
let elem_iv_other = document.getElementById('link-iv-other');
|
||||
if (elem_iv_other) {
|
||||
let base_url_iv_other = elem_iv_other.getAttribute('data-base-url');
|
||||
elem_iv_other.href = addCurrentTimeToURL(base_url_iv_other, domain);
|
||||
}
|
||||
|
||||
let elem_iv_listen = document.getElementById('link-iv-listen');
|
||||
if (elem_iv_listen) {
|
||||
let base_url_iv_listen = elem_iv_listen.getAttribute('data-base-url');
|
||||
elem_iv_listen.href = addCurrentTimeToURL(base_url_iv_listen, domain);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -628,7 +635,7 @@ function toggle_caption_window() {
|
||||
player.textTrackSettings.setValues({ windowOpacity: options.windowOpacity[newIndex] });
|
||||
update_captions();
|
||||
}
|
||||
|
||||
|
||||
function toggle_caption_opacity() {
|
||||
const numOptions = options.textOpacity.length;
|
||||
const textOpacity = player.textTrackSettings.getValues().textOpacity || '1';
|
||||
@@ -733,7 +740,7 @@ addEventListener('keydown', function (e) {
|
||||
|
||||
case '>': action = increase_playback_rate.bind(this, 1); break;
|
||||
case '<': action = increase_playback_rate.bind(this, -1); break;
|
||||
|
||||
|
||||
case '=': action = increase_caption_size.bind(this, 1); break;
|
||||
case '-': action = increase_caption_size.bind(this, -1); break;
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
## Database configuration with separate parameters.
|
||||
## This setting is MANDATORY, unless 'database_url' is used.
|
||||
##
|
||||
## Note: The 'db' setting allows the use of UNIX
|
||||
## sockets. To do so, set 'host' to ""
|
||||
## E.g:
|
||||
## password: kemal
|
||||
## host: ""
|
||||
## port: 5432
|
||||
##
|
||||
db:
|
||||
user: kemal
|
||||
password: kemal
|
||||
@@ -46,7 +53,7 @@ db:
|
||||
##
|
||||
## When this setting is commented out, Invidious companion is not used.
|
||||
## Otherwise, Invidious will proxy the requests to Invidious companion.
|
||||
##
|
||||
##
|
||||
## Note: multiple URL can be configured. In this case, Invidious will
|
||||
## randomly pick one every time video data needs to be retrieved. This
|
||||
## URL is then kept in the video metadata cache to allow video playback
|
||||
@@ -56,7 +63,7 @@ db:
|
||||
## The parameter private_url is required for the internal communication
|
||||
## between Invidious companion and Invidious.
|
||||
##
|
||||
## The optional parameter public_url is the public URL from which
|
||||
## The optional parameter public_url is the public URL from which
|
||||
## Invidious companion is listening to the requests from the user(s).
|
||||
## When this setting is commented out, Invidious proxy all requests to
|
||||
## Invidious companion. Useful for simple setups.
|
||||
@@ -225,7 +232,7 @@ https_only: false
|
||||
## Configuration for using a HTTP proxy
|
||||
## If unset, then no HTTP proxy will be used.
|
||||
## Proxy type supported: HTTP, HTTPS
|
||||
##
|
||||
##
|
||||
## This is not used for loading the video streams from YouTube servers (circumvent YouTube restrictions)
|
||||
## Please instead configure the proxy in Invidious companion:
|
||||
## https://github.com/iv-org/invidious-companion/blob/master/config/config.example.toml
|
||||
@@ -878,7 +885,7 @@ default_user_preferences:
|
||||
## Default: true
|
||||
##
|
||||
#vr_mode: true
|
||||
|
||||
|
||||
##
|
||||
## Save the playback position
|
||||
## Allow to continue watching at the previous position when
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
# https://github.com/openssl/openssl/releases/tag/openssl-3.5.2
|
||||
ARG OPENSSL_VERSION='3.5.2'
|
||||
ARG OPENSSL_SHA256='c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec'
|
||||
# https://github.com/openssl/openssl/releases/tag/openssl-3.6.2
|
||||
ARG OPENSSL_VERSION='3.6.2'
|
||||
ARG OPENSSL_SHA256='aaf51a1fe064384f811daeaeb4ec4dce7340ec8bd893027eee676af31e83a04f'
|
||||
|
||||
FROM crystallang/crystal:1.16.3-alpine AS dependabot-crystal
|
||||
FROM 84codes/crystal:1.20.2-alpine AS dependabot-crystal
|
||||
|
||||
# We compile openssl ourselves due to a memory leak in how crystal interacts
|
||||
# with openssl
|
||||
@@ -43,7 +43,7 @@ COPY ./assets/ ./assets/
|
||||
COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
||||
|
||||
RUN crystal spec --warnings all \
|
||||
--link-flags "-lxml2 -llzma"
|
||||
--link-flags "-lxml2 -llzma"
|
||||
|
||||
ARG OPENSSL_VERSION
|
||||
COPY --from=openssl-builder /openssl-${OPENSSL_VERSION} /openssl-${OPENSSL_VERSION}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# https://github.com/openssl/openssl/releases/tag/openssl-3.5.2
|
||||
ARG OPENSSL_VERSION='3.5.2'
|
||||
ARG OPENSSL_SHA256='c53a47e5e441c930c3928cf7bf6fb00e5d129b630e0aa873b08258656e7345ec'
|
||||
|
||||
FROM alpine:3.22 AS dependabot-alpine
|
||||
|
||||
# We compile openssl ourselves due to a memory leak in how crystal interacts
|
||||
# with openssl
|
||||
# Reference: https://github.com/iv-org/invidious/issues/1438#issuecomment-3087636228
|
||||
FROM dependabot-alpine AS openssl-builder
|
||||
RUN apk add --no-cache curl perl linux-headers build-base
|
||||
|
||||
WORKDIR /
|
||||
|
||||
ARG OPENSSL_VERSION
|
||||
ARG OPENSSL_SHA256
|
||||
RUN curl -Ls "https://github.com/openssl/openssl/releases/download/openssl-${OPENSSL_VERSION}/openssl-${OPENSSL_VERSION}.tar.gz" --output openssl-${OPENSSL_VERSION}.tar.gz
|
||||
RUN echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c
|
||||
RUN tar -xzvf openssl-${OPENSSL_VERSION}.tar.gz
|
||||
|
||||
RUN cd openssl-${OPENSSL_VERSION} && ./Configure --openssldir=/etc/ssl && make -j$(nproc)
|
||||
|
||||
FROM dependabot-alpine AS builder
|
||||
RUN apk add --no-cache 'crystal=1.16.3-r0' shards \
|
||||
sqlite-static yaml-static yaml-dev \
|
||||
pcre2-static gc-static \
|
||||
libxml2-static zlib-static \
|
||||
openssl-libs-static openssl-dev musl-dev xz-static
|
||||
|
||||
ARG release
|
||||
|
||||
WORKDIR /invidious
|
||||
COPY ./shard.yml ./shard.yml
|
||||
COPY ./shard.lock ./shard.lock
|
||||
RUN shards install --production
|
||||
|
||||
COPY ./src/ ./src/
|
||||
# TODO: .git folder is required for building – this is destructive.
|
||||
# See definition of CURRENT_BRANCH, CURRENT_COMMIT and CURRENT_VERSION.
|
||||
COPY ./.git/ ./.git/
|
||||
|
||||
# Required for fetching player dependencies
|
||||
COPY ./scripts/ ./scripts/
|
||||
COPY ./assets/ ./assets/
|
||||
COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
||||
|
||||
RUN crystal spec --warnings all \
|
||||
--link-flags "-lxml2 -llzma"
|
||||
|
||||
ARG OPENSSL_VERSION
|
||||
COPY --from=openssl-builder /openssl-${OPENSSL_VERSION} /openssl-${OPENSSL_VERSION}
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
|
||||
PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \
|
||||
crystal build ./src/invidious.cr \
|
||||
--release \
|
||||
--static --warnings all \
|
||||
--link-flags "-lxml2 -llzma"; \
|
||||
else \
|
||||
PKG_CONFIG_PATH=/openssl-${OPENSSL_VERSION} \
|
||||
crystal build ./src/invidious.cr \
|
||||
--static --warnings all \
|
||||
--link-flags "-lxml2 -llzma"; \
|
||||
fi
|
||||
|
||||
FROM alpine:3.22
|
||||
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
|
||||
COPY --chown=invidious ./config/config.* ./config/
|
||||
RUN mv -n config/config.example.yml config/config.yml
|
||||
RUN sed -i 's/host: \(127.0.0.1\|localhost\)/host: invidious-db/' config/config.yml
|
||||
COPY ./config/sql/ ./config/sql/
|
||||
COPY ./locales/ ./locales/
|
||||
COPY --from=builder /invidious/assets ./assets/
|
||||
COPY --from=builder /invidious/invidious .
|
||||
RUN chmod o+rX -R ./assets ./config ./locales
|
||||
|
||||
EXPOSE 3000
|
||||
USER invidious
|
||||
ENTRYPOINT ["/sbin/tini", "--"]
|
||||
CMD [ "/invidious/invidious" ]
|
||||
@@ -124,6 +124,8 @@
|
||||
"preferences_sort_label": "Sort videos by: ",
|
||||
"preferences_default_playlist": "Default playlist: ",
|
||||
"preferences_default_playlist_none": "No default playlist set",
|
||||
"preferences_search_privacy_label": "Search privacy: ",
|
||||
"preferences_search_privacy_description": "Enabling this preference will prevent your search queries from being saved in your browser history.",
|
||||
"published": "published",
|
||||
"published - reverse": "published - reverse",
|
||||
"alphabetically": "alphabetically",
|
||||
|
||||
@@ -24,7 +24,7 @@ def create_licence_tr(path, file_name, licence_name, licence_link, source_locati
|
||||
"<tr>
|
||||
<td><a href=\\"/#{path}\\">#{file_name}</a></td>
|
||||
<td><a href=\\"#{licence_link}\\">#{licence_name}</a></td>
|
||||
<td><a href=\\"#{source_location}\\">\#{translate(locale, "source")}</a></td>
|
||||
<td><a href=\\"#{source_location}\\">\#{I18n.translate(locale, "source")}</a></td>
|
||||
</tr>"
|
||||
HTML
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
# Crystal linter
|
||||
# This is a modified version of the pre-commit hook from the crystal repo. https://github.com/crystal-lang/crystal/blob/master/scripts/git/pre-commit
|
||||
# Please refer to that if you'd like an version that doesn't automatically format staged files.
|
||||
# Please refer to that if you'd like an version that doesn't automatically format staged files.
|
||||
changed_cr_files=$(git diff --cached --name-only --diff-filter=ACM | grep '\.cr$')
|
||||
if [ ! -z "$changed_cr_files" ]; then
|
||||
if [ -x bin/crystal ]; then
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
name: invidious
|
||||
version: 2.20250913.0-dev
|
||||
version: 2.20260207.0-dev
|
||||
|
||||
authors:
|
||||
- Invidious team <contact@invidious.io>
|
||||
- Contributors!
|
||||
|
||||
targets:
|
||||
invidious:
|
||||
main: src/invidious.cr
|
||||
|
||||
description: |
|
||||
Invidious is an alternative front-end to YouTube
|
||||
|
||||
|
||||
@@ -48,9 +48,7 @@ FEATURE_FILTERS = {
|
||||
|
||||
SORT_FILTERS = {
|
||||
Invidious::Search::Filters::Sort::Relevance => "8AEB",
|
||||
Invidious::Search::Filters::Sort::Date => "CALwAQE%3D",
|
||||
Invidious::Search::Filters::Sort::Views => "CAPwAQE%3D",
|
||||
Invidious::Search::Filters::Sort::Rating => "CAHwAQE%3D",
|
||||
}
|
||||
|
||||
Spectator.describe Invidious::Search::Filters do
|
||||
|
||||
@@ -7,7 +7,7 @@ Spectator.describe "parse_video_info" do
|
||||
_next = load_mock("video/regular_mrbeast.next")
|
||||
|
||||
raw_data = _player.merge!(_next)
|
||||
info = parse_video_info("2isYuQZMbdU", raw_data)
|
||||
info = Invidious::Videos::Parser.parse_video_info("2isYuQZMbdU", raw_data)
|
||||
|
||||
# Some basic verifications
|
||||
expect(typeof(info)).to eq(Hash(String, JSON::Any))
|
||||
@@ -88,7 +88,7 @@ Spectator.describe "parse_video_info" do
|
||||
_next = load_mock("video/regular_no-description.next")
|
||||
|
||||
raw_data = _player.merge!(_next)
|
||||
info = parse_video_info("iuevw6218F0", raw_data)
|
||||
info = Invidious::Videos::Parser.parse_video_info("iuevw6218F0", raw_data)
|
||||
|
||||
# Some basic verifications
|
||||
expect(typeof(info)).to eq(Hash(String, JSON::Any))
|
||||
|
||||
@@ -7,7 +7,7 @@ Spectator.describe "parse_video_info" do
|
||||
_next = load_mock("video/scheduled_live_PBD-Podcast.next")
|
||||
|
||||
raw_data = _player.merge!(_next)
|
||||
info = parse_video_info("N-yVic7BbY0", raw_data)
|
||||
info = Invidious::Videos::Parser.parse_video_info("N-yVic7BbY0", raw_data)
|
||||
|
||||
# Some basic verifications
|
||||
expect(typeof(info)).to eq(Hash(String, JSON::Any))
|
||||
|
||||
+2
-13
@@ -67,20 +67,9 @@ rescue ex
|
||||
puts "Check your 'config.yml' database settings or PostgreSQL settings."
|
||||
exit(1)
|
||||
end
|
||||
ARCHIVE_URL = URI.parse("https://archive.org")
|
||||
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
||||
REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||
YT_URL = URI.parse("https://www.youtube.com")
|
||||
HOST_URL = make_host_url(Kemal.config)
|
||||
|
||||
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
||||
HOST_URL = make_host_url(Kemal.config)
|
||||
MAX_ITEMS_PER_PAGE = 1500
|
||||
|
||||
REQUEST_HEADERS_WHITELIST = {"accept", "accept-encoding", "cache-control", "content-length", "if-none-match", "range"}
|
||||
RESPONSE_HEADERS_BLACKLIST = {"access-control-allow-origin", "alt-svc", "server"}
|
||||
HTTP_CHUNK_SIZE = 10485760 # ~10MB
|
||||
|
||||
CURRENT_BRANCH = {{ "#{`git branch | sed -n '/* /s///p'`.strip}" }}
|
||||
CURRENT_COMMIT = {{ "#{`git rev-list HEAD --max-count=1 --abbrev-commit`.strip}" }}
|
||||
CURRENT_VERSION = {{ "#{`git log -1 --format=%ci | awk '{print $1}' | sed s/-/./g`.strip}" }}
|
||||
@@ -97,7 +86,7 @@ SOFTWARE = {
|
||||
"branch" => "#{CURRENT_BRANCH}",
|
||||
}
|
||||
|
||||
YT_POOL = YoutubeConnectionPool.new(YT_URL, capacity: CONFIG.pool_size)
|
||||
YT_POOL = YoutubeConnectionPool.new(URI.parse("https://www.youtube.com"), capacity: CONFIG.pool_size)
|
||||
|
||||
# Image request pool
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ record AboutChannel,
|
||||
sub_count : Int32,
|
||||
joined : Time,
|
||||
is_family_friendly : Bool,
|
||||
pronouns : String?,
|
||||
allowed_regions : Array(String),
|
||||
tabs : Array(String),
|
||||
tags : Array(String),
|
||||
@@ -82,11 +83,16 @@ def get_about_info(ucid, locale) : AboutChannel
|
||||
author = initdata["metadata"]["channelMetadataRenderer"]["title"].as_s
|
||||
author_url = initdata["metadata"]["channelMetadataRenderer"]["channelUrl"].as_s
|
||||
author_thumbnail = initdata["metadata"]["channelMetadataRenderer"]["avatar"]["thumbnails"][0]["url"].as_s
|
||||
author_verified = has_verified_badge?(initdata.dig?("header", "c4TabbedHeaderRenderer", "badges"))
|
||||
|
||||
author_badge = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "title", "dynamicTextViewModel", "text", "attachmentRuns", 0, "element", "type", "imageType", "image", "sources", 0, "clientResource", "imageName")
|
||||
.try &.as_s
|
||||
# CHECK_CIRCLE_FILLED is used for normal channels and AUDIO_BADGE if used For
|
||||
# music/artist channels
|
||||
# TODO: Maybe separate verified author from verified artist?
|
||||
author_verified = author_badge.try { |badge| badge == "CHECK_CIRCLE_FILLED" || badge == "AUDIO_BADGE" } || false
|
||||
ucid = initdata["metadata"]["channelMetadataRenderer"]["externalId"].as_s
|
||||
|
||||
# Raises a KeyError on failure.
|
||||
# TODO: Check if `c4TabbedHeaderRenderer` still exists on some channels.
|
||||
banners = initdata["header"]["c4TabbedHeaderRenderer"]?.try &.["banner"]?.try &.["thumbnails"]?
|
||||
banners ||= initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "banner", "imageBannerViewModel", "image", "sources")
|
||||
banner = banners.try &.[-1]?.try &.["url"].as_s?
|
||||
@@ -160,14 +166,21 @@ def get_about_info(ucid, locale) : AboutChannel
|
||||
end
|
||||
|
||||
sub_count = 0
|
||||
pronouns = nil
|
||||
|
||||
if (metadata_rows = initdata.dig?("header", "pageHeaderRenderer", "content", "pageHeaderViewModel", "metadata", "contentMetadataViewModel", "metadataRows").try &.as_a)
|
||||
metadata_rows.each do |row|
|
||||
metadata_part = row.dig?("metadataParts").try &.as_a.find { |i| i.dig?("text", "content").try &.as_s.includes?("subscribers") }
|
||||
if !metadata_part.nil?
|
||||
sub_count = short_text_to_number(metadata_part.dig("text", "content").as_s.split(" ")[0]).to_i32
|
||||
subscribe_metadata_part = row.dig?("metadataParts").try &.as_a.find { |i| i.dig?("text", "content").try &.as_s.includes?("subscribers") }
|
||||
if !subscribe_metadata_part.nil?
|
||||
sub_count = short_text_to_number(subscribe_metadata_part.dig("text", "content").as_s.split(" ")[0]).to_i32
|
||||
end
|
||||
break if sub_count != 0
|
||||
|
||||
pronoun_metadata_part = row.dig?("metadataParts").try &.as_a.find { |i| i.dig?("tooltip").try &.as_s.includes?("Pronouns") }
|
||||
if !pronoun_metadata_part.nil?
|
||||
pronouns = pronoun_metadata_part.dig("text", "content").as_s
|
||||
end
|
||||
|
||||
break if sub_count != 0 && !pronouns.nil?
|
||||
end
|
||||
end
|
||||
|
||||
@@ -184,6 +197,7 @@ def get_about_info(ucid, locale) : AboutChannel
|
||||
sub_count: sub_count,
|
||||
joined: joined,
|
||||
is_family_friendly: is_family_friendly,
|
||||
pronouns: pronouns,
|
||||
allowed_regions: allowed_regions,
|
||||
tabs: tab_names,
|
||||
tags: tags,
|
||||
|
||||
@@ -38,7 +38,7 @@ struct ChannelVideo
|
||||
json.field "authorId", self.ucid
|
||||
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||
json.field "published", self.published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
||||
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(self.published, locale))
|
||||
|
||||
json.field "viewCount", self.views
|
||||
end
|
||||
|
||||
@@ -127,11 +127,11 @@ def extract_channel_community(items, *, ucid, locale, format, thin_mode, is_sing
|
||||
|
||||
reply_count = short_text_to_number(post.dig?("actionButtons", "commentActionButtonsRenderer", "replyButton", "buttonRenderer", "text", "simpleText").try &.as_s || "0")
|
||||
|
||||
json.field "content", html_to_content(content_html)
|
||||
json.field "content", Helpers.html_to_content(content_html)
|
||||
json.field "contentHtml", content_html
|
||||
|
||||
json.field "published", published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
|
||||
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(published, locale))
|
||||
|
||||
json.field "likeCount", like_count
|
||||
json.field "replyCount", reply_count
|
||||
|
||||
@@ -114,7 +114,11 @@ module Invidious::Channel::Tabs
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"4:varint" => sort_options_videos_short(sort_by),
|
||||
"4:varint" => sort_options_videos_short(sort_by),
|
||||
"8:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
"3:varint" => sort_options_videos_short(sort_by),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -130,7 +134,11 @@ module Invidious::Channel::Tabs
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"4:varint" => sort_options_videos_short(sort_by),
|
||||
"4:varint" => sort_options_videos_short(sort_by),
|
||||
"7:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
"3:varint" => sort_options_videos_short(sort_by),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -154,7 +162,11 @@ module Invidious::Channel::Tabs
|
||||
"2:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
},
|
||||
"5:varint" => sort_by_numerical,
|
||||
"5:varint" => sort_by_numerical,
|
||||
"8:embedded" => {
|
||||
"1:string" => "00000000-0000-0000-0000-000000000000",
|
||||
"3:varint" => sort_by_numerical,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
module Invidious::Comments
|
||||
extend self
|
||||
private REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||
|
||||
def fetch_reddit(id, sort_by = "confidence")
|
||||
client = make_client(REDDIT_URL)
|
||||
|
||||
@@ -254,7 +254,7 @@ module Invidious::Comments
|
||||
end
|
||||
|
||||
content_html = html_content || ""
|
||||
json.field "content", html_to_content(content_html)
|
||||
json.field "content", Helpers.html_to_content(content_html)
|
||||
json.field "contentHtml", content_html
|
||||
|
||||
if published_text != nil
|
||||
@@ -268,7 +268,7 @@ module Invidious::Comments
|
||||
end
|
||||
|
||||
json.field "published", published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(published, locale))
|
||||
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(published, locale))
|
||||
end
|
||||
|
||||
if node_replies && !response["commentRepliesContinuation"]?
|
||||
|
||||
@@ -54,6 +54,7 @@ struct ConfigPreferences
|
||||
property save_player_pos : Bool = false
|
||||
@[YAML::Field(ignore: true)]
|
||||
property default_playlist : String? = nil
|
||||
property search_privacy : Bool = false
|
||||
|
||||
def to_tuple
|
||||
{% begin %}
|
||||
|
||||
@@ -28,14 +28,14 @@ module Invidious::Frontend::ChannelPage
|
||||
|
||||
if tab == selected_tab
|
||||
str << "\t<b>"
|
||||
str << translate(locale, "channel_tab_#{tab_name}_label")
|
||||
str << I18n.translate(locale, "channel_tab_#{tab_name}_label")
|
||||
str << "</b>\n"
|
||||
else
|
||||
# Video tab doesn't have the last path component
|
||||
url = tab.videos? ? base_url : "#{base_url}/#{tab_name}"
|
||||
|
||||
str << %(\t<a href=") << url << %(">)
|
||||
str << translate(locale, "channel_tab_#{tab_name}_label")
|
||||
str << I18n.translate(locale, "channel_tab_#{tab_name}_label")
|
||||
str << "</a>\n"
|
||||
end
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ module Invidious::Frontend::Comments
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
|
||||
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
|
||||
#{translate_count(locale, "comments_points_count", child.score, NumberFormatting::Separator)}
|
||||
<span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
|
||||
<a href="https://www.reddit.com#{child.permalink}" title="#{translate(locale, "permalink")}">#{translate(locale, "permalink")}</a>
|
||||
#{I18n.translate_count(locale, "comments_points_count", child.score, I18n::NumberFormatting::Separator)}
|
||||
<span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{I18n.translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
|
||||
<a href="https://www.reddit.com#{child.permalink}" title="#{I18n.translate(locale, "permalink")}">#{I18n.translate(locale, "permalink")}</a>
|
||||
</p>
|
||||
<div>
|
||||
#{body_html}
|
||||
|
||||
@@ -6,10 +6,10 @@ module Invidious::Frontend::Comments
|
||||
root = comments["comments"].as_a
|
||||
root.each do |child|
|
||||
if child["replies"]?
|
||||
replies_count_text = translate_count(locale,
|
||||
replies_count_text = I18n.translate_count(locale,
|
||||
"comments_view_x_replies",
|
||||
child["replies"]["replyCount"].as_i64 || 0,
|
||||
NumberFormatting::Separator
|
||||
I18n::NumberFormatting::Separator
|
||||
)
|
||||
|
||||
replies_html = <<-END_HTML
|
||||
@@ -25,10 +25,10 @@ module Invidious::Frontend::Comments
|
||||
END_HTML
|
||||
elsif comments["authorId"]? && !comments["singlePost"]?
|
||||
# for posts we should display a link to the post
|
||||
replies_count_text = translate_count(locale,
|
||||
replies_count_text = I18n.translate_count(locale,
|
||||
"comments_view_x_replies",
|
||||
child["replyCount"].as_i64 || 0,
|
||||
NumberFormatting::Separator
|
||||
I18n::NumberFormatting::Separator
|
||||
)
|
||||
|
||||
replies_html = <<-END_HTML
|
||||
@@ -61,7 +61,7 @@ module Invidious::Frontend::Comments
|
||||
sponsor_icon = String.build do |str|
|
||||
str << %(<img alt="" )
|
||||
str << %(src="/ggpht) << URI.parse(child["sponsorIconUrl"].as_s).request_target << "\" "
|
||||
str << %(title=") << translate(locale, "Channel Sponsor") << "\" "
|
||||
str << %(title=") << I18n.translate(locale, "Channel Sponsor") << "\" "
|
||||
str << %(width="16" height="16" />)
|
||||
end
|
||||
end
|
||||
@@ -110,14 +110,14 @@ module Invidious::Frontend::Comments
|
||||
when "multiImage"
|
||||
html << <<-END_HTML
|
||||
<section class="carousel">
|
||||
<a class="skip-link" href="#skip-#{child["commentId"]}">#{translate(locale, "carousel_skip")}</a>
|
||||
<a class="skip-link" href="#skip-#{child["commentId"]}">#{I18n.translate(locale, "carousel_skip")}</a>
|
||||
<div class="slides">
|
||||
END_HTML
|
||||
image_array = attachment["images"].as_a
|
||||
|
||||
image_array.each_index do |i|
|
||||
html << <<-END_HTML
|
||||
<div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
|
||||
<div class="slides-item slide-#{i + 1}" id="#{child["commentId"]}-slide-#{i + 1}" aria-label="#{I18n.translate(locale, "carousel_slide", {"current" => (i + 1).to_s, "total" => image_array.size.to_s})}" tabindex="0">
|
||||
<img loading="lazy" src="/ggpht#{URI.parse(image_array[i][1]["url"].as_s).request_target}" alt="" />
|
||||
</div>
|
||||
END_HTML
|
||||
@@ -129,7 +129,7 @@ module Invidious::Frontend::Comments
|
||||
END_HTML
|
||||
attachment["images"].as_a.each_index do |i|
|
||||
html << <<-END_HTML
|
||||
<a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a>
|
||||
<a class="slider-nav" href="##{child["commentId"]}-slide-#{i + 1}" aria-label="#{I18n.translate(locale, "carousel_go_to", (i + 1).to_s)}" tabindex="-1" aria-hidden="true">#{i + 1}</a>
|
||||
END_HTML
|
||||
end
|
||||
html << <<-END_HTML
|
||||
@@ -143,18 +143,18 @@ module Invidious::Frontend::Comments
|
||||
|
||||
html << <<-END_HTML
|
||||
<p>
|
||||
<span title="#{Time.unix(child["published"].as_i64).to_s(translate(locale, "%A %B %-d, %Y"))}">#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span>
|
||||
<span title="#{Time.unix(child["published"].as_i64).to_s(I18n.translate(locale, "%A %B %-d, %Y"))}">#{I18n.translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? I18n.translate(locale, "(edited)") : ""}</span>
|
||||
|
|
||||
END_HTML
|
||||
|
||||
if 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>
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{I18n.translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
elsif comments["authorId"]?
|
||||
html << <<-END_HTML
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{I18n.translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
end
|
||||
@@ -172,7 +172,7 @@ module Invidious::Frontend::Comments
|
||||
|
||||
html << <<-END_HTML
|
||||
|
||||
<span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
|
||||
<span class="creator-heart-container" title="#{I18n.translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
|
||||
<span class="creator-heart">
|
||||
<img loading="lazy" class="creator-heart-background-hearted" src="#{creator_thumbnail}" alt="" />
|
||||
<span class="creator-heart-small-hearted">
|
||||
@@ -197,7 +197,7 @@ module Invidious::Frontend::Comments
|
||||
<div class="pure-u-1">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{translate(locale, "Load more")}</a>
|
||||
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{I18n.translate(locale, "Load more")}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,16 +6,16 @@ module Invidious::Frontend::Pagination
|
||||
private def first_page(str : String::Builder, locale : String?, url : String)
|
||||
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
|
||||
|
||||
if locale_is_rtl?(locale)
|
||||
if I18n.locale_is_rtl?(locale)
|
||||
# Inverted arrow ("first" points to the right)
|
||||
str << translate(locale, "First page")
|
||||
str << I18n.translate(locale, "First page")
|
||||
str << " "
|
||||
str << %(<i class="icon ion-ios-arrow-forward"></i>)
|
||||
else
|
||||
# Regular arrow ("first" points to the left)
|
||||
str << %(<i class="icon ion-ios-arrow-back"></i>)
|
||||
str << " "
|
||||
str << translate(locale, "First page")
|
||||
str << I18n.translate(locale, "First page")
|
||||
end
|
||||
|
||||
str << "</a>"
|
||||
@@ -25,16 +25,16 @@ module Invidious::Frontend::Pagination
|
||||
# Link
|
||||
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
|
||||
|
||||
if locale_is_rtl?(locale)
|
||||
if I18n.locale_is_rtl?(locale)
|
||||
# Inverted arrow ("previous" points to the right)
|
||||
str << translate(locale, "Previous page")
|
||||
str << I18n.translate(locale, "Previous page")
|
||||
str << " "
|
||||
str << %(<i class="icon ion-ios-arrow-forward"></i>)
|
||||
else
|
||||
# Regular arrow ("previous" points to the left)
|
||||
str << %(<i class="icon ion-ios-arrow-back"></i>)
|
||||
str << " "
|
||||
str << translate(locale, "Previous page")
|
||||
str << I18n.translate(locale, "Previous page")
|
||||
end
|
||||
|
||||
str << "</a>"
|
||||
@@ -44,14 +44,14 @@ module Invidious::Frontend::Pagination
|
||||
# Link
|
||||
str << %(<a href=") << url << %(" class="pure-button pure-button-secondary">)
|
||||
|
||||
if locale_is_rtl?(locale)
|
||||
if I18n.locale_is_rtl?(locale)
|
||||
# Inverted arrow ("next" points to the left)
|
||||
str << %(<i class="icon ion-ios-arrow-back"></i>)
|
||||
str << " "
|
||||
str << translate(locale, "Next page")
|
||||
str << I18n.translate(locale, "Next page")
|
||||
else
|
||||
# Regular arrow ("next" points to the right)
|
||||
str << translate(locale, "Next page")
|
||||
str << I18n.translate(locale, "Next page")
|
||||
str << " "
|
||||
str << %(<i class="icon ion-ios-arrow-forward"></i>)
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ module Invidious::Frontend::SearchFilters
|
||||
return String.build(8000) do |str|
|
||||
str << "<div id='filters'>\n"
|
||||
str << "\t<details id='filters-collapse'>"
|
||||
str << "\t\t<summary>" << translate(locale, "search_filters_title") << "</summary>\n"
|
||||
str << "\t\t<summary>" << I18n.translate(locale, "search_filters_title") << "</summary>\n"
|
||||
|
||||
str << "\t\t<div id='filters-box'><form action='/search' method='get'>\n"
|
||||
|
||||
@@ -25,7 +25,7 @@ module Invidious::Frontend::SearchFilters
|
||||
|
||||
str << "\t\t\t<div id='filters-apply'>"
|
||||
str << "<button type='submit' class=\"pure-button pure-button-primary\">"
|
||||
str << translate(locale, "search_filters_apply_button")
|
||||
str << I18n.translate(locale, "search_filters_apply_button")
|
||||
str << "</button></div>\n"
|
||||
|
||||
str << "\t\t</form></div>\n"
|
||||
@@ -41,7 +41,7 @@ module Invidious::Frontend::SearchFilters
|
||||
str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n"
|
||||
|
||||
str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">"
|
||||
str << translate(locale, "search_filters_{{name}}_label")
|
||||
str << I18n.translate(locale, "search_filters_{{name}}_label")
|
||||
str << "</div></legend>\n"
|
||||
|
||||
str << "\t\t\t\t\t<div class=\"filter-options\">\n"
|
||||
@@ -62,7 +62,7 @@ module Invidious::Frontend::SearchFilters
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-date-{{date}}'>"
|
||||
str << translate(locale, "search_filters_date_option_{{date}}")
|
||||
str << I18n.translate(locale, "search_filters_date_option_{{date}}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
end
|
||||
@@ -78,7 +78,7 @@ module Invidious::Frontend::SearchFilters
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-type-{{type}}'>"
|
||||
str << translate(locale, "search_filters_type_option_{{type}}")
|
||||
str << I18n.translate(locale, "search_filters_type_option_{{type}}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
end
|
||||
@@ -94,7 +94,7 @@ module Invidious::Frontend::SearchFilters
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-duration-{{duration}}'>"
|
||||
str << translate(locale, "search_filters_duration_option_{{duration}}")
|
||||
str << I18n.translate(locale, "search_filters_duration_option_{{duration}}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
end
|
||||
@@ -111,7 +111,7 @@ module Invidious::Frontend::SearchFilters
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-feature-{{feature}}'>"
|
||||
str << translate(locale, "search_filters_features_option_{{feature}}")
|
||||
str << I18n.translate(locale, "search_filters_features_option_{{feature}}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
{% end %}
|
||||
@@ -128,7 +128,7 @@ module Invidious::Frontend::SearchFilters
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-sort-{{sort}}'>"
|
||||
str << translate(locale, "search_filters_sort_option_{{sort}}")
|
||||
str << I18n.translate(locale, "search_filters_sort_option_{{sort}}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
end
|
||||
|
||||
@@ -20,11 +20,11 @@ module Invidious::Frontend::WatchPage
|
||||
|
||||
def download_widget(locale : String, video : Video, video_assets : VideoAssets) : String
|
||||
if CONFIG.disabled?("downloads")
|
||||
return "<p id=\"download\">#{translate(locale, "Download is disabled")}</p>"
|
||||
return "<p id=\"download\">#{I18n.translate(locale, "Download is disabled")}</p>"
|
||||
end
|
||||
|
||||
if CONFIG.dmca_content.includes?(video.id)
|
||||
return "<p id=\"download\">#{translate(locale, "dmca_content")}</p>"
|
||||
return "<p id=\"download\">#{I18n.translate(locale, "dmca_content")}</p>"
|
||||
end
|
||||
|
||||
url = "/download"
|
||||
@@ -36,7 +36,7 @@ module Invidious::Frontend::WatchPage
|
||||
return String.build(4000) do |str|
|
||||
str << "<form"
|
||||
str << " class=\"pure-form pure-form-stacked\""
|
||||
str << " action='#{url}'"
|
||||
str << " action='" << HTML.escape(url) << "'"
|
||||
str << " method='post'"
|
||||
str << " rel='noopener noreferrer'"
|
||||
str << " target='_blank'>"
|
||||
@@ -49,7 +49,7 @@ module Invidious::Frontend::WatchPage
|
||||
str << "\t<div class=\"pure-control-group\">\n"
|
||||
|
||||
str << "\t\t<label for='download_widget'>"
|
||||
str << translate(locale, "Download as: ")
|
||||
str << I18n.translate(locale, "Download as: ")
|
||||
str << "</label>\n"
|
||||
|
||||
str << "\t\t<select name='download_widget' id='download_widget'>\n"
|
||||
@@ -98,7 +98,7 @@ module Invidious::Frontend::WatchPage
|
||||
value = {"label": caption.name, "ext": "#{caption.language_code}.vtt"}.to_json
|
||||
|
||||
str << "\t\t\t<option value='" << value << "'>"
|
||||
str << translate(locale, "download_subtitles", translate(locale, caption.name))
|
||||
str << I18n.translate(locale, "download_subtitles", I18n.translate(locale, caption.name))
|
||||
str << "</option>\n"
|
||||
end
|
||||
|
||||
@@ -108,7 +108,7 @@ module Invidious::Frontend::WatchPage
|
||||
str << "\t</div>\n"
|
||||
|
||||
str << "\t<button type=\"submit\" class=\"pure-button pure-button-primary\">\n"
|
||||
str << "\t\t<b>" << translate(locale, "Download") << "</b>\n"
|
||||
str << "\t\t<b>" << I18n.translate(locale, "Download") << "</b>\n"
|
||||
str << "\t</button>\n"
|
||||
|
||||
str << "</form>\n"
|
||||
|
||||
@@ -63,19 +63,19 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
|
||||
|
||||
error_message = <<-END_HTML
|
||||
<div class="error_message">
|
||||
<h2>#{translate(locale, "crash_page_you_found_a_bug")}</h2>
|
||||
<h2>#{I18n.translate(locale, "crash_page_you_found_a_bug")}</h2>
|
||||
<br/><br/>
|
||||
|
||||
<p><b>#{translate(locale, "crash_page_before_reporting")}</b></p>
|
||||
<p><b>#{I18n.translate(locale, "crash_page_before_reporting")}</b></p>
|
||||
<ul>
|
||||
<li>#{translate(locale, "crash_page_refresh", env.request.resource)}</li>
|
||||
<li>#{translate(locale, "crash_page_switch_instance", url_switch)}</li>
|
||||
<li>#{translate(locale, "crash_page_read_the_faq", url_faq)}</li>
|
||||
<li>#{translate(locale, "crash_page_search_issue", url_search_issues)}</li>
|
||||
<li>#{I18n.translate(locale, "crash_page_refresh", env.request.resource)}</li>
|
||||
<li>#{I18n.translate(locale, "crash_page_switch_instance", url_switch)}</li>
|
||||
<li>#{I18n.translate(locale, "crash_page_read_the_faq", url_faq)}</li>
|
||||
<li>#{I18n.translate(locale, "crash_page_search_issue", url_search_issues)}</li>
|
||||
</ul>
|
||||
|
||||
<br/>
|
||||
<p>#{translate(locale, "crash_page_report_issue", url_new_issue)}</p>
|
||||
<p>#{I18n.translate(locale, "crash_page_report_issue", url_new_issue)}</p>
|
||||
|
||||
<!-- TODO: Add a "copy to clipboard" button -->
|
||||
<pre class="error-issue-template">#{issue_template}</pre>
|
||||
@@ -95,7 +95,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, mess
|
||||
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
|
||||
error_message = translate(locale, message)
|
||||
error_message = I18n.translate(locale, message)
|
||||
next_steps = error_redirect_helper(env)
|
||||
|
||||
return templated "error"
|
||||
@@ -186,10 +186,10 @@ def error_redirect_helper(env : HTTP::Server::Context)
|
||||
|
||||
if request_path.starts_with?("/search") || request_path.starts_with?("/watch") ||
|
||||
request_path.starts_with?("/channel") || request_path.starts_with?("/playlist?list=PL")
|
||||
next_steps_text = translate(locale, "next_steps_error_message")
|
||||
refresh = translate(locale, "next_steps_error_message_refresh")
|
||||
go_to_youtube = translate(locale, "next_steps_error_message_go_to_youtube")
|
||||
switch_instance = translate(locale, "Switch Invidious Instance")
|
||||
next_steps_text = I18n.translate(locale, "next_steps_error_message")
|
||||
refresh = I18n.translate(locale, "next_steps_error_message_refresh")
|
||||
go_to_youtube = I18n.translate(locale, "next_steps_error_message_go_to_youtube")
|
||||
switch_instance = I18n.translate(locale, "Switch Invidious Instance")
|
||||
|
||||
return <<-END_HTML
|
||||
<p style="margin-bottom: 4px;">#{next_steps_text}</p>
|
||||
|
||||
+140
-134
@@ -22,60 +22,124 @@ struct Annotation
|
||||
property annotations : String
|
||||
end
|
||||
|
||||
def html_to_content(description_html : String)
|
||||
description = description_html.gsub(/(<br>)|(<br\/>)/, {
|
||||
"<br>": "\n",
|
||||
"<br/>": "\n",
|
||||
})
|
||||
module Helpers
|
||||
extend self
|
||||
|
||||
if !description.empty?
|
||||
description = XML.parse_html(description).content.strip("\n ")
|
||||
end
|
||||
private TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
||||
|
||||
return description
|
||||
end
|
||||
def html_to_content(description_html : String)
|
||||
description = description_html.gsub(/(<br>)|(<br\/>)/, {
|
||||
"<br>": "\n",
|
||||
"<br/>": "\n",
|
||||
})
|
||||
|
||||
def cache_annotation(id, annotations)
|
||||
if !CONFIG.cache_annotations
|
||||
return
|
||||
end
|
||||
|
||||
body = XML.parse(annotations)
|
||||
nodeset = body.xpath_nodes(%q(/document/annotations/annotation))
|
||||
|
||||
return if nodeset == 0
|
||||
|
||||
has_legacy_annotations = false
|
||||
nodeset.each do |node|
|
||||
if !{"branding", "card", "drawer"}.includes? node["type"]?
|
||||
has_legacy_annotations = true
|
||||
break
|
||||
if !description.empty?
|
||||
description = XML.parse_html(description).content.strip("\n ")
|
||||
end
|
||||
|
||||
return description
|
||||
end
|
||||
|
||||
Invidious::Database::Annotations.insert(id, annotations) if has_legacy_annotations
|
||||
end
|
||||
def cache_annotation(id, annotations)
|
||||
if !CONFIG.cache_annotations
|
||||
return
|
||||
end
|
||||
|
||||
def create_notification_stream(env, topics, connection_channel)
|
||||
connection = Channel(PQ::Notification).new(8)
|
||||
connection_channel.send({true, connection})
|
||||
body = XML.parse(annotations)
|
||||
nodeset = body.xpath_nodes(%q(/document/annotations/annotation))
|
||||
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
return if nodeset == 0
|
||||
|
||||
since = env.params.query["since"]?.try &.to_i?
|
||||
id = 0
|
||||
has_legacy_annotations = false
|
||||
nodeset.each do |node|
|
||||
if !{"branding", "card", "drawer"}.includes? node["type"]?
|
||||
has_legacy_annotations = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
Invidious::Database::Annotations.insert(id, annotations) if has_legacy_annotations
|
||||
end
|
||||
|
||||
def create_notification_stream(env, topics, connection_channel)
|
||||
connection = Channel(PQ::Notification).new(8)
|
||||
connection_channel.send({true, connection})
|
||||
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
|
||||
since = env.params.query["since"]?.try &.to_i?
|
||||
id = 0
|
||||
|
||||
if topics.includes? "debug"
|
||||
spawn do
|
||||
begin
|
||||
loop do
|
||||
time_span = [0, 0, 0, 0]
|
||||
time_span[rand(4)] = rand(30) + 5
|
||||
published = Time.utc - Time::Span.new(days: time_span[0], hours: time_span[1], minutes: time_span[2], seconds: time_span[3])
|
||||
video_id = TEST_IDS[rand(TEST_IDS.size)]
|
||||
|
||||
video = get_video(video_id)
|
||||
video.published = published
|
||||
response = JSON.parse(video.to_json(locale, nil))
|
||||
|
||||
env.response.puts "id: #{id}"
|
||||
env.response.puts "data: #{response.to_json}"
|
||||
env.response.puts
|
||||
env.response.flush
|
||||
|
||||
id += 1
|
||||
|
||||
sleep 1.minute
|
||||
Fiber.yield
|
||||
end
|
||||
rescue ex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
spawn do
|
||||
begin
|
||||
if since
|
||||
since_unix = Time.unix(since.not_nil!)
|
||||
|
||||
topics.try &.each do |topic|
|
||||
case topic
|
||||
when .match(/UC[A-Za-z0-9_-]{22}/)
|
||||
Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video|
|
||||
response = JSON.parse(video.to_json(locale))
|
||||
|
||||
env.response.puts "id: #{id}"
|
||||
env.response.puts "data: #{response.to_json}"
|
||||
env.response.puts
|
||||
env.response.flush
|
||||
|
||||
id += 1
|
||||
end
|
||||
else
|
||||
# TODO
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if topics.includes? "debug"
|
||||
spawn do
|
||||
begin
|
||||
loop do
|
||||
time_span = [0, 0, 0, 0]
|
||||
time_span[rand(4)] = rand(30) + 5
|
||||
published = Time.utc - Time::Span.new(days: time_span[0], hours: time_span[1], minutes: time_span[2], seconds: time_span[3])
|
||||
video_id = TEST_IDS[rand(TEST_IDS.size)]
|
||||
event = connection.receive
|
||||
|
||||
notification = JSON.parse(event.payload)
|
||||
topic = notification["topic"].as_s
|
||||
video_id = notification["videoId"].as_s
|
||||
published = notification["published"].as_i64
|
||||
|
||||
if !topics.try &.includes? topic
|
||||
next
|
||||
end
|
||||
|
||||
video = get_video(video_id)
|
||||
video.published = published
|
||||
video.published = Time.unix(published)
|
||||
response = JSON.parse(video.to_json(locale, nil))
|
||||
|
||||
env.response.puts "id: #{id}"
|
||||
@@ -84,65 +148,20 @@ def create_notification_stream(env, topics, connection_channel)
|
||||
env.response.flush
|
||||
|
||||
id += 1
|
||||
|
||||
sleep 1.minute
|
||||
Fiber.yield
|
||||
end
|
||||
rescue ex
|
||||
ensure
|
||||
connection_channel.send({false, connection})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
spawn do
|
||||
begin
|
||||
if since
|
||||
since_unix = Time.unix(since.not_nil!)
|
||||
|
||||
topics.try &.each do |topic|
|
||||
case topic
|
||||
when .match(/UC[A-Za-z0-9_-]{22}/)
|
||||
Invidious::Database::ChannelVideos.select_notfications(topic, since_unix).each do |video|
|
||||
response = JSON.parse(video.to_json(locale))
|
||||
|
||||
env.response.puts "id: #{id}"
|
||||
env.response.puts "data: #{response.to_json}"
|
||||
env.response.puts
|
||||
env.response.flush
|
||||
|
||||
id += 1
|
||||
end
|
||||
else
|
||||
# TODO
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
spawn do
|
||||
begin
|
||||
# Send heartbeat
|
||||
loop do
|
||||
event = connection.receive
|
||||
|
||||
notification = JSON.parse(event.payload)
|
||||
topic = notification["topic"].as_s
|
||||
video_id = notification["videoId"].as_s
|
||||
published = notification["published"].as_i64
|
||||
|
||||
if !topics.try &.includes? topic
|
||||
next
|
||||
end
|
||||
|
||||
video = get_video(video_id)
|
||||
video.published = Time.unix(published)
|
||||
response = JSON.parse(video.to_json(locale, nil))
|
||||
|
||||
env.response.puts "id: #{id}"
|
||||
env.response.puts "data: #{response.to_json}"
|
||||
env.response.puts ":keepalive #{Time.utc.to_unix}"
|
||||
env.response.puts
|
||||
env.response.flush
|
||||
|
||||
id += 1
|
||||
sleep (20 + rand(11)).seconds
|
||||
end
|
||||
rescue ex
|
||||
ensure
|
||||
@@ -150,51 +169,38 @@ def create_notification_stream(env, topics, connection_channel)
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
# Send heartbeat
|
||||
loop do
|
||||
env.response.puts ":keepalive #{Time.utc.to_unix}"
|
||||
env.response.puts
|
||||
env.response.flush
|
||||
sleep (20 + rand(11)).seconds
|
||||
end
|
||||
rescue ex
|
||||
ensure
|
||||
connection_channel.send({false, connection})
|
||||
end
|
||||
end
|
||||
|
||||
def extract_initial_data(body) : Hash(String, JSON::Any)
|
||||
return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?<info>{.*?});<\/script>/mx).try &.["info"] || "{}").as_h
|
||||
end
|
||||
|
||||
def proxy_file(response, env)
|
||||
if response.headers.includes_word?("Content-Encoding", "gzip")
|
||||
Compress::Gzip::Writer.open(env.response) do |deflate|
|
||||
IO.copy response.body_io, deflate
|
||||
end
|
||||
elsif response.headers.includes_word?("Content-Encoding", "deflate")
|
||||
Compress::Deflate::Writer.open(env.response) do |deflate|
|
||||
IO.copy response.body_io, deflate
|
||||
end
|
||||
else
|
||||
IO.copy response.body_io, env.response
|
||||
end
|
||||
end
|
||||
|
||||
# Fetch the playback requests tracker from the statistics endpoint.
|
||||
#
|
||||
# Creates a new tracker when unavailable.
|
||||
def get_playback_statistic
|
||||
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]) && tracker.as(Hash).empty?
|
||||
tracker = {
|
||||
"totalRequests" => 0_i64,
|
||||
"successfulRequests" => 0_i64,
|
||||
"ratio" => 0_f64,
|
||||
}
|
||||
|
||||
Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker
|
||||
def extract_initial_data(body) : Hash(String, JSON::Any)
|
||||
return JSON.parse(body.match(/(window\["ytInitialData"\]|var\s*ytInitialData)\s*=\s*(?<info>{.*?});<\/script>/mx).try &.["info"] || "{}").as_h
|
||||
end
|
||||
|
||||
return tracker.as(Hash(String, Int64 | Float64))
|
||||
def proxy_file(response, env)
|
||||
if response.headers.includes_word?("Content-Encoding", "gzip")
|
||||
Compress::Gzip::Writer.open(env.response) do |deflate|
|
||||
IO.copy response.body_io, deflate
|
||||
end
|
||||
elsif response.headers.includes_word?("Content-Encoding", "deflate")
|
||||
Compress::Deflate::Writer.open(env.response) do |deflate|
|
||||
IO.copy response.body_io, deflate
|
||||
end
|
||||
else
|
||||
IO.copy response.body_io, env.response
|
||||
end
|
||||
end
|
||||
|
||||
# Fetch the playback requests tracker from the statistics endpoint.
|
||||
#
|
||||
# Creates a new tracker when unavailable.
|
||||
def get_playback_statistic
|
||||
if (tracker = Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"]) && tracker.as(Hash).empty?
|
||||
tracker = {
|
||||
"totalRequests" => 0_i64,
|
||||
"successfulRequests" => 0_i64,
|
||||
"ratio" => 0_f64,
|
||||
}
|
||||
|
||||
Invidious::Jobs::StatisticsRefreshJob::STATISTICS["playback"] = tracker
|
||||
end
|
||||
|
||||
return tracker.as(Hash(String, Int64 | Float64))
|
||||
end
|
||||
end
|
||||
|
||||
+183
-179
@@ -1,199 +1,203 @@
|
||||
# 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
|
||||
"en-US" => "English", # English
|
||||
"eo" => "Esperanto", # Esperanto
|
||||
"es" => "Español", # Spanish
|
||||
"et" => "Eesti keel", # Estonian
|
||||
"eu" => "Euskara", # Basque
|
||||
"fa" => "فارسی", # Persian
|
||||
"fi" => "Suomi", # Finnish
|
||||
"fr" => "Français", # French
|
||||
"he" => "עברית", # Hebrew
|
||||
"hi" => "हिन्दी", # Hindi
|
||||
"hr" => "Hrvatski", # Croatian
|
||||
"hu-HU" => "Magyar Nyelv", # Hungarian
|
||||
"id" => "Bahasa Indonesia", # Indonesian
|
||||
"is" => "Íslenska", # Icelandic
|
||||
"it" => "Italiano", # Italian
|
||||
"ja" => "日本語", # Japanese
|
||||
"ko" => "한국어", # Korean
|
||||
"lmo" => "Lombard", # Lombard
|
||||
"lt" => "Lietuvių", # Lithuanian
|
||||
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
|
||||
"nl" => "Nederlands", # Dutch
|
||||
"pl" => "Polski", # Polish
|
||||
"pt" => "Português", # Portuguese
|
||||
"pt-BR" => "Português Brasileiro", # Portuguese (Brazil)
|
||||
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
|
||||
"ro" => "Română", # Romanian
|
||||
"ru" => "Русский", # Russian
|
||||
"si" => "සිංහල", # Sinhala
|
||||
"sk" => "Slovenčina", # Slovak
|
||||
"sl" => "Slovenščina", # Slovenian
|
||||
"sq" => "Shqip", # Albanian
|
||||
"sr" => "Srpski (latinica)", # Serbian (Latin)
|
||||
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
|
||||
"sv-SE" => "Svenska", # Swedish
|
||||
"ta" => "தமிழ்", # Tamil
|
||||
"tr" => "Türkçe", # Turkish
|
||||
"uk" => "Українська", # Ukrainian
|
||||
"vi" => "Tiếng Việt", # Vietnamese
|
||||
"zh-CN" => "汉语", # Chinese (Simplified)
|
||||
"zh-TW" => "漢語", # Chinese (Traditional)
|
||||
}
|
||||
module I18n
|
||||
extend self
|
||||
|
||||
LOCALES = load_all_locales()
|
||||
# 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
|
||||
"en-US" => "English", # English
|
||||
"eo" => "Esperanto", # Esperanto
|
||||
"es" => "Español", # Spanish
|
||||
"et" => "Eesti keel", # Estonian
|
||||
"eu" => "Euskara", # Basque
|
||||
"fa" => "فارسی", # Persian
|
||||
"fi" => "Suomi", # Finnish
|
||||
"fr" => "Français", # French
|
||||
"he" => "עברית", # Hebrew
|
||||
"hi" => "हिन्दी", # Hindi
|
||||
"hr" => "Hrvatski", # Croatian
|
||||
"hu-HU" => "Magyar Nyelv", # Hungarian
|
||||
"id" => "Bahasa Indonesia", # Indonesian
|
||||
"is" => "Íslenska", # Icelandic
|
||||
"it" => "Italiano", # Italian
|
||||
"ja" => "日本語", # Japanese
|
||||
"ko" => "한국어", # Korean
|
||||
"lmo" => "Lombard", # Lombard
|
||||
"lt" => "Lietuvių", # Lithuanian
|
||||
"nb-NO" => "Norsk bokmål", # Norwegian Bokmål
|
||||
"nl" => "Nederlands", # Dutch
|
||||
"pl" => "Polski", # Polish
|
||||
"pt" => "Português", # Portuguese
|
||||
"pt-BR" => "Português Brasileiro", # Portuguese (Brazil)
|
||||
"pt-PT" => "Português de Portugal", # Portuguese (Portugal)
|
||||
"ro" => "Română", # Romanian
|
||||
"ru" => "Русский", # Russian
|
||||
"si" => "සිංහල", # Sinhala
|
||||
"sk" => "Slovenčina", # Slovak
|
||||
"sl" => "Slovenščina", # Slovenian
|
||||
"sq" => "Shqip", # Albanian
|
||||
"sr" => "Srpski (latinica)", # Serbian (Latin)
|
||||
"sr_Cyrl" => "Српски (ћирилица)", # Serbian (Cyrillic)
|
||||
"sv-SE" => "Svenska", # Swedish
|
||||
"ta" => "தமிழ்", # Tamil
|
||||
"tr" => "Türkçe", # Turkish
|
||||
"uk" => "Українська", # Ukrainian
|
||||
"vi" => "Tiếng Việt", # Vietnamese
|
||||
"zh-CN" => "汉语", # Chinese (Simplified)
|
||||
"zh-TW" => "漢語", # Chinese (Traditional)
|
||||
}
|
||||
|
||||
CONTENT_REGIONS = {
|
||||
"AE", "AR", "AT", "AU", "AZ", "BA", "BD", "BE", "BG", "BH", "BO", "BR", "BY",
|
||||
"CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE",
|
||||
"EG", "ES", "FI", "FR", "GB", "GE", "GH", "GR", "GT", "HK", "HN", "HR", "HU",
|
||||
"ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KR", "KW",
|
||||
"KZ", "LB", "LI", "LK", "LT", "LU", "LV", "LY", "MA", "ME", "MK", "MT", "MX",
|
||||
"MY", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK",
|
||||
"PL", "PR", "PT", "PY", "QA", "RO", "RS", "RU", "SA", "SE", "SG", "SI", "SK",
|
||||
"SN", "SV", "TH", "TN", "TR", "TW", "TZ", "UA", "UG", "US", "UY", "VE", "VN",
|
||||
"YE", "ZA", "ZW",
|
||||
}
|
||||
LOCALES = load_all_locales()
|
||||
|
||||
# Enum for the different types of number formats
|
||||
enum NumberFormatting
|
||||
None # Print the number as-is
|
||||
Separator # Use a separator for thousands
|
||||
Short # Use short notation (k/M/B)
|
||||
HtmlSpan # Surround with <span id="count"></span>
|
||||
end
|
||||
CONTENT_REGIONS = {
|
||||
"AE", "AR", "AT", "AU", "AZ", "BA", "BD", "BE", "BG", "BH", "BO", "BR", "BY",
|
||||
"CA", "CH", "CL", "CO", "CR", "CY", "CZ", "DE", "DK", "DO", "DZ", "EC", "EE",
|
||||
"EG", "ES", "FI", "FR", "GB", "GE", "GH", "GR", "GT", "HK", "HN", "HR", "HU",
|
||||
"ID", "IE", "IL", "IN", "IQ", "IS", "IT", "JM", "JO", "JP", "KE", "KR", "KW",
|
||||
"KZ", "LB", "LI", "LK", "LT", "LU", "LV", "LY", "MA", "ME", "MK", "MT", "MX",
|
||||
"MY", "NG", "NI", "NL", "NO", "NP", "NZ", "OM", "PA", "PE", "PG", "PH", "PK",
|
||||
"PL", "PR", "PT", "PY", "QA", "RO", "RS", "RU", "SA", "SE", "SG", "SI", "SK",
|
||||
"SN", "SV", "TH", "TN", "TR", "TW", "TZ", "UA", "UG", "US", "UY", "VE", "VN",
|
||||
"YE", "ZA", "ZW",
|
||||
}
|
||||
|
||||
def load_all_locales
|
||||
locales = {} of String => Hash(String, JSON::Any)
|
||||
|
||||
LOCALES_LIST.each_key do |name|
|
||||
locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h
|
||||
# Enum for the different types of number formats
|
||||
enum NumberFormatting
|
||||
None # Print the number as-is
|
||||
Separator # Use a separator for thousands
|
||||
Short # Use short notation (k/M/B)
|
||||
HtmlSpan # Surround with <span id="count"></span>
|
||||
end
|
||||
|
||||
return locales
|
||||
end
|
||||
def load_all_locales
|
||||
locales = {} of String => Hash(String, JSON::Any)
|
||||
|
||||
def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
|
||||
# Log a warning if "key" doesn't exist in en-US locale and return
|
||||
# that key as the text, so this is more or less transparent to the user.
|
||||
if !LOCALES["en-US"].has_key?(key)
|
||||
LOGGER.warn("i18n: Missing translation key \"#{key}\"")
|
||||
return key
|
||||
end
|
||||
|
||||
# Default to english, whenever the locale doesn't exist,
|
||||
# or the key requested has not been translated
|
||||
if locale && LOCALES.has_key?(locale) && LOCALES[locale].has_key?(key)
|
||||
raw_data = LOCALES[locale][key]
|
||||
else
|
||||
raw_data = LOCALES["en-US"][key]
|
||||
end
|
||||
|
||||
case raw_data
|
||||
when .as_h?
|
||||
# Init
|
||||
translation = ""
|
||||
match_length = 0
|
||||
|
||||
raw_data.as_h.each do |hash_key, value|
|
||||
if text.is_a?(String)
|
||||
if md = text.try &.match(/#{hash_key}/)
|
||||
if md[0].size >= match_length
|
||||
translation = value.as_s
|
||||
match_length = md[0].size
|
||||
end
|
||||
end
|
||||
end
|
||||
LOCALES_LIST.each_key do |name|
|
||||
locales[name] = JSON.parse(File.read("locales/#{name}.json")).as_h
|
||||
end
|
||||
when .as_s?
|
||||
translation = raw_data.as_s
|
||||
else
|
||||
raise "Invalid translation \"#{raw_data}\""
|
||||
|
||||
return locales
|
||||
end
|
||||
|
||||
if text.is_a?(String)
|
||||
translation = translation.gsub("`x`", text)
|
||||
elsif text.is_a?(Hash(String, String))
|
||||
# adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic
|
||||
text.each_key do |hash_key|
|
||||
translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
|
||||
end
|
||||
end
|
||||
|
||||
return translation
|
||||
end
|
||||
|
||||
def translate_count(locale : String, key : String, count : Int, format = NumberFormatting::None) : String
|
||||
# Fallback on english if locale doesn't exist
|
||||
locale = "en-US" if !LOCALES.has_key?(locale)
|
||||
|
||||
# Retrieve suffix
|
||||
suffix = I18next::Plurals::RESOLVER.get_suffix(locale, count)
|
||||
plural_key = key + suffix
|
||||
|
||||
if LOCALES[locale].has_key?(plural_key)
|
||||
translation = LOCALES[locale][plural_key].as_s
|
||||
else
|
||||
# Try #1: Fallback to singular in the same locale
|
||||
singular_suffix = I18next::Plurals::RESOLVER.get_suffix(locale, 1)
|
||||
|
||||
if LOCALES[locale].has_key?(key + singular_suffix)
|
||||
translation = LOCALES[locale][key + singular_suffix].as_s
|
||||
elsif locale != "en-US"
|
||||
# Try #2: Fallback to english
|
||||
translation = translate_count("en-US", key, count)
|
||||
else
|
||||
# Return key if we're already in english, as the translation is missing
|
||||
def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
|
||||
# Log a warning if "key" doesn't exist in en-US locale and return
|
||||
# that key as the text, so this is more or less transparent to the user.
|
||||
if !LOCALES["en-US"].has_key?(key)
|
||||
LOGGER.warn("i18n: Missing translation key \"#{key}\"")
|
||||
return key
|
||||
end
|
||||
|
||||
# Default to english, whenever the locale doesn't exist,
|
||||
# or the key requested has not been translated
|
||||
if locale && LOCALES.has_key?(locale) && LOCALES[locale].has_key?(key)
|
||||
raw_data = LOCALES[locale][key]
|
||||
else
|
||||
raw_data = LOCALES["en-US"][key]
|
||||
end
|
||||
|
||||
case raw_data
|
||||
when .as_h?
|
||||
# Init
|
||||
translation = ""
|
||||
match_length = 0
|
||||
|
||||
raw_data.as_h.each do |hash_key, value|
|
||||
if text.is_a?(String)
|
||||
if md = text.try &.match(/#{hash_key}/)
|
||||
if md[0].size >= match_length
|
||||
translation = value.as_s
|
||||
match_length = md[0].size
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
when .as_s?
|
||||
translation = raw_data.as_s
|
||||
else
|
||||
raise "Invalid translation \"#{raw_data}\""
|
||||
end
|
||||
|
||||
if text.is_a?(String)
|
||||
translation = translation.gsub("`x`", text)
|
||||
elsif text.is_a?(Hash(String, String))
|
||||
# adds support for multi string interpolation. Based on i18next https://www.i18next.com/translation-function/interpolation#basic
|
||||
text.each_key do |hash_key|
|
||||
translation = translation.gsub("{{#{hash_key}}}", text[hash_key])
|
||||
end
|
||||
end
|
||||
|
||||
return translation
|
||||
end
|
||||
|
||||
case format
|
||||
when .separator? then count_txt = number_with_separator(count)
|
||||
when .short? then count_txt = number_to_short_text(count)
|
||||
when .html_span? then count_txt = "<span id=\"count\">" + count.to_s + "</span>"
|
||||
else count_txt = count.to_s
|
||||
def translate_count(locale : String, key : String, count : Int, format = NumberFormatting::None) : String
|
||||
# Fallback on english if locale doesn't exist
|
||||
locale = "en-US" if !LOCALES.has_key?(locale)
|
||||
|
||||
# Retrieve suffix
|
||||
suffix = I18next::Plurals::RESOLVER.get_suffix(locale, count)
|
||||
plural_key = key + suffix
|
||||
|
||||
if LOCALES[locale].has_key?(plural_key)
|
||||
translation = LOCALES[locale][plural_key].as_s
|
||||
else
|
||||
# Try #1: Fallback to singular in the same locale
|
||||
singular_suffix = I18next::Plurals::RESOLVER.get_suffix(locale, 1)
|
||||
|
||||
if LOCALES[locale].has_key?(key + singular_suffix)
|
||||
translation = LOCALES[locale][key + singular_suffix].as_s
|
||||
elsif locale != "en-US"
|
||||
# Try #2: Fallback to english
|
||||
translation = self.translate_count("en-US", key, count)
|
||||
else
|
||||
# Return key if we're already in english, as the translation is missing
|
||||
LOGGER.warn("i18n: Missing translation key \"#{key}\"")
|
||||
return key
|
||||
end
|
||||
end
|
||||
|
||||
case format
|
||||
when .separator? then count_txt = number_with_separator(count)
|
||||
when .short? then count_txt = number_to_short_text(count)
|
||||
when .html_span? then count_txt = "<span id=\"count\">" + count.to_s + "</span>"
|
||||
else count_txt = count.to_s
|
||||
end
|
||||
|
||||
return translation.gsub("{{count}}", count_txt)
|
||||
end
|
||||
|
||||
return translation.gsub("{{count}}", count_txt)
|
||||
end
|
||||
def translate_bool(locale : String?, translation : Bool)
|
||||
case translation
|
||||
when true
|
||||
return self.translate(locale, "Yes")
|
||||
when false
|
||||
return self.translate(locale, "No")
|
||||
end
|
||||
end
|
||||
|
||||
def translate_bool(locale : String?, translation : Bool)
|
||||
case translation
|
||||
when true
|
||||
return translate(locale, "Yes")
|
||||
when false
|
||||
return translate(locale, "No")
|
||||
def locale_is_rtl?(locale : String?)
|
||||
# Fallback to en-US
|
||||
return false if locale.nil?
|
||||
|
||||
# Arabic, Persian, Hebrew
|
||||
# See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts
|
||||
return {"ar", "fa", "he"}.includes? locale
|
||||
end
|
||||
end
|
||||
|
||||
def locale_is_rtl?(locale : String?)
|
||||
# Fallback to en-US
|
||||
return false if locale.nil?
|
||||
|
||||
# Arabic, Persian, Hebrew
|
||||
# See https://en.wikipedia.org/wiki/Right-to-left_script#List_of_RTL_scripts
|
||||
return {"ar", "fa", "he"}.includes? locale
|
||||
end
|
||||
|
||||
@@ -53,7 +53,7 @@ struct SearchVideo
|
||||
xml.element("img", src: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg")
|
||||
end
|
||||
|
||||
xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text html_to_content(self.description_html) }
|
||||
xml.element("p", style: "word-break:break-word;white-space:pre-wrap") { xml.text Helpers.html_to_content(self.description_html) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -63,7 +63,7 @@ struct SearchVideo
|
||||
xml.element("media:title") { xml.text self.title }
|
||||
xml.element("media:thumbnail", url: "#{HOST_URL}/vi/#{self.id}/mqdefault.jpg",
|
||||
width: "320", height: "180")
|
||||
xml.element("media:description") { xml.text html_to_content(self.description_html) }
|
||||
xml.element("media:description") { xml.text Helpers.html_to_content(self.description_html) }
|
||||
end
|
||||
|
||||
xml.element("media:community") do
|
||||
@@ -111,13 +111,13 @@ struct SearchVideo
|
||||
Invidious::JSONify::APIv1.thumbnails(json, self.id)
|
||||
end
|
||||
|
||||
json.field "description", html_to_content(self.description_html)
|
||||
json.field "description", Helpers.html_to_content(self.description_html)
|
||||
json.field "descriptionHtml", self.description_html
|
||||
|
||||
json.field "viewCount", self.views
|
||||
json.field "viewCountText", translate_count(locale, "generic_views_count", self.views, NumberFormatting::Short)
|
||||
json.field "viewCountText", I18n.translate_count(locale, "generic_views_count", self.views, I18n::NumberFormatting::Short)
|
||||
json.field "published", self.published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(self.published, locale))
|
||||
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(self.published, locale))
|
||||
json.field "lengthSeconds", self.length_seconds
|
||||
json.field "liveNow", self.badges.live_now?
|
||||
json.field "premium", self.badges.premium?
|
||||
@@ -255,7 +255,7 @@ struct SearchChannel
|
||||
json.field "videoCount", self.video_count
|
||||
json.field "channelHandle", self.channel_handle
|
||||
|
||||
json.field "description", html_to_content(self.description_html)
|
||||
json.field "description", Helpers.html_to_content(self.description_html)
|
||||
json.field "descriptionHtml", self.description_html
|
||||
end
|
||||
end
|
||||
@@ -327,8 +327,8 @@ struct ProblematicTimelineItem
|
||||
xml.element("content", type: "xhtml") do
|
||||
xml.element("div", xmlns: "http://www.w3.org/1999/xhtml") do
|
||||
xml.element("div") do
|
||||
xml.element("h4") { translate(locale, "timeline_parse_error_placeholder_heading") }
|
||||
xml.element("p") { translate(locale, "timeline_parse_error_placeholder_message") }
|
||||
xml.element("h4") { I18n.translate(locale, "timeline_parse_error_placeholder_heading") }
|
||||
xml.element("p") { I18n.translate(locale, "timeline_parse_error_placeholder_message") }
|
||||
end
|
||||
|
||||
xml.element("pre") do
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
||||
|
||||
# See http://www.evanmiller.org/how-not-to-sort-by-average-rating.html
|
||||
def ci_lower_bound(pos, n)
|
||||
if n == 0
|
||||
@@ -144,19 +146,19 @@ def recode_date(time : Time, locale)
|
||||
span = Time.utc - time
|
||||
|
||||
if span.total_days > 365.0
|
||||
return translate_count(locale, "generic_count_years", span.total_days.to_i // 365)
|
||||
return I18n.translate_count(locale, "generic_count_years", span.total_days.to_i // 365)
|
||||
elsif span.total_days > 30.0
|
||||
return translate_count(locale, "generic_count_months", span.total_days.to_i // 30)
|
||||
return I18n.translate_count(locale, "generic_count_months", span.total_days.to_i // 30)
|
||||
elsif span.total_days > 7.0
|
||||
return translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7)
|
||||
return I18n.translate_count(locale, "generic_count_weeks", span.total_days.to_i // 7)
|
||||
elsif span.total_hours > 24.0
|
||||
return translate_count(locale, "generic_count_days", span.total_days.to_i)
|
||||
return I18n.translate_count(locale, "generic_count_days", span.total_days.to_i)
|
||||
elsif span.total_minutes > 60.0
|
||||
return translate_count(locale, "generic_count_hours", span.total_hours.to_i)
|
||||
return I18n.translate_count(locale, "generic_count_hours", span.total_hours.to_i)
|
||||
elsif span.total_seconds > 60.0
|
||||
return translate_count(locale, "generic_count_minutes", span.total_minutes.to_i)
|
||||
return I18n.translate_count(locale, "generic_count_minutes", span.total_minutes.to_i)
|
||||
else
|
||||
return translate_count(locale, "generic_count_seconds", span.total_seconds.to_i)
|
||||
return I18n.translate_count(locale, "generic_count_seconds", span.total_seconds.to_i)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ module Invidious::JSONify::APIv1
|
||||
json.field "description", video.description
|
||||
json.field "descriptionHtml", video.description_html
|
||||
json.field "published", video.published.to_unix
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(video.published, locale))
|
||||
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(video.published, locale))
|
||||
json.field "keywords", video.keywords
|
||||
|
||||
json.field "viewCount", video.views
|
||||
@@ -268,7 +268,7 @@ module Invidious::JSONify::APIv1
|
||||
json.field "viewCountText", rv["short_view_count"]?
|
||||
json.field "published", rv["published"]?
|
||||
if rv["published"]?.try &.presence
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
|
||||
json.field "publishedText", I18n.translate(locale, "`x` ago", recode_date(Time.parse_rfc3339(rv["published"].to_s), locale))
|
||||
else
|
||||
json.field "publishedText", ""
|
||||
end
|
||||
|
||||
@@ -27,7 +27,7 @@ def fetch_mix(rdid, video_id, cookies = nil, locale = nil)
|
||||
|
||||
video_id = "CvFH_6DNRCY" if rdid.starts_with? "OLAK5uy_"
|
||||
response = YT_POOL.client &.get("/watch?v=#{video_id}&list=#{rdid}&gl=US&hl=en", headers)
|
||||
initial_data = extract_initial_data(response.body)
|
||||
initial_data = Helpers.extract_initial_data(response.body)
|
||||
|
||||
if !initial_data["contents"]["twoColumnWatchNextResults"]["playlist"]?
|
||||
raise InfoException.new("Could not create mix.")
|
||||
|
||||
@@ -107,7 +107,11 @@ struct Playlist
|
||||
|
||||
json.field "author", self.author
|
||||
json.field "authorId", self.ucid
|
||||
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||
if !self.ucid.empty?
|
||||
json.field "authorUrl", "/channel/#{self.ucid}"
|
||||
else
|
||||
json.field "authorUrl", ""
|
||||
end
|
||||
json.field "subtitle", self.subtitle
|
||||
|
||||
json.field "authorThumbnails" do
|
||||
@@ -195,7 +199,7 @@ struct InvidiousPlaylist
|
||||
json.field "authorUrl", nil
|
||||
json.field "authorThumbnails", [] of String
|
||||
|
||||
json.field "description", html_to_content(self.description_html)
|
||||
json.field "description", Helpers.html_to_content(self.description_html)
|
||||
json.field "descriptionHtml", self.description_html
|
||||
json.field "videoCount", self.video_count
|
||||
|
||||
@@ -359,6 +363,9 @@ def fetch_playlist(plid : String)
|
||||
thumbnail = playlist_info.dig?(
|
||||
"thumbnailRenderer", "playlistVideoThumbnailRenderer",
|
||||
"thumbnail", "thumbnails", 0, "url"
|
||||
).try &.as_s || playlist_info.dig?(
|
||||
"thumbnailRenderer", "playlistCustomThumbnailRenderer",
|
||||
"thumbnail", "thumbnails", 0, "url"
|
||||
).try &.as_s
|
||||
|
||||
views = 0_i64
|
||||
@@ -377,7 +384,7 @@ def fetch_playlist(plid : String)
|
||||
video_count = text.gsub(/\D/, "").to_i? || 0
|
||||
elsif text.includes? "view"
|
||||
views = text.gsub(/\D/, "").to_i64? || 0_i64
|
||||
else
|
||||
elsif !text.includes? "Pay to watch"
|
||||
updated = decode_date(text.lchop("Last updated on ").lchop("Updated "))
|
||||
end
|
||||
end
|
||||
@@ -438,7 +445,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
|
||||
# 100 videos per request
|
||||
ctoken = produce_playlist_continuation(playlist.id, offset)
|
||||
initial_data = YoutubeAPI.browse(ctoken)
|
||||
videos += extract_playlist_videos(initial_data)
|
||||
videos += extract_playlist_videos(playlist.id, initial_data)
|
||||
|
||||
offset += 100
|
||||
end
|
||||
@@ -447,7 +454,7 @@ def get_playlist_videos(playlist : InvidiousPlaylist | Playlist, offset : Int32,
|
||||
end
|
||||
end
|
||||
|
||||
def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
|
||||
def extract_playlist_videos(playlist_id : String, initial_data : Hash(String, JSON::Any))
|
||||
videos = [] of PlaylistVideo | ProblematicTimelineItem
|
||||
|
||||
if initial_data["contents"]?
|
||||
@@ -473,9 +480,9 @@ def extract_playlist_videos(initial_data : Hash(String, JSON::Any))
|
||||
|
||||
contents.try &.each do |item|
|
||||
if i = item["playlistVideoRenderer"]?
|
||||
video_id = i["navigationEndpoint"]["watchEndpoint"]["videoId"].as_s
|
||||
plid = i["navigationEndpoint"]["watchEndpoint"]["playlistId"].as_s
|
||||
index = i["navigationEndpoint"]["watchEndpoint"]["index"].as_i64
|
||||
video_id = i.dig?("navigationEndpoint", "watchEndpoint", "videoId").try &.as_s || i.dig("videoId").as_s
|
||||
plid = i.dig?("navigationEndpoint", "watchEndpoint", "playlistId").try &.as_s || playlist_id
|
||||
index = i.dig?("navigationEndpoint", "watchEndpoint", "index").try &.as_i64 || i.dig("index", "simpleText").as_s.to_i64
|
||||
|
||||
title = i["title"].try { |t| t["simpleText"]? || t["runs"]?.try &.[0]["text"]? }.try &.as_s || ""
|
||||
author = i["shortBylineText"]?.try &.["runs"][0]["text"].as_s || ""
|
||||
|
||||
@@ -8,7 +8,7 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
# topics = env.params.body["topics"]?.try &.split(",").uniq.first(1000)
|
||||
# topics ||= [] of String
|
||||
|
||||
# create_notification_stream(env, topics, connection_channel)
|
||||
# Helpers.create_notification_stream(env, topics, connection_channel)
|
||||
# end
|
||||
|
||||
def self.get_preferences(env)
|
||||
@@ -485,6 +485,6 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
topics = raw_topics.try &.split(",").uniq.first(1000)
|
||||
topics ||= [] of String
|
||||
|
||||
create_notification_stream(env, topics, CONNECTION_CHANNEL)
|
||||
Helpers.create_notification_stream(env, topics, CONNECTION_CHANNEL)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -97,13 +97,14 @@ module Invidious::Routes::API::V1::Channels
|
||||
json.field "autoGenerated", channel.auto_generated
|
||||
json.field "ageGated", channel.is_age_gated
|
||||
json.field "isFamilyFriendly", channel.is_family_friendly
|
||||
json.field "description", html_to_content(channel.description_html)
|
||||
json.field "description", Helpers.html_to_content(channel.description_html)
|
||||
json.field "descriptionHtml", channel.description_html
|
||||
|
||||
json.field "allowedRegions", channel.allowed_regions
|
||||
json.field "tabs", channel.tabs
|
||||
json.field "tags", channel.tags
|
||||
json.field "authorVerified", channel.verified
|
||||
json.field "pronouns", channel.pronouns
|
||||
|
||||
json.field "latestVideos" do
|
||||
json.array do
|
||||
@@ -127,7 +128,6 @@ module Invidious::Routes::API::V1::Channels
|
||||
end
|
||||
end
|
||||
end # relatedChannels
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
require "html"
|
||||
|
||||
module Invidious::Routes::API::V1::Videos
|
||||
private INTERNET_ARCHIVE_URL = URI.parse("https://archive.org")
|
||||
private CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
|
||||
def self.videos(env)
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
|
||||
@@ -279,7 +282,7 @@ module Invidious::Routes::API::V1::Videos
|
||||
|
||||
file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
|
||||
|
||||
location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
||||
location = make_client(INTERNET_ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
||||
|
||||
if !location.headers["Location"]?
|
||||
env.response.status_code = location.status_code
|
||||
@@ -297,7 +300,7 @@ module Invidious::Routes::API::V1::Videos
|
||||
|
||||
annotations = response.body
|
||||
|
||||
cache_annotation(id, annotations)
|
||||
Helpers.cache_annotation(id, annotations)
|
||||
end
|
||||
else # "youtube"
|
||||
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
|
||||
@@ -407,7 +410,7 @@ module Invidious::Routes::API::V1::Videos
|
||||
clip_title = nil
|
||||
|
||||
if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s
|
||||
start_time, end_time, clip_title = parse_clip_parameters(params)
|
||||
start_time, end_time, clip_title = Invidious::Videos::Clip.parse_clip_parameters(params)
|
||||
end
|
||||
|
||||
begin
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
module Invidious::Routes::BeforeAll
|
||||
struct CompanionCSP
|
||||
property companion_urls : String = ""
|
||||
|
||||
def initialize
|
||||
self.companion_urls = CONFIG.invidious_companion.reject(&.builtin_proxy).map do |companion|
|
||||
"#{companion.public_url.scheme}://#{companion.public_url.host}#{companion.public_url.port ? ":#{companion.public_url.port}" : ""}"
|
||||
end.join(" ")
|
||||
end
|
||||
end
|
||||
|
||||
private COMPANION_CSP = CompanionCSP.new
|
||||
|
||||
def self.handle(env)
|
||||
preferences = Preferences.from_json("{}")
|
||||
|
||||
@@ -7,7 +19,7 @@ module Invidious::Routes::BeforeAll
|
||||
preferences = Preferences.from_json(URI.decode_www_form(prefs_cookie.value))
|
||||
else
|
||||
if language_header = env.request.headers["Accept-Language"]?
|
||||
if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
|
||||
if language = ANG.language_negotiator.best(language_header, I18n::LOCALES.keys)
|
||||
preferences.locale = language.header
|
||||
end
|
||||
end
|
||||
@@ -35,9 +47,9 @@ module Invidious::Routes::BeforeAll
|
||||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data:",
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'self'",
|
||||
"connect-src 'self' " + COMPANION_CSP.companion_urls,
|
||||
"manifest-src 'self'",
|
||||
"media-src 'self' blob:",
|
||||
"media-src 'self' blob: " + COMPANION_CSP.companion_urls,
|
||||
"child-src 'self' blob:",
|
||||
"frame-src 'self'",
|
||||
"frame-ancestors " + frame_ancestors,
|
||||
@@ -94,8 +106,8 @@ module Invidious::Routes::BeforeAll
|
||||
end
|
||||
|
||||
dark_mode = convert_theme(env.params.query["dark_mode"]?) || preferences.dark_mode.to_s
|
||||
thin_mode = env.params.query["thin_mode"]? || preferences.thin_mode.to_s
|
||||
thin_mode = thin_mode == "true"
|
||||
thin_mode = env.params.query["thin_mode"]?
|
||||
thin_mode = (thin_mode == "true") || preferences.thin_mode
|
||||
locale = env.params.query["hl"]? || preferences.locale
|
||||
|
||||
preferences.dark_mode = dark_mode
|
||||
|
||||
@@ -231,8 +231,10 @@ module Invidious::Routes::Channels
|
||||
env.redirect "/post/#{URI.encode_www_form(lb)}?ucid=#{URI.encode_www_form(ucid)}"
|
||||
end
|
||||
|
||||
thin_mode = env.params.query["thin_mode"]? || env.get("preferences").as(Preferences).thin_mode
|
||||
thin_mode = thin_mode == "true"
|
||||
preferences = env.get("preferences").as(Preferences)
|
||||
|
||||
thin_mode = env.params.query["thin_mode"]?
|
||||
thin_mode = (thin_mode == "true") || preferences.thin_mode
|
||||
|
||||
continuation = env.params.query["continuation"]?
|
||||
|
||||
@@ -352,7 +354,7 @@ module Invidious::Routes::Channels
|
||||
resolved_url = YoutubeAPI.resolve_url("https://youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}")
|
||||
ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"]
|
||||
rescue ex : InfoException | KeyError
|
||||
return error_template(404, translate(locale, "This channel does not exist."))
|
||||
return error_template(404, I18n.translate(locale, "This channel does not exist."))
|
||||
end
|
||||
|
||||
selected_tab = env.params.url["tab"]?
|
||||
|
||||
@@ -10,7 +10,7 @@ module Invidious::Routes::Embed
|
||||
videos = get_playlist_videos(playlist, offset: offset)
|
||||
if videos.empty?
|
||||
url = "/playlist?list=#{plid}"
|
||||
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
|
||||
raise NotFoundException.new(I18n.translate(locale, "error_video_not_in_playlist", url))
|
||||
end
|
||||
|
||||
first_playlist_video = videos[0].as(PlaylistVideo)
|
||||
@@ -71,7 +71,7 @@ module Invidious::Routes::Embed
|
||||
videos = get_playlist_videos(playlist, offset: offset)
|
||||
if videos.empty?
|
||||
url = "/playlist?list=#{plid}"
|
||||
raise NotFoundException.new(translate(locale, "error_video_not_in_playlist", url))
|
||||
raise NotFoundException.new(I18n.translate(locale, "error_video_not_in_playlist", url))
|
||||
end
|
||||
|
||||
first_playlist_video = videos[0].as(PlaylistVideo)
|
||||
@@ -122,7 +122,7 @@ module Invidious::Routes::Embed
|
||||
else nil # Continue
|
||||
end
|
||||
|
||||
params = process_video_params(env.params.query, preferences)
|
||||
params = Invidious::Videos.process_video_params(env.params.query, preferences)
|
||||
|
||||
user = env.get?("user").try &.as(User)
|
||||
if user
|
||||
@@ -208,17 +208,6 @@ module Invidious::Routes::Embed
|
||||
|
||||
if CONFIG.invidious_companion.present?
|
||||
invidious_companion = CONFIG.invidious_companion.sample
|
||||
invidious_companion_urls = CONFIG.invidious_companion.reject(&.builtin_proxy).map do |companion|
|
||||
uri =
|
||||
"#{companion.public_url.scheme}://#{companion.public_url.host}#{companion.public_url.port ? ":#{companion.public_url.port}" : ""}"
|
||||
end.join(" ")
|
||||
|
||||
if !invidious_companion_urls.empty?
|
||||
env.response.headers["Content-Security-Policy"] =
|
||||
env.response.headers["Content-Security-Policy"]
|
||||
.gsub("media-src", "media-src #{invidious_companion_urls}")
|
||||
.gsub("connect-src", "connect-src #{invidious_companion_urls}")
|
||||
end
|
||||
end
|
||||
|
||||
rendered "embed"
|
||||
|
||||
@@ -37,7 +37,7 @@ module Invidious::Routes::Feeds
|
||||
if CONFIG.popular_enabled
|
||||
templated "feeds/popular"
|
||||
else
|
||||
message = translate(locale, "The Popular feed has been disabled by the administrator.")
|
||||
message = I18n.translate(locale, "The Popular feed has been disabled by the administrator.")
|
||||
templated "message"
|
||||
end
|
||||
end
|
||||
@@ -259,7 +259,7 @@ module Invidious::Routes::Feeds
|
||||
xml.element("link", "type": "text/html", rel: "alternate", href: "#{HOST_URL}/feed/subscriptions")
|
||||
xml.element("link", "type": "application/atom+xml", rel: "self",
|
||||
href: "#{HOST_URL}#{env.request.resource}")
|
||||
xml.element("title") { xml.text translate(locale, "Invidious Private Feed for `x`", user.email) }
|
||||
xml.element("title") { xml.text I18n.translate(locale, "Invidious Private Feed for `x`", user.email) }
|
||||
|
||||
(notifications + videos).each do |video|
|
||||
video.to_xml(locale, params, xml)
|
||||
|
||||
@@ -51,7 +51,7 @@ module Invidious::Routes::Images
|
||||
end
|
||||
|
||||
# ??? maybe also for storyboards?
|
||||
def self.s_p_image(env)
|
||||
def self.s_p_image(env, authority = "i9")
|
||||
id = env.params.url["id"]
|
||||
name = env.params.url["name"]
|
||||
url = env.request.resource
|
||||
@@ -65,13 +65,23 @@ module Invidious::Routes::Images
|
||||
end
|
||||
|
||||
begin
|
||||
get_ytimg_pool("i9").client &.get(url, headers) do |resp|
|
||||
get_ytimg_pool(authority).client &.get(url, headers) do |resp|
|
||||
return self.proxy_image(env, resp)
|
||||
end
|
||||
rescue ex
|
||||
end
|
||||
end
|
||||
|
||||
# Both pl_c and tvfilm_banner use the same logic used in s_p_image(env)
|
||||
# just with a different authority ("i").
|
||||
def self.pl_c_image(env)
|
||||
self.s_p_image(env, "i")
|
||||
end
|
||||
|
||||
def self.tvfilm_banner_image(env)
|
||||
self.s_p_image(env, "i")
|
||||
end
|
||||
|
||||
def self.yts_image(env)
|
||||
headers = HTTP::Headers.new
|
||||
REQUEST_HEADERS_WHITELIST.each do |header|
|
||||
@@ -96,7 +106,7 @@ module Invidious::Routes::Images
|
||||
break
|
||||
end
|
||||
|
||||
proxy_file(response, env)
|
||||
Helpers.proxy_file(response, env)
|
||||
end
|
||||
rescue ex
|
||||
end
|
||||
@@ -148,6 +158,6 @@ module Invidious::Routes::Images
|
||||
return env.response.headers.delete("Transfer-Encoding")
|
||||
end
|
||||
|
||||
return proxy_file(response, env)
|
||||
return Helpers.proxy_file(response, env)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -112,7 +112,7 @@ module Invidious::Routes::Login
|
||||
user, sid = create_user(sid, email, password)
|
||||
|
||||
if language_header = env.request.headers["Accept-Language"]?
|
||||
if language = ANG.language_negotiator.best(language_header, LOCALES.keys)
|
||||
if language = ANG.language_negotiator.best(language_header, I18n::LOCALES.keys)
|
||||
user.preferences.locale = language.header
|
||||
end
|
||||
end
|
||||
|
||||
@@ -145,6 +145,10 @@ module Invidious::Routes::PreferencesRoute
|
||||
|
||||
default_playlist = env.params.body["default_playlist"]?.try &.as(String)
|
||||
|
||||
search_privacy = env.params.body["search_privacy"]?.try &.as(String)
|
||||
search_privacy ||= "off"
|
||||
search_privacy = search_privacy == "on"
|
||||
|
||||
# Convert to JSON and back again to take advantage of converters used for compatibility
|
||||
preferences = Preferences.from_json({
|
||||
annotations: annotations,
|
||||
@@ -182,6 +186,7 @@ module Invidious::Routes::PreferencesRoute
|
||||
show_nick: show_nick,
|
||||
save_player_pos: save_player_pos,
|
||||
default_playlist: default_playlist,
|
||||
search_privacy: search_privacy,
|
||||
}.to_json)
|
||||
|
||||
if user = env.get? "user"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
module Invidious::Routes
|
||||
private REQUEST_HEADERS_WHITELIST = {
|
||||
"accept",
|
||||
"accept-encoding",
|
||||
"cache-control",
|
||||
"content-length",
|
||||
"if-none-match",
|
||||
"range",
|
||||
}
|
||||
private RESPONSE_HEADERS_BLACKLIST = {
|
||||
"access-control-allow-origin",
|
||||
"alt-svc",
|
||||
"server",
|
||||
"cross-origin-opener-policy-report-only",
|
||||
"report-to",
|
||||
"cross-origin",
|
||||
"timing-allow-origin",
|
||||
"cross-origin-resource-policy",
|
||||
}
|
||||
end
|
||||
@@ -40,9 +40,16 @@ module Invidious::Routes::Search
|
||||
preferences = env.get("preferences").as(Preferences)
|
||||
locale = preferences.locale
|
||||
|
||||
region = env.params.query["region"]? || preferences.region
|
||||
uri_params = URI::Params.new
|
||||
if env.request.method == "GET"
|
||||
uri_params = env.params.query
|
||||
else
|
||||
uri_params = env.params.body
|
||||
end
|
||||
|
||||
query = Invidious::Search::Query.new(env.params.query, :regular, region)
|
||||
region = uri_params["region"]? || preferences.region
|
||||
|
||||
query = Invidious::Search::Query.new(uri_params, :regular, region)
|
||||
|
||||
if query.empty?
|
||||
# Display the full page search box implemented in #1977
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
module Invidious::Routes::VideoPlayback
|
||||
private HTTP_CHUNK_SIZE = 10485760 # ~10MB
|
||||
|
||||
# /videoplayback
|
||||
def self.get_video_playback(env)
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
@@ -81,7 +83,7 @@ module Invidious::Routes::VideoPlayback
|
||||
# Remove the Range header added previously.
|
||||
headers.delete("Range") if range_header.nil?
|
||||
|
||||
playback_statistics = get_playback_statistic()
|
||||
playback_statistics = Helpers.get_playback_statistic
|
||||
playback_statistics["totalRequests"] += 1
|
||||
|
||||
if response.status_code >= 400
|
||||
@@ -193,7 +195,7 @@ module Invidious::Routes::VideoPlayback
|
||||
end
|
||||
end
|
||||
|
||||
proxy_file(resp, env)
|
||||
Helpers.proxy_file(resp, env)
|
||||
end
|
||||
rescue ex
|
||||
if ex.message != "Error reading socket: Connection reset by peer"
|
||||
|
||||
@@ -47,7 +47,7 @@ module Invidious::Routes::Watch
|
||||
end
|
||||
subscriptions ||= [] of String
|
||||
|
||||
params = process_video_params(env.params.query, preferences)
|
||||
params = Invidious::Videos.process_video_params(env.params.query, preferences)
|
||||
env.params.query.delete_all("listen")
|
||||
|
||||
begin
|
||||
@@ -129,17 +129,20 @@ module Invidious::Routes::Watch
|
||||
video_streams = video.video_streams
|
||||
audio_streams = video.audio_streams
|
||||
|
||||
# Older videos may not have audio sources available.
|
||||
# We redirect here so they're not unplayable
|
||||
if audio_streams.empty? && !video.live_now
|
||||
if params.quality == "dash"
|
||||
env.params.query.delete_all("quality")
|
||||
env.params.query["quality"] = "medium"
|
||||
return env.redirect "/watch?#{env.params.query}"
|
||||
elsif params.listen
|
||||
env.params.query.delete_all("listen")
|
||||
env.params.query["listen"] = "0"
|
||||
return env.redirect "/watch?#{env.params.query}"
|
||||
# Videos that are a premiere do not have audio streams.
|
||||
if video.premiere_timestamp.nil?
|
||||
# Older videos may not have audio sources available.
|
||||
# We redirect here so they're not unplayable
|
||||
if audio_streams.empty? && !video.live_now
|
||||
if params.quality == "dash"
|
||||
env.params.query.delete_all("quality")
|
||||
env.params.query["quality"] = "medium"
|
||||
return env.redirect "/watch?#{env.params.query}"
|
||||
elsif params.listen
|
||||
env.params.query.delete_all("listen")
|
||||
env.params.query["listen"] = "0"
|
||||
return env.redirect "/watch?#{env.params.query}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -193,17 +196,6 @@ module Invidious::Routes::Watch
|
||||
|
||||
if CONFIG.invidious_companion.present?
|
||||
invidious_companion = CONFIG.invidious_companion.sample
|
||||
invidious_companion_urls = CONFIG.invidious_companion.reject(&.builtin_proxy).map do |companion|
|
||||
uri =
|
||||
"#{companion.public_url.scheme}://#{companion.public_url.host}#{companion.public_url.port ? ":#{companion.public_url.port}" : ""}"
|
||||
end.join(" ")
|
||||
|
||||
if !invidious_companion_urls.empty?
|
||||
env.response.headers["Content-Security-Policy"] =
|
||||
env.response.headers["Content-Security-Policy"]
|
||||
.gsub("media-src", "media-src #{invidious_companion_urls}")
|
||||
.gsub("connect-src", "connect-src #{invidious_companion_urls}")
|
||||
end
|
||||
end
|
||||
|
||||
templated "watch"
|
||||
@@ -284,7 +276,7 @@ module Invidious::Routes::Watch
|
||||
|
||||
if video_id = response.dig?("endpoint", "watchEndpoint", "videoId")
|
||||
if params = response.dig?("endpoint", "watchEndpoint", "params").try &.as_s
|
||||
start_time, end_time, _ = parse_clip_parameters(params)
|
||||
start_time, end_time, _ = Invidious::Videos::Clip.parse_clip_parameters(params)
|
||||
env.params.query["start"] = start_time.to_s if start_time != nil
|
||||
env.params.query["end"] = end_time.to_s if end_time != nil
|
||||
end
|
||||
|
||||
@@ -185,6 +185,7 @@ module Invidious::Routing
|
||||
get "/opensearch.xml", Routes::Search, :opensearch
|
||||
get "/results", Routes::Search, :results
|
||||
get "/search", Routes::Search, :search
|
||||
post "/search", Routes::Search, :search
|
||||
get "/hashtag/:hashtag", Routes::Search, :hashtag
|
||||
end
|
||||
|
||||
@@ -222,6 +223,8 @@ module Invidious::Routing
|
||||
get "/s_p/:id/:name", Routes::Images, :s_p_image
|
||||
get "/yts/img/:name", Routes::Images, :yts_image
|
||||
get "/vi/:id/:name", Routes::Images, :thumbnails
|
||||
get "/pl_c/:id/:name", Routes::Images, :pl_c_image
|
||||
get "/tvfilm_banner/:id/:name", Routes::Images, :tvfilm_banner_image
|
||||
end
|
||||
|
||||
def register_companion_routes
|
||||
|
||||
@@ -57,8 +57,6 @@ module Invidious::Search
|
||||
# Values correspond to { "1:varint": <X> }
|
||||
enum Sort
|
||||
Relevance = 0
|
||||
Rating = 1
|
||||
Date = 2
|
||||
Views = 3
|
||||
end
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ module Invidious::Search
|
||||
if response.status_code == 404
|
||||
response = YT_POOL.client &.get("/user/#{query.channel}")
|
||||
response = YT_POOL.client &.get("/c/#{query.channel}") if response.status_code == 404
|
||||
initial_data = extract_initial_data(response.body)
|
||||
initial_data = Helpers.extract_initial_data(response.body)
|
||||
ucid = initial_data.dig?("header", "c4TabbedHeaderRenderer", "channelId").try(&.as_s?)
|
||||
raise ChannelSearchException.new(query.channel) if !ucid
|
||||
else
|
||||
|
||||
@@ -15,7 +15,7 @@ struct Invidious::User
|
||||
playlists.each do |playlist|
|
||||
json.object do
|
||||
json.field "title", playlist.title
|
||||
json.field "description", html_to_content(playlist.description_html)
|
||||
json.field "description", Helpers.html_to_content(playlist.description_html)
|
||||
json.field "privacy", playlist.privacy.to_s
|
||||
json.field "videos" do
|
||||
json.array do
|
||||
|
||||
@@ -30,28 +30,24 @@ struct Invidious::User
|
||||
return subscriptions
|
||||
end
|
||||
|
||||
def parse_playlist_export_csv(user : User, raw_input : String)
|
||||
# Parse a CSV Google Takeout - Youtube Playlist file
|
||||
def parse_playlist_export_csv(user : User, playlist_name : String, raw_input : String)
|
||||
# Split the input into head and body content
|
||||
raw_head, raw_body = raw_input.strip('\n').split("\n\n", limit: 2, remove_empty: true)
|
||||
raw_head, raw_body = raw_input.split("\n", limit: 2, remove_empty: true)
|
||||
|
||||
# Create the playlist from the head content
|
||||
csv_head = CSV.new(raw_head.strip('\n'), headers: true)
|
||||
csv_head.next
|
||||
title = csv_head[4]
|
||||
description = csv_head[5]
|
||||
visibility = csv_head[6]
|
||||
title = playlist_name
|
||||
|
||||
if visibility.compare("Public", case_insensitive: true) == 0
|
||||
privacy = PlaylistPrivacy::Public
|
||||
else
|
||||
privacy = PlaylistPrivacy::Private
|
||||
end
|
||||
description = "This is the default description of an imported playlist. Feel Free to change it as you see fit."
|
||||
privacy = PlaylistPrivacy::Private
|
||||
|
||||
playlist = create_playlist(title, privacy, user)
|
||||
Invidious::Database::Playlists.update_description(playlist.id, description)
|
||||
|
||||
# Add each video to the playlist from the body content
|
||||
csv_body = CSV.new(raw_body.strip('\n'), headers: true)
|
||||
csv_body = CSV.new(raw_body.strip('\n'), headers: false)
|
||||
csv_body.each do |row|
|
||||
video_id = row[0]
|
||||
if playlist
|
||||
@@ -204,10 +200,12 @@ struct Invidious::User
|
||||
end
|
||||
|
||||
def from_youtube_pl(user : User, body : String, filename : String, type : String) : Bool
|
||||
extension = filename.split(".").last
|
||||
filename_array = filename.split(".")
|
||||
playlist_name = filename_array.first
|
||||
extension = filename_array.last
|
||||
|
||||
if extension == "csv" || type == "text/csv"
|
||||
playlist = parse_playlist_export_csv(user, body)
|
||||
playlist = parse_playlist_export_csv(user, playlist_name, body)
|
||||
if playlist
|
||||
return true
|
||||
else
|
||||
|
||||
@@ -57,6 +57,7 @@ struct Preferences
|
||||
property volume : Int32 = CONFIG.default_user_preferences.volume
|
||||
property save_player_pos : Bool = CONFIG.default_user_preferences.save_player_pos
|
||||
property default_playlist : String? = nil
|
||||
property search_privacy : Bool = CONFIG.default_user_preferences.search_privacy
|
||||
|
||||
module BoolToString
|
||||
def self.to_json(value : String, json : JSON::Builder)
|
||||
|
||||
@@ -81,9 +81,10 @@ struct Video
|
||||
end
|
||||
|
||||
def premiere_timestamp : Time?
|
||||
info
|
||||
.dig?("microformat", "playerMicroformatRenderer", "liveBroadcastDetails", "startTimestamp")
|
||||
.try { |t| Time.parse_rfc3339(t.as_s) }
|
||||
if self.video_type == VideoType::Scheduled
|
||||
return info["published"]?
|
||||
.try { |t| Time.parse_rfc3339(t.as_s) }
|
||||
end
|
||||
end
|
||||
|
||||
def related_videos
|
||||
@@ -324,7 +325,7 @@ rescue DB::Error
|
||||
end
|
||||
|
||||
def fetch_video(id, region)
|
||||
info = extract_video_info(video_id: id)
|
||||
info = Invidious::Videos::Parser.extract_video_info(video_id: id)
|
||||
|
||||
if info.nil?
|
||||
raise InfoException.new("Invidious companion is not available. \
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
require "json"
|
||||
|
||||
# returns start_time, end_time and clip_title
|
||||
def parse_clip_parameters(params) : {Float64?, Float64?, String?}
|
||||
decoded_protobuf = params.try { |i| URI.decode_www_form(i) }
|
||||
.try { |i| Base64.decode(i) }
|
||||
.try { |i| IO::Memory.new(i) }
|
||||
.try { |i| Protodec::Any.parse(i) }
|
||||
module Invidious::Videos::Clip
|
||||
extend self
|
||||
|
||||
start_time = decoded_protobuf
|
||||
.try(&.["50:0:embedded"]["2:1:varint"].as_i64)
|
||||
.try { |i| i/1000 }
|
||||
# returns start_time, end_time and clip_title
|
||||
def parse_clip_parameters(params) : {Float64?, Float64?, String?}
|
||||
decoded_protobuf = params.try { |i| URI.decode_www_form(i) }
|
||||
.try { |i| Base64.decode(i) }
|
||||
.try { |i| IO::Memory.new(i) }
|
||||
.try { |i| Protodec::Any.parse(i) }
|
||||
|
||||
end_time = decoded_protobuf
|
||||
.try(&.["50:0:embedded"]["3:2:varint"].as_i64)
|
||||
.try { |i| i/1000 }
|
||||
start_time = decoded_protobuf
|
||||
.try(&.["50:0:embedded"]["2:1:varint"].as_i64)
|
||||
.try { |i| i/1000 }
|
||||
|
||||
clip_title = decoded_protobuf
|
||||
.try(&.["50:0:embedded"]["4:3:string"].as_s)
|
||||
end_time = decoded_protobuf
|
||||
.try(&.["50:0:embedded"]["3:2:varint"].as_i64)
|
||||
.try { |i| i/1000 }
|
||||
|
||||
return start_time, end_time, clip_title
|
||||
clip_title = decoded_protobuf
|
||||
.try(&.["50:0:embedded"]["4:3:string"].as_s)
|
||||
|
||||
return start_time, end_time, clip_title
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,8 +21,6 @@ private def copy_string(str : String::Builder, iter : Iterator, count : Int) : I
|
||||
str << cp.chr
|
||||
end
|
||||
|
||||
# A codepoint from the SMP counts twice
|
||||
copied += 1 if cp > 0xFFFF
|
||||
copied += 1
|
||||
end
|
||||
|
||||
@@ -44,10 +42,6 @@ def parse_description(desc, video_id : String) : String?
|
||||
end
|
||||
end
|
||||
|
||||
# Not everything is stored in UTF-8 on youtube's side. The SMP codepoints
|
||||
# (0x10000 and above) are encoded as UTF-16 surrogate pairs, which are
|
||||
# automatically decoded by the JSON parser. It means that we need to count
|
||||
# copied byte in a special manner, preventing the use of regular string copy.
|
||||
iter = content.each_codepoint
|
||||
|
||||
index = 0
|
||||
|
||||
+412
-408
@@ -1,465 +1,469 @@
|
||||
require "json"
|
||||
|
||||
# Use to parse both "compactVideoRenderer" and "endScreenVideoRenderer".
|
||||
# The former is preferred as it has more videos in it. The second has
|
||||
# the same 11 first entries as the compact rendered.
|
||||
#
|
||||
# TODO: "compactRadioRenderer" (Mix) and
|
||||
# TODO: Use a proper struct/class instead of a hacky JSON object
|
||||
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||
return nil if !related["videoId"]?
|
||||
module Invidious::Videos::Parser
|
||||
extend self
|
||||
|
||||
# The compact renderer has video length in seconds, where the end
|
||||
# screen rendered has a full text version ("42:40")
|
||||
length = related["lengthInSeconds"]?.try &.as_i.to_s
|
||||
length ||= related.dig?("lengthText", "simpleText").try do |box|
|
||||
decode_length_seconds(box.as_s).to_s
|
||||
# Use to parse both "compactVideoRenderer" and "endScreenVideoRenderer".
|
||||
# The former is preferred as it has more videos in it. The second has
|
||||
# the same 11 first entries as the compact rendered.
|
||||
#
|
||||
# TODO: "compactRadioRenderer" (Mix) and
|
||||
# TODO: Use a proper struct/class instead of a hacky JSON object
|
||||
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||
return nil if !related["videoId"]?
|
||||
|
||||
# The compact renderer has video length in seconds, where the end
|
||||
# screen rendered has a full text version ("42:40")
|
||||
length = related["lengthInSeconds"]?.try &.as_i.to_s
|
||||
length ||= related.dig?("lengthText", "simpleText").try do |box|
|
||||
decode_length_seconds(box.as_s).to_s
|
||||
end
|
||||
|
||||
# Both have "short", so the "long" option shouldn't be required
|
||||
channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
|
||||
.try &.dig?("runs", 0)
|
||||
|
||||
author = channel_info.try &.dig?("text")
|
||||
author_verified = has_verified_badge?(related["ownerBadges"]?).to_s
|
||||
|
||||
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
|
||||
|
||||
short_view_count = related.try do |r|
|
||||
HelperExtractors.get_short_view_count(r).to_s
|
||||
end
|
||||
|
||||
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 {
|
||||
"id" => related["videoId"],
|
||||
"title" => related["title"]["simpleText"],
|
||||
"author" => author || JSON::Any.new(""),
|
||||
"ucid" => JSON::Any.new(ucid || ""),
|
||||
"length_seconds" => JSON::Any.new(length || "0"),
|
||||
"short_view_count" => JSON::Any.new(short_view_count || "0"),
|
||||
"author_verified" => JSON::Any.new(author_verified),
|
||||
"published" => JSON::Any.new(published || ""),
|
||||
}
|
||||
end
|
||||
|
||||
# Both have "short", so the "long" option shouldn't be required
|
||||
channel_info = (related["shortBylineText"]? || related["longBylineText"]?)
|
||||
.try &.dig?("runs", 0)
|
||||
def extract_video_info(video_id : String)
|
||||
# Fetch data from the player endpoint
|
||||
player_response = YoutubeAPI.player(video_id: video_id)
|
||||
|
||||
author = channel_info.try &.dig?("text")
|
||||
author_verified = has_verified_badge?(related["ownerBadges"]?).to_s
|
||||
if player_response.nil?
|
||||
return nil
|
||||
end
|
||||
|
||||
ucid = channel_info.try { |ci| HelperExtractors.get_browse_id(ci) }
|
||||
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
||||
|
||||
short_view_count = related.try do |r|
|
||||
HelperExtractors.get_short_view_count(r).to_s
|
||||
end
|
||||
if playability_status != "OK"
|
||||
subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason")
|
||||
reason = subreason.try &.[]?("simpleText").try &.as_s
|
||||
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
|
||||
reason ||= player_response.dig("playabilityStatus", "reason").as_s
|
||||
|
||||
LOGGER.trace("parse_related_video: Found \"watchNextEndScreenRenderer\" container")
|
||||
# Stop here if video is not a scheduled livestream or
|
||||
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help
|
||||
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) ||
|
||||
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
|
||||
return {
|
||||
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
||||
"reason" => JSON::Any.new(reason),
|
||||
}
|
||||
end
|
||||
elsif video_id != player_response.dig?("videoDetails", "videoId")
|
||||
# YouTube may return a different video player response than expected.
|
||||
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
|
||||
# Line to be reverted if one day we solve the video not available issue.
|
||||
|
||||
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
|
||||
# Although technically not a call to /videoplayback the fact that YouTube is returning the
|
||||
# wrong video means that we should count it as a failure.
|
||||
Helpers.get_playback_statistic["totalRequests"] += 1
|
||||
|
||||
# TODO: when refactoring video types, make a struct for related videos
|
||||
# or reuse an existing type, if that fits.
|
||||
return {
|
||||
"id" => related["videoId"],
|
||||
"title" => related["title"]["simpleText"],
|
||||
"author" => author || JSON::Any.new(""),
|
||||
"ucid" => JSON::Any.new(ucid || ""),
|
||||
"length_seconds" => JSON::Any.new(length || "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)
|
||||
# Fetch data from the player endpoint
|
||||
player_response = YoutubeAPI.player(video_id: video_id)
|
||||
|
||||
if player_response.nil?
|
||||
return nil
|
||||
end
|
||||
|
||||
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
||||
|
||||
if playability_status != "OK"
|
||||
subreason = player_response.dig?("playabilityStatus", "errorScreen", "playerErrorMessageRenderer", "subreason")
|
||||
reason = subreason.try &.[]?("simpleText").try &.as_s
|
||||
reason ||= subreason.try &.[]("runs").as_a.map(&.[]("text")).join("")
|
||||
reason ||= player_response.dig("playabilityStatus", "reason").as_s
|
||||
|
||||
# Stop here if video is not a scheduled livestream or
|
||||
# for LOGIN_REQUIRED when videoDetails element is not found because retrying won't help
|
||||
if !{"LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status) ||
|
||||
playability_status == "LOGIN_REQUIRED" && !player_response.dig?("videoDetails")
|
||||
return {
|
||||
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
||||
"reason" => JSON::Any.new(reason),
|
||||
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
|
||||
}
|
||||
else
|
||||
reason = nil
|
||||
end
|
||||
elsif video_id != player_response.dig?("videoDetails", "videoId")
|
||||
# YouTube may return a different video player response than expected.
|
||||
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
|
||||
# Line to be reverted if one day we solve the video not available issue.
|
||||
|
||||
# Although technically not a call to /videoplayback the fact that YouTube is returning the
|
||||
# wrong video means that we should count it as a failure.
|
||||
get_playback_statistic()["totalRequests"] += 1
|
||||
# Don't fetch the next endpoint if the video is unavailable.
|
||||
if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
|
||||
next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
|
||||
# Remove the microformat returned by the /next endpoint on some videos
|
||||
# to prevent player_response microformat from being overwritten.
|
||||
next_response.delete("microformat")
|
||||
player_response = player_response.merge(next_response)
|
||||
end
|
||||
|
||||
return {
|
||||
"version" => JSON::Any.new(Video::SCHEMA_VERSION.to_i64),
|
||||
"reason" => JSON::Any.new("Can't load the video on this Invidious instance. YouTube is currently trying to block Invidious instances. <a href=\"https://github.com/iv-org/invidious/issues/3822\">Click here for more info about the issue.</a>"),
|
||||
}
|
||||
else
|
||||
reason = nil
|
||||
end
|
||||
params = self.parse_video_info(video_id, player_response)
|
||||
params["reason"] = JSON::Any.new(reason) if reason
|
||||
|
||||
# Don't fetch the next endpoint if the video is unavailable.
|
||||
if {"OK", "LIVE_STREAM_OFFLINE", "LOGIN_REQUIRED"}.any?(playability_status)
|
||||
next_response = YoutubeAPI.next({"videoId": video_id, "params": ""})
|
||||
# Remove the microformat returned by the /next endpoint on some videos
|
||||
# to prevent player_response microformat from being overwritten.
|
||||
next_response.delete("microformat")
|
||||
player_response = player_response.merge(next_response)
|
||||
end
|
||||
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
|
||||
params[f] = player_response[f] if player_response[f]?
|
||||
end
|
||||
|
||||
params = parse_video_info(video_id, player_response)
|
||||
params["reason"] = JSON::Any.new(reason) if reason
|
||||
|
||||
{"captions", "playabilityStatus", "playerConfig", "storyboards"}.each do |f|
|
||||
params[f] = player_response[f] if player_response[f]?
|
||||
end
|
||||
|
||||
# Convert URLs, if those are present
|
||||
if streaming_data = player_response["streamingData"]?
|
||||
%w[formats adaptiveFormats].each do |key|
|
||||
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
||||
format = format.as_h
|
||||
if format["url"]?.nil?
|
||||
format["url"] = format["signatureCipher"]
|
||||
# Convert URLs, if those are present
|
||||
if streaming_data = player_response["streamingData"]?
|
||||
%w[formats adaptiveFormats].each do |key|
|
||||
streaming_data.as_h[key]?.try &.as_a.each do |format|
|
||||
format = format.as_h
|
||||
if format["url"]?.nil?
|
||||
format["url"] = format["signatureCipher"]
|
||||
end
|
||||
format["url"] = JSON::Any.new(convert_url(format))
|
||||
end
|
||||
format["url"] = JSON::Any.new(convert_url(format))
|
||||
end
|
||||
|
||||
params["streamingData"] = streaming_data
|
||||
end
|
||||
|
||||
params["streamingData"] = streaming_data
|
||||
# Data structure version, for cache control
|
||||
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
|
||||
|
||||
return params
|
||||
end
|
||||
|
||||
# Data structure version, for cache control
|
||||
params["version"] = JSON::Any.new(Video::SCHEMA_VERSION.to_i64)
|
||||
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
|
||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
||||
response = YoutubeAPI.player(video_id: id)
|
||||
|
||||
return params
|
||||
end
|
||||
playability_status = response["playabilityStatus"]["status"]
|
||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
||||
|
||||
def try_fetch_streaming_data(id : String, client_config : YoutubeAPI::ClientConfig) : Hash(String, JSON::Any)?
|
||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Using #{client_config.client_type} client.")
|
||||
response = YoutubeAPI.player(video_id: id)
|
||||
|
||||
playability_status = response["playabilityStatus"]["status"]
|
||||
LOGGER.debug("try_fetch_streaming_data: [#{id}] Got playabilityStatus == #{playability_status}.")
|
||||
|
||||
if id != response.dig?("videoDetails", "videoId")
|
||||
# YouTube may return a different video player response than expected.
|
||||
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
|
||||
raise InfoException.new(
|
||||
"The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)"
|
||||
)
|
||||
elsif playability_status == "OK"
|
||||
return response
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any)) : Hash(String, JSON::Any)
|
||||
# Top level elements
|
||||
|
||||
main_results = player_response.dig?("contents", "twoColumnWatchNextResults")
|
||||
|
||||
raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results
|
||||
|
||||
# Primary results are not available on Music videos
|
||||
# See: https://github.com/iv-org/invidious/pull/3238#issuecomment-1207193725
|
||||
if primary_results = main_results.dig?("results", "results", "contents")
|
||||
video_primary_renderer = primary_results
|
||||
.as_a.find(&.["videoPrimaryInfoRenderer"]?)
|
||||
.try &.["videoPrimaryInfoRenderer"]
|
||||
|
||||
video_secondary_renderer = primary_results
|
||||
.as_a.find(&.["videoSecondaryInfoRenderer"]?)
|
||||
.try &.["videoSecondaryInfoRenderer"]
|
||||
|
||||
raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
|
||||
raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
|
||||
end
|
||||
|
||||
video_details = player_response.dig?("videoDetails")
|
||||
if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
|
||||
microformat = {} of String => JSON::Any
|
||||
end
|
||||
|
||||
raise BrokenTubeException.new("videoDetails") if !video_details
|
||||
|
||||
# Basic video infos
|
||||
|
||||
title = video_details["title"]?.try &.as_s
|
||||
|
||||
# We have to try to extract viewCount from videoPrimaryInfoRenderer first,
|
||||
# then from videoDetails, as the latter is "0" for livestreams (we want
|
||||
# to get the amount of viewers watching).
|
||||
views_txt = extract_text(
|
||||
video_primary_renderer
|
||||
.try &.dig?("viewCount", "videoViewCountRenderer", "viewCount")
|
||||
)
|
||||
views_txt ||= video_details["viewCount"]?.try &.as_s || ""
|
||||
views = views_txt.gsub(/\D/, "").to_i64?
|
||||
|
||||
length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"])
|
||||
.try &.as_s.to_i64
|
||||
|
||||
published = microformat["publishDate"]?
|
||||
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
|
||||
|
||||
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
|
||||
live_now ||= video_details.dig?("isLive").try &.as_bool || false
|
||||
|
||||
post_live_dvr = video_details.dig?("isPostLiveDvr")
|
||||
.try &.as_bool || false
|
||||
|
||||
# Extra video infos
|
||||
|
||||
allowed_regions = microformat["availableCountries"]?
|
||||
.try &.as_a.map &.as_s || [] of String
|
||||
|
||||
allow_ratings = video_details["allowRatings"]?.try &.as_bool
|
||||
family_friendly = microformat["isFamilySafe"]?.try &.as_bool
|
||||
is_listed = video_details["isCrawlable"]?.try &.as_bool
|
||||
is_upcoming = video_details["isUpcoming"]?.try &.as_bool
|
||||
|
||||
keywords = video_details["keywords"]?
|
||||
.try &.as_a.map &.as_s || [] of String
|
||||
|
||||
# Related videos
|
||||
|
||||
LOGGER.debug("extract_video_info: parsing related videos...")
|
||||
|
||||
related = [] of JSON::Any
|
||||
|
||||
# Parse "compactVideoRenderer" items (under secondary results)
|
||||
secondary_results = main_results
|
||||
.dig?("secondaryResults", "secondaryResults", "results")
|
||||
secondary_results.try &.as_a.each do |element|
|
||||
if item = element["compactVideoRenderer"]?
|
||||
related_video = parse_related_video(item)
|
||||
related << JSON::Any.new(related_video) if related_video
|
||||
if id != response.dig?("videoDetails", "videoId")
|
||||
# YouTube may return a different video player response than expected.
|
||||
# See: https://github.com/TeamNewPipe/NewPipe/issues/8713
|
||||
raise InfoException.new(
|
||||
"The video returned by YouTube isn't the requested one. (#{client_config.client_type} client)"
|
||||
)
|
||||
elsif playability_status == "OK"
|
||||
return response
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
# If nothing was found previously, fall back to end screen renderer
|
||||
if related.empty?
|
||||
# Container for "endScreenVideoRenderer" items
|
||||
player_overlays = player_response.dig?(
|
||||
"playerOverlays", "playerOverlayRenderer",
|
||||
"endScreen", "watchNextEndScreenRenderer", "results"
|
||||
)
|
||||
def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any)) : Hash(String, JSON::Any)
|
||||
# Top level elements
|
||||
|
||||
player_overlays.try &.as_a.each do |element|
|
||||
if item = element["endScreenVideoRenderer"]?
|
||||
related_video = parse_related_video(item)
|
||||
main_results = player_response.dig?("contents", "twoColumnWatchNextResults")
|
||||
|
||||
raise BrokenTubeException.new("twoColumnWatchNextResults") if !main_results
|
||||
|
||||
# Primary results are not available on Music videos
|
||||
# See: https://github.com/iv-org/invidious/pull/3238#issuecomment-1207193725
|
||||
if primary_results = main_results.dig?("results", "results", "contents")
|
||||
video_primary_renderer = primary_results
|
||||
.as_a.find(&.["videoPrimaryInfoRenderer"]?)
|
||||
.try &.["videoPrimaryInfoRenderer"]
|
||||
|
||||
video_secondary_renderer = primary_results
|
||||
.as_a.find(&.["videoSecondaryInfoRenderer"]?)
|
||||
.try &.["videoSecondaryInfoRenderer"]
|
||||
|
||||
raise BrokenTubeException.new("videoPrimaryInfoRenderer") if !video_primary_renderer
|
||||
raise BrokenTubeException.new("videoSecondaryInfoRenderer") if !video_secondary_renderer
|
||||
end
|
||||
|
||||
video_details = player_response.dig?("videoDetails")
|
||||
if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
|
||||
microformat = {} of String => JSON::Any
|
||||
end
|
||||
|
||||
raise BrokenTubeException.new("videoDetails") if !video_details
|
||||
|
||||
# Basic video infos
|
||||
|
||||
title = video_details["title"]?.try &.as_s
|
||||
|
||||
# We have to try to extract viewCount from videoPrimaryInfoRenderer first,
|
||||
# then from videoDetails, as the latter is "0" for livestreams (we want
|
||||
# to get the amount of viewers watching).
|
||||
views_txt = extract_text(
|
||||
video_primary_renderer
|
||||
.try &.dig?("viewCount", "videoViewCountRenderer", "viewCount")
|
||||
)
|
||||
views_txt ||= video_details["viewCount"]?.try &.as_s || ""
|
||||
views = views_txt.gsub(/\D/, "").to_i64?
|
||||
|
||||
length_txt = (microformat["lengthSeconds"]? || video_details["lengthSeconds"])
|
||||
.try &.as_s.to_i64
|
||||
|
||||
published = microformat["publishDate"]?
|
||||
.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
|
||||
|
||||
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
|
||||
live_now ||= video_details.dig?("isLive").try &.as_bool || false
|
||||
|
||||
post_live_dvr = video_details.dig?("isPostLiveDvr")
|
||||
.try &.as_bool || false
|
||||
|
||||
# Extra video infos
|
||||
|
||||
allowed_regions = microformat["availableCountries"]?
|
||||
.try &.as_a.map &.as_s || [] of String
|
||||
|
||||
allow_ratings = video_details["allowRatings"]?.try &.as_bool
|
||||
family_friendly = microformat["isFamilySafe"]?.try &.as_bool
|
||||
is_listed = video_details["isCrawlable"]?.try &.as_bool
|
||||
is_upcoming = video_details["isUpcoming"]?.try &.as_bool
|
||||
|
||||
keywords = video_details["keywords"]?
|
||||
.try &.as_a.map &.as_s || [] of String
|
||||
|
||||
# Related videos
|
||||
|
||||
LOGGER.debug("extract_video_info: parsing related videos...")
|
||||
|
||||
related = [] of JSON::Any
|
||||
|
||||
# Parse "compactVideoRenderer" items (under secondary results)
|
||||
secondary_results = main_results
|
||||
.dig?("secondaryResults", "secondaryResults", "results")
|
||||
secondary_results.try &.as_a.each do |element|
|
||||
if item = element["compactVideoRenderer"]?
|
||||
related_video = self.parse_related_video(item)
|
||||
related << JSON::Any.new(related_video) if related_video
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Likes
|
||||
|
||||
toplevel_buttons = video_primary_renderer
|
||||
.try &.dig?("videoActions", "menuRenderer", "topLevelButtons")
|
||||
|
||||
if toplevel_buttons
|
||||
# New Format as of december 2023
|
||||
likes_button = toplevel_buttons.dig?(0,
|
||||
"segmentedLikeDislikeButtonViewModel",
|
||||
"likeButtonViewModel",
|
||||
"likeButtonViewModel",
|
||||
"toggleButtonViewModel",
|
||||
"toggleButtonViewModel",
|
||||
"defaultButtonViewModel",
|
||||
"buttonViewModel"
|
||||
)
|
||||
|
||||
likes_button ||= toplevel_buttons.try &.as_a
|
||||
.find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE")
|
||||
.try &.["toggleButtonRenderer"]
|
||||
|
||||
# New format as of september 2022
|
||||
likes_button ||= toplevel_buttons.try &.as_a
|
||||
.find(&.["segmentedLikeDislikeButtonRenderer"]?)
|
||||
.try &.dig?(
|
||||
"segmentedLikeDislikeButtonRenderer",
|
||||
"likeButton", "toggleButtonRenderer"
|
||||
# If nothing was found previously, fall back to end screen renderer
|
||||
if related.empty?
|
||||
# Container for "endScreenVideoRenderer" items
|
||||
player_overlays = player_response.dig?(
|
||||
"playerOverlays", "playerOverlayRenderer",
|
||||
"endScreen", "watchNextEndScreenRenderer", "results"
|
||||
)
|
||||
|
||||
if likes_button
|
||||
likes_txt = likes_button.dig?("accessibilityText")
|
||||
# Note: The like count from `toggledText` is off by one, as it would
|
||||
# represent the new like count in the event where the user clicks on "like".
|
||||
likes_txt ||= (likes_button["defaultText"]? || likes_button["toggledText"]?)
|
||||
.try &.dig?("accessibility", "accessibilityData", "label")
|
||||
likes = likes_txt.as_s.gsub(/\D/, "").to_i64? if likes_txt
|
||||
|
||||
LOGGER.trace("extract_video_info: Found \"likes\" button. Button text is \"#{likes_txt}\"")
|
||||
LOGGER.debug("extract_video_info: Likes count is #{likes}") if likes
|
||||
end
|
||||
end
|
||||
|
||||
# Description
|
||||
|
||||
description = microformat.dig?("description", "simpleText").try &.as_s || ""
|
||||
short_description = player_response.dig?("videoDetails", "shortDescription")
|
||||
|
||||
# description_html = video_secondary_renderer.try &.dig?("description", "runs")
|
||||
# .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
|
||||
|
||||
description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id)
|
||||
|
||||
# Video metadata
|
||||
|
||||
metadata = video_secondary_renderer
|
||||
.try &.dig?("metadataRowContainer", "metadataRowContainerRenderer", "rows")
|
||||
.try &.as_a
|
||||
|
||||
genre = microformat["category"]?
|
||||
genre_ucid = nil
|
||||
license = nil
|
||||
|
||||
metadata.try &.each do |row|
|
||||
metadata_title = extract_text(row.dig?("metadataRowRenderer", "title"))
|
||||
contents = row.dig?("metadataRowRenderer", "contents", 0)
|
||||
|
||||
if metadata_title == "Category"
|
||||
contents = contents.try &.dig?("runs", 0)
|
||||
|
||||
genre = contents.try &.["text"]?
|
||||
genre_ucid = contents.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId")
|
||||
elsif metadata_title == "License"
|
||||
license = contents.try &.dig?("runs", 0, "text")
|
||||
elsif metadata_title == "Licensed to YouTube by"
|
||||
license = contents.try &.["simpleText"]?
|
||||
end
|
||||
end
|
||||
|
||||
# Music section
|
||||
|
||||
music_list = [] of VideoMusic
|
||||
music_desclist = player_response.dig?(
|
||||
"engagementPanels", 1, "engagementPanelSectionListRenderer",
|
||||
"content", "structuredDescriptionContentRenderer", "items", 2,
|
||||
"videoDescriptionMusicSectionRenderer", "carouselLockups"
|
||||
)
|
||||
|
||||
music_desclist.try &.as_a.each do |music_desc|
|
||||
artist = nil
|
||||
album = nil
|
||||
music_license = nil
|
||||
|
||||
# Used when the video has multiple songs
|
||||
if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title")
|
||||
# "simpleText" for plain text / "runs" when song has a link
|
||||
song = song_title["simpleText"]? || song_title.dig?("runs", 0, "text")
|
||||
|
||||
# some videos can have empty tracks. See: https://www.youtube.com/watch?v=eBGIQ7ZuuiU
|
||||
next if !song
|
||||
end
|
||||
|
||||
music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc|
|
||||
desc_title = extract_text(desc.dig?("infoRowRenderer", "title"))
|
||||
if desc_title == "ARTIST"
|
||||
artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
|
||||
elsif desc_title == "SONG"
|
||||
song = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
|
||||
elsif desc_title == "ALBUM"
|
||||
album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
|
||||
elsif desc_title == "LICENSES"
|
||||
music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata"))
|
||||
player_overlays.try &.as_a.each do |element|
|
||||
if item = element["endScreenVideoRenderer"]?
|
||||
related_video = self.parse_related_video(item)
|
||||
related << JSON::Any.new(related_video) if related_video
|
||||
end
|
||||
end
|
||||
end
|
||||
music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s)
|
||||
end
|
||||
|
||||
# Author infos
|
||||
# Likes
|
||||
|
||||
author = video_details["author"]?.try &.as_s
|
||||
ucid = video_details["channelId"]?.try &.as_s
|
||||
toplevel_buttons = video_primary_renderer
|
||||
.try &.dig?("videoActions", "menuRenderer", "topLevelButtons")
|
||||
|
||||
if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
|
||||
author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url")
|
||||
author_verified = has_verified_badge?(author_info["badges"]?)
|
||||
if toplevel_buttons
|
||||
# New Format as of december 2023
|
||||
likes_button = toplevel_buttons.dig?(0,
|
||||
"segmentedLikeDislikeButtonViewModel",
|
||||
"likeButtonViewModel",
|
||||
"likeButtonViewModel",
|
||||
"toggleButtonViewModel",
|
||||
"toggleButtonViewModel",
|
||||
"defaultButtonViewModel",
|
||||
"buttonViewModel"
|
||||
)
|
||||
|
||||
subs_text = author_info["subscriberCountText"]?
|
||||
.try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
|
||||
.try &.as_s.split(" ", 2)[0]
|
||||
end
|
||||
likes_button ||= toplevel_buttons.try &.as_a
|
||||
.find(&.dig?("toggleButtonRenderer", "defaultIcon", "iconType").=== "LIKE")
|
||||
.try &.["toggleButtonRenderer"]
|
||||
|
||||
# Return data
|
||||
# New format as of september 2022
|
||||
likes_button ||= toplevel_buttons.try &.as_a
|
||||
.find(&.["segmentedLikeDislikeButtonRenderer"]?)
|
||||
.try &.dig?(
|
||||
"segmentedLikeDislikeButtonRenderer",
|
||||
"likeButton", "toggleButtonRenderer"
|
||||
)
|
||||
|
||||
if live_now
|
||||
video_type = VideoType::Livestream
|
||||
elsif !premiere_timestamp.nil?
|
||||
video_type = VideoType::Scheduled
|
||||
published = premiere_timestamp || Time.utc
|
||||
else
|
||||
video_type = VideoType::Video
|
||||
end
|
||||
if likes_button
|
||||
likes_txt = likes_button.dig?("accessibilityText")
|
||||
# Note: The like count from `toggledText` is off by one, as it would
|
||||
# represent the new like count in the event where the user clicks on "like".
|
||||
likes_txt ||= (likes_button["defaultText"]? || likes_button["toggledText"]?)
|
||||
.try &.dig?("accessibility", "accessibilityData", "label")
|
||||
likes = likes_txt.as_s.gsub(/\D/, "").to_i64? if likes_txt
|
||||
|
||||
LOGGER.trace("extract_video_info: Found \"likes\" button. Button text is \"#{likes_txt}\"")
|
||||
LOGGER.debug("extract_video_info: Likes count is #{likes}") if likes
|
||||
end
|
||||
end
|
||||
|
||||
params = {
|
||||
"videoType" => JSON::Any.new(video_type.to_s),
|
||||
# Basic video infos
|
||||
"title" => JSON::Any.new(title || ""),
|
||||
"views" => JSON::Any.new(views || 0_i64),
|
||||
"likes" => JSON::Any.new(likes || 0_i64),
|
||||
"lengthSeconds" => JSON::Any.new(length_txt || 0_i64),
|
||||
"published" => JSON::Any.new(published.to_rfc3339),
|
||||
# Extra video infos
|
||||
"allowedRegions" => JSON::Any.new(allowed_regions.map { |v| JSON::Any.new(v) }),
|
||||
"allowRatings" => JSON::Any.new(allow_ratings || false),
|
||||
"isFamilyFriendly" => JSON::Any.new(family_friendly || false),
|
||||
"isListed" => JSON::Any.new(is_listed || false),
|
||||
"isUpcoming" => JSON::Any.new(is_upcoming || false),
|
||||
"keywords" => JSON::Any.new(keywords.map { |v| JSON::Any.new(v) }),
|
||||
"isPostLiveDvr" => JSON::Any.new(post_live_dvr),
|
||||
# Related videos
|
||||
"relatedVideos" => JSON::Any.new(related),
|
||||
# Description
|
||||
"description" => JSON::Any.new(description || ""),
|
||||
"descriptionHtml" => JSON::Any.new(description_html || "<p></p>"),
|
||||
"shortDescription" => JSON::Any.new(short_description.try &.as_s || nil),
|
||||
|
||||
description = microformat.dig?("description", "simpleText").try &.as_s || ""
|
||||
short_description = player_response.dig?("videoDetails", "shortDescription")
|
||||
|
||||
# description_html = video_secondary_renderer.try &.dig?("description", "runs")
|
||||
# .try &.as_a.try { |t| content_to_comment_html(t, video_id) }
|
||||
|
||||
description_html = parse_description(video_secondary_renderer.try &.dig?("attributedDescription"), video_id)
|
||||
|
||||
# Video metadata
|
||||
"genre" => JSON::Any.new(genre.try &.as_s || ""),
|
||||
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?),
|
||||
"license" => JSON::Any.new(license.try &.as_s || ""),
|
||||
|
||||
metadata = video_secondary_renderer
|
||||
.try &.dig?("metadataRowContainer", "metadataRowContainerRenderer", "rows")
|
||||
.try &.as_a
|
||||
|
||||
genre = microformat["category"]?
|
||||
genre_ucid = nil
|
||||
license = nil
|
||||
|
||||
metadata.try &.each do |row|
|
||||
metadata_title = extract_text(row.dig?("metadataRowRenderer", "title"))
|
||||
contents = row.dig?("metadataRowRenderer", "contents", 0)
|
||||
|
||||
if metadata_title == "Category"
|
||||
contents = contents.try &.dig?("runs", 0)
|
||||
|
||||
genre = contents.try &.["text"]?
|
||||
genre_ucid = contents.try &.dig?("navigationEndpoint", "browseEndpoint", "browseId")
|
||||
elsif metadata_title == "License"
|
||||
license = contents.try &.dig?("runs", 0, "text")
|
||||
elsif metadata_title == "Licensed to YouTube by"
|
||||
license = contents.try &.["simpleText"]?
|
||||
end
|
||||
end
|
||||
|
||||
# Music section
|
||||
"music" => JSON.parse(music_list.to_json),
|
||||
|
||||
music_list = [] of VideoMusic
|
||||
music_desclist = player_response.dig?(
|
||||
"engagementPanels", 1, "engagementPanelSectionListRenderer",
|
||||
"content", "structuredDescriptionContentRenderer", "items", 2,
|
||||
"videoDescriptionMusicSectionRenderer", "carouselLockups"
|
||||
)
|
||||
|
||||
music_desclist.try &.as_a.each do |music_desc|
|
||||
artist = nil
|
||||
album = nil
|
||||
music_license = nil
|
||||
|
||||
# Used when the video has multiple songs
|
||||
if song_title = music_desc.dig?("carouselLockupRenderer", "videoLockup", "compactVideoRenderer", "title")
|
||||
# "simpleText" for plain text / "runs" when song has a link
|
||||
song = song_title["simpleText"]? || song_title.dig?("runs", 0, "text")
|
||||
|
||||
# some videos can have empty tracks. See: https://www.youtube.com/watch?v=eBGIQ7ZuuiU
|
||||
next if !song
|
||||
end
|
||||
|
||||
music_desc.dig?("carouselLockupRenderer", "infoRows").try &.as_a.each do |desc|
|
||||
desc_title = extract_text(desc.dig?("infoRowRenderer", "title"))
|
||||
if desc_title == "ARTIST"
|
||||
artist = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
|
||||
elsif desc_title == "SONG"
|
||||
song = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
|
||||
elsif desc_title == "ALBUM"
|
||||
album = extract_text(desc.dig?("infoRowRenderer", "defaultMetadata"))
|
||||
elsif desc_title == "LICENSES"
|
||||
music_license = extract_text(desc.dig?("infoRowRenderer", "expandedMetadata"))
|
||||
end
|
||||
end
|
||||
music_list << VideoMusic.new(song.to_s, album.to_s, artist.to_s, music_license.to_s)
|
||||
end
|
||||
|
||||
# Author infos
|
||||
"author" => JSON::Any.new(author || ""),
|
||||
"ucid" => JSON::Any.new(ucid || ""),
|
||||
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
|
||||
"authorVerified" => JSON::Any.new(author_verified || false),
|
||||
"subCountText" => JSON::Any.new(subs_text || "-"),
|
||||
}
|
||||
|
||||
return params
|
||||
end
|
||||
author = video_details["author"]?.try &.as_s
|
||||
ucid = video_details["channelId"]?.try &.as_s
|
||||
|
||||
private def convert_url(fmt)
|
||||
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
||||
url = URI.parse(cfr["url"])
|
||||
params = url.query_params
|
||||
if author_info = video_secondary_renderer.try &.dig?("owner", "videoOwnerRenderer")
|
||||
author_thumbnail = author_info.dig?("thumbnail", "thumbnails", 0, "url")
|
||||
author_verified = has_verified_badge?(author_info["badges"]?)
|
||||
|
||||
LOGGER.debug("convert_url: Decoding '#{cfr}'")
|
||||
else
|
||||
url = URI.parse(fmt["url"].as_s)
|
||||
params = url.query_params
|
||||
subs_text = author_info["subscriberCountText"]?
|
||||
.try { |t| t["simpleText"]? || t.dig?("runs", 0, "text") }
|
||||
.try &.as_s.split(" ", 2)[0]
|
||||
end
|
||||
|
||||
# Return data
|
||||
|
||||
if live_now
|
||||
video_type = VideoType::Livestream
|
||||
elsif !premiere_timestamp.nil?
|
||||
video_type = VideoType::Scheduled
|
||||
published = premiere_timestamp || Time.utc
|
||||
else
|
||||
video_type = VideoType::Video
|
||||
end
|
||||
|
||||
params = {
|
||||
"videoType" => JSON::Any.new(video_type.to_s),
|
||||
# Basic video infos
|
||||
"title" => JSON::Any.new(title || ""),
|
||||
"views" => JSON::Any.new(views || 0_i64),
|
||||
"likes" => JSON::Any.new(likes || 0_i64),
|
||||
"lengthSeconds" => JSON::Any.new(length_txt || 0_i64),
|
||||
"published" => JSON::Any.new(published.to_rfc3339),
|
||||
# Extra video infos
|
||||
"allowedRegions" => JSON::Any.new(allowed_regions.map { |v| JSON::Any.new(v) }),
|
||||
"allowRatings" => JSON::Any.new(allow_ratings || false),
|
||||
"isFamilyFriendly" => JSON::Any.new(family_friendly || false),
|
||||
"isListed" => JSON::Any.new(is_listed || false),
|
||||
"isUpcoming" => JSON::Any.new(is_upcoming || false),
|
||||
"keywords" => JSON::Any.new(keywords.map { |v| JSON::Any.new(v) }),
|
||||
"isPostLiveDvr" => JSON::Any.new(post_live_dvr),
|
||||
# Related videos
|
||||
"relatedVideos" => JSON::Any.new(related),
|
||||
# Description
|
||||
"description" => JSON::Any.new(description || ""),
|
||||
"descriptionHtml" => JSON::Any.new(description_html || "<p></p>"),
|
||||
"shortDescription" => JSON::Any.new(short_description.try &.as_s || nil),
|
||||
# Video metadata
|
||||
"genre" => JSON::Any.new(genre.try &.as_s || ""),
|
||||
"genreUcid" => JSON::Any.new(genre_ucid.try &.as_s?),
|
||||
"license" => JSON::Any.new(license.try &.as_s || ""),
|
||||
# Music section
|
||||
"music" => JSON.parse(music_list.to_json),
|
||||
# Author infos
|
||||
"author" => JSON::Any.new(author || ""),
|
||||
"ucid" => JSON::Any.new(ucid || ""),
|
||||
"authorThumbnail" => JSON::Any.new(author_thumbnail.try &.as_s || ""),
|
||||
"authorVerified" => JSON::Any.new(author_verified || false),
|
||||
"subCountText" => JSON::Any.new(subs_text || "-"),
|
||||
}
|
||||
|
||||
return params
|
||||
end
|
||||
|
||||
url.query_params = params
|
||||
LOGGER.trace("convert_url: new url is '#{url}'")
|
||||
private def convert_url(fmt)
|
||||
if cfr = fmt["signatureCipher"]?.try { |json| HTTP::Params.parse(json.as_s) }
|
||||
url = URI.parse(cfr["url"])
|
||||
params = url.query_params
|
||||
|
||||
return url.to_s
|
||||
rescue ex
|
||||
LOGGER.debug("convert_url: Error when parsing video URL")
|
||||
LOGGER.trace(ex.inspect_with_backtrace)
|
||||
return ""
|
||||
LOGGER.debug("convert_url: Decoding '#{cfr}'")
|
||||
else
|
||||
url = URI.parse(fmt["url"].as_s)
|
||||
params = url.query_params
|
||||
end
|
||||
|
||||
url.query_params = params
|
||||
LOGGER.trace("convert_url: new url is '#{url}'")
|
||||
|
||||
return url.to_s
|
||||
rescue ex
|
||||
LOGGER.debug("convert_url: Error when parsing video URL")
|
||||
LOGGER.trace(ex.inspect_with_backtrace)
|
||||
return ""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,162 +1,166 @@
|
||||
struct VideoPreferences
|
||||
include JSON::Serializable
|
||||
module Invidious::Videos
|
||||
extend self
|
||||
|
||||
property annotations : Bool
|
||||
property preload : Bool
|
||||
property autoplay : Bool
|
||||
property comments : Array(String)
|
||||
property continue : Bool
|
||||
property continue_autoplay : Bool
|
||||
property controls : Bool
|
||||
property listen : Bool
|
||||
property local : Bool
|
||||
property preferred_captions : Array(String)
|
||||
property player_style : String
|
||||
property quality : String
|
||||
property quality_dash : String
|
||||
property raw : Bool
|
||||
property region : String?
|
||||
property related_videos : Bool
|
||||
property speed : Float32 | Float64
|
||||
property video_end : Float64 | Int32
|
||||
property video_loop : Bool
|
||||
property extend_desc : Bool
|
||||
property video_start : Float64 | Int32
|
||||
property volume : Int32
|
||||
property vr_mode : Bool
|
||||
property save_player_pos : Bool
|
||||
end
|
||||
|
||||
def process_video_params(query, preferences)
|
||||
annotations = query["iv_load_policy"]?.try &.to_i?
|
||||
preload = query["preload"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
comments = query["comments"]?.try &.split(",").map(&.downcase)
|
||||
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
continue_autoplay = query["continue_autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
listen = query["listen"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
local = query["local"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
player_style = query["player_style"]?
|
||||
preferred_captions = query["subtitles"]?.try &.split(",").map(&.downcase)
|
||||
quality = query["quality"]?
|
||||
quality_dash = query["quality_dash"]?
|
||||
region = query["region"]?
|
||||
related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
speed = query["speed"]?.try &.rchop("x").to_f?
|
||||
video_loop = query["loop"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
extend_desc = query["extend_desc"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
volume = query["volume"]?.try &.to_i?
|
||||
vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
save_player_pos = query["save_player_pos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
|
||||
if preferences
|
||||
# region ||= preferences.region
|
||||
annotations ||= preferences.annotations.to_unsafe
|
||||
preload ||= preferences.preload.to_unsafe
|
||||
autoplay ||= preferences.autoplay.to_unsafe
|
||||
comments ||= preferences.comments
|
||||
continue ||= preferences.continue.to_unsafe
|
||||
continue_autoplay ||= preferences.continue_autoplay.to_unsafe
|
||||
listen ||= preferences.listen.to_unsafe
|
||||
local ||= preferences.local.to_unsafe
|
||||
player_style ||= preferences.player_style
|
||||
preferred_captions ||= preferences.captions
|
||||
quality ||= preferences.quality
|
||||
quality_dash ||= preferences.quality_dash
|
||||
related_videos ||= preferences.related_videos.to_unsafe
|
||||
speed ||= preferences.speed
|
||||
video_loop ||= preferences.video_loop.to_unsafe
|
||||
extend_desc ||= preferences.extend_desc.to_unsafe
|
||||
volume ||= preferences.volume
|
||||
vr_mode ||= preferences.vr_mode.to_unsafe
|
||||
save_player_pos ||= preferences.save_player_pos.to_unsafe
|
||||
end
|
||||
|
||||
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
|
||||
preload ||= CONFIG.default_user_preferences.preload.to_unsafe
|
||||
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
|
||||
comments ||= CONFIG.default_user_preferences.comments
|
||||
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
|
||||
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
|
||||
listen ||= CONFIG.default_user_preferences.listen.to_unsafe
|
||||
local ||= CONFIG.default_user_preferences.local.to_unsafe
|
||||
player_style ||= CONFIG.default_user_preferences.player_style
|
||||
preferred_captions ||= CONFIG.default_user_preferences.captions
|
||||
quality ||= CONFIG.default_user_preferences.quality
|
||||
quality_dash ||= CONFIG.default_user_preferences.quality_dash
|
||||
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
|
||||
speed ||= CONFIG.default_user_preferences.speed
|
||||
video_loop ||= CONFIG.default_user_preferences.video_loop.to_unsafe
|
||||
extend_desc ||= CONFIG.default_user_preferences.extend_desc.to_unsafe
|
||||
volume ||= CONFIG.default_user_preferences.volume
|
||||
vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe
|
||||
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe
|
||||
|
||||
annotations = annotations == 1
|
||||
preload = preload == 1
|
||||
autoplay = autoplay == 1
|
||||
continue = continue == 1
|
||||
continue_autoplay = continue_autoplay == 1
|
||||
listen = listen == 1
|
||||
local = local == 1
|
||||
related_videos = related_videos == 1
|
||||
video_loop = video_loop == 1
|
||||
extend_desc = extend_desc == 1
|
||||
vr_mode = vr_mode == 1
|
||||
save_player_pos = save_player_pos == 1
|
||||
|
||||
if CONFIG.disabled?("dash") && quality == "dash"
|
||||
quality = "high"
|
||||
end
|
||||
|
||||
if CONFIG.disabled?("local") && local
|
||||
local = false
|
||||
end
|
||||
|
||||
if start = query["t"]? || query["time_continue"]? || query["start"]?
|
||||
video_start = decode_time(start)
|
||||
end
|
||||
video_start ||= 0
|
||||
|
||||
if query["end"]?
|
||||
video_end = decode_time(query["end"])
|
||||
end
|
||||
video_end ||= -1
|
||||
|
||||
raw = query["raw"]?.try &.to_i?
|
||||
raw ||= 0
|
||||
raw = raw == 1
|
||||
|
||||
controls = query["controls"]?.try &.to_i?
|
||||
controls ||= 1
|
||||
controls = controls >= 1
|
||||
|
||||
params = VideoPreferences.new({
|
||||
annotations: annotations,
|
||||
preload: preload,
|
||||
autoplay: autoplay,
|
||||
comments: comments,
|
||||
continue: continue,
|
||||
continue_autoplay: continue_autoplay,
|
||||
controls: controls,
|
||||
listen: listen,
|
||||
local: local,
|
||||
player_style: player_style,
|
||||
preferred_captions: preferred_captions,
|
||||
quality: quality,
|
||||
quality_dash: quality_dash,
|
||||
raw: raw,
|
||||
region: region,
|
||||
related_videos: related_videos,
|
||||
speed: speed,
|
||||
video_end: video_end,
|
||||
video_loop: video_loop,
|
||||
extend_desc: extend_desc,
|
||||
video_start: video_start,
|
||||
volume: volume,
|
||||
vr_mode: vr_mode,
|
||||
save_player_pos: save_player_pos,
|
||||
})
|
||||
|
||||
return params
|
||||
struct VideoPreferences
|
||||
include JSON::Serializable
|
||||
|
||||
property annotations : Bool
|
||||
property preload : Bool
|
||||
property autoplay : Bool
|
||||
property comments : Array(String)
|
||||
property continue : Bool
|
||||
property continue_autoplay : Bool
|
||||
property controls : Bool
|
||||
property listen : Bool
|
||||
property local : Bool
|
||||
property preferred_captions : Array(String)
|
||||
property player_style : String
|
||||
property quality : String
|
||||
property quality_dash : String
|
||||
property raw : Bool
|
||||
property region : String?
|
||||
property related_videos : Bool
|
||||
property speed : Float32 | Float64
|
||||
property video_end : Float64 | Int32
|
||||
property video_loop : Bool
|
||||
property extend_desc : Bool
|
||||
property video_start : Float64 | Int32
|
||||
property volume : Int32
|
||||
property vr_mode : Bool
|
||||
property save_player_pos : Bool
|
||||
end
|
||||
|
||||
def process_video_params(query, preferences)
|
||||
annotations = query["iv_load_policy"]?.try &.to_i?
|
||||
preload = query["preload"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
autoplay = query["autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
comments = query["comments"]?.try &.split(",").map(&.downcase)
|
||||
continue = query["continue"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
continue_autoplay = query["continue_autoplay"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
listen = query["listen"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
local = query["local"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
player_style = query["player_style"]?
|
||||
preferred_captions = query["subtitles"]?.try &.split(",").map(&.downcase)
|
||||
quality = query["quality"]?
|
||||
quality_dash = query["quality_dash"]?
|
||||
region = query["region"]?
|
||||
related_videos = query["related_videos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
speed = query["speed"]?.try &.rchop("x").to_f?
|
||||
video_loop = query["loop"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
extend_desc = query["extend_desc"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
volume = query["volume"]?.try &.to_i?
|
||||
vr_mode = query["vr_mode"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
save_player_pos = query["save_player_pos"]?.try { |q| (q == "true" || q == "1").to_unsafe }
|
||||
|
||||
if preferences
|
||||
# region ||= preferences.region
|
||||
annotations ||= preferences.annotations.to_unsafe
|
||||
preload ||= preferences.preload.to_unsafe
|
||||
autoplay ||= preferences.autoplay.to_unsafe
|
||||
comments ||= preferences.comments
|
||||
continue ||= preferences.continue.to_unsafe
|
||||
continue_autoplay ||= preferences.continue_autoplay.to_unsafe
|
||||
listen ||= preferences.listen.to_unsafe
|
||||
local ||= preferences.local.to_unsafe
|
||||
player_style ||= preferences.player_style
|
||||
preferred_captions ||= preferences.captions
|
||||
quality ||= preferences.quality
|
||||
quality_dash ||= preferences.quality_dash
|
||||
related_videos ||= preferences.related_videos.to_unsafe
|
||||
speed ||= preferences.speed
|
||||
video_loop ||= preferences.video_loop.to_unsafe
|
||||
extend_desc ||= preferences.extend_desc.to_unsafe
|
||||
volume ||= preferences.volume
|
||||
vr_mode ||= preferences.vr_mode.to_unsafe
|
||||
save_player_pos ||= preferences.save_player_pos.to_unsafe
|
||||
end
|
||||
|
||||
annotations ||= CONFIG.default_user_preferences.annotations.to_unsafe
|
||||
preload ||= CONFIG.default_user_preferences.preload.to_unsafe
|
||||
autoplay ||= CONFIG.default_user_preferences.autoplay.to_unsafe
|
||||
comments ||= CONFIG.default_user_preferences.comments
|
||||
continue ||= CONFIG.default_user_preferences.continue.to_unsafe
|
||||
continue_autoplay ||= CONFIG.default_user_preferences.continue_autoplay.to_unsafe
|
||||
listen ||= CONFIG.default_user_preferences.listen.to_unsafe
|
||||
local ||= CONFIG.default_user_preferences.local.to_unsafe
|
||||
player_style ||= CONFIG.default_user_preferences.player_style
|
||||
preferred_captions ||= CONFIG.default_user_preferences.captions
|
||||
quality ||= CONFIG.default_user_preferences.quality
|
||||
quality_dash ||= CONFIG.default_user_preferences.quality_dash
|
||||
related_videos ||= CONFIG.default_user_preferences.related_videos.to_unsafe
|
||||
speed ||= CONFIG.default_user_preferences.speed
|
||||
video_loop ||= CONFIG.default_user_preferences.video_loop.to_unsafe
|
||||
extend_desc ||= CONFIG.default_user_preferences.extend_desc.to_unsafe
|
||||
volume ||= CONFIG.default_user_preferences.volume
|
||||
vr_mode ||= CONFIG.default_user_preferences.vr_mode.to_unsafe
|
||||
save_player_pos ||= CONFIG.default_user_preferences.save_player_pos.to_unsafe
|
||||
|
||||
annotations = annotations == 1
|
||||
preload = preload == 1
|
||||
autoplay = autoplay == 1
|
||||
continue = continue == 1
|
||||
continue_autoplay = continue_autoplay == 1
|
||||
listen = listen == 1
|
||||
local = local == 1
|
||||
related_videos = related_videos == 1
|
||||
video_loop = video_loop == 1
|
||||
extend_desc = extend_desc == 1
|
||||
vr_mode = vr_mode == 1
|
||||
save_player_pos = save_player_pos == 1
|
||||
|
||||
if CONFIG.disabled?("dash") && quality == "dash"
|
||||
quality = "high"
|
||||
end
|
||||
|
||||
if CONFIG.disabled?("local") && local
|
||||
local = false
|
||||
end
|
||||
|
||||
if start = query["t"]? || query["time_continue"]? || query["start"]?
|
||||
video_start = decode_time(start)
|
||||
end
|
||||
video_start ||= 0
|
||||
|
||||
if query["end"]?
|
||||
video_end = decode_time(query["end"])
|
||||
end
|
||||
video_end ||= -1
|
||||
|
||||
raw = query["raw"]?.try &.to_i?
|
||||
raw ||= 0
|
||||
raw = raw == 1
|
||||
|
||||
controls = query["controls"]?.try &.to_i?
|
||||
controls ||= 1
|
||||
controls = controls >= 1
|
||||
|
||||
params = VideoPreferences.new({
|
||||
annotations: annotations,
|
||||
preload: preload,
|
||||
autoplay: autoplay,
|
||||
comments: comments,
|
||||
continue: continue,
|
||||
continue_autoplay: continue_autoplay,
|
||||
controls: controls,
|
||||
listen: listen,
|
||||
local: local,
|
||||
player_style: player_style,
|
||||
preferred_captions: preferred_captions,
|
||||
quality: quality,
|
||||
quality_dash: quality_dash,
|
||||
raw: raw,
|
||||
region: region,
|
||||
related_videos: related_videos,
|
||||
speed: speed,
|
||||
video_end: video_end,
|
||||
video_loop: video_loop,
|
||||
extend_desc: extend_desc,
|
||||
video_start: video_start,
|
||||
volume: volume,
|
||||
vr_mode: vr_mode,
|
||||
save_player_pos: save_player_pos,
|
||||
})
|
||||
|
||||
return params
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<div class="pure-u-1 pure-u-lg-3-5">
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" action="/add_playlist_items" method="get">
|
||||
<legend><a href="/playlist?list=<%= playlist.id %>"><%= translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %></a></legend>
|
||||
<legend><a href="/playlist?list=<%= playlist.id %>"><%= I18n.translate(locale, "Editing playlist `x`", %|"#{HTML.escape(playlist.title)}"|) %></a></legend>
|
||||
|
||||
<fieldset>
|
||||
<input class="pure-input-1" type="search" name="q"
|
||||
<% if query %>value="<%= HTML.escape(query.text) %>"<% end %>
|
||||
placeholder="<%= translate(locale, "Search for videos") %>">
|
||||
placeholder="<%= I18n.translate(locale, "Search for videos") %>">
|
||||
<input type="hidden" name="list" value="<%= plid %>">
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -35,10 +35,10 @@
|
||||
<%=
|
||||
{
|
||||
"ucid" => ucid,
|
||||
"youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")),
|
||||
"comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")),
|
||||
"hide_replies_text" => HTML.escape(translate(locale, "Hide replies")),
|
||||
"show_replies_text" => HTML.escape(translate(locale, "Show replies")),
|
||||
"youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")),
|
||||
"comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")),
|
||||
"hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")),
|
||||
"show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")),
|
||||
"preferences" => env.get("preferences").as(Preferences)
|
||||
}.to_pretty_json
|
||||
%>
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
<div class="pure-u-1-2 flex-left flexible">
|
||||
<div class="channel-profile">
|
||||
<img src="/ggpht<%= channel_profile_pic %>" alt="" />
|
||||
<span class="channel-name"><%= author %></span><% if !channel.verified.nil? && channel.verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %>
|
||||
<div class="channel-name-pronouns">
|
||||
<span class="channel-name"><%= author %></span><% if !channel.verified.nil? && channel.verified %> <i class="icon ion ion-md-checkmark-circle"></i><% end %>
|
||||
<% if !channel.pronouns.nil? %><br /><span class="channel-pronouns"><%= channel.pronouns %></span><% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,7 +27,7 @@
|
||||
|
||||
<div class="pure-u">
|
||||
<a class="pure-button pure-button-secondary" dir="auto" href="/feed/channel/<%= ucid %>">
|
||||
<i class="icon ion-logo-rss"></i> <%= translate(locale, "generic_button_rss") %>
|
||||
<i class="icon ion-logo-rss"></i> <%= I18n.translate(locale, "generic_button_rss") %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,10 +40,10 @@
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1-2">
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<a href="<%= youtube_url %>"><%= translate(locale, "View channel on YouTube") %></a>
|
||||
<a href="<%= youtube_url %>"><%= I18n.translate(locale, "View channel on YouTube") %></a>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<a href="<%= redirect_url %>"><%= translate(locale, "Switch Invidious Instance") %></a>
|
||||
<a href="<%= redirect_url %>"><%= I18n.translate(locale, "Switch Invidious Instance") %></a>
|
||||
</div>
|
||||
|
||||
<%= Invidious::Frontend::ChannelPage.generate_tabs_links(locale, channel, selected_tab) %>
|
||||
@@ -50,9 +53,9 @@
|
||||
<% sort_options.each do |sort| %>
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<% if sort_by == sort %>
|
||||
<b><%= translate(locale, sort) %></b>
|
||||
<b><%= I18n.translate(locale, sort) %></b>
|
||||
<% else %>
|
||||
<a href="<%= relative_url %>?sort_by=<%= sort %>"><%= translate(locale, sort) %></a>
|
||||
<a href="<%= relative_url %>?sort_by=<%= sort %>"><%= I18n.translate(locale, sort) %></a>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<% end %>
|
||||
<% feed_menu.each do |feed| %>
|
||||
<a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading">
|
||||
<%= translate(locale, feed) %>
|
||||
<%= I18n.translate(locale, feed) %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
</div>
|
||||
|
||||
<% if !item.channel_handle.nil? %><p class="channel-name" dir="auto"><%= item.channel_handle %></p><% end %>
|
||||
<p><%= translate_count(locale, "generic_subscribers_count", item.subscriber_count, NumberFormatting::Separator) %></p>
|
||||
<% if !item.auto_generated && item.channel_handle.nil? %><p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p><% end %>
|
||||
<p><%= I18n.translate_count(locale, "generic_subscribers_count", item.subscriber_count, I18n::NumberFormatting::Separator) %></p>
|
||||
<% if !item.auto_generated && item.channel_handle.nil? %><p><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p><% end %>
|
||||
<h5><%= item.description_html %></h5>
|
||||
<% when SearchHashtag %>
|
||||
<% if !thin_mode %>
|
||||
@@ -45,13 +45,13 @@
|
||||
|
||||
<div class="video-card-row">
|
||||
<%- if item.video_count != 0 -%>
|
||||
<p><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p>
|
||||
<p><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p>
|
||||
<%- end -%>
|
||||
</div>
|
||||
|
||||
<div class="video-card-row">
|
||||
<%- if item.channel_count != 0 -%>
|
||||
<p><%= translate_count(locale, "generic_channels_count", item.channel_count, NumberFormatting::Separator) %></p>
|
||||
<p><%= I18n.translate_count(locale, "generic_channels_count", item.channel_count, I18n::NumberFormatting::Separator) %></p>
|
||||
<%- end -%>
|
||||
</div>
|
||||
<% when SearchPlaylist, InvidiousPlaylist %>
|
||||
@@ -73,7 +73,7 @@
|
||||
<%- end -%>
|
||||
|
||||
<div class="bottom-right-overlay">
|
||||
<p class="length"><%= translate_count(locale, "generic_videos_count", item.video_count, NumberFormatting::Separator) %></p>
|
||||
<p class="length"><%= I18n.translate_count(locale, "generic_videos_count", item.video_count, I18n::NumberFormatting::Separator) %></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -101,11 +101,11 @@
|
||||
<div class="error-card">
|
||||
<div class="explanation">
|
||||
<i class="icon ion-ios-alert"></i>
|
||||
<h4><%=translate(locale, "timeline_parse_error_placeholder_heading")%></h4>
|
||||
<p><%=translate(locale, "timeline_parse_error_placeholder_message")%></p>
|
||||
<h4><%=I18n.translate(locale, "timeline_parse_error_placeholder_heading")%></h4>
|
||||
<p><%=I18n.translate(locale, "timeline_parse_error_placeholder_message")%></p>
|
||||
</div>
|
||||
<details>
|
||||
<summary class="pure-button pure-button-secondary"><%=translate(locale, "timeline_parse_error_show_technical_details")%></summary>
|
||||
<summary class="pure-button pure-button-secondary"><%=I18n.translate(locale, "timeline_parse_error_show_technical_details")%></summary>
|
||||
<pre class="error-issue-template"><%=get_issue_template(env, item.parse_exception)[1]%></pre>
|
||||
</details>
|
||||
</div>
|
||||
@@ -168,7 +168,7 @@
|
||||
|
||||
<div class="bottom-right-overlay">
|
||||
<%- if item.responds_to?(:live_now) && item.live_now -%>
|
||||
<p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i> <%= translate(locale, "LIVE") %></p>
|
||||
<p class="length" dir="auto"><i class="icon ion-ios-play-circle"></i> <%= I18n.translate(locale, "LIVE") %></p>
|
||||
<%- elsif item.length_seconds != 0 -%>
|
||||
<p class="length"><%= recode_length_seconds(item.length_seconds) %></p>
|
||||
<%- end -%>
|
||||
@@ -200,15 +200,15 @@
|
||||
<div class="video-card-row flexible">
|
||||
<div class="flex-left">
|
||||
<% if item.responds_to?(:premiere_timestamp) && item.premiere_timestamp.try &.> Time.utc %>
|
||||
<p class="video-data" dir="auto"><%= translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p>
|
||||
<p class="video-data" dir="auto"><%= I18n.translate(locale, "Premieres in `x`", recode_date((item.premiere_timestamp.as(Time) - Time.utc).ago, locale)) %></p>
|
||||
<% elsif item.responds_to?(:published) && (Time.utc - item.published) > 1.minute %>
|
||||
<p class="video-data" dir="auto"><%= translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p>
|
||||
<p class="video-data" dir="auto"><%= I18n.translate(locale, "Shared `x` ago", recode_date(item.published, locale)) %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if item.responds_to?(:views) && item.views %>
|
||||
<div class="flex-right">
|
||||
<p class="video-data" dir="auto"><%= translate_count(locale, "generic_views_count", item.views || 0, NumberFormatting::Short) %></p>
|
||||
<p class="video-data" dir="auto"><%= I18n.translate_count(locale, "generic_views_count", item.views || 0, I18n::NumberFormatting::Short) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<script id="pagination-data" type="application/json">
|
||||
<%=
|
||||
{
|
||||
"next_page" => translate(locale, "Next page"),
|
||||
"prev_page" => translate(locale, "Previous page"),
|
||||
"is_rtl" => locale_is_rtl?(locale)
|
||||
"next_page" => I18n.translate(locale, "Next page"),
|
||||
"prev_page" => I18n.translate(locale, "Previous page"),
|
||||
"is_rtl" => I18n.locale_is_rtl?(locale)
|
||||
}.to_pretty_json
|
||||
%>
|
||||
</script>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
audio_streams.each_with_index do |fmt, i|
|
||||
src_url = "/latest_version?id=#{video.id}&itag=#{fmt["itag"]}"
|
||||
src_url += "&local=true" if params.local
|
||||
src_url = invidious_companion.public_url.to_s + src_url +
|
||||
src_url = invidious_companion.public_url.to_s + src_url +
|
||||
"&check=#{invidious_companion_check_id}" if (invidious_companion)
|
||||
|
||||
bitrate = fmt["bitrate"]
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<%
|
||||
search_privacy = preferences.search_privacy
|
||||
%>
|
||||
|
||||
<% if search_privacy %>
|
||||
<form class="pure-form" action="/search" method="post">
|
||||
<% else %>
|
||||
<form class="pure-form" action="/search" method="get">
|
||||
<% end %>
|
||||
<fieldset>
|
||||
<input type="search" id="searchbox" autocorrect="off"
|
||||
autocapitalize="none" spellcheck="false" <% if autofocus %>autofocus<% end %>
|
||||
name="q" placeholder="<%= translate(locale, "search") %>"
|
||||
title="<%= translate(locale, "search") %>"
|
||||
name="q" placeholder="<%= I18n.translate(locale, "search") %>"
|
||||
title="<%= I18n.translate(locale, "search") %>"
|
||||
value="<%= env.get?("search").try {|x| HTML.escape(x.as(String)) } %>">
|
||||
</fieldset>
|
||||
<button type="submit" id="searchbutton" aria-label="<%= translate(locale, "search") %>">
|
||||
<button type="submit" id="searchbutton" aria-label="<%= I18n.translate(locale, "search") %>">
|
||||
<i class="icon ion-ios-search"></i>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<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>
|
||||
<b><input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Unsubscribe") %> | <%= sub_count_text %>"></b>
|
||||
</button>
|
||||
</form>
|
||||
<% else %>
|
||||
<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>
|
||||
<b><input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Subscribe") %> | <%= sub_count_text %>"></b>
|
||||
</button>
|
||||
</form>
|
||||
<% end %>
|
||||
@@ -22,8 +22,8 @@
|
||||
"author" => HTML.escape(author),
|
||||
"sub_count_text" => HTML.escape(sub_count_text),
|
||||
"csrf_token" => URI.encode_www_form(env.get?("csrf_token").try &.as(String) || ""),
|
||||
"subscribe_text" => HTML.escape(translate(locale, "Subscribe")),
|
||||
"unsubscribe_text" => HTML.escape(translate(locale, "Unsubscribe"))
|
||||
"subscribe_text" => HTML.escape(I18n.translate(locale, "Subscribe")),
|
||||
"unsubscribe_text" => HTML.escape(I18n.translate(locale, "Unsubscribe"))
|
||||
}.to_pretty_json
|
||||
%>
|
||||
</script>
|
||||
@@ -31,6 +31,6 @@
|
||||
<% else %>
|
||||
<a id="subscribe" class="pure-button pure-button-primary"
|
||||
href="/login?referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b>
|
||||
<b><%= I18n.translate(locale, "Subscribe") %> | <%= sub_count_text %></b>
|
||||
</a>
|
||||
<% end %>
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<div class="flex-right flexible">
|
||||
<div class="icon-buttons">
|
||||
<a title="<%=translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>">
|
||||
<a title="<%=I18n.translate(locale, "videoinfo_watch_on_youTube")%>" rel="noreferrer noopener" href="https://www.youtube.com/watch<%=endpoint_params%>">
|
||||
<i class="icon ion-logo-youtube"></i>
|
||||
</a>
|
||||
<a title="<%=translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1">
|
||||
<a title="<%=I18n.translate(locale, "Audio mode")%>" href="/watch<%=endpoint_params%>&listen=1">
|
||||
<i class="icon ion-md-headset"></i>
|
||||
</a>
|
||||
|
||||
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
|
||||
<a title="<%=translate(locale, "Switch Invidious Instance")%>" href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>">
|
||||
<a title="<%=I18n.translate(locale, "Switch Invidious Instance")%>" href="/redirect?referer=%2Fwatch<%=URI.encode_www_form(endpoint_params)%>">
|
||||
<i class="icon ion-md-jet"></i>
|
||||
</a>
|
||||
<% else %>
|
||||
<a title="<%=translate(locale, "Switch Invidious Instance")%>" href="https://redirect.invidious.io/watch<%=endpoint_params%>">
|
||||
<a title="<%=I18n.translate(locale, "Switch Invidious Instance")%>" href="https://redirect.invidious.io/watch<%=endpoint_params%>">
|
||||
<i class="icon ion-md-jet"></i>
|
||||
</a>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Create playlist") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Create playlist") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g">
|
||||
@@ -8,25 +8,25 @@
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" action="/create_playlist?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<fieldset>
|
||||
<legend><%= translate(locale, "Create playlist") %></legend>
|
||||
<legend><%= I18n.translate(locale, "Create playlist") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="title"><%= translate(locale, "Title") %> :</label>
|
||||
<input required name="title" type="text" placeholder="<%= translate(locale, "Title") %>">
|
||||
<label for="title"><%= I18n.translate(locale, "Title") %> :</label>
|
||||
<input required name="title" type="text" placeholder="<%= I18n.translate(locale, "Title") %>">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="privacy"><%= translate(locale, "Playlist privacy") %> :</label>
|
||||
<label for="privacy"><%= I18n.translate(locale, "Playlist privacy") %> :</label>
|
||||
<select name="privacy" id="privacy">
|
||||
<% PlaylistPrivacy.names.each do |option| %>
|
||||
<option value="<%= option %>" <% if option == "Public" %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<option value="<%= option %>" <% if option == "Public" %> selected <% end %>><%= I18n.translate(locale, option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-controls">
|
||||
<button type="submit" name="action" value="create_playlist" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Create playlist") %>
|
||||
<%= I18n.translate(locale, "Create playlist") %>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Delete playlist") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Delete playlist") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" action="/delete_playlist?list=<%= plid %>&referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<legend><%= translate(locale, "Delete playlist `x`?", %|"#{HTML.escape(playlist.title)}"|) %></legend>
|
||||
<legend><%= I18n.translate(locale, "Delete playlist `x`?", %|"#{HTML.escape(playlist.title)}"|) %></legend>
|
||||
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-2">
|
||||
<button type="submit" name="submit" value="delete_playlist" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Yes") %>
|
||||
<%= I18n.translate(locale, "Yes") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pure-u-1-2">
|
||||
<a class="pure-button" href="/playlist?list=<%= plid %>">
|
||||
<%= translate(locale, "No") %>
|
||||
<%= I18n.translate(locale, "No") %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
<div class="flex-right button-container">
|
||||
<div class="pure-u">
|
||||
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/playlist?list=<%= plid %>">
|
||||
<i class="icon ion-md-close"></i> <%= translate(locale, "generic_button_cancel") %>
|
||||
<i class="icon ion-md-close"></i> <%= I18n.translate(locale, "generic_button_cancel") %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u">
|
||||
<button class="pure-button pure-button-secondary low-profile" dir="auto" type="submit">
|
||||
<i class="icon ion-md-save"></i> <%= translate(locale, "generic_button_save") %>
|
||||
<i class="icon ion-md-save"></i> <%= I18n.translate(locale, "generic_button_save") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pure-u">
|
||||
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
|
||||
<i class="icon ion-md-trash"></i> <%= translate(locale, "generic_button_delete") %>
|
||||
<i class="icon ion-md-trash"></i> <%= I18n.translate(locale, "generic_button_delete") %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,11 +36,11 @@
|
||||
<div class="pure-u-1-1">
|
||||
<b>
|
||||
<%= HTML.escape(playlist.author) %> |
|
||||
<%= translate_count(locale, "generic_videos_count", playlist.video_count) %> |
|
||||
<%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> |
|
||||
</b>
|
||||
<select name="privacy">
|
||||
<%- {"Public", "Unlisted", "Private"}.each do |option| -%>
|
||||
<option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= translate(locale, option) %></option>
|
||||
<option value="<%= option %>" <% if option == playlist.privacy.to_s %>selected<% end %>><%= I18n.translate(locale, option) %></option>
|
||||
<%- end -%>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "History") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "History") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1-3">
|
||||
<h3><%= translate_count(locale, "generic_videos_count", user.watched.size, NumberFormatting::HtmlSpan) %></h3>
|
||||
<h3><%= I18n.translate_count(locale, "generic_videos_count", user.watched.size, I18n::NumberFormatting::HtmlSpan) %></h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:center">
|
||||
<a href="/feed/subscriptions"><%= translate_count(locale, "generic_subscriptions_count", user.subscriptions.size, NumberFormatting::HtmlSpan) %></a>
|
||||
<a href="/feed/subscriptions"><%= I18n.translate_count(locale, "generic_subscriptions_count", user.subscriptions.size, I18n::NumberFormatting::HtmlSpan) %></a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:right">
|
||||
<a href="/clear_watch_history"><%= translate(locale, "Clear watch history") %></a>
|
||||
<a href="/clear_watch_history"><%= I18n.translate(locale, "Clear watch history") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Playlists") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Playlists") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<%= rendered "components/feed_menu" %>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1-3">
|
||||
<h3><%= translate(locale, "user_created_playlists", %(<span id="count">#{items_created.size}</span>)) %></h3>
|
||||
<h3><%= I18n.translate(locale, "user_created_playlists", %(<span id="count">#{items_created.size}</span>)) %></h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:center">
|
||||
<a href="/create_playlist?referer=<%= URI.encode_www_form("/feed/playlists") %>"><%= translate(locale, "Create playlist") %></a>
|
||||
<a href="/create_playlist?referer=<%= URI.encode_www_form("/feed/playlists") %>"><%= I18n.translate(locale, "Create playlist") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:right">
|
||||
<a href="/data_control?referer=<%= URI.encode_www_form("/feed/playlists") %>">
|
||||
<%= translate(locale, "Import/export") %>
|
||||
<%= I18n.translate(locale, "Import/export") %>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1">
|
||||
<h3><%= translate(locale, "user_saved_playlists", %(<span id="count">#{items_saved.size}</span>)) %></h3>
|
||||
<h3><%= I18n.translate(locale, "user_saved_playlists", %(<span id="count">#{items_saved.size}</span>)) %></h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<% content_for "header" do %>
|
||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||
<meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>">
|
||||
<title>
|
||||
<% if env.get("preferences").as(Preferences).default_home != "Popular" %>
|
||||
<%= translate(locale, "Popular") %> - Invidious
|
||||
<%= I18n.translate(locale, "Popular") %> - Invidious
|
||||
<% else %>
|
||||
Invidious
|
||||
<% end %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Subscriptions") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Subscriptions") %> - Invidious</title>
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS" href="/feed/private?token=<%= token %>" />
|
||||
<% end %>
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1-3">
|
||||
<h3>
|
||||
<a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a>
|
||||
<a href="/subscription_manager"><%= I18n.translate(locale, "Manage subscriptions") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:center">
|
||||
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
||||
<a href="/feed/history"><%= I18n.translate(locale, "Watch history") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
@@ -26,7 +26,7 @@
|
||||
<% if CONFIG.enable_user_notifications %>
|
||||
|
||||
<center>
|
||||
<%= translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %>
|
||||
<%= I18n.translate_count(locale, "subscriptions_unseen_notifs_count", notifications.size) %>
|
||||
</center>
|
||||
|
||||
<% if !notifications.empty? %>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<% content_for "header" do %>
|
||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||
<meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>">
|
||||
<title>
|
||||
<% if env.get("preferences").as(Preferences).default_home != "Trending" %>
|
||||
<%= translate(locale, "Trending") %> - Invidious
|
||||
<%= I18n.translate(locale, "Trending") %> - Invidious
|
||||
<% else %>
|
||||
Invidious
|
||||
<% end %>
|
||||
@@ -15,7 +15,7 @@
|
||||
<div style="align-self:flex-end" class="pure-u-2-3">
|
||||
<% if plid %>
|
||||
<a href="/playlist?list=<%= plid %>">
|
||||
<%= translate(locale, "View as playlist") %>
|
||||
<%= I18n.translate(locale, "View as playlist") %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -24,10 +24,10 @@
|
||||
<% {"Livestreams", "Gaming"}.each do |option| %>
|
||||
<div class="pure-u-1 pure-md-1-3">
|
||||
<% if trending_type == option %>
|
||||
<b><%= translate(locale, option) %></b>
|
||||
<b><%= I18n.translate(locale, option) %></b>
|
||||
<% else %>
|
||||
<a href="/feed/trending?type=<%= option %>®ion=<%= region %>">
|
||||
<%= translate(locale, option) %>
|
||||
<%= I18n.translate(locale, option) %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1><%= translate(locale, "JavaScript license information") %></h1>
|
||||
<h1><%= I18n.translate(locale, "JavaScript license information") %></h1>
|
||||
<table id="jslicense-labels1">
|
||||
<tr>
|
||||
<td>
|
||||
@@ -19,7 +19,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/iv-org/videojs-quality-selector"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/iv-org/videojs-quality-selector"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/mpetazzoni/sse.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/mpetazzoni/sse.js"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/videojs/videojs-contrib-quality-levels"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/videojs/videojs-contrib-quality-levels"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/jfujita/videojs-http-source-selector"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/jfujita/videojs-http-source-selector"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/mister-ben/videojs-mobile-ui"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/mister-ben/videojs-mobile-ui"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/spchuang/videojs-markers"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/spchuang/videojs-markers"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/brightcove/videojs-overlay"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/brightcove/videojs-overlay"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/mkhazov/videojs-share"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/mkhazov/videojs-share"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/chrisboustead/videojs-vtt-thumbnails"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/chrisboustead/videojs-vtt-thumbnails"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/afrmtbl/videojs-youtube-annotations"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/afrmtbl/videojs-youtube-annotations"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/videojs/videojs-vr"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/videojs/videojs-vr"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="https://github.com/videojs/video.js"><%= translate(locale, "source") %></a>
|
||||
<a href="https://github.com/videojs/video.js"><%= I18n.translate(locale, "source") %></a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% content_for "header" do %>
|
||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||
<meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>">
|
||||
<title>
|
||||
Invidious
|
||||
</title>
|
||||
|
||||
@@ -13,28 +13,28 @@
|
||||
<%- if playlist.is_a?(InvidiousPlaylist) && playlist.author == user.try &.email -%>
|
||||
<div class="pure-u">
|
||||
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/add_playlist_items?list=<%= plid %>">
|
||||
<i class="icon ion-md-add"></i> <%= translate(locale, "playlist_button_add_items") %>
|
||||
<i class="icon ion-md-add"></i> <%= I18n.translate(locale, "playlist_button_add_items") %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u">
|
||||
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/edit_playlist?list=<%= plid %>">
|
||||
<i class="icon ion-md-create"></i> <%= translate(locale, "generic_button_edit") %>
|
||||
<i class="icon ion-md-create"></i> <%= I18n.translate(locale, "generic_button_edit") %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u">
|
||||
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
|
||||
<i class="icon ion-md-trash"></i> <%= translate(locale, "generic_button_delete") %>
|
||||
<i class="icon ion-md-trash"></i> <%= I18n.translate(locale, "generic_button_delete") %>
|
||||
</a>
|
||||
</div>
|
||||
<%- else -%>
|
||||
<div class="pure-u">
|
||||
<%- if IV::Database::Playlists.exists?(playlist.id) -%>
|
||||
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/subscribe_playlist?list=<%= plid %>">
|
||||
<i class="icon ion-md-add"></i> <%= translate(locale, "Subscribe") %>
|
||||
<i class="icon ion-md-add"></i> <%= I18n.translate(locale, "Subscribe") %>
|
||||
</a>
|
||||
<%- else -%>
|
||||
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/delete_playlist?list=<%= plid %>">
|
||||
<i class="icon ion-md-trash"></i> <%= translate(locale, "Unsubscribe") %>
|
||||
<i class="icon ion-md-trash"></i> <%= I18n.translate(locale, "Unsubscribe") %>
|
||||
</a>
|
||||
<%- end -%>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
<div class="pure-u">
|
||||
<a class="pure-button pure-button-secondary low-profile" dir="auto" href="/feed/playlist/<%= plid %>">
|
||||
<i class="icon ion-logo-rss"></i> <%= translate(locale, "generic_button_rss") %>
|
||||
<i class="icon ion-logo-rss"></i> <%= I18n.translate(locale, "generic_button_rss") %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,15 +57,15 @@
|
||||
<% else %>
|
||||
<%= author %> |
|
||||
<% end %>
|
||||
<%= translate_count(locale, "generic_videos_count", playlist.video_count) %> |
|
||||
<%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> |
|
||||
<%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> |
|
||||
<%= I18n.translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %> |
|
||||
<% case playlist.as(InvidiousPlaylist).privacy when %>
|
||||
<% when PlaylistPrivacy::Public %>
|
||||
<i class="icon ion-md-globe"></i> <%= translate(locale, "Public") %>
|
||||
<i class="icon ion-md-globe"></i> <%= I18n.translate(locale, "Public") %>
|
||||
<% when PlaylistPrivacy::Unlisted %>
|
||||
<i class="icon ion-ios-unlock"></i> <%= translate(locale, "Unlisted") %>
|
||||
<i class="icon ion-ios-unlock"></i> <%= I18n.translate(locale, "Unlisted") %>
|
||||
<% when PlaylistPrivacy::Private %>
|
||||
<i class="icon ion-ios-lock"></i> <%= translate(locale, "Private") %>
|
||||
<i class="icon ion-ios-lock"></i> <%= I18n.translate(locale, "Private") %>
|
||||
<% end %>
|
||||
</b>
|
||||
<% else %>
|
||||
@@ -76,25 +76,25 @@
|
||||
<% subtitle = playlist.subtitle || "" %>
|
||||
<span><%= HTML.escape(subtitle[0..subtitle.rindex(" • ") || subtitle.size]) %></span> |
|
||||
<% end %>
|
||||
<%= translate_count(locale, "generic_videos_count", playlist.video_count) %> |
|
||||
<%= translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %>
|
||||
<%= I18n.translate_count(locale, "generic_videos_count", playlist.video_count) %> |
|
||||
<%= I18n.translate(locale, "Updated `x` ago", recode_date(playlist.updated, locale)) %>
|
||||
</b>
|
||||
<% end %>
|
||||
|
||||
<% if !playlist.is_a? InvidiousPlaylist %>
|
||||
<div class="pure-u-2-3">
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/playlist?list=<%= playlist.id %>">
|
||||
<%= translate(locale, "View playlist on YouTube") %>
|
||||
<%= I18n.translate(locale, "View playlist on YouTube") %>
|
||||
</a>
|
||||
<span> | </span>
|
||||
|
||||
<% if env.get("preferences").as(Preferences).automatic_instance_redirect%>
|
||||
<a href="/redirect?referer=<%= env.get?("current_page") %>">
|
||||
<%= translate(locale, "Switch Invidious Instance") %>
|
||||
<%= I18n.translate(locale, "Switch Invidious Instance") %>
|
||||
</a>
|
||||
<% else %>
|
||||
<a href="https://redirect.invidious.io/playlist?list=<%= playlist.id %>">
|
||||
<%= translate(locale, "Switch Invidious Instance") %>
|
||||
<%= I18n.translate(locale, "Switch Invidious Instance") %>
|
||||
</a>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<% else %>
|
||||
<noscript>
|
||||
<a href="/post/<%= id %>?ucid=<%= ucid %>&nojs=1">
|
||||
<%= translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
|
||||
<%= I18n.translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
|
||||
</a>
|
||||
</noscript>
|
||||
<% end %>
|
||||
@@ -29,12 +29,12 @@
|
||||
<%=
|
||||
{
|
||||
"id" => id,
|
||||
"youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")),
|
||||
"youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")),
|
||||
"reddit_comments_text" => "",
|
||||
"reddit_permalink_text" => "",
|
||||
"comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")),
|
||||
"hide_replies_text" => HTML.escape(translate(locale, "Hide replies")),
|
||||
"show_replies_text" => HTML.escape(translate(locale, "Show replies")),
|
||||
"comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")),
|
||||
"hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")),
|
||||
"show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")),
|
||||
"params" => {
|
||||
"comments": ["youtube"]
|
||||
},
|
||||
@@ -45,4 +45,4 @@
|
||||
%>
|
||||
</script>
|
||||
<script src="/js/comments.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
<script src="/js/post.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
<script src="/js/post.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<%
|
||||
search_privacy = preferences.search_privacy
|
||||
search_query = query.text.size > 30 ? HTML.escape(query.text[0,30].rstrip(".")) + "…" : HTML.escape(query.text)
|
||||
%>
|
||||
|
||||
<% content_for "header" do %>
|
||||
<title><%= query.text.size > 30 ? HTML.escape(query.text[0,30].rstrip(".")) + "…" : HTML.escape(query.text) %> - Invidious</title>
|
||||
<title><%= search_privacy ? "Search" : search_query %> - Invidious</title>
|
||||
<link rel="stylesheet" href="/css/search.css?v=<%= ASSET_COMMIT %>">
|
||||
<% end %>
|
||||
|
||||
@@ -11,9 +16,9 @@
|
||||
<%- if items.empty? -%>
|
||||
<div class="h-box no-results-error">
|
||||
<div>
|
||||
<%= translate(locale, "search_message_no_results") %><br/><br/>
|
||||
<%= translate(locale, "search_message_change_filters_or_query") %><br/><br/>
|
||||
<%= translate(locale, "search_message_use_another_instance", redirect_url) %>
|
||||
<%= I18n.translate(locale, "search_message_no_results") %><br/><br/>
|
||||
<%= I18n.translate(locale, "search_message_change_filters_or_query") %><br/><br/>
|
||||
<%= I18n.translate(locale, "search_message_use_another_instance", redirect_url) %>
|
||||
</div>
|
||||
</div>
|
||||
<%- else -%>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<% content_for "header" do %>
|
||||
<meta name="description" content="<%= translate(locale, "An alternative front-end to YouTube") %>">
|
||||
<meta name="description" content="<%= I18n.translate(locale, "An alternative front-end to YouTube") %>">
|
||||
<title>
|
||||
Invidious - <%= translate(locale, "search") %>
|
||||
Invidious - <%= I18n.translate(locale, "search") %>
|
||||
</title>
|
||||
<link rel="stylesheet" href="/css/empty.css?v=<%= ASSET_COMMIT %>">
|
||||
<% end %>
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
||||
<% if env.get? "user" %>
|
||||
<div class="pure-u-1-4">
|
||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
|
||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= I18n.translate(locale, "toggle_theme") %>">
|
||||
<% if dark_mode == "dark" %>
|
||||
<i class="icon ion-ios-sunny"></i>
|
||||
<% else %>
|
||||
@@ -52,7 +52,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
|
||||
<a id="notification_ticker" title="<%= I18n.translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
|
||||
<% notification_count = env.get("user").as(Invidious::User).notifications.size %>
|
||||
<% if CONFIG.enable_user_notifications && notification_count > 0 %>
|
||||
<span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i>
|
||||
@@ -62,7 +62,7 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<a title="<%= I18n.translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<i class="icon ion-ios-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
@@ -75,13 +75,13 @@
|
||||
<form action="/signout?referer=<%= env.get?("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<a class="pure-menu-heading" href="#">
|
||||
<input style="all:unset" type="submit" value="<%= translate(locale, "Log out") %>">
|
||||
<input style="all:unset" type="submit" value="<%= I18n.translate(locale, "Log out") %>">
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="pure-u-1-3">
|
||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
|
||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= I18n.translate(locale, "toggle_theme") %>">
|
||||
<% if dark_mode == "dark" %>
|
||||
<i class="icon ion-ios-sunny"></i>
|
||||
<% else %>
|
||||
@@ -90,14 +90,14 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<a title="<%= I18n.translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<i class="icon ion-ios-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
<% if CONFIG.login_enabled %>
|
||||
<div class="pure-u-1-3">
|
||||
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<%= translate(locale, "Log in") %>
|
||||
<%= I18n.translate(locale, "Log in") %>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
@@ -119,39 +119,39 @@
|
||||
<span>
|
||||
<i class="icon ion-logo-github"></i>
|
||||
<% if CONFIG.modified_source_code_url %>
|
||||
<a href="https://github.com/iv-org/invidious"><%= translate(locale, "footer_original_source_code") %></a> /
|
||||
<a href="<%= CONFIG.modified_source_code_url %>"><%= translate(locale, "footer_modfied_source_code") %></a>
|
||||
<a href="https://github.com/iv-org/invidious"><%= I18n.translate(locale, "footer_original_source_code") %></a> /
|
||||
<a href="<%= CONFIG.modified_source_code_url %>"><%= I18n.translate(locale, "footer_modfied_source_code") %></a>
|
||||
<% else %>
|
||||
<a href="https://github.com/iv-org/invidious"><%= translate(locale, "footer_source_code") %></a>
|
||||
<a href="https://github.com/iv-org/invidious"><%= I18n.translate(locale, "footer_source_code") %></a>
|
||||
<% end %>
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon ion-ios-paper"></i>
|
||||
<a href="https://github.com/iv-org/documentation"><%= translate(locale, "footer_documentation") %></a>
|
||||
<a href="https://github.com/iv-org/documentation"><%= I18n.translate(locale, "footer_documentation") %></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<span>
|
||||
<a href="https://github.com/iv-org/invidious/blob/master/LICENSE"><%= translate(locale, "Released under the AGPLv3 on Github.") %></a>
|
||||
<a href="https://github.com/iv-org/invidious/blob/master/LICENSE"><%= I18n.translate(locale, "Released under the AGPLv3 on Github.") %></a>
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon ion-logo-javascript"></i>
|
||||
<a rel="jslicense" href="/licenses"><%= translate(locale, "View JavaScript license information.") %></a>
|
||||
<a rel="jslicense" href="/licenses"><%= I18n.translate(locale, "View JavaScript license information.") %></a>
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon ion-ios-paper"></i>
|
||||
<a href="/privacy"><%= translate(locale, "View privacy policy.") %></a>
|
||||
<a href="/privacy"><%= I18n.translate(locale, "View privacy policy.") %></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<span>
|
||||
<i class="icon ion-ios-wallet"></i>
|
||||
<a href="https://invidious.io/donate/"><%= translate(locale, "footer_donate_page") %></a>
|
||||
<a href="https://invidious.io/donate/"><%= I18n.translate(locale, "footer_donate_page") %></a>
|
||||
</span>
|
||||
<span>
|
||||
<%= translate(locale, "Current version: ") %>
|
||||
<%= I18n.translate(locale, "Current version: ") %>
|
||||
<% if CONFIG.modified_source_code_url %>
|
||||
<a href="<%= CONFIG.modified_source_code_url %>/commit/<%= CURRENT_COMMIT %>"><%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %></a>
|
||||
<% else %>
|
||||
@@ -159,7 +159,7 @@
|
||||
<% end %>
|
||||
@ <%= CURRENT_BRANCH %>
|
||||
<% if CURRENT_TAG != "" %>
|
||||
(
|
||||
(
|
||||
<% if CONFIG.modified_source_code_url %>
|
||||
<a href="<%= CONFIG.modified_source_code_url %>/releases/tag/<%= CURRENT_TAG %>"><%= CURRENT_TAG %></a>
|
||||
<% else %>
|
||||
@@ -181,8 +181,8 @@
|
||||
<script id="notification_data" type="application/json">
|
||||
<%=
|
||||
{
|
||||
"upload_text" => HTML.escape(translate(locale, "`x` uploaded a video")),
|
||||
"live_upload_text" => HTML.escape(translate(locale, "`x` is live"))
|
||||
"upload_text" => HTML.escape(I18n.translate(locale, "`x` uploaded a video")),
|
||||
"live_upload_text" => HTML.escape(I18n.translate(locale, "`x` is live"))
|
||||
}.to_pretty_json
|
||||
%>
|
||||
</script>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Token") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Token") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<% if env.get? "access_token" %>
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1-3">
|
||||
<h3>
|
||||
<%= translate(locale, "Token") %>
|
||||
<%= I18n.translate(locale, "Token") %>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:center">
|
||||
<a href="/token_manager"><%= translate(locale, "Token manager") %></a>
|
||||
<a href="/token_manager"><%= I18n.translate(locale, "Token manager") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:right">
|
||||
<a href="/preferences"><%= translate(locale, "Preferences") %></a>
|
||||
<a href="/preferences"><%= I18n.translate(locale, "Preferences") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,9 +30,9 @@
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" action="/authorize_token" method="post">
|
||||
<% if callback_url %>
|
||||
<legend><%= translate(locale, "Authorize token for `x`?", "#{callback_url.scheme}://#{callback_url.host}") %></legend>
|
||||
<legend><%= I18n.translate(locale, "Authorize token for `x`?", "#{callback_url.scheme}://#{callback_url.host}") %></legend>
|
||||
<% else %>
|
||||
<legend><%= translate(locale, "Authorize token?") %></legend>
|
||||
<legend><%= I18n.translate(locale, "Authorize token?") %></legend>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g">
|
||||
@@ -48,7 +48,7 @@
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-2">
|
||||
<button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Yes") %>
|
||||
<%= I18n.translate(locale, "Yes") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pure-u-1-2">
|
||||
@@ -57,7 +57,7 @@
|
||||
<% else %>
|
||||
<a class="pure-button" href="/">
|
||||
<% end %>
|
||||
<%= translate(locale, "No") %>
|
||||
<%= I18n.translate(locale, "No") %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Change password") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Change password") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g">
|
||||
@@ -7,20 +7,20 @@
|
||||
<div class="pure-u-1 pure-u-lg-3-5">
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" action="/change_password?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<legend><%= translate(locale, "Change password") %></legend>
|
||||
<legend><%= I18n.translate(locale, "Change password") %></legend>
|
||||
|
||||
<fieldset>
|
||||
<label for="password"><%= translate(locale, "Password") %> :</label>
|
||||
<input required class="pure-input-1" name="password" type="password" placeholder="<%= translate(locale, "Password") %>">
|
||||
<label for="password"><%= I18n.translate(locale, "Password") %> :</label>
|
||||
<input required class="pure-input-1" name="password" type="password" placeholder="<%= I18n.translate(locale, "Password") %>">
|
||||
|
||||
<label for="new_password[0]"><%= translate(locale, "New password") %> :</label>
|
||||
<input required class="pure-input-1" name="new_password[0]" type="password" placeholder="<%= translate(locale, "New password") %>">
|
||||
<label for="new_password[0]"><%= I18n.translate(locale, "New password") %> :</label>
|
||||
<input required class="pure-input-1" name="new_password[0]" type="password" placeholder="<%= I18n.translate(locale, "New password") %>">
|
||||
|
||||
<label for="new_password[1]"><%= translate(locale, "New password") %> :</label>
|
||||
<input required class="pure-input-1" name="new_password[1]" type="password" placeholder="<%= translate(locale, "New password") %>">
|
||||
<label for="new_password[1]"><%= I18n.translate(locale, "New password") %> :</label>
|
||||
<input required class="pure-input-1" name="new_password[1]" type="password" placeholder="<%= I18n.translate(locale, "New password") %>">
|
||||
|
||||
<button type="submit" name="action" value="change_password" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Change password") %>
|
||||
<%= I18n.translate(locale, "Change password") %>
|
||||
</button>
|
||||
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(csrf_token) %>">
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Clear watch history") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Clear watch history") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" action="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<legend><%= translate(locale, "Clear watch history?") %></legend>
|
||||
<legend><%= I18n.translate(locale, "Clear watch history?") %></legend>
|
||||
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-2">
|
||||
<button type="submit" name="submit" value="clear_watch_history" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Yes") %>
|
||||
<%= I18n.translate(locale, "Yes") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pure-u-1-2">
|
||||
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
|
||||
<%= translate(locale, "No") %>
|
||||
<%= I18n.translate(locale, "No") %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Import and Export Data") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Import and Export Data") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" enctype="multipart/form-data" action="/data_control?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<fieldset>
|
||||
<legend><%= translate(locale, "Import") %></legend>
|
||||
<legend><%= I18n.translate(locale, "Import") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_invidious"><%= translate(locale, "Import Invidious data") %></label>
|
||||
<label for="import_invidious"><%= I18n.translate(locale, "Import Invidious data") %></label>
|
||||
<input type="file" id="import_invidious" name="import_invidious">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_youtube">
|
||||
<a rel="noopener noreferrer" target="_blank" href="https://github.com/iv-org/documentation/blob/master/docs/export-youtube-subscriptions.md">
|
||||
<%= translate(locale, "Import YouTube subscriptions") %>
|
||||
<%= I18n.translate(locale, "Import YouTube subscriptions") %>
|
||||
</a>
|
||||
</label>
|
||||
<input type="file" id="import_youtube" name="import_youtube">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_youtube_pl"><%= translate(locale, "Import YouTube playlist (.csv)") %></label>
|
||||
<label for="import_youtube_pl"><%= I18n.translate(locale, "Import YouTube playlist (.csv)") %></label>
|
||||
<input type="file" id="import_youtube_pl" name="import_youtube_pl">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_youtube_wh"><%= translate(locale, "Import YouTube watch history (.json)") %></label>
|
||||
<label for="import_youtube_wh"><%= I18n.translate(locale, "Import YouTube watch history (.json)") %></label>
|
||||
<input type="file" id="import_youtube_wh" name="import_youtube_wh">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_freetube"><%= translate(locale, "Import FreeTube subscriptions (.db)") %></label>
|
||||
<label for="import_freetube"><%= I18n.translate(locale, "Import FreeTube subscriptions (.db)") %></label>
|
||||
<input type="file" id="import_freetube" name="import_freetube">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_newpipe_subscriptions"><%= translate(locale, "Import NewPipe subscriptions (.json)") %></label>
|
||||
<label for="import_newpipe_subscriptions"><%= I18n.translate(locale, "Import NewPipe subscriptions (.json)") %></label>
|
||||
<input type="file" id="import_newpipe_subscriptions" name="import_newpipe_subscriptions">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="import_newpipe"><%= translate(locale, "Import NewPipe data (.zip)") %></label>
|
||||
<label for="import_newpipe"><%= I18n.translate(locale, "Import NewPipe data (.zip)") %></label>
|
||||
<input type="file" id="import_newpipe" name="import_newpipe">
|
||||
</div>
|
||||
|
||||
<div class="pure-controls">
|
||||
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Import") %></button>
|
||||
<button type="submit" class="pure-button pure-button-primary"><%= I18n.translate(locale, "Import") %></button>
|
||||
</div>
|
||||
|
||||
<legend><%= translate(locale, "Export") %></legend>
|
||||
<legend><%= I18n.translate(locale, "Export") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1"><%= translate(locale, "Export subscriptions as OPML") %></a>
|
||||
<a href="/subscription_manager?action_takeout=1"><%= I18n.translate(locale, "Export subscriptions as OPML") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1&format=newpipe"><%= translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a>
|
||||
<a href="/subscription_manager?action_takeout=1&format=newpipe"><%= I18n.translate(locale, "Export subscriptions as OPML (for NewPipe & FreeTube)") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager?action_takeout=1&format=json"><%= translate(locale, "Export data as JSON") %></a>
|
||||
<a href="/subscription_manager?action_takeout=1&format=json"><%= I18n.translate(locale, "Export data as JSON") %></a>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Delete account") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Delete account") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" action="/delete_account?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<legend><%= translate(locale, "Delete account?") %></legend>
|
||||
<legend><%= I18n.translate(locale, "Delete account?") %></legend>
|
||||
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-2">
|
||||
<button type="submit" name="submit" value="delete_account" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Yes") %>
|
||||
<%= I18n.translate(locale, "Yes") %>
|
||||
</button>
|
||||
</div>
|
||||
<div class="pure-u-1-2">
|
||||
<a class="pure-button" href="<%= URI.encode_www_form(referer) %>">
|
||||
<%= translate(locale, "No") %>
|
||||
<%= I18n.translate(locale, "No") %>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Log in") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Log in") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g">
|
||||
@@ -13,15 +13,15 @@
|
||||
<% if email %>
|
||||
<input name="email" type="hidden" value="<%= HTML.escape(email) %>">
|
||||
<% else %>
|
||||
<label for="email"><%= translate(locale, "User ID") %> :</label>
|
||||
<input required class="pure-input-1" name="email" type="text" placeholder="<%= translate(locale, "User ID") %>">
|
||||
<label for="email"><%= I18n.translate(locale, "User ID") %> :</label>
|
||||
<input required class="pure-input-1" name="email" type="text" placeholder="<%= I18n.translate(locale, "User ID") %>">
|
||||
<% end %>
|
||||
|
||||
<% if password %>
|
||||
<input name="password" type="hidden" value="<%= HTML.escape(password) %>">
|
||||
<% else %>
|
||||
<label for="password"><%= translate(locale, "Password") %> :</label>
|
||||
<input required class="pure-input-1" name="password" type="password" placeholder="<%= translate(locale, "Password") %>">
|
||||
<label for="password"><%= I18n.translate(locale, "Password") %> :</label>
|
||||
<input required class="pure-input-1" name="password" type="password" placeholder="<%= I18n.translate(locale, "Password") %>">
|
||||
<% end %>
|
||||
|
||||
<% if captcha %>
|
||||
@@ -30,15 +30,15 @@
|
||||
<% captcha[:tokens].each_with_index do |token, i| %>
|
||||
<input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>">
|
||||
<% end %>
|
||||
<label for="answer"><%= translate(locale, "Time (h:mm:ss):") %></label>
|
||||
<label for="answer"><%= I18n.translate(locale, "Time (h:mm:ss):") %></label>
|
||||
<input type="text" name="answer" type="text" placeholder="h:mm:ss">
|
||||
|
||||
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Register") %>
|
||||
<%= I18n.translate(locale, "Register") %>
|
||||
</button>
|
||||
<% else %>
|
||||
<button type="submit" name="action" value="signin" class="pure-button pure-button-primary">
|
||||
<%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %>
|
||||
<%= I18n.translate(locale, "Sign In") %>/<%= I18n.translate(locale, "Register") %>
|
||||
</button>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Preferences") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Preferences") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="h-box">
|
||||
<form class="pure-form pure-form-aligned" action="/preferences?referer=<%= URI.encode_www_form(referer) %>" method="post">
|
||||
<fieldset>
|
||||
<legend><%= translate(locale, "preferences_category_player") %></legend>
|
||||
<legend><%= I18n.translate(locale, "preferences_category_player") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="video_loop"><%= translate(locale, "preferences_video_loop_label") %></label>
|
||||
<label for="video_loop"><%= I18n.translate(locale, "preferences_video_loop_label") %></label>
|
||||
<input name="video_loop" id="video_loop" type="checkbox" <% if preferences.video_loop %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="preload"><%= translate(locale, "preferences_preload_label") %></label>
|
||||
<label for="preload"><%= I18n.translate(locale, "preferences_preload_label") %></label>
|
||||
<input name="preload" id="preload" type="checkbox" <% if preferences.preload %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="autoplay"><%= translate(locale, "preferences_autoplay_label") %></label>
|
||||
<label for="autoplay"><%= I18n.translate(locale, "preferences_autoplay_label") %></label>
|
||||
<input name="autoplay" id="autoplay" type="checkbox" <% if preferences.autoplay %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="continue"><%= translate(locale, "preferences_continue_label") %></label>
|
||||
<label for="continue"><%= I18n.translate(locale, "preferences_continue_label") %></label>
|
||||
<input name="continue" id="continue" type="checkbox" <% if preferences.continue %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="continue_autoplay"><%= translate(locale, "preferences_continue_autoplay_label") %></label>
|
||||
<label for="continue_autoplay"><%= I18n.translate(locale, "preferences_continue_autoplay_label") %></label>
|
||||
<input name="continue_autoplay" id="continue_autoplay" type="checkbox" <% if preferences.continue_autoplay %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="local"><%= translate(locale, "preferences_local_label") %></label>
|
||||
<label for="local"><%= I18n.translate(locale, "preferences_local_label") %></label>
|
||||
<input name="local" id="local" type="checkbox" <% if preferences.local && !CONFIG.disabled?("local") %>checked<% end %> <% if CONFIG.disabled?("local") %>disabled<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="listen"><%= translate(locale, "preferences_listen_label") %></label>
|
||||
<label for="listen"><%= I18n.translate(locale, "preferences_listen_label") %></label>
|
||||
<input name="listen" id="listen" type="checkbox" <% if preferences.listen %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="speed"><%= translate(locale, "preferences_speed_label") %></label>
|
||||
<label for="speed"><%= I18n.translate(locale, "preferences_speed_label") %></label>
|
||||
<select name="speed" id="speed">
|
||||
<% {2.0, 1.75, 1.5, 1.25, 1.0, 0.75, 0.5, 0.25}.each do |option| %>
|
||||
<option <% if preferences.speed == option %> selected <% end %>><%= option %></option>
|
||||
@@ -52,11 +52,11 @@
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="quality"><%= translate(locale, "preferences_quality_label") %></label>
|
||||
<label for="quality"><%= I18n.translate(locale, "preferences_quality_label") %></label>
|
||||
<select name="quality" id="quality">
|
||||
<% {"dash", "hd720", "medium", "small"}.each do |option| %>
|
||||
<% if !(option == "dash" && CONFIG.disabled?("dash")) %>
|
||||
<option value="<%= option %>" <% if preferences.quality == option %> selected <% end %>><%= translate(locale, "preferences_quality_option_" + option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.quality == option %> selected <% end %>><%= I18n.translate(locale, "preferences_quality_option_" + option) %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</select>
|
||||
@@ -64,74 +64,74 @@
|
||||
|
||||
<% if !CONFIG.disabled?("dash") %>
|
||||
<div class="pure-control-group">
|
||||
<label for="quality_dash"><%= translate(locale, "preferences_quality_dash_label") %></label>
|
||||
<label for="quality_dash"><%= I18n.translate(locale, "preferences_quality_dash_label") %></label>
|
||||
<select name="quality_dash" id="quality_dash">
|
||||
<% {"auto", "best", "4320p", "2160p", "1440p", "1080p", "720p", "480p", "360p", "240p", "144p", "worst"}.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.quality_dash == option %> selected <% end %>><%= translate(locale, "preferences_quality_dash_option_" + option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.quality_dash == option %> selected <% end %>><%= I18n.translate(locale, "preferences_quality_dash_option_" + option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="volume"><%= translate(locale, "preferences_volume_label") %></label>
|
||||
<label for="volume"><%= I18n.translate(locale, "preferences_volume_label") %></label>
|
||||
<input name="volume" id="volume" data-onrange="update_volume_value" type="range" min="0" max="100" step="5" value="<%= preferences.volume %>">
|
||||
<span class="pure-form-message-inline" id="volume-value"><%= preferences.volume %></span>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="comments[0]"><%= translate(locale, "preferences_comments_label") %></label>
|
||||
<label for="comments[0]"><%= I18n.translate(locale, "preferences_comments_label") %></label>
|
||||
<% preferences.comments.each_with_index do |comments, index| %>
|
||||
<select name="comments[<%= index %>]" id="comments[<%= index %>]">
|
||||
<% {"", "youtube", "reddit"}.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.comments[index] == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="captions[0]"><%= translate(locale, "preferences_captions_label") %></label>
|
||||
<label for="captions[0]"><%= I18n.translate(locale, "preferences_captions_label") %></label>
|
||||
<% preferences.captions.each_with_index do |caption, index| %>
|
||||
<select class="pure-u-1-6" name="captions[<%= index %>]" id="captions[<%= index %>]">
|
||||
<% Invidious::Videos::Captions::LANGUAGES.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.captions[index] == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="related_videos"><%= translate(locale, "preferences_related_videos_label") %></label>
|
||||
<label for="related_videos"><%= I18n.translate(locale, "preferences_related_videos_label") %></label>
|
||||
<input name="related_videos" id="related_videos" type="checkbox" <% if preferences.related_videos %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="annotations"><%= translate(locale, "preferences_annotations_label") %></label>
|
||||
<label for="annotations"><%= I18n.translate(locale, "preferences_annotations_label") %></label>
|
||||
<input name="annotations" id="annotations" type="checkbox" <% if preferences.annotations %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="extend_desc"><%= translate(locale, "preferences_extend_desc_label") %></label>
|
||||
<label for="extend_desc"><%= I18n.translate(locale, "preferences_extend_desc_label") %></label>
|
||||
<input name="extend_desc" id="extend_desc" type="checkbox" <% if preferences.extend_desc %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="vr_mode"><%= translate(locale, "preferences_vr_mode_label") %></label>
|
||||
<label for="vr_mode"><%= I18n.translate(locale, "preferences_vr_mode_label") %></label>
|
||||
<input name="vr_mode" id="vr_mode" type="checkbox" <% if preferences.vr_mode %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="save_player_pos"><%= translate(locale, "preferences_save_player_pos_label") %></label>
|
||||
<label for="save_player_pos"><%= I18n.translate(locale, "preferences_save_player_pos_label") %></label>
|
||||
<input name="save_player_pos" id="save_player_pos" type="checkbox" <% if preferences.save_player_pos %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<% if user = env.get?("user").try &.as(User) %>
|
||||
<% playlists = Invidious::Database::Playlists.select_user_created_playlists(user.email) %>
|
||||
<div class="pure-control-group">
|
||||
<label for="default_playlist"><%= translate(locale, "preferences_default_playlist") %></label>
|
||||
<label for="default_playlist"><%= I18n.translate(locale, "preferences_default_playlist") %></label>
|
||||
<select name="default_playlist" id="default_playlist">
|
||||
<option value=""><%= translate(locale, "preferences_default_playlist_none") %></option>
|
||||
<option value=""><%= I18n.translate(locale, "preferences_default_playlist_none") %></option>
|
||||
<% playlists.each do |plid, playlist_title| %>
|
||||
<option value="<%= plid %>" <%= "selected" if user.preferences.default_playlist == plid %>><%= HTML.escape(playlist_title) %></option>
|
||||
<% end %>
|
||||
@@ -139,46 +139,46 @@
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<legend><%= translate(locale, "preferences_category_visual") %></legend>
|
||||
<legend><%= I18n.translate(locale, "preferences_category_visual") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="locale"><%= translate(locale, "preferences_locale_label") %></label>
|
||||
<label for="locale"><%= I18n.translate(locale, "preferences_locale_label") %></label>
|
||||
<select name="locale" id="locale">
|
||||
<% LOCALES_LIST.each do |iso_name, full_name| %>
|
||||
<% I18n::LOCALES_LIST.each do |iso_name, full_name| %>
|
||||
<option value="<%= iso_name %>" <% if preferences.locale == iso_name %> selected <% end %>><%= HTML.escape(full_name) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="region"><%= translate(locale, "preferences_region_label") %></label>
|
||||
<label for="region"><%= I18n.translate(locale, "preferences_region_label") %></label>
|
||||
<select name="region" id="region">
|
||||
<% CONTENT_REGIONS.each do |option| %>
|
||||
<% I18n::CONTENT_REGIONS.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.region == option %> selected <% end %>><%= option %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="player_style"><%= translate(locale, "preferences_player_style_label") %></label>
|
||||
<label for="player_style"><%= I18n.translate(locale, "preferences_player_style_label") %></label>
|
||||
<select name="player_style" id="player_style">
|
||||
<% {"invidious", "youtube"}.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.player_style == option %> selected <% end %>><%= I18n.translate(locale, option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="dark_mode"><%= translate(locale, "preferences_dark_mode_label") %></label>
|
||||
<label for="dark_mode"><%= I18n.translate(locale, "preferences_dark_mode_label") %></label>
|
||||
<select name="dark_mode" id="dark_mode">
|
||||
<% {"", "light", "dark"}.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= translate(locale, option.blank? ? "auto" : option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.dark_mode == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "auto" : option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="thin_mode"><%= translate(locale, "preferences_thin_mode_label") %></label>
|
||||
<label for="thin_mode"><%= I18n.translate(locale, "preferences_thin_mode_label") %></label>
|
||||
<input name="thin_mode" id="thin_mode" type="checkbox" <% if preferences.thin_mode %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
@@ -189,187 +189,193 @@
|
||||
<% end %>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="default_home"><%= translate(locale, "preferences_default_home_label") %></label>
|
||||
<label for="default_home"><%= I18n.translate(locale, "preferences_default_home_label") %></label>
|
||||
<select name="default_home" id="default_home">
|
||||
<% feed_options.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.default_home == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "Search" : option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="feed_menu"><%= translate(locale, "preferences_feed_menu_label") %></label>
|
||||
<label for="feed_menu"><%= I18n.translate(locale, "preferences_feed_menu_label") %></label>
|
||||
<% (feed_options.size - 1).times do |index| %>
|
||||
<select name="feed_menu[<%= index %>]" id="feed_menu[<%= index %>]">
|
||||
<% feed_options.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "Search" : option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.feed_menu[index]? == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "Search" : option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if env.get? "user" %>
|
||||
<div class="pure-control-group">
|
||||
<label for="show_nick"><%= translate(locale, "preferences_show_nick_label") %></label>
|
||||
<label for="show_nick"><%= I18n.translate(locale, "preferences_show_nick_label") %></label>
|
||||
<input name="show_nick" id="show_nick" type="checkbox" <% if preferences.show_nick %>checked<% end %>>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<legend><%= translate(locale, "preferences_category_misc") %></legend>
|
||||
<legend><%= I18n.translate(locale, "preferences_category_misc") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="automatic_instance_redirect"><%= translate(locale, "preferences_automatic_instance_redirect_label") %></label>
|
||||
<label for="automatic_instance_redirect"><%= I18n.translate(locale, "preferences_automatic_instance_redirect_label") %></label>
|
||||
<input name="automatic_instance_redirect" id="automatic_instance_redirect" type="checkbox" <% if preferences.automatic_instance_redirect %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="search_privacy"><%= I18n.translate(locale, "preferences_search_privacy_label") %></label>
|
||||
<input name="search_privacy" id="search_privacy" type="checkbox" <% if preferences.search_privacy %>checked<% end %>>
|
||||
<span class="preference-description"><%= I18n.translate(locale, "preferences_search_privacy_description") %></span>
|
||||
</div>
|
||||
|
||||
<% if env.get? "user" %>
|
||||
<legend><%= translate(locale, "preferences_category_subscription") %></legend>
|
||||
<legend><%= I18n.translate(locale, "preferences_category_subscription") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="watch_history"><%= translate(locale, "preferences_watch_history_label") %></label>
|
||||
<label for="watch_history"><%= I18n.translate(locale, "preferences_watch_history_label") %></label>
|
||||
<input name="watch_history" id="watch_history" type="checkbox" <% if preferences.watch_history %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="annotations_subscribed"><%= translate(locale, "preferences_annotations_subscribed_label") %></label>
|
||||
<label for="annotations_subscribed"><%= I18n.translate(locale, "preferences_annotations_subscribed_label") %></label>
|
||||
<input name="annotations_subscribed" id="annotations_subscribed" type="checkbox" <% if preferences.annotations_subscribed %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="max_results"><%= translate(locale, "preferences_max_results_label") %></label>
|
||||
<label for="max_results"><%= I18n.translate(locale, "preferences_max_results_label") %></label>
|
||||
<input name="max_results" id="max_results" type="number" value="<%= preferences.max_results %>">
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="sort"><%= translate(locale, "preferences_sort_label") %></label>
|
||||
<label for="sort"><%= I18n.translate(locale, "preferences_sort_label") %></label>
|
||||
<select name="sort" id="sort">
|
||||
<% {"published", "published - reverse", "alphabetically", "alphabetically - reverse", "channel name", "channel name - reverse"}.each do |option| %>
|
||||
<option value="<%= option %>" <% if preferences.sort == option %> selected <% end %>><%= translate(locale, option) %></option>
|
||||
<option value="<%= option %>" <% if preferences.sort == option %> selected <% end %>><%= I18n.translate(locale, option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<% if preferences.unseen_only %>
|
||||
<label for="latest_only"><%= translate(locale, "Only show latest unwatched video from channel: ") %></label>
|
||||
<label for="latest_only"><%= I18n.translate(locale, "Only show latest unwatched video from channel: ") %></label>
|
||||
<% else %>
|
||||
<label for="latest_only"><%= translate(locale, "Only show latest video from channel: ") %></label>
|
||||
<label for="latest_only"><%= I18n.translate(locale, "Only show latest video from channel: ") %></label>
|
||||
<% end %>
|
||||
<input name="latest_only" id="latest_only" type="checkbox" <% if preferences.latest_only %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="unseen_only"><%= translate(locale, "preferences_unseen_only_label") %></label>
|
||||
<label for="unseen_only"><%= I18n.translate(locale, "preferences_unseen_only_label") %></label>
|
||||
<input name="unseen_only" id="unseen_only" type="checkbox" <% if preferences.unseen_only %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<% if CONFIG.enable_user_notifications %>
|
||||
<div class="pure-control-group">
|
||||
<label for="notifications_only"><%= translate(locale, "preferences_notifications_only_label") %></label>
|
||||
<label for="notifications_only"><%= I18n.translate(locale, "preferences_notifications_only_label") %></label>
|
||||
<input name="notifications_only" id="notifications_only" type="checkbox" <% if preferences.notifications_only %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<% # Web notifications are only supported over HTTPS %>
|
||||
<% if Kemal.config.ssl || CONFIG.https_only %>
|
||||
<div class="pure-control-group">
|
||||
<a href="#" data-onclick="notification_requestPermission"><%= translate(locale, "Enable web notifications") %></a>
|
||||
<a href="#" data-onclick="notification_requestPermission"><%= I18n.translate(locale, "Enable web notifications") %></a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% if env.get?("user") && CONFIG.admins.includes? env.get?("user").as(Invidious::User).email %>
|
||||
<legend><%= translate(locale, "preferences_category_admin") %></legend>
|
||||
<legend><%= I18n.translate(locale, "preferences_category_admin") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="admin_default_home"><%= translate(locale, "preferences_default_home_label") %></label>
|
||||
<label for="admin_default_home"><%= I18n.translate(locale, "preferences_default_home_label") %></label>
|
||||
<select name="admin_default_home" id="admin_default_home">
|
||||
<% feed_options.each do |option| %>
|
||||
<option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<option value="<%= option %>" <% if CONFIG.default_user_preferences.default_home == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="admin_feed_menu"><%= translate(locale, "preferences_feed_menu_label") %></label>
|
||||
<label for="admin_feed_menu"><%= I18n.translate(locale, "preferences_feed_menu_label") %></label>
|
||||
<% (feed_options.size - 1).times do |index| %>
|
||||
<select name="admin_feed_menu[<%= index %>]" id="admin_feed_menu[<%= index %>]">
|
||||
<% feed_options.each do |option| %>
|
||||
<option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<option value="<%= option %>" <% if CONFIG.default_user_preferences.feed_menu[index]? == option %> selected <% end %>><%= I18n.translate(locale, option.blank? ? "none" : option) %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="popular_enabled"><%= translate(locale, "Popular enabled: ") %></label>
|
||||
<label for="popular_enabled"><%= I18n.translate(locale, "Popular enabled: ") %></label>
|
||||
<input name="popular_enabled" id="popular_enabled" type="checkbox" <% if CONFIG.popular_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="captcha_enabled"><%= translate(locale, "CAPTCHA enabled: ") %></label>
|
||||
<label for="captcha_enabled"><%= I18n.translate(locale, "CAPTCHA enabled: ") %></label>
|
||||
<input name="captcha_enabled" id="captcha_enabled" type="checkbox" <% if CONFIG.captcha_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="login_enabled"><%= translate(locale, "Login enabled: ") %></label>
|
||||
<label for="login_enabled"><%= I18n.translate(locale, "Login enabled: ") %></label>
|
||||
<input name="login_enabled" id="login_enabled" type="checkbox" <% if CONFIG.login_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="registration_enabled"><%= translate(locale, "Registration enabled: ") %></label>
|
||||
<label for="registration_enabled"><%= I18n.translate(locale, "Registration enabled: ") %></label>
|
||||
<input name="registration_enabled" id="registration_enabled" type="checkbox" <% if CONFIG.registration_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="statistics_enabled"><%= translate(locale, "Report statistics: ") %></label>
|
||||
<label for="statistics_enabled"><%= I18n.translate(locale, "Report statistics: ") %></label>
|
||||
<input name="statistics_enabled" id="statistics_enabled" type="checkbox" <% if CONFIG.statistics_enabled %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="modified_source_code_url"><%= translate(locale, "adminprefs_modified_source_code_url_label") %></label>
|
||||
<label for="modified_source_code_url"><%= I18n.translate(locale, "adminprefs_modified_source_code_url_label") %></label>
|
||||
<input name="modified_source_code_url" id="modified_source_code_url" type="url" value="<%= CONFIG.modified_source_code_url %>">
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if env.get? "user" %>
|
||||
<legend><%= translate(locale, "preferences_category_data") %></legend>
|
||||
<legend><%= I18n.translate(locale, "preferences_category_data") %></legend>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Clear watch history") %></a>
|
||||
<a href="/clear_watch_history?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Clear watch history") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Change password") %></a>
|
||||
<a href="/change_password?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Change password") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Import/export data") %></a>
|
||||
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Import/export data") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a>
|
||||
<a href="/subscription_manager"><%= I18n.translate(locale, "Manage subscriptions") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/token_manager"><%= translate(locale, "Manage tokens") %></a>
|
||||
<a href="/token_manager"><%= I18n.translate(locale, "Manage tokens") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/feed/playlists"><%= translate(locale, "View all playlists") %></a>
|
||||
<a href="/feed/playlists"><%= I18n.translate(locale, "View all playlists") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
||||
<a href="/feed/history"><%= I18n.translate(locale, "Watch history") %></a>
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Delete account") %></a>
|
||||
<a href="/delete_account?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Delete account") %></a>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-controls">
|
||||
<button type="submit" class="pure-button pure-button-primary"><%= translate(locale, "Save preferences") %></button>
|
||||
<button type="submit" class="pure-button pure-button-primary"><%= I18n.translate(locale, "Save preferences") %></button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Subscription manager") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Subscription manager") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1-3">
|
||||
<h3>
|
||||
<a href="/feed/subscriptions">
|
||||
<%= translate_count(locale, "generic_subscriptions_count", subscriptions.size, NumberFormatting::HtmlSpan) %>
|
||||
<%= I18n.translate_count(locale, "generic_subscriptions_count", subscriptions.size, I18n::NumberFormatting::HtmlSpan) %>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:center">
|
||||
<a href="/feed/history">
|
||||
<%= translate(locale, "Watch history") %>
|
||||
<%= I18n.translate(locale, "Watch history") %>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:right">
|
||||
<a href="/data_control?referer=<%= URI.encode_www_form(referer) %>">
|
||||
<%= translate(locale, "Import/export") %>
|
||||
<%= I18n.translate(locale, "Import/export") %>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<h3 style="padding-right:0.5em">
|
||||
<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") %>">
|
||||
<input style="all:unset" type="submit" data-onclick="remove_subscription" data-ucid="<%= channel.id %>" value="<%= I18n.translate(locale, "unsubscribe") %>">
|
||||
</form>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<% content_for "header" do %>
|
||||
<title><%= translate(locale, "Token manager") %> - Invidious</title>
|
||||
<title><%= I18n.translate(locale, "Token manager") %> - Invidious</title>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-1-3">
|
||||
<h3>
|
||||
<%= translate_count(locale, "tokens_count", tokens.size, NumberFormatting::HtmlSpan) %>
|
||||
<%= I18n.translate_count(locale, "tokens_count", tokens.size, I18n::NumberFormatting::HtmlSpan) %>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3"></div>
|
||||
<div class="pure-u-1-3" style="text-align:right">
|
||||
<h3>
|
||||
<a href="/preferences?referer=<%= URI.encode_www_form(referer) %>"><%= translate(locale, "Preferences") %></a>
|
||||
<a href="/preferences?referer=<%= URI.encode_www_form(referer) %>"><%= I18n.translate(locale, "Preferences") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25,13 +25,13 @@
|
||||
</h4>
|
||||
</div>
|
||||
<div class="pure-u-1-5" style="text-align:center">
|
||||
<h4><%= translate(locale, "`x` ago", recode_date(token[:issued], locale)) %></h4>
|
||||
<h4><%= I18n.translate(locale, "`x` ago", recode_date(token[:issued], locale)) %></h4>
|
||||
</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&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") %>">
|
||||
<input style="all:unset" type="submit" data-onclick="revoke_token" data-session="<%= token[:session] %>" value="<%= I18n.translate(locale, "revoke") %>">
|
||||
</form>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
@@ -35,11 +35,11 @@ we're going to need to do it here in order to allow for translations.
|
||||
-->
|
||||
<style>
|
||||
#descexpansionbutton ~ label > a::after {
|
||||
content: "<%= translate(locale, "Show more") %>"
|
||||
content: "<%= I18n.translate(locale, "Show more") %>"
|
||||
}
|
||||
|
||||
#descexpansionbutton:checked ~ label > a::after {
|
||||
content: "<%= translate(locale, "Show less") %>"
|
||||
content: "<%= I18n.translate(locale, "Show less") %>"
|
||||
}
|
||||
</style>
|
||||
<% end %>
|
||||
@@ -53,12 +53,12 @@ we're going to need to do it here in order to allow for translations.
|
||||
"length_seconds" => video.length_seconds.to_f,
|
||||
"play_next" => !video.related_videos.empty? && !plid && params.continue,
|
||||
"next_video" => video.related_videos.select { |rv| rv["id"]? }[0]?.try &.["id"],
|
||||
"youtube_comments_text" => HTML.escape(translate(locale, "View YouTube comments")),
|
||||
"reddit_comments_text" => HTML.escape(translate(locale, "View Reddit comments")),
|
||||
"reddit_permalink_text" => HTML.escape(translate(locale, "View more comments on Reddit")),
|
||||
"comments_text" => HTML.escape(translate(locale, "View `x` comments", "{commentCount}")),
|
||||
"hide_replies_text" => HTML.escape(translate(locale, "Hide replies")),
|
||||
"show_replies_text" => HTML.escape(translate(locale, "Show replies")),
|
||||
"youtube_comments_text" => HTML.escape(I18n.translate(locale, "View YouTube comments")),
|
||||
"reddit_comments_text" => HTML.escape(I18n.translate(locale, "View Reddit comments")),
|
||||
"reddit_permalink_text" => HTML.escape(I18n.translate(locale, "View more comments on Reddit")),
|
||||
"comments_text" => HTML.escape(I18n.translate(locale, "View `x` comments", "{commentCount}")),
|
||||
"hide_replies_text" => HTML.escape(I18n.translate(locale, "Hide replies")),
|
||||
"show_replies_text" => HTML.escape(I18n.translate(locale, "Show replies")),
|
||||
"params" => params,
|
||||
"preferences" => preferences,
|
||||
"premiere_timestamp" => video.premiere_timestamp.try &.to_unix,
|
||||
@@ -79,11 +79,11 @@ we're going to need to do it here in order to allow for translations.
|
||||
<h1>
|
||||
<%= title %>
|
||||
<% if params.listen %>
|
||||
<a title="<%=translate(locale, "Video mode")%>" href="/watch?<%= env.params.query %>&listen=0">
|
||||
<a title="<%=I18n.translate(locale, "Video mode")%>" id="link-iv-listen" data-base-url="/watch?<%= env.params.query %>&listen=0" href="/watch?<%= env.params.query %>&listen=0">
|
||||
<i class="icon ion-ios-videocam"></i>
|
||||
</a>
|
||||
<% else %>
|
||||
<a title="<%=translate(locale, "Audio mode")%>" href="/watch?<%= env.params.query %>&listen=1">
|
||||
<a title="<%=I18n.translate(locale, "Audio mode")%>" id="link-iv-listen" data-base-url="/watch?<%= env.params.query %>&listen=1" href="/watch?<%= env.params.query %>&listen=1">
|
||||
<i class="icon ion-md-headset"></i>
|
||||
</a>
|
||||
<% end %>
|
||||
@@ -91,7 +91,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
|
||||
<% if !video.is_listed %>
|
||||
<h3>
|
||||
<i class="icon ion-ios-unlock"></i> <%= translate(locale, "Unlisted") %>
|
||||
<i class="icon ion-ios-unlock"></i> <%= I18n.translate(locale, "Unlisted") %>
|
||||
</h3>
|
||||
<% end %>
|
||||
|
||||
@@ -101,11 +101,11 @@ we're going to need to do it here in order to allow for translations.
|
||||
</h3>
|
||||
<% elsif video.premiere_timestamp.try &.> Time.utc %>
|
||||
<h3>
|
||||
<%= video.premiere_timestamp.try { |t| translate(locale, "Premieres in `x`", recode_date((t - Time.utc).ago, locale)) } %>
|
||||
<%= video.premiere_timestamp.try { |t| I18n.translate(locale, "Premieres in `x`", recode_date((t - Time.utc).ago, locale)) } %>
|
||||
</h3>
|
||||
<% elsif video.live_now %>
|
||||
<h3>
|
||||
<%= video.premiere_timestamp.try { |t| translate(locale, "videoinfo_started_streaming_x_ago", recode_date((Time.utc - t).ago, locale)) } %>
|
||||
<%= video.premiere_timestamp.try { |t| I18n.translate(locale, "videoinfo_started_streaming_x_ago", recode_date((Time.utc - t).ago, locale)) } %>
|
||||
</h3>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -124,13 +124,13 @@ we're going to need to do it here in order to allow for translations.
|
||||
link_yt_embed = IV::HttpServer::Utils.add_params_to_url(link_yt_embed, link_yt_param)
|
||||
end
|
||||
-%>
|
||||
<a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= translate(locale, "videoinfo_watch_on_youTube") %></a>
|
||||
(<a id="link-yt-embed" rel="noreferrer noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>)
|
||||
<a id="link-yt-watch" rel="noreferrer noopener" data-base-url="<%= link_yt_watch %>" href="<%= link_yt_watch %>"><%= I18n.translate(locale, "videoinfo_watch_on_youTube") %></a>
|
||||
(<a id="link-yt-embed" referrerpolicy="origin-when-cross-origin" rel="noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= I18n.translate(locale, "videoinfo_youTube_embed_link") %></a>)
|
||||
</span>
|
||||
|
||||
<p id="watch-on-another-invidious-instance">
|
||||
<%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%>
|
||||
<a id="link-iv-other" data-base-url="<%= link_iv_other %>" href="<%= link_iv_other %>"><%= translate(locale, "Switch Invidious Instance") %></a>
|
||||
<a id="link-iv-other" data-base-url="<%= link_iv_other %>" href="<%= link_iv_other %>"><%= I18n.translate(locale, "Switch Invidious Instance") %></a>
|
||||
</p>
|
||||
|
||||
<p id="embed-link">
|
||||
@@ -141,17 +141,17 @@ we're going to need to do it here in order to allow for translations.
|
||||
link_iv_embed = URI.new(path: "/embed/#{id}")
|
||||
link_iv_embed = IV::HttpServer::Utils.add_params_to_url(link_iv_embed, params_iv_embed)
|
||||
-%>
|
||||
<a id="link-iv-embed" data-base-url="<%= link_iv_embed %>" href="<%= link_iv_embed %>"><%= translate(locale, "videoinfo_invidious_embed_link") %></a>
|
||||
<a id="link-iv-embed" data-base-url="<%= link_iv_embed %>" href="<%= link_iv_embed %>"><%= I18n.translate(locale, "videoinfo_invidious_embed_link") %></a>
|
||||
</p>
|
||||
|
||||
<p id="annotations">
|
||||
<% if params.annotations %>
|
||||
<a href="/watch?<%= env.params.query %>&iv_load_policy=3">
|
||||
<%= translate(locale, "Hide annotations") %>
|
||||
<%= I18n.translate(locale, "Hide annotations") %>
|
||||
</a>
|
||||
<% else %>
|
||||
<a href="/watch?<%= env.params.query %>&iv_load_policy=1">
|
||||
<%=translate(locale, "Show annotations")%>
|
||||
<%=I18n.translate(locale, "Show annotations")%>
|
||||
</a>
|
||||
<% end %>
|
||||
</p>
|
||||
@@ -161,7 +161,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
<% if !playlists.empty? %>
|
||||
<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>
|
||||
<label for="playlist_id"><%= I18n.translate(locale, "Add to playlist: ") %></label>
|
||||
<select style="width:100%" name="playlist_id" id="playlist_id">
|
||||
<% playlists.each do |plid, playlist_title| %>
|
||||
<option data-plid="<%= plid %>" value="<%= plid %>" <%= "selected" if user.preferences.default_playlist == plid %>><%= HTML.escape(playlist_title) %></option>
|
||||
@@ -172,7 +172,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
<input type="hidden" name="csrf_token" value="<%= URI.encode_www_form(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<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>
|
||||
<b><%= I18n.translate(locale, "Add to playlist") %></b>
|
||||
</button>
|
||||
</form>
|
||||
<script id="playlist_data" type="application/json">
|
||||
@@ -191,7 +191,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
<p id="views"><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
|
||||
<p id="likes"><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
|
||||
<p id="dislikes" style="display: none; visibility: hidden;"></p>
|
||||
<p id="genre"><%= translate(locale, "Genre: ") %>
|
||||
<p id="genre"><%= I18n.translate(locale, "Genre: ") %>
|
||||
<% if !video.genre_url %>
|
||||
<%= video.genre %>
|
||||
<% else %>
|
||||
@@ -200,21 +200,21 @@ we're going to need to do it here in order to allow for translations.
|
||||
</p>
|
||||
<% if video.license %>
|
||||
<% if video.license.empty? %>
|
||||
<p id="license"><%= translate(locale, "License: ") %><%= translate(locale, "Standard YouTube license") %></p>
|
||||
<p id="license"><%= I18n.translate(locale, "License: ") %><%= I18n.translate(locale, "Standard YouTube license") %></p>
|
||||
<% else %>
|
||||
<p id="license"><%= translate(locale, "License: ") %><%= video.license %></p>
|
||||
<p id="license"><%= I18n.translate(locale, "License: ") %><%= video.license %></p>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<p id="family_friendly"><%= translate(locale, "Family friendly? ") %><%= translate_bool(locale, video.is_family_friendly) %></p>
|
||||
<p id="family_friendly"><%= I18n.translate(locale, "Family friendly? ") %><%= I18n.translate_bool(locale, video.is_family_friendly) %></p>
|
||||
<p id="wilson" style="display: none; visibility: hidden;"></p>
|
||||
<p id="rating" style="display: none; visibility: hidden;"></p>
|
||||
<p id="engagement" style="display: none; visibility: hidden;"></p>
|
||||
<% if video.allowed_regions.size != REGIONS.size %>
|
||||
<p id="allowed_regions">
|
||||
<% if video.allowed_regions.size < REGIONS.size // 2 %>
|
||||
<%= translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %>
|
||||
<%= I18n.translate(locale, "Whitelisted regions: ") %><%= video.allowed_regions.join(", ") %>
|
||||
<% else %>
|
||||
<%= translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
|
||||
<%= I18n.translate(locale, "Blacklisted regions: ") %><%= (REGIONS.to_a - video.allowed_regions).join(", ") %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -246,9 +246,9 @@ we're going to need to do it here in order to allow for translations.
|
||||
<div class="h-box">
|
||||
<p id="published-date">
|
||||
<% if video.premiere_timestamp.try &.> Time.utc %>
|
||||
<b><%= video.premiere_timestamp.try { |t| translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %></b>
|
||||
<b><%= video.premiere_timestamp.try { |t| I18n.translate(locale, "Premieres `x`", t.to_s("%B %-d, %R UTC")) } %></b>
|
||||
<% else %>
|
||||
<b><%= translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %></b>
|
||||
<b><%= I18n.translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %></b>
|
||||
<% end %>
|
||||
</p>
|
||||
|
||||
@@ -270,7 +270,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
<input id="music-desc-expansion" type="checkbox"/>
|
||||
<label for="music-desc-expansion">
|
||||
<h3 id="music-description-title">
|
||||
<%= translate(locale, "Music in this video") %>
|
||||
<%= I18n.translate(locale, "Music in this video") %>
|
||||
<span class="icon ion-ios-arrow-up"></span>
|
||||
<span class="icon ion-ios-arrow-down"></span>
|
||||
</h3>
|
||||
@@ -279,9 +279,9 @@ we're going to need to do it here in order to allow for translations.
|
||||
<div id="music-description-box">
|
||||
<% video.music.each do |music| %>
|
||||
<div class="music-item">
|
||||
<p class="music-song"><%= translate(locale, "Song: ") %><%= music.song %></p>
|
||||
<p class="music-artist"><%= translate(locale, "Artist: ") %><%= music.artist %></p>
|
||||
<p class="music-album"><%= translate(locale, "Album: ") %><%= music.album %></p>
|
||||
<p class="music-song"><%= I18n.translate(locale, "Song: ") %><%= music.song %></p>
|
||||
<p class="music-artist"><%= I18n.translate(locale, "Artist: ") %><%= music.artist %></p>
|
||||
<p class="music-album"><%= I18n.translate(locale, "Album: ") %><%= music.album %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
@@ -294,7 +294,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
<% else %>
|
||||
<noscript>
|
||||
<a href="/watch?<%= env.params.query %>&nojs=1">
|
||||
<%= translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
|
||||
<%= I18n.translate(locale, "Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.") %>
|
||||
</a>
|
||||
</noscript>
|
||||
<% end %>
|
||||
@@ -313,7 +313,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
<% if !video.related_videos.empty? %>
|
||||
<div <% if plid %>style="display:none"<% end %>>
|
||||
<div class="pure-control-group">
|
||||
<label for="continue"><%= translate(locale, "preferences_continue_label") %></label>
|
||||
<label for="continue"><%= I18n.translate(locale, "preferences_continue_label") %></label>
|
||||
<input name="continue" id="continue" type="checkbox" <% if params.continue %>checked<% end %>>
|
||||
</div>
|
||||
<hr>
|
||||
@@ -356,7 +356,7 @@ we're going to need to do it here in order to allow for translations.
|
||||
<div class="pure-u-10-24" style="text-align:right">
|
||||
<b class="width:100%"><%=
|
||||
views = short_text_to_number(rv["short_view_count"]? || "0")
|
||||
translate_count(locale, "generic_views_count", views, NumberFormatting::Short)
|
||||
I18n.translate_count(locale, "generic_views_count", views, I18n::NumberFormatting::Short)
|
||||
%></b>
|
||||
</div>
|
||||
</h5>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user