mirror of
https://github.com/iv-org/invidious.git
synced 2024-11-21 21:17:20 +00:00
Merge branch 'master' into patch-1
This commit is contained in:
commit
bcb44ab600
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve Invidious
|
||||
title: '[Bug] '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use the search function to check if the bug you found has already been reported by someone else -->
|
||||
<!-- If you want to suggest a new feature please use "Feature request" instead -->
|
||||
<!-- If you want to suggest an enhancement to an existing feature please use "Enhancement" instead -->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**Steps to Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
-->
|
||||
|
||||
**Logs**
|
||||
<!-- If applicable, copy the log that appear in the browser page where the error is reported. -->
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the problem here.
|
||||
- Browser (if applicable):
|
||||
- OS (if applicable):
|
||||
-->
|
24
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/enhancement.md
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Enhancement
|
||||
about: Suggest an enhancement for an existing feature
|
||||
title: '[Enhancement] '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use the search function to check if the desired function has already been requested by someone else -->
|
||||
<!-- If you want to suggest a new feature please use "Feature request" instead -->
|
||||
<!-- If you want to report a bug, please use "Bug report" instead -->
|
||||
|
||||
**Is your enhancement request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the enhancement here. -->
|
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: '[Feature request] '
|
||||
labels: feature-request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please use the search function to check if the desired function has already been requested by someone else -->
|
||||
<!-- If you want to suggest an enhancement to an existing feature please use "Enhancement" instead -->
|
||||
<!-- If you want to report a bug, please use "Bug report" instead -->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!-- A clear and concise description of what you want to happen. -->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
||||
- name: Install Crystal
|
||||
uses: oprypin/install-crystal@v1.2.4
|
||||
with:
|
||||
crystal: 0.35.1
|
||||
crystal: 0.36.1
|
||||
|
||||
- name: Cache Shards
|
||||
uses: actions/cache@v2
|
||||
|
256
README.md
256
README.md
@ -1,43 +1,42 @@
|
||||
# Invidious
|
||||
<h1 align="center">Invidious</h1>
|
||||
|
||||
[![Build Status](https://github.com/iv-org/invidious/workflows/Invidious%20CI/badge.svg)](https://github.com/iv-org/invidious/actions) [![Translation Status](https://hosted.weblate.org/widgets/invidious/-/translations/svg-badge.svg)](https://hosted.weblate.org/engage/invidious/)
|
||||
<h2 align="center">Invidious is an alternative front-end to YouTube.</h2>
|
||||
|
||||
## Invidious is an alternative front-end to YouTube
|
||||
---
|
||||
|
||||
## Invidious instances:
|
||||
|
||||
[Public Invidious instances are listed here.](https://github.com/iv-org/documentation/blob/master/Invidious-Instances.md)
|
||||
Public Invidious instances are listed on the documentation website: https://instances.invidious.io/
|
||||
|
||||
---
|
||||
|
||||
## Invidious features:
|
||||
|
||||
- [Copylefted libre software](https://github.com/iv-org/invidious) (AGPLv3+ licensed)
|
||||
- Audio-only mode (and no need to keep window open on mobile)
|
||||
- Lightweight (the homepage is ~4 KB compressed)
|
||||
- No ads
|
||||
- No tracking
|
||||
- Javascript is 100% optional
|
||||
- Tools for managing subscriptions:
|
||||
- Only show unseen videos
|
||||
- Only show latest (or latest unseen) video from each channel
|
||||
- Delivers notifications from all subscribed channels
|
||||
- Automatically redirect homepage to feed
|
||||
- Import subscriptions from YouTube
|
||||
- Audio-only mode (and no need to keep window open on mobile)
|
||||
- Dark mode
|
||||
- Embed support
|
||||
- Set default player options (speed, quality, autoplay, loop)
|
||||
- Support for Reddit comments in place of YouTube comments
|
||||
- Import/Export subscriptions, watch history, preferences
|
||||
- [Developer API](https://github.com/iv-org/documentation/blob/master/API.md)
|
||||
- [Developer API](https://docs.invidious.io/API.md)
|
||||
- Does not use any of the official YouTube APIs
|
||||
- Does not require JavaScript to play videos
|
||||
- No need to create a Google account to save subscriptions
|
||||
- No ads
|
||||
- No CoC
|
||||
- No CLA
|
||||
- [Multilingual](https://hosted.weblate.org/projects/invidious/#languages) (translated into many languages)
|
||||
- No Code of Conduct
|
||||
- No Contributor license Agreement
|
||||
- Available in many languages, thanks to [Weblate](https://hosted.weblate.org/projects/invidious/)
|
||||
|
||||
## Donate:
|
||||
|
||||
Bitcoin (BTC): [bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr](bitcoin:bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr)
|
||||
|
||||
Monero (XMR): [41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR](monero:41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR)
|
||||
---
|
||||
|
||||
## Screenshots:
|
||||
|
||||
@ -46,226 +45,61 @@ Monero (XMR): [41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3
|
||||
| [<img src="screenshots/01_player.png?raw=true" height="140" width="280">](screenshots/01_player.png?raw=true) | [<img src="screenshots/02_preferences.png?raw=true" height="140" width="280">](screenshots/02_preferences.png?raw=true) | [<img src="screenshots/03_subscriptions.png?raw=true" height="140" width="280">](screenshots/03_subscriptions.png?raw=true) |
|
||||
| [<img src="screenshots/04_description.png?raw=true" height="140" width="280">](screenshots/04_description.png?raw=true) | [<img src="screenshots/05_preferences.png?raw=true" height="140" width="280">](screenshots/05_preferences.png?raw=true) | [<img src="screenshots/06_subscriptions.png?raw=true" height="140" width="280">](screenshots/06_subscriptions.png?raw=true) |
|
||||
|
||||
## Installation:
|
||||
---
|
||||
|
||||
To manually compile invidious you need at least 2GB of RAM. If you have less you can setup SWAP to have a combined amount of 2 GB or use Docker instead.
|
||||
## Donate:
|
||||
|
||||
After installation take a look at the [Post-install steps](#post-install-configuration).
|
||||
Bitcoin (BTC): [bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr](bitcoin:bc1qfhe7rq3lqzuayzjxzyt9waz9ytrs09kla3tsgr)
|
||||
|
||||
### Automated installation:
|
||||
Monero (XMR): [41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR](monero:41nMCtek197boJtiUvGnTFYMatrLEpnpkQDmUECqx5Es2uX3sTKKWVhSL76suXsG3LXqkEJBrCZBgPTwJrDp1FrZJfycGPR)
|
||||
|
||||
[Invidious-Updater](https://github.com/tmiland/Invidious-Updater) is a self-contained script that can automatically install and update Invidious.
|
||||
---
|
||||
|
||||
### Docker:
|
||||
## Documentation:
|
||||
|
||||
#### Build and start cluster:
|
||||
The complete documentation is available on https://docs.invidious.io/ (or alternatively on its own [Github repository](https://github.com/iv-org/documentation)).
|
||||
|
||||
```bash
|
||||
$ docker-compose up
|
||||
```
|
||||
---
|
||||
|
||||
Then visit `localhost:3000` in your browser.
|
||||
## Extensions:
|
||||
|
||||
#### Rebuild cluster:
|
||||
[Extensions](https://docs.invidious.io/Extensions.md) can be found in the wiki, as well as documentation for integrating it into other projects.
|
||||
|
||||
```bash
|
||||
$ docker-compose build
|
||||
```
|
||||
---
|
||||
|
||||
#### Delete data and rebuild:
|
||||
|
||||
```bash
|
||||
$ docker volume rm invidious_postgresdata
|
||||
$ docker-compose build
|
||||
```
|
||||
|
||||
### Manual installation:
|
||||
|
||||
### Linux:
|
||||
|
||||
#### Install the dependencies
|
||||
|
||||
```bash
|
||||
# Arch Linux
|
||||
$ sudo pacman -S base-devel shards crystal librsvg postgresql
|
||||
|
||||
# Ubuntu or Debian
|
||||
# First you have to add the repository to your APT configuration. For easy setup just run in your command line:
|
||||
$ curl -sSL https://dist.crystal-lang.org/apt/setup.sh | sudo bash
|
||||
# That will add the signing key and the repository configuration. If you prefer to do it manually, execute the following commands:
|
||||
$ curl -sL "https://keybase.io/crystal/pgp_keys.asc" | sudo apt-key add -
|
||||
$ echo "deb https://dist.crystal-lang.org/apt crystal main" | sudo tee /etc/apt/sources.list.d/crystal.list
|
||||
$ sudo apt-get update
|
||||
$ sudo apt install crystal libssl-dev libxml2-dev libyaml-dev libgmp-dev libreadline-dev postgresql librsvg2-bin libsqlite3-dev zlib1g-dev
|
||||
```
|
||||
|
||||
#### Add an Invidious user and clone the repository
|
||||
|
||||
```bash
|
||||
$ useradd -m invidious
|
||||
$ sudo -i -u invidious
|
||||
$ git clone https://github.com/iv-org/invidious
|
||||
$ exit
|
||||
```
|
||||
|
||||
#### Set up PostgresSQL
|
||||
|
||||
```bash
|
||||
$ sudo systemctl enable --now postgresql
|
||||
$ sudo -i -u postgres
|
||||
$ psql -c "CREATE USER kemal WITH PASSWORD 'kemal';" # Change 'kemal' here to a stronger password, and update `password` in config/config.yml
|
||||
$ createdb -O kemal invidious
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/channels.sql
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/videos.sql
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/channel_videos.sql
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/users.sql
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/session_ids.sql
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/nonces.sql
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/annotations.sql
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/playlists.sql
|
||||
$ psql invidious kemal < /home/invidious/invidious/config/sql/playlist_videos.sql
|
||||
$ exit
|
||||
```
|
||||
|
||||
#### Set up Invidious
|
||||
|
||||
```bash
|
||||
$ sudo -i -u invidious
|
||||
$ cd invidious
|
||||
$ shards update && shards install
|
||||
$ crystal build src/invidious.cr --release
|
||||
# test compiled binary
|
||||
$ ./invidious # stop with ctrl c
|
||||
$ exit
|
||||
```
|
||||
|
||||
#### Systemd service:
|
||||
|
||||
```bash
|
||||
$ sudo cp /home/invidious/invidious/invidious.service /etc/systemd/system/invidious.service
|
||||
$ sudo systemctl enable --now invidious.service
|
||||
```
|
||||
|
||||
#### Logrotate:
|
||||
|
||||
```bash
|
||||
$ echo "/home/invidious/invidious/invidious.log {
|
||||
rotate 4
|
||||
weekly
|
||||
notifempty
|
||||
missingok
|
||||
compress
|
||||
minsize 1048576
|
||||
}" | sudo tee /etc/logrotate.d/invidious.logrotate
|
||||
$ sudo chmod 0644 /etc/logrotate.d/invidious.logrotate
|
||||
```
|
||||
|
||||
### MacOS:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
$ brew update
|
||||
$ brew install shards crystal postgres imagemagick librsvg
|
||||
|
||||
# Clone the repository and set up a PostgreSQL database
|
||||
$ git clone https://github.com/iv-org/invidious
|
||||
$ cd invidious
|
||||
$ brew services start postgresql
|
||||
$ psql -c "CREATE ROLE kemal WITH PASSWORD 'kemal';" # Change 'kemal' here to a stronger password, and update `password` in config/config.yml
|
||||
$ createdb -O kemal invidious
|
||||
$ psql invidious kemal < config/sql/channels.sql
|
||||
$ psql invidious kemal < config/sql/videos.sql
|
||||
$ psql invidious kemal < config/sql/channel_videos.sql
|
||||
$ psql invidious kemal < config/sql/users.sql
|
||||
$ psql invidious kemal < config/sql/session_ids.sql
|
||||
$ psql invidious kemal < config/sql/nonces.sql
|
||||
$ psql invidious kemal < config/sql/annotations.sql
|
||||
$ psql invidious kemal < config/sql/privacy.sql
|
||||
$ psql invidious kemal < config/sql/playlists.sql
|
||||
$ psql invidious kemal < config/sql/playlist_videos.sql
|
||||
|
||||
# Set up Invidious
|
||||
$ shards update && shards install
|
||||
$ crystal build src/invidious.cr --release
|
||||
```
|
||||
|
||||
## Post-install configuration:
|
||||
|
||||
Detailed configuration available in the [configuration guide](https://github.com/iv-org/documentation/blob/master/Configuration.md).
|
||||
|
||||
If you use a reverse proxy, you **must** configure invidious to properly serve request through it:
|
||||
|
||||
`https_only: true` : if your are serving your instance via https, set it to true
|
||||
|
||||
`domain: domain.ext`: if you are serving your instance via a domain name, set it here
|
||||
|
||||
`external_port: 443`: if your are serving your instance via https, set it to 443
|
||||
|
||||
## Update Invidious
|
||||
|
||||
Instructions are available in the [updating guide](https://github.com/iv-org/documentation/blob/master/Updating.md).
|
||||
|
||||
## Usage:
|
||||
|
||||
```bash
|
||||
$ ./invidious -h
|
||||
Usage: invidious [arguments]
|
||||
-b HOST, --bind HOST Host to bind (defaults to 0.0.0.0)
|
||||
-p PORT, --port PORT Port to listen for connections (defaults to 3000)
|
||||
-s, --ssl Enables SSL
|
||||
--ssl-key-file FILE SSL key file
|
||||
--ssl-cert-file FILE SSL certificate file
|
||||
-h, --help Shows this help
|
||||
-c THREADS, --channel-threads=THREADS
|
||||
Number of threads for refreshing channels (default: 1)
|
||||
-f THREADS, --feed-threads=THREADS
|
||||
Number of threads for refreshing feeds (default: 1)
|
||||
-o OUTPUT, --output=OUTPUT Redirect output (default: STDOUT)
|
||||
-v, --version Print version
|
||||
```
|
||||
|
||||
Or for development:
|
||||
|
||||
```bash
|
||||
$ curl -fsSLo- https://raw.githubusercontent.com/samueleaton/sentry/master/install.cr | crystal eval
|
||||
$ ./sentry
|
||||
🤖 Your SentryBot is vigilant. beep-boop...
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
The [documentation](https://github.com/iv-org/documentation) can be found in its own repository.
|
||||
|
||||
## Extensions
|
||||
|
||||
[Extensions](https://github.com/iv-org/documentation/blob/master/Extensions.md) can be found in the wiki, as well as documentation for integrating it into other projects.
|
||||
|
||||
## Made with Invidious
|
||||
## Made with Invidious:
|
||||
|
||||
- [FreeTube](https://github.com/FreeTubeApp/FreeTube): A libre software YouTube app for privacy.
|
||||
- [CloudTube](https://cadence.moe/cloudtube/subscriptions): A JavaScript-rich alternate YouTube player
|
||||
- [CloudTube](https://sr.ht/~cadence/tube/): A JavaScript-rich alternate YouTube player.
|
||||
- [PeerTubeify](https://gitlab.com/Cha_deL/peertubeify): On YouTube, displays a link to the same video on PeerTube, if it exists.
|
||||
- [MusicPiped](https://github.com/deep-gaurav/MusicPiped): A material design music player that streams music from YouTube.
|
||||
- [LapisTube](https://github.com/blubbll/lapis-tube): A fancy and advanced (experimental) YouTube front-end. Combined streams & custom YT features.
|
||||
- [HoloPlay](https://github.com/stephane-r/HoloPlay): Funny Android application connecting on Invidious API's with search, playlists and favoris.
|
||||
|
||||
## Contributing
|
||||
---
|
||||
|
||||
1. Fork it ( https://github.com/iv-org/invidious/fork )
|
||||
2. Create your feature branch (git checkout -b my-new-feature)
|
||||
3. Commit your changes (git commit -am 'Add some feature')
|
||||
4. Push to the branch (git push origin my-new-feature)
|
||||
5. Create a new pull request
|
||||
## Contributing:
|
||||
|
||||
#### Translation
|
||||
[![Build Status](https://github.com/iv-org/invidious/workflows/Invidious%20CI/badge.svg)](https://github.com/iv-org/invidious/actions) [![Translation Status](https://hosted.weblate.org/widgets/invidious/-/translations/svg-badge.svg)](https://hosted.weblate.org/engage/invidious/)
|
||||
|
||||
1. Fork it ( https://github.com/iv-org/invidious/fork ).
|
||||
2. Create your feature branch (git checkout -b my-new-feature).
|
||||
3. Commit your changes (git commit -am 'Add some feature').
|
||||
4. Push to the branch (git push origin my-new-feature).
|
||||
5. Create a new pull request.
|
||||
|
||||
### Translation:
|
||||
|
||||
- Log in with an account you have elsewhere, or register an account and start translating at [Hosted Weblate](https://hosted.weblate.org/engage/invidious/).
|
||||
|
||||
## Contact
|
||||
---
|
||||
|
||||
## Contact:
|
||||
|
||||
Feel free to join our [Matrix room](https://matrix.to/#/#invidious:matrix.org), or #invidious on freenode. Both platforms are bridged together.
|
||||
|
||||
## Liability
|
||||
---
|
||||
|
||||
## Liability:
|
||||
|
||||
We take no responsibility for the use of our tool, or external instances provided by third parties. We strongly recommend you abide by the valid official regulations in your country. Furthermore, we refuse liability for any inappropriate use of Invidious, such as illegal downloading. This tool is provided to you in the spirit of free, open software.
|
||||
|
||||
|
@ -21,6 +21,7 @@ var options = {
|
||||
]
|
||||
},
|
||||
html5: {
|
||||
preloadTextTracks: false,
|
||||
hls: {
|
||||
overrideNative: true
|
||||
}
|
||||
@ -430,17 +431,17 @@ window.addEventListener('keydown', e => {
|
||||
|
||||
case 'ArrowRight':
|
||||
case 'MediaFastForward':
|
||||
action = skip_seconds.bind(this, 5);
|
||||
action = skip_seconds.bind(this, 5 * player.playbackRate());
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
case 'MediaTrackPrevious':
|
||||
action = skip_seconds.bind(this, -5);
|
||||
action = skip_seconds.bind(this, -5 * player.playbackRate());
|
||||
break;
|
||||
case 'l':
|
||||
action = skip_seconds.bind(this, 10);
|
||||
action = skip_seconds.bind(this, 10 * player.playbackRate());
|
||||
break;
|
||||
case 'j':
|
||||
action = skip_seconds.bind(this, -10);
|
||||
action = skip_seconds.bind(this, -10 * player.playbackRate());
|
||||
break;
|
||||
|
||||
case '0':
|
||||
@ -548,6 +549,13 @@ if (player.share) {
|
||||
player.share(shareOptions);
|
||||
}
|
||||
|
||||
// show the preferred caption by default
|
||||
if (player_data.preferred_caption_found) {
|
||||
player.ready(() => {
|
||||
player.textTracks()[1].mode = 'showing';
|
||||
});
|
||||
}
|
||||
|
||||
// Safari audio double duration fix
|
||||
if (navigator.vendor == "Apple Computer, Inc." && video_data.params.listen) {
|
||||
player.on('loadedmetadata', function () {
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM crystallang/crystal:0.35.1-alpine AS builder
|
||||
FROM crystallang/crystal:0.36.1-alpine AS builder
|
||||
RUN apk add --no-cache curl sqlite-static
|
||||
WORKDIR /invidious
|
||||
COPY ./shard.yml ./shard.yml
|
||||
|
@ -1,10 +1,10 @@
|
||||
{
|
||||
"`x` subscribers.([^.,0-9]|^)1([^.,0-9]|$)": "`x` tilaaja",
|
||||
"`x` subscribers.": "`x` tilaajaa",
|
||||
"`x` subscribers.": "`x` tilaajaa.",
|
||||
"`x` videos.([^.,0-9]|^)1([^.,0-9]|$)": "`x` video",
|
||||
"`x` videos.": "`x` videota",
|
||||
"`x` videos.": "`x` videota.",
|
||||
"`x` playlists.([^.,0-9]|^)1([^.,0-9]|$)": "`x` soittolista.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"`x` playlists.": "`x` soittolistaa",
|
||||
"`x` playlists.": "`x` soittolistaa.",
|
||||
"LIVE": "SUORA",
|
||||
"Shared `x` ago": "Jaettu `x` sitten",
|
||||
"Unsubscribe": "Peruuta tilaus",
|
||||
@ -41,7 +41,7 @@
|
||||
"An alternative front-end to YouTube": "Vaihtoehtoinen käyttöliittymä YouTubelle",
|
||||
"JavaScript license information": "JavaScript-käyttöoikeustiedot",
|
||||
"source": "lähde",
|
||||
"Log in": "Kirjaudu sisään",
|
||||
"Log in": "Kirjaudu",
|
||||
"Log in/register": "Kirjaudu sisään / Rekisteröidy",
|
||||
"Log in with Google": "Kirjaudu sisään Googlella",
|
||||
"User ID": "Käyttäjätunnus",
|
||||
|
414
locales/he.json
Normal file
414
locales/he.json
Normal file
@ -0,0 +1,414 @@
|
||||
{
|
||||
"`x` subscribers": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` רשומים.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` רשומים."
|
||||
},
|
||||
"`x` videos": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` סרטונים.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` סרטונים."
|
||||
},
|
||||
"`x` playlists": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` פלייליסטים.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` פלייליסטים."
|
||||
},
|
||||
"LIVE": "שידור חי",
|
||||
"Shared `x` ago": "",
|
||||
"Unsubscribe": "ביטול מינוי",
|
||||
"Subscribe": "הרשמה למינוי",
|
||||
"View channel on YouTube": "צפייה בערוץ ב־YouTube",
|
||||
"View playlist on YouTube": "צפייה בפלייליסט ב־YouTube",
|
||||
"newest": "החדש ביותר",
|
||||
"oldest": "הישן ביותר",
|
||||
"popular": "פופולארי",
|
||||
"last": "אחרון",
|
||||
"Next page": "העמוד הבא",
|
||||
"Previous page": "העמוד הקודם",
|
||||
"Clear watch history?": "לנקות את היסטוריית הצפייה?",
|
||||
"New password": "סיסמה חדשה",
|
||||
"New passwords must match": "על הסיסמאות החדשות להתאים",
|
||||
"Cannot change password for Google accounts": "לא ניתן לשנות את הסיסמה לחשבונות Google",
|
||||
"Authorize token?": "",
|
||||
"Authorize token for `x`?": "",
|
||||
"Yes": "כן",
|
||||
"No": "לא",
|
||||
"Import and Export Data": "ייבוא וייצוא נתונים",
|
||||
"Import": "ייבוא",
|
||||
"Import Invidious data": "ייבוא נתוני Invidious",
|
||||
"Import YouTube subscriptions": "ייבוא מינויים מ־YouTube",
|
||||
"Import FreeTube subscriptions (.db)": "ייבוא מינויים מ־FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "ייבוא מינויים מ־NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "ייבוא נתוני NewPipe (.zip)",
|
||||
"Export": "ייצוא",
|
||||
"Export subscriptions as OPML": "ייצוא המינויים בתור OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "ייצוא המינויים בתור OPML (עבור NewPipe ו־FreeTube)",
|
||||
"Export data as JSON": "ייצוא הנתונים בתור קובץ JSON",
|
||||
"Delete account?": "למחוק את החשבון?",
|
||||
"History": "היסטוריה",
|
||||
"An alternative front-end to YouTube": "ממשק משתמש חלופי ל־YouTube",
|
||||
"JavaScript license information": "",
|
||||
"source": "source",
|
||||
"Log in": "כניסה",
|
||||
"Log in/register": "כניסה/הרשמה",
|
||||
"Log in with Google": "כניסה עם Google",
|
||||
"User ID": "שם משתמש",
|
||||
"Password": "סיסמה",
|
||||
"Time (h:mm:ss):": "זמן (h:mm:ss):",
|
||||
"Text CAPTCHA": "Text CAPTCHA",
|
||||
"Image CAPTCHA": "Image CAPTCHA",
|
||||
"Sign In": "התחברות",
|
||||
"Register": "הרשמה",
|
||||
"E-mail": "דוא״ל",
|
||||
"Google verification code": "קוד האימות של Google",
|
||||
"Preferences": "העדפות",
|
||||
"Player preferences": "העדפות הנגן",
|
||||
"Always loop: ": "",
|
||||
"Autoplay: ": "ניגון אוטומטי: ",
|
||||
"Play next by default: ": "",
|
||||
"Autoplay next video: ": "נגן אוטומטית את הסרטון הבא ",
|
||||
"Listen by default: ": "האזן כברירת מחדל: ",
|
||||
"Proxy videos: ": "",
|
||||
"Default speed: ": "מהירות ברירת המחדל: ",
|
||||
"Preferred video quality: ": "איכות הווידאו המועדפת: ",
|
||||
"Player volume: ": "עוצמת שמע בנגן: ",
|
||||
"Default comments: ": "תגובות ברירת מחדל ",
|
||||
"youtube": "יוטיוב",
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "כתוביות ברירת מחדל ",
|
||||
"Fallback captions: ": "כתוביות גיבוי ",
|
||||
"Show related videos: ": "הראה סרטונים קשורים: ",
|
||||
"Show annotations by default: ": "הראה הסברים כברירת מחדל: ",
|
||||
"Visual preferences": "העדפות חזותיות",
|
||||
"Player style: ": "סגנון הנגן: ",
|
||||
"Dark mode: ": "מצב כהה: ",
|
||||
"Theme: ": "ערכת נושא: ",
|
||||
"dark": "כהה",
|
||||
"light": "בהיר",
|
||||
"Thin mode: ": "",
|
||||
"Subscription preferences": "העדפות מינויים",
|
||||
"Show annotations by default for subscribed channels: ": "Show annotations by default for subscribed channels? ",
|
||||
"Redirect homepage to feed: ": "",
|
||||
"Number of videos shown in feed: ": "מספר הסרטונים שמוצגים בהזנה: ",
|
||||
"Sort videos by: ": "מיון הסרטונים לפי: ",
|
||||
"published": "פורסם",
|
||||
"published - reverse": "",
|
||||
"alphabetically": "בסדר אלפביתי",
|
||||
"alphabetically - reverse": "בסדר אלפביתי - הפוך",
|
||||
"channel name": "שם הערוץ",
|
||||
"channel name - reverse": "שם הערוץ - הפוך",
|
||||
"Only show latest video from channel: ": "הראה רק את הסרטון האחרון מהערוץ: ",
|
||||
"Only show latest unwatched video from channel: ": "הראה רק את הסרטון האחרון שלא נצפה מהערוץ: ",
|
||||
"Only show unwatched: ": "הראה רק סרטונים שלא נצפו ",
|
||||
"Only show notifications (if there are any): ": "הראה רק התראות (אם יש) ",
|
||||
"Enable web notifications": "",
|
||||
"`x` uploaded a video": "סרטון הועלה על ידי `x`",
|
||||
"`x` is live": "`x` בשידור חי",
|
||||
"Data preferences": "העדפות נתונים",
|
||||
"Clear watch history": "ניקוי היסטוריית הצפייה",
|
||||
"Import/export data": "ייבוא/ייצוא נתונים",
|
||||
"Change password": "שינוי הסיסמה",
|
||||
"Manage subscriptions": "ניהול מינויים",
|
||||
"Manage tokens": "",
|
||||
"Watch history": "היסטוריית צפייה",
|
||||
"Delete account": "מחיקת החשבון",
|
||||
"Administrator preferences": "",
|
||||
"Default homepage: ": "Default homepage: ",
|
||||
"Feed menu: ": "תפריט ההזנה: ",
|
||||
"Top enabled: ": "",
|
||||
"CAPTCHA enabled: ": "",
|
||||
"Login enabled: ": "",
|
||||
"Registration enabled: ": "",
|
||||
"Report statistics: ": "",
|
||||
"Save preferences": "שמירת ההעדפות",
|
||||
"Subscription manager": "מנהל המינויים",
|
||||
"Token manager": "Token manager",
|
||||
"Token": "Token",
|
||||
"`x` subscriptions": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` מינויים.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` מינויים."
|
||||
},
|
||||
"`x` tokens": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "",
|
||||
"": ""
|
||||
},
|
||||
"Import/export": "ייבוא/ייצוא",
|
||||
"unsubscribe": "ביטול מנוי",
|
||||
"revoke": "",
|
||||
"Subscriptions": "מינויים",
|
||||
"`x` unseen notifications": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` הודעות שלא נראו.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` הודעות שלא נראו."
|
||||
},
|
||||
"search": "חיפוש",
|
||||
"Log out": "יציאה",
|
||||
"Released under the AGPLv3 by Omar Roth.": "מופץ תחת רישיון AGPLv3 על ידי עמר רות׳ (Omar Roth).",
|
||||
"Source available here.": "קוד המקור זמין כאן.",
|
||||
"View JavaScript license information.": "",
|
||||
"View privacy policy.": "להצגת מדיניות הפרטיות.",
|
||||
"Trending": "הסרטונים החמים",
|
||||
"Public": "ציבורי",
|
||||
"Unlisted": "לא רשום",
|
||||
"Private": "פרטי",
|
||||
"View all playlists": "הצגת כל הפלייליסטים",
|
||||
"Updated `x` ago": "הועלה לפני `x`",
|
||||
"Delete playlist `x`?": "למחוק את פלייליסט `x`?",
|
||||
"Delete playlist": "מחיקת פלייליסט",
|
||||
"Create playlist": "יצירת פלייליסט",
|
||||
"Title": "",
|
||||
"Playlist privacy": "Playlist privacy",
|
||||
"Editing playlist `x`": "",
|
||||
"Watch on YouTube": "צפייה ב־YouTube",
|
||||
"Hide annotations": "",
|
||||
"Show annotations": "",
|
||||
"Genre: ": "Genre: ",
|
||||
"License: ": "רישיון: ",
|
||||
"Family friendly? ": "לכל המשפחה? ",
|
||||
"Wilson score: ": "",
|
||||
"Engagement: ": "",
|
||||
"Whitelisted regions: ": "",
|
||||
"Blacklisted regions: ": "",
|
||||
"Shared `x`": "",
|
||||
"`x` views": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` צפיות.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` צפיות."
|
||||
},
|
||||
"Premieres in `x`": "",
|
||||
"Premieres `x`": "",
|
||||
"Hi! Looks like you have JavaScript turned off. Click here to view comments, keep in mind they may take a bit longer to load.": "שלום! נראה ש־JavaScript כבוי. יש ללחוץ כאן להצגת התגובות, נא לקחת בחשבון שהטעינה תיקח קצת יותר זמן.",
|
||||
"View YouTube comments": "",
|
||||
"View more comments on Reddit": "",
|
||||
"View `x` comments": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "הצגת `x` תגובות.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "הצגת `x` תגובות."
|
||||
},
|
||||
"View Reddit comments": "",
|
||||
"Hide replies": "הסתרת תגובות",
|
||||
"Show replies": "הצגת תגובות",
|
||||
"Incorrect password": "סיסמה שגויה",
|
||||
"Quota exceeded, try again in a few hours": "",
|
||||
"Unable to log in, make sure two-factor authentication (Authenticator or SMS) is turned on.": "",
|
||||
"Invalid TFA code": "",
|
||||
"Login failed. This may be because two-factor authentication is not turned on for your account.": "",
|
||||
"Wrong answer": "תשובה שגויה",
|
||||
"Erroneous CAPTCHA": "",
|
||||
"CAPTCHA is a required field": "",
|
||||
"User ID is a required field": "חובה למלא את שדה שם המשתמש",
|
||||
"Password is a required field": "חובה למלא את שדה הסיסמה",
|
||||
"Wrong username or password": "שם משתמש שגוי או סיסמה שגויה",
|
||||
"Please sign in using 'Log in with Google'": "",
|
||||
"Password cannot be empty": "",
|
||||
"Password cannot be longer than 55 characters": "",
|
||||
"Please log in": "נא להתחבר",
|
||||
"Invidious Private Feed for `x`": "",
|
||||
"channel:`x`": "ערוץ:`x`",
|
||||
"Deleted or invalid channel": "",
|
||||
"This channel does not exist.": "הערוץ הזה אינו קיים.",
|
||||
"Could not get channel info.": "לא היה ניתן לקבל מידע על הערוץ.",
|
||||
"Could not fetch comments": "לא היה ניתן למשוך את התגובות",
|
||||
"View `x` replies": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "הצגת `x` תגובות.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "הצגת `x` תגובות."
|
||||
},
|
||||
"`x` ago": "לפני `x`",
|
||||
"Load more": "לטעון עוד",
|
||||
"`x` points": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "",
|
||||
"": ""
|
||||
},
|
||||
"Could not create mix.": "",
|
||||
"Empty playlist": "פלייליסט ריק",
|
||||
"Not a playlist.": "לא פלייליסט.",
|
||||
"Playlist does not exist.": "הפלייליסט אינו קיים.",
|
||||
"Could not pull trending pages.": "",
|
||||
"Hidden field \"challenge\" is a required field": "",
|
||||
"Hidden field \"token\" is a required field": "",
|
||||
"Erroneous challenge": "",
|
||||
"Erroneous token": "",
|
||||
"No such user": "אין משתמש כזה",
|
||||
"Token is expired, please try again": "",
|
||||
"English": "אנגלית",
|
||||
"English (auto-generated)": "אנגלית (נוצר באופן אוטומטי)",
|
||||
"Afrikaans": "Afrikaans",
|
||||
"Albanian": "אלבנית",
|
||||
"Amharic": "אמהרית",
|
||||
"Arabic": "ערבית",
|
||||
"Armenian": "ארמנית",
|
||||
"Azerbaijani": "Azerbaijani",
|
||||
"Bangla": "בנגלית",
|
||||
"Basque": "בסקית",
|
||||
"Belarusian": "Belarusian",
|
||||
"Bosnian": "Bosnian",
|
||||
"Bulgarian": "בולגרית",
|
||||
"Burmese": "Burmese",
|
||||
"Catalan": "Catalan",
|
||||
"Cebuano": "סבואנו",
|
||||
"Chinese (Simplified)": "סינית (מפושטת)",
|
||||
"Chinese (Traditional)": "סינית (מסורתית)",
|
||||
"Corsican": "קורסיקאית",
|
||||
"Croatian": "קרואטית",
|
||||
"Czech": "צ׳כית",
|
||||
"Danish": "Danish",
|
||||
"Dutch": "Dutch",
|
||||
"Esperanto": "אספרנטו",
|
||||
"Estonian": "אסטונית",
|
||||
"Filipino": "Filipino",
|
||||
"Finnish": "Finnish",
|
||||
"French": "צרפתית",
|
||||
"Galician": "גליסית",
|
||||
"Georgian": "גאורגית",
|
||||
"German": "גרמנית",
|
||||
"Greek": "יוונית",
|
||||
"Gujarati": "גוג׳ראטית",
|
||||
"Haitian Creole": "קריאולית האיטית",
|
||||
"Hausa": "האוסה",
|
||||
"Hawaiian": "הוואית",
|
||||
"Hebrew": "עברית",
|
||||
"Hindi": "הינדית",
|
||||
"Hmong": "",
|
||||
"Hungarian": "הונגרית",
|
||||
"Icelandic": "איסלנדית",
|
||||
"Igbo": "",
|
||||
"Indonesian": "אינדונזית",
|
||||
"Irish": "Irish",
|
||||
"Italian": "איטלקית",
|
||||
"Japanese": "יפנית",
|
||||
"Javanese": "ג'אווה",
|
||||
"Kannada": "קאנדה",
|
||||
"Kazakh": "קזחית",
|
||||
"Khmer": "חמרית",
|
||||
"Korean": "קוריאנית",
|
||||
"Kurdish": "כורדית",
|
||||
"Kyrgyz": "קירגיזית",
|
||||
"Lao": "לאית",
|
||||
"Latin": "לטינית",
|
||||
"Latvian": "לטבית",
|
||||
"Lithuanian": "ליטאית",
|
||||
"Luxembourgish": "לוקסמבורגית",
|
||||
"Macedonian": "מקדונית",
|
||||
"Malagasy": "מלגשית",
|
||||
"Malay": "מלאית",
|
||||
"Malayalam": "",
|
||||
"Maltese": "מלטזית",
|
||||
"Maori": "מאורית",
|
||||
"Marathi": "מראטהית",
|
||||
"Mongolian": "מונגולית",
|
||||
"Nepali": "נפאלית",
|
||||
"Norwegian Bokmål": "Norwegian Bokmål",
|
||||
"Nyanja": "",
|
||||
"Pashto": "פשטו",
|
||||
"Persian": "פרסית",
|
||||
"Polish": "פולנית",
|
||||
"Portuguese": "פורטוגלית",
|
||||
"Punjabi": "פנג'אבי",
|
||||
"Romanian": "רומנית",
|
||||
"Russian": "רוסית",
|
||||
"Samoan": "",
|
||||
"Scottish Gaelic": "גאלית סקוטית",
|
||||
"Serbian": "Serbian",
|
||||
"Shona": "",
|
||||
"Sindhi": "סינדהי",
|
||||
"Sinhala": "סינהלית",
|
||||
"Slovak": "Slovak",
|
||||
"Slovenian": "Slovenian",
|
||||
"Somali": "סומלית",
|
||||
"Southern Sotho": "",
|
||||
"Spanish": "ספרדית",
|
||||
"Spanish (Latin America)": "ספרדית (אמריקה הלטינית)",
|
||||
"Sundanese": "",
|
||||
"Swahili": "סווהילי",
|
||||
"Swedish": "שוודית",
|
||||
"Tajik": "טג׳יקית",
|
||||
"Tamil": "טמילית",
|
||||
"Telugu": "טלוגו",
|
||||
"Thai": "תאית",
|
||||
"Turkish": "טורקית",
|
||||
"Ukrainian": "אוקראינית",
|
||||
"Urdu": "אורדו",
|
||||
"Uzbek": "אוזבקית",
|
||||
"Vietnamese": "וייטנאמית",
|
||||
"Welsh": "ולשית",
|
||||
"Western Frisian": "",
|
||||
"Xhosa": "קוסה",
|
||||
"Yiddish": "יידיש",
|
||||
"Yoruba": "",
|
||||
"Zulu": "זולו",
|
||||
"`x` years": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שנים.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` שנים."
|
||||
},
|
||||
"`x` months": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` חודשים.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` חודשים."
|
||||
},
|
||||
"`x` weeks": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שבועות.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` שבועות."
|
||||
},
|
||||
"`x` days": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` ימים.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` ימים."
|
||||
},
|
||||
"`x` hours": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שעות.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` שעות."
|
||||
},
|
||||
"`x` minutes": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` דקות.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` דקות."
|
||||
},
|
||||
"`x` seconds": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` שניות.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` שניות."
|
||||
},
|
||||
"Fallback comments: ": "",
|
||||
"Popular": "",
|
||||
"Top": "Top",
|
||||
"About": "על אודות",
|
||||
"Rating: ": "דירוג: ",
|
||||
"Language: ": "שפה: ",
|
||||
"View as playlist": "הצגה כפלייליסט",
|
||||
"Default": "ברירת מחדל",
|
||||
"Music": "מוזיקה",
|
||||
"Gaming": "משחקים",
|
||||
"News": "חדשות",
|
||||
"Movies": "סרטים",
|
||||
"Download": "הורדה",
|
||||
"Download as: ": "הורדה בתור: ",
|
||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||
"(edited)": "",
|
||||
"YouTube comment permalink": "",
|
||||
"permalink": "",
|
||||
"`x` marked it with a ❤": "סומנה ב־❤ על ידי `x`",
|
||||
"Audio mode": "Audio mode",
|
||||
"Video mode": "Video mode",
|
||||
"Videos": "סרטונים",
|
||||
"Playlists": "פלייליסטים",
|
||||
"Community": "קהילה",
|
||||
"relevance": "רלוונטיות",
|
||||
"rating": "דירוג",
|
||||
"date": "תאריך העלאה",
|
||||
"views": "מספר צפיות",
|
||||
"content_type": "סוג",
|
||||
"duration": "משך זמן",
|
||||
"features": "תכונות",
|
||||
"sort": "מיון לפי",
|
||||
"hour": "השעה האחרונה",
|
||||
"today": "היום",
|
||||
"week": "השבוע",
|
||||
"month": "החודש",
|
||||
"year": "השנה",
|
||||
"video": "סרטון",
|
||||
"channel": "ערוץ",
|
||||
"playlist": "פלייליסט",
|
||||
"movie": "סרט",
|
||||
"show": "תכנית טלוויזיה",
|
||||
"hd": "HD",
|
||||
"subtitles": "כתוביות",
|
||||
"creative_commons": "Creative Commons",
|
||||
"3d": "3D",
|
||||
"live": "Live",
|
||||
"4k": "4K",
|
||||
"location": "מיקום",
|
||||
"hdr": "HDR",
|
||||
"filter": "סינון",
|
||||
"Current version: ": "הגרסה הנוכחית: "
|
||||
}
|
116
locales/id.json
116
locales/id.json
@ -74,7 +74,7 @@
|
||||
"youtube": "youtube",
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "Subtitel default: ",
|
||||
"Fallback captions: ": "",
|
||||
"Fallback captions: ": "Subtitel fallback: ",
|
||||
"Show related videos: ": "Tampilkan video terkait: ",
|
||||
"Show annotations by default: ": "Tampilkan anotasi secara default: ",
|
||||
"Visual preferences": "Preferensi visual",
|
||||
@ -163,7 +163,7 @@
|
||||
"License: ": "Lisensi: ",
|
||||
"Family friendly? ": "Ramah keluarga? ",
|
||||
"Wilson score: ": "Skor Wilson: ",
|
||||
"Engagement: ": "Keterikatan: ",
|
||||
"Engagement: ": "Keterlibatan: ",
|
||||
"Whitelisted regions: ": "Wilayah daftar-putih: ",
|
||||
"Blacklisted regions: ": "Wilayah daftar-hitam: ",
|
||||
"Shared `x`": "Berbagi`x`",
|
||||
@ -211,10 +211,10 @@
|
||||
"`x` ago": "`x` lalu",
|
||||
"Load more": "Muat lebih banyak",
|
||||
"`x` points": {
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "",
|
||||
"": ""
|
||||
"([^.,0-9]|^)1([^.,0-9]|$)": "`x` titik.([^.,0-9]|^)1([^.,0-9]|$)",
|
||||
"": "`x` titik."
|
||||
},
|
||||
"Could not create mix.": "",
|
||||
"Could not create mix.": "Tidak dapat membuat mix.",
|
||||
"Empty playlist": "Daftar putar kosong",
|
||||
"Not a playlist.": "Bukan daftar putar.",
|
||||
"Playlist does not exist.": "Daftar putar tidak ada.",
|
||||
@ -232,100 +232,100 @@
|
||||
"Amharic": "Bahasa Amharik",
|
||||
"Arabic": "Bahasa arab",
|
||||
"Armenian": "Bahasa Armenia",
|
||||
"Azerbaijani": "",
|
||||
"Bangla": "",
|
||||
"Basque": "",
|
||||
"Belarusian": "",
|
||||
"Azerbaijani": "Bahasa Azeri",
|
||||
"Bangla": "Bahasa Bangla",
|
||||
"Basque": "Bahasa Basque",
|
||||
"Belarusian": "Bahasa Belarusia",
|
||||
"Bosnian": "Bahasa Bosnia",
|
||||
"Bulgarian": "Bahasa Bulgaria",
|
||||
"Burmese": "Bahasa Birma",
|
||||
"Catalan": "",
|
||||
"Cebuano": "",
|
||||
"Chinese (Simplified)": "",
|
||||
"Chinese (Traditional)": "",
|
||||
"Catalan": "Bahasa Catalan",
|
||||
"Cebuano": "Bahasa Cebu",
|
||||
"Chinese (Simplified)": "Bahasa Cina",
|
||||
"Chinese (Traditional)": "Bahasa Cina (Tradisonal)",
|
||||
"Corsican": "",
|
||||
"Croatian": "Bahasa Kroasia",
|
||||
"Czech": "Bahasa Ceko",
|
||||
"Danish": "",
|
||||
"Danish": "Bahasa Denmak",
|
||||
"Dutch": "Bahasa Belanda",
|
||||
"Esperanto": "",
|
||||
"Estonian": "",
|
||||
"Filipino": "",
|
||||
"Finnish": "",
|
||||
"French": "",
|
||||
"Esperanto": "Bahasa Esperanto",
|
||||
"Estonian": "Bahasa Estonia",
|
||||
"Filipino": "Bahasa Filipina",
|
||||
"Finnish": "Bahasa Finlandia",
|
||||
"French": "Bahasa Perancis",
|
||||
"Galician": "",
|
||||
"Georgian": "",
|
||||
"German": "",
|
||||
"Georgian": "Bahasa Georgia",
|
||||
"German": "Bahasa Jerman",
|
||||
"Greek": "Bahasa Yunani",
|
||||
"Gujarati": "",
|
||||
"Haitian Creole": "",
|
||||
"Hausa": "",
|
||||
"Hawaiian": "",
|
||||
"Hebrew": "",
|
||||
"Hindi": "",
|
||||
"Hawaiian": "Bahasa Hawai",
|
||||
"Hebrew": "Bahasa Ibrani",
|
||||
"Hindi": "Bahasa Hindi",
|
||||
"Hmong": "",
|
||||
"Hungarian": "",
|
||||
"Icelandic": "",
|
||||
"Igbo": "",
|
||||
"Hungarian": "Bahasa Hungaria",
|
||||
"Icelandic": "Bahasa Islandia",
|
||||
"Igbo": "Bahasa Igbo",
|
||||
"Indonesian": "Bahasa Indonesia",
|
||||
"Irish": "",
|
||||
"Italian": "",
|
||||
"Irish": "Bahasa Irlandia",
|
||||
"Italian": "Bahasa Italia",
|
||||
"Japanese": "Bahasa Jepang",
|
||||
"Javanese": "Bahasa Jawa",
|
||||
"Kannada": "",
|
||||
"Kazakh": "",
|
||||
"Khmer": "",
|
||||
"Korean": "Bahasa Korea",
|
||||
"Kurdish": "",
|
||||
"Kurdish": "Bahasa Kurdistan",
|
||||
"Kyrgyz": "",
|
||||
"Lao": "",
|
||||
"Latin": "",
|
||||
"Latvian": "",
|
||||
"Lithuanian": "",
|
||||
"Latin": "Bahasa Latin",
|
||||
"Latvian": "Bahasa Latvia",
|
||||
"Lithuanian": "Bahasa Lithuania",
|
||||
"Luxembourgish": "",
|
||||
"Macedonian": "",
|
||||
"Malagasy": "",
|
||||
"Malay": "Bahasa Melayu",
|
||||
"Malayalam": "",
|
||||
"Maltese": "",
|
||||
"Maori": "",
|
||||
"Marathi": "",
|
||||
"Mongolian": "",
|
||||
"Nepali": "",
|
||||
"Maori": "Bahasa Maori",
|
||||
"Marathi": "Bahasa Marathi",
|
||||
"Mongolian": "Bahasa Mongolia",
|
||||
"Nepali": "Bahasa Nepal",
|
||||
"Norwegian Bokmål": "",
|
||||
"Nyanja": "",
|
||||
"Pashto": "",
|
||||
"Persian": "",
|
||||
"Polish": "",
|
||||
"Portuguese": "",
|
||||
"Punjabi": "",
|
||||
"Romanian": "",
|
||||
"Russian": "",
|
||||
"Persian": "Bahasa Persia",
|
||||
"Polish": "Bahasa Polandia",
|
||||
"Portuguese": "Bahasa Portugis",
|
||||
"Punjabi": "Bahasa Punjabi",
|
||||
"Romanian": "Bahasa Romania",
|
||||
"Russian": "Bahasa Russia",
|
||||
"Samoan": "",
|
||||
"Scottish Gaelic": "",
|
||||
"Serbian": "",
|
||||
"Serbian": "Bahasa Serbia",
|
||||
"Shona": "",
|
||||
"Sindhi": "",
|
||||
"Sinhala": "",
|
||||
"Slovak": "",
|
||||
"Slovenian": "",
|
||||
"Somali": "",
|
||||
"Slovak": "Bahasa Slovakia",
|
||||
"Slovenian": "Bahasa Slovenia",
|
||||
"Somali": "Bahasa Somalia",
|
||||
"Southern Sotho": "",
|
||||
"Spanish": "",
|
||||
"Spanish (Latin America)": "",
|
||||
"Spanish": "Bahasa Spanyol",
|
||||
"Spanish (Latin America)": "Bahasa Spanyol (Amerika Latin)",
|
||||
"Sundanese": "Bahasa Sunda",
|
||||
"Swahili": "",
|
||||
"Swedish": "",
|
||||
"Swahili": "Bahasa Swahili",
|
||||
"Swedish": "Bahasa Swedia",
|
||||
"Tajik": "",
|
||||
"Tamil": "",
|
||||
"Tamil": "Bahasa Tamil",
|
||||
"Telugu": "",
|
||||
"Thai": "Bahasa Thailand",
|
||||
"Turkish": "",
|
||||
"Ukrainian": "",
|
||||
"Urdu": "",
|
||||
"Uzbek": "",
|
||||
"Turkish": "Bahasa Turki",
|
||||
"Ukrainian": "Bahasa Ukraina",
|
||||
"Urdu": "Bahasa Urdu",
|
||||
"Uzbek": "Bahasa Uzbek",
|
||||
"Vietnamese": "Bahasa Vietnam",
|
||||
"Welsh": "",
|
||||
"Welsh": "Bahasa Wales",
|
||||
"Western Frisian": "",
|
||||
"Xhosa": "",
|
||||
"Yiddish": "",
|
||||
@ -376,7 +376,7 @@
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "(disunting)",
|
||||
"YouTube comment permalink": "",
|
||||
"permalink": "",
|
||||
"permalink": "permalink",
|
||||
"`x` marked it with a ❤": "`x` telah ditandai dengan ❤",
|
||||
"Audio mode": "Mode audio",
|
||||
"Video mode": "Mode video",
|
||||
|
@ -22,7 +22,7 @@ shards:
|
||||
|
||||
pg:
|
||||
git: https://github.com/will/crystal-pg.git
|
||||
version: 0.22.1
|
||||
version: 0.23.1
|
||||
|
||||
pool:
|
||||
git: https://github.com/ysbaddaden/pool.git
|
||||
@ -38,5 +38,5 @@ shards:
|
||||
|
||||
sqlite3:
|
||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||
version: 0.17.0
|
||||
version: 0.18.0
|
||||
|
||||
|
@ -12,10 +12,10 @@ targets:
|
||||
dependencies:
|
||||
pg:
|
||||
github: will/crystal-pg
|
||||
version: ~> 0.22.1
|
||||
version: ~> 0.23.1
|
||||
sqlite3:
|
||||
github: crystal-lang/crystal-sqlite3
|
||||
version: ~> 0.17.0
|
||||
version: ~> 0.18.0
|
||||
kemal:
|
||||
github: kemalcr/kemal
|
||||
version: ~> 0.27.0
|
||||
@ -29,6 +29,6 @@ dependencies:
|
||||
github: iv-org/lsquic.cr
|
||||
version: ~> 2.18.1-1
|
||||
|
||||
crystal: 0.35.1
|
||||
crystal: 0.36.1
|
||||
|
||||
license: AGPLv3
|
||||
|
@ -84,7 +84,9 @@ LOCALES = {
|
||||
"fa" => load_locale("fa"),
|
||||
"fi" => load_locale("fi"),
|
||||
"fr" => load_locale("fr"),
|
||||
"he" => load_locale("he"),
|
||||
"hr" => load_locale("hr"),
|
||||
"id" => load_locale("id"),
|
||||
"is" => load_locale("is"),
|
||||
"it" => load_locale("it"),
|
||||
"ja" => load_locale("ja"),
|
||||
@ -311,12 +313,12 @@ before_all do |env|
|
||||
env.set "current_page", URI.encode_www_form(current_page)
|
||||
end
|
||||
|
||||
Invidious::Routing.get "/", Invidious::Routes::Home
|
||||
Invidious::Routing.get "/privacy", Invidious::Routes::Privacy
|
||||
Invidious::Routing.get "/licenses", Invidious::Routes::Licenses
|
||||
Invidious::Routing.get "/", Invidious::Routes::Misc, :home
|
||||
Invidious::Routing.get "/privacy", Invidious::Routes::Misc, :privacy
|
||||
Invidious::Routing.get "/licenses", Invidious::Routes::Misc, :licenses
|
||||
Invidious::Routing.get "/watch", Invidious::Routes::Watch
|
||||
Invidious::Routing.get "/embed/", Invidious::Routes::Embed::Index
|
||||
Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed::Show
|
||||
Invidious::Routing.get "/embed/", Invidious::Routes::Embed, :redirect
|
||||
Invidious::Routing.get "/embed/:id", Invidious::Routes::Embed, :show
|
||||
Invidious::Routing.get "/view_all_playlists", Invidious::Routes::Playlists, :index
|
||||
Invidious::Routing.get "/create_playlist", Invidious::Routes::Playlists, :new
|
||||
Invidious::Routing.post "/create_playlist", Invidious::Routes::Playlists, :create
|
||||
@ -335,9 +337,9 @@ Invidious::Routing.get "/search", Invidious::Routes::Search, :search
|
||||
Invidious::Routing.get "/login", Invidious::Routes::Login, :login_page
|
||||
Invidious::Routing.post "/login", Invidious::Routes::Login, :login
|
||||
Invidious::Routing.post "/signout", Invidious::Routes::Login, :signout
|
||||
Invidious::Routing.get "/preferences", Invidious::Routes::UserPreferences, :show
|
||||
Invidious::Routing.post "/preferences", Invidious::Routes::UserPreferences, :update
|
||||
Invidious::Routing.get "/toggle_theme", Invidious::Routes::UserPreferences, :toggle_theme
|
||||
Invidious::Routing.get "/preferences", Invidious::Routes::PreferencesRoute, :show
|
||||
Invidious::Routing.post "/preferences", Invidious::Routes::PreferencesRoute, :update
|
||||
Invidious::Routing.get "/toggle_theme", Invidious::Routes::PreferencesRoute, :toggle_theme
|
||||
|
||||
# Users
|
||||
|
||||
@ -1428,9 +1430,9 @@ get "/feed/playlist/:plid" do |env|
|
||||
node.attributes.each do |attribute|
|
||||
case attribute.name
|
||||
when "url", "href"
|
||||
full_path = URI.parse(node[attribute.name]).full_path
|
||||
query_string_opt = full_path.starts_with?("/watch?v=") ? "&#{params}" : ""
|
||||
node[attribute.name] = "#{HOST_URL}#{full_path}#{query_string_opt}"
|
||||
request_target = URI.parse(node[attribute.name]).request_target
|
||||
query_string_opt = request_target.starts_with?("/watch?v=") ? "&#{params}" : ""
|
||||
node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}"
|
||||
else nil # Skip
|
||||
end
|
||||
end
|
||||
@ -1439,7 +1441,7 @@ get "/feed/playlist/:plid" do |env|
|
||||
document = document.to_xml(options: XML::SaveOptions::NO_DECL)
|
||||
|
||||
document.scan(/<uri>(?<url>[^<]+)<\/uri>/).each do |match|
|
||||
content = "#{HOST_URL}#{URI.parse(match["url"]).full_path}"
|
||||
content = "#{HOST_URL}#{URI.parse(match["url"]).request_target}"
|
||||
document = document.gsub(match[0], "<uri>#{content}</uri>")
|
||||
end
|
||||
|
||||
@ -1634,7 +1636,7 @@ end
|
||||
|
||||
get "/attribution_link" do |env|
|
||||
if query = env.params.query["u"]?
|
||||
url = URI.parse(query).full_path
|
||||
url = URI.parse(query).request_target
|
||||
else
|
||||
url = "/"
|
||||
end
|
||||
@ -1978,7 +1980,7 @@ get "/api/v1/captions/:id" do |env|
|
||||
caption = caption[0]
|
||||
end
|
||||
|
||||
url = URI.parse("#{caption.baseUrl}&tlang=#{tlang}").full_path
|
||||
url = URI.parse("#{caption.baseUrl}&tlang=#{tlang}").request_target
|
||||
|
||||
# Auto-generated captions often have cues that aren't aligned properly with the video,
|
||||
# as well as some other markup that makes it cumbersome, so we try to fix that here
|
||||
@ -3184,7 +3186,7 @@ get "/api/manifest/dash/id/:id" do |env|
|
||||
end
|
||||
|
||||
if dashmpd = video.dash_manifest_url
|
||||
manifest = YT_POOL.client &.get(URI.parse(dashmpd).full_path).body
|
||||
manifest = YT_POOL.client &.get(URI.parse(dashmpd).request_target).body
|
||||
|
||||
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
||||
url = baseurl.lchop("<BaseURL>")
|
||||
@ -3192,7 +3194,7 @@ get "/api/manifest/dash/id/:id" do |env|
|
||||
|
||||
if local
|
||||
uri = URI.parse(url)
|
||||
url = "#{uri.full_path}host/#{uri.host}/"
|
||||
url = "#{uri.request_target}host/#{uri.host}/"
|
||||
end
|
||||
|
||||
"<BaseURL>#{url}</BaseURL>"
|
||||
@ -3205,7 +3207,7 @@ get "/api/manifest/dash/id/:id" do |env|
|
||||
|
||||
if local
|
||||
adaptive_fmts.each do |fmt|
|
||||
fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path)
|
||||
fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target)
|
||||
end
|
||||
end
|
||||
|
||||
@ -3403,7 +3405,7 @@ get "/latest_version" do |env|
|
||||
next
|
||||
end
|
||||
|
||||
url = URI.parse(url).full_path.not_nil! if local
|
||||
url = URI.parse(url).request_target.not_nil! if local
|
||||
url = "#{url}&title=#{title}" if title
|
||||
|
||||
env.redirect url
|
||||
@ -3515,7 +3517,7 @@ get "/videoplayback" do |env|
|
||||
client = make_client(URI.parse(new_host), region)
|
||||
end
|
||||
|
||||
url = "#{location.full_path}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
||||
url = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
||||
else
|
||||
break
|
||||
end
|
||||
@ -3555,7 +3557,7 @@ get "/videoplayback" do |env|
|
||||
|
||||
if location = response.headers["Location"]?
|
||||
location = URI.parse(location)
|
||||
location = "#{location.full_path}&host=#{location.host}"
|
||||
location = "#{location.request_target}&host=#{location.host}"
|
||||
|
||||
if region
|
||||
location += "®ion=#{region}"
|
||||
@ -3619,7 +3621,7 @@ get "/videoplayback" do |env|
|
||||
|
||||
if location = response.headers["Location"]?
|
||||
location = URI.parse(location)
|
||||
location = "#{location.full_path}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
||||
location = "#{location.request_target}&host=#{location.host}#{region ? "®ion=#{region}" : ""}"
|
||||
|
||||
env.redirect location
|
||||
break
|
||||
@ -3859,7 +3861,7 @@ end
|
||||
get "/watch_videos" do |env|
|
||||
response = YT_POOL.client &.get(env.request.resource)
|
||||
if url = response.headers["Location"]?
|
||||
url = URI.parse(url).full_path
|
||||
url = URI.parse(url).request_target
|
||||
next env.redirect url
|
||||
end
|
||||
|
||||
@ -3874,7 +3876,7 @@ error 404 do |env|
|
||||
response = YT_POOL.client &.get("/#{item}")
|
||||
|
||||
if response.status_code == 301
|
||||
response = YT_POOL.client &.get(URI.parse(response.headers["Location"]).full_path)
|
||||
response = YT_POOL.client &.get(URI.parse(response.headers["Location"]).request_target)
|
||||
end
|
||||
|
||||
if response.body.empty?
|
||||
|
@ -195,8 +195,14 @@ def fetch_youtube_comments(id, db, cursor, format, locale, thin_mode, region, so
|
||||
end
|
||||
|
||||
if node_replies && !response["commentRepliesContinuation"]?
|
||||
reply_count = (node_replies["moreText"]["simpleText"]? || node_replies["moreText"]["runs"]?.try &.[0]?.try &.["text"]?)
|
||||
.try &.as_s.gsub(/\D/, "").to_i? || 1
|
||||
if node_replies["moreText"]?
|
||||
reply_count = (node_replies["moreText"]["simpleText"]? || node_replies["moreText"]["runs"]?.try &.[0]?.try &.["text"]?)
|
||||
.try &.as_s.gsub(/\D/, "").to_i? || 1
|
||||
elsif node_replies["viewReplies"]?
|
||||
reply_count = node_replies["viewReplies"]["buttonRenderer"]["text"]?.try &.["runs"][1]?.try &.["text"]?.try &.as_s.to_i? || 1
|
||||
else
|
||||
reply_count = 1
|
||||
end
|
||||
|
||||
continuation = node_replies["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s
|
||||
continuation ||= ""
|
||||
@ -294,7 +300,7 @@ def template_youtube_comments(comments, locale, thin_mode)
|
||||
end
|
||||
|
||||
if !thin_mode
|
||||
author_thumbnail = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).full_path}"
|
||||
author_thumbnail = "/ggpht#{URI.parse(child["authorThumbnails"][-1]["url"].as_s).request_target}"
|
||||
else
|
||||
author_thumbnail = ""
|
||||
end
|
||||
@ -322,7 +328,7 @@ def template_youtube_comments(comments, locale, thin_mode)
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-2">
|
||||
<img style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).full_path}">
|
||||
<img style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}">
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
@ -375,7 +381,7 @@ def template_youtube_comments(comments, locale, thin_mode)
|
||||
|
||||
if child["creatorHeart"]?
|
||||
if !thin_mode
|
||||
creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).full_path}"
|
||||
creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).request_target}"
|
||||
else
|
||||
creator_thumbnail = ""
|
||||
end
|
||||
@ -473,7 +479,7 @@ def replace_links(html)
|
||||
params = HTTP::Params.parse(url.query.not_nil!)
|
||||
anchor["href"] = params["q"]?
|
||||
else
|
||||
anchor["href"] = url.full_path
|
||||
anchor["href"] = url.request_target
|
||||
end
|
||||
elsif url.to_s == "#"
|
||||
begin
|
||||
@ -544,7 +550,7 @@ def content_to_comment_html(content)
|
||||
if url.path == "/redirect"
|
||||
url = HTTP::Params.parse(url.query.not_nil!)["q"]
|
||||
else
|
||||
url = url.full_path
|
||||
url = url.request_target
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -231,7 +231,7 @@ def extract_item(item : JSON::Any, author_fallback : String? = nil, author_id_fa
|
||||
video_id = i["videoId"].as_s
|
||||
title = i["title"].try { |t| t["simpleText"]?.try &.as_s || t["runs"]?.try &.as_a.map(&.["text"].as_s).join("") } || ""
|
||||
|
||||
author_info = i["ownerText"]?.try &.["runs"].as_a[0]?
|
||||
author_info = i["ownerText"]?.try &.["runs"]?.try &.as_a?.try &.[0]?
|
||||
author = author_info.try &.["text"].as_s || author_fallback || ""
|
||||
author_id = author_info.try &.["navigationEndpoint"]?.try &.["browseEndpoint"]["browseId"].as_s || author_id_fallback || ""
|
||||
|
||||
@ -322,7 +322,7 @@ def extract_item(item : JSON::Any, author_fallback : String? = nil, author_id_fa
|
||||
video_count = i["videoCount"]?.try &.as_s.to_i || 0
|
||||
playlist_thumbnail = i["thumbnails"].as_a[0]?.try &.["thumbnails"]?.try &.as_a[0]?.try &.["url"].as_s || ""
|
||||
|
||||
author_info = i["shortBylineText"]?.try &.["runs"].as_a[0]?
|
||||
author_info = i["shortBylineText"]?.try &.["runs"]?.try &.as_a?.try &.[0]?
|
||||
author = author_info.try &.["text"].as_s || author_fallback || ""
|
||||
author_id = author_info.try &.["navigationEndpoint"]?.try &.["browseEndpoint"]["browseId"].as_s || author_id_fallback || ""
|
||||
|
||||
|
@ -71,14 +71,14 @@ end
|
||||
class HTTPClient < HTTP::Client
|
||||
def set_proxy(proxy : HTTPProxy)
|
||||
begin
|
||||
@socket = proxy.open(host: @host, port: @port, tls: @tls, connection_options: proxy_connection_options)
|
||||
@io = proxy.open(host: @host, port: @port, tls: @tls, connection_options: proxy_connection_options)
|
||||
rescue IO::Error
|
||||
@socket = nil
|
||||
@io = nil
|
||||
end
|
||||
end
|
||||
|
||||
def unset_proxy
|
||||
@socket = nil
|
||||
@io = nil
|
||||
end
|
||||
|
||||
def proxy_connection_options
|
||||
|
@ -329,7 +329,7 @@ def get_referer(env, fallback = "/", unroll = true)
|
||||
end
|
||||
end
|
||||
|
||||
referer = referer.full_path
|
||||
referer = referer.request_target
|
||||
referer = "/" + referer.gsub(/[^\/?@&%=\-_.0-9a-zA-Z]/, "").lstrip("/\\")
|
||||
|
||||
if referer == env.request.path
|
||||
|
@ -60,7 +60,7 @@ class Invidious::Jobs::BypassCaptchaJob < Invidious::Jobs::BaseJob
|
||||
elsif response.headers["Location"]?.try &.includes?("/sorry/index")
|
||||
location = response.headers["Location"].try { |u| URI.parse(u) }
|
||||
headers = HTTP::Headers{":authority" => location.host.not_nil!}
|
||||
response = YT_POOL.client &.get(location.full_path, headers)
|
||||
response = YT_POOL.client &.get(location.request_target, headers)
|
||||
|
||||
html = XML.parse_html(response.body)
|
||||
form = html.xpath_node(%(//form[@action="index"])).not_nil!
|
||||
|
@ -1,5 +1,29 @@
|
||||
class Invidious::Routes::Embed::Show < Invidious::Routes::BaseRoute
|
||||
def handle(env)
|
||||
class Invidious::Routes::Embed < Invidious::Routes::BaseRoute
|
||||
def redirect(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
|
||||
begin
|
||||
playlist = get_playlist(PG_DB, plid, locale: locale)
|
||||
offset = env.params.query["index"]?.try &.to_i? || 0
|
||||
videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale)
|
||||
rescue ex
|
||||
return error_template(500, ex)
|
||||
end
|
||||
|
||||
url = "/embed/#{videos[0].id}?#{env.params.query}"
|
||||
|
||||
if env.params.query.size > 0
|
||||
url += "?#{env.params.query}"
|
||||
end
|
||||
else
|
||||
url = "/"
|
||||
end
|
||||
|
||||
env.redirect url
|
||||
end
|
||||
|
||||
def show(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
id = env.params.url["id"]
|
||||
|
||||
@ -120,8 +144,8 @@ class Invidious::Routes::Embed::Show < Invidious::Routes::BaseRoute
|
||||
adaptive_fmts = video.adaptive_fmts
|
||||
|
||||
if params.local
|
||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) }
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) }
|
||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||
end
|
||||
|
||||
video_streams = video.video_streams
|
@ -1,25 +0,0 @@
|
||||
class Invidious::Routes::Embed::Index < Invidious::Routes::BaseRoute
|
||||
def handle(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
||||
if plid = env.params.query["list"]?.try &.gsub(/[^a-zA-Z0-9_-]/, "")
|
||||
begin
|
||||
playlist = get_playlist(PG_DB, plid, locale: locale)
|
||||
offset = env.params.query["index"]?.try &.to_i? || 0
|
||||
videos = get_playlist_videos(PG_DB, playlist, offset: offset, locale: locale)
|
||||
rescue ex
|
||||
return error_template(500, ex)
|
||||
end
|
||||
|
||||
url = "/embed/#{videos[0].id}?#{env.params.query}"
|
||||
|
||||
if env.params.query.size > 0
|
||||
url += "?#{env.params.query}"
|
||||
end
|
||||
else
|
||||
url = "/"
|
||||
end
|
||||
|
||||
env.redirect url
|
||||
end
|
||||
end
|
@ -1,6 +0,0 @@
|
||||
class Invidious::Routes::Licenses < Invidious::Routes::BaseRoute
|
||||
def handle(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
rendered "licenses"
|
||||
end
|
||||
end
|
@ -255,7 +255,7 @@ class Invidious::Routes::Login < Invidious::Routes::BaseRoute
|
||||
traceback << "Unhandled dialog /b/0/SmsAuthInterstitial."
|
||||
end
|
||||
|
||||
login = client.get(location.full_path, headers)
|
||||
login = client.get(location.request_target, headers)
|
||||
|
||||
headers = login.cookies.add_request_headers(headers)
|
||||
location = login.headers["Location"]?.try { |u| URI.parse(u) }
|
||||
|
@ -1,5 +1,5 @@
|
||||
class Invidious::Routes::Home < Invidious::Routes::BaseRoute
|
||||
def handle(env)
|
||||
class Invidious::Routes::Misc < Invidious::Routes::BaseRoute
|
||||
def home(env)
|
||||
preferences = env.get("preferences").as(Preferences)
|
||||
locale = LOCALES[preferences.locale]?
|
||||
user = env.get? "user"
|
||||
@ -25,4 +25,14 @@ class Invidious::Routes::Home < Invidious::Routes::BaseRoute
|
||||
templated "empty"
|
||||
end
|
||||
end
|
||||
|
||||
def privacy(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
templated "privacy"
|
||||
end
|
||||
|
||||
def licenses(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
rendered "licenses"
|
||||
end
|
||||
end
|
@ -1,4 +1,4 @@
|
||||
class Invidious::Routes::UserPreferences < Invidious::Routes::BaseRoute
|
||||
class Invidious::Routes::PreferencesRoute < Invidious::Routes::BaseRoute
|
||||
def show(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
|
@ -1,6 +0,0 @@
|
||||
class Invidious::Routes::Privacy < Invidious::Routes::BaseRoute
|
||||
def handle(env)
|
||||
locale = LOCALES[env.get("preferences").as(Preferences).locale]?
|
||||
templated "privacy"
|
||||
end
|
||||
end
|
@ -126,8 +126,8 @@ class Invidious::Routes::Watch < Invidious::Routes::BaseRoute
|
||||
adaptive_fmts = video.adaptive_fmts
|
||||
|
||||
if params.local
|
||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) }
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).full_path) }
|
||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||
end
|
||||
|
||||
video_streams = video.video_streams
|
||||
|
@ -517,7 +517,7 @@ struct Video
|
||||
end
|
||||
|
||||
def published : Time
|
||||
info["microformat"]?.try &.["playerMicroformatRenderer"]?.try &.["publishDate"]?.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location.local) } || Time.local
|
||||
info["microformat"]?.try &.["playerMicroformatRenderer"]?.try &.["publishDate"]?.try { |t| Time.parse(t.as_s, "%Y-%m-%d", Time::Location::UTC) } || Time.utc
|
||||
end
|
||||
|
||||
def published=(other : Time)
|
||||
@ -534,7 +534,8 @@ struct Video
|
||||
end
|
||||
|
||||
def live_now
|
||||
info["videoDetails"]["isLiveContent"]?.try &.as_bool || false
|
||||
info["microformat"]?.try &.["playerMicroformatRenderer"]?
|
||||
.try &.["liveBroadcastDetails"]?.try &.["isLiveNow"]?.try &.as_bool || false
|
||||
end
|
||||
|
||||
def is_listed
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
<% if channel.banner %>
|
||||
<div class="h-box">
|
||||
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).full_path %>">
|
||||
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
@ -16,7 +16,7 @@
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-2-3">
|
||||
<div class="channel-profile">
|
||||
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).full_path %>">
|
||||
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
|
||||
<span><%= channel.author %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<% if channel.banner %>
|
||||
<div class="h-box">
|
||||
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).full_path %>">
|
||||
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-2-3">
|
||||
<div class="channel-profile">
|
||||
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).full_path %>">
|
||||
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
|
||||
<span><%= channel.author %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<a style="width:100%" href="/channel/<%= item.ucid %>">
|
||||
<% if !env.get("preferences").as(Preferences).thin_mode %>
|
||||
<center>
|
||||
<img style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).full_path %>"/>
|
||||
<img style="width:56.25%" src="/ggpht<%= URI.parse(item.author_thumbnail).request_target.gsub(/=s\d+/, "=s176") %>"/>
|
||||
</center>
|
||||
<% end %>
|
||||
<p><%= item.author %></p>
|
||||
@ -15,7 +15,7 @@
|
||||
<h5><%= item.description_html %></h5>
|
||||
<% when SearchPlaylist, InvidiousPlaylist %>
|
||||
<% if item.id.starts_with? "RD" %>
|
||||
<% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").full_path.split("/")[2]}" %>
|
||||
<% url = "/mix?list=#{item.id}&continuation=#{URI.parse(item.thumbnail || "/vi/-----------").request_target.split("/")[2]}" %>
|
||||
<% else %>
|
||||
<% url = "/playlist?list=#{item.id}" %>
|
||||
<% end %>
|
||||
@ -23,7 +23,7 @@
|
||||
<a style="width:100%" href="<%= url %>">
|
||||
<% if !env.get("preferences").as(Preferences).thin_mode %>
|
||||
<div class="thumbnail">
|
||||
<img class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").full_path %>"/>
|
||||
<img class="thumbnail" src="<%= URI.parse(item.thumbnail || "/").request_target %>"/>
|
||||
<p class="length"><%= number_with_separator(item.video_count) %> videos</p>
|
||||
</div>
|
||||
<% end %>
|
||||
|
@ -4,7 +4,7 @@
|
||||
<% if params.video_loop %>loop<% end %>
|
||||
<% if params.controls %>controls<% end %>>
|
||||
<% if (hlsvp = video.hls_manifest_url) && !CONFIG.disabled?("livestreams") %>
|
||||
<source src="<%= URI.parse(hlsvp).full_path %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream">
|
||||
<source src="<%= URI.parse(hlsvp).request_target %><% if params.local %>?local=true<% end %>" type="application/x-mpegURL" label="livestream">
|
||||
<% else %>
|
||||
<% if params.listen %>
|
||||
<% audio_streams.each_with_index do |fmt, i| %>
|
||||
@ -24,9 +24,9 @@
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<% preferred_captions.each_with_index do |caption, i| %>
|
||||
<% preferred_captions.each do |caption| %>
|
||||
<track kind="captions" src="/api/v1/captions/<%= video.id %>?label=<%= caption.name.simpleText %>&hl=<%= env.get("preferences").as(Preferences).locale %>"
|
||||
label="<%= caption.name.simpleText %>" <% if i == 0 %>default<% end %>>
|
||||
label="<%= caption.name.simpleText %>">
|
||||
<% end %>
|
||||
|
||||
<% captions.each do |caption| %>
|
||||
@ -42,7 +42,8 @@
|
||||
"aspect_ratio" => aspect_ratio,
|
||||
"title" => video.title,
|
||||
"description" => HTML.escape(video.short_description),
|
||||
"thumbnail" => thumbnail
|
||||
"thumbnail" => thumbnail,
|
||||
"preferred_caption_found" => !preferred_captions.empty?
|
||||
}.to_pretty_json
|
||||
%>
|
||||
</script>
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
<% if channel.banner %>
|
||||
<div class="h-box">
|
||||
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).full_path %>">
|
||||
<img style="width:100%" src="/ggpht<%= URI.parse(channel.banner.not_nil!.gsub("=w1060-", "=w1280-")).request_target %>">
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
@ -15,7 +15,7 @@
|
||||
<div class="pure-g h-box">
|
||||
<div class="pure-u-2-3">
|
||||
<div class="channel-profile">
|
||||
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).full_path %>">
|
||||
<img src="/ggpht<%= URI.parse(channel.author_thumbnail).request_target %>">
|
||||
<span><%= channel.author %></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -22,6 +22,7 @@
|
||||
<meta name="twitter:player" content="<%= HOST_URL %>/embed/<%= video.id %>">
|
||||
<meta name="twitter:player:width" content="1280">
|
||||
<meta name="twitter:player:height" content="720">
|
||||
<link rel="alternate" href="https://www.youtube.com/watch?v=<%= video.id %>">
|
||||
<%= rendered "components/player_sources" %>
|
||||
<title><%= HTML.escape(video.title) %> - Invidious</title>
|
||||
<% end %>
|
||||
@ -80,6 +81,10 @@
|
||||
<h3>
|
||||
<%= video.premiere_timestamp.try { |t| 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, "Started streaming `x` ago", recode_date((Time.utc - t).ago, locale)) } %>
|
||||
</h3>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@ -203,7 +208,7 @@
|
||||
<a href="/channel/<%= video.ucid %>" style="display:block;width:fit-content;width:-moz-fit-content">
|
||||
<div class="channel-profile">
|
||||
<% if !video.author_thumbnail.empty? %>
|
||||
<img src="/ggpht<%= URI.parse(video.author_thumbnail).full_path %>">
|
||||
<img src="/ggpht<%= URI.parse(video.author_thumbnail).request_target %>">
|
||||
<% end %>
|
||||
<span id="channel-name"><%= video.author %></span>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user