mirror of
https://github.com/iv-org/invidious.git
synced 2025-05-31 14:11:54 +05:30
Compare commits
101 Commits
Author | SHA1 | Date | |
---|---|---|---|
699f85e773 | |||
f225d38680 | |||
2630dc8dcd | |||
276662a147 | |||
ed8a9af355 | |||
e6e3d826b9 | |||
5b3606ad1d | |||
072cc13f14 | |||
c1ed660ca0 | |||
2c44051318 | |||
d0a690c303 | |||
87e1fa0a28 | |||
a1af27b125 | |||
ceaddbc821 | |||
9989c8100a | |||
c0e73e71c5 | |||
b0ba670c91 | |||
d5c9b7dfe8 | |||
095b5fcea0 | |||
aeee40c894 | |||
a7fbcd0aa8 | |||
c9bc081f8c | |||
fbb5df0849 | |||
cef061d6fb | |||
def58ff11f | |||
9e73e3b153 | |||
e9ea365f2f | |||
55118a6768 | |||
1e214aae7c | |||
ff09a7255a | |||
26b7200360 | |||
b38a2bbd12 | |||
097cbcdae3 | |||
c0fdc28a84 | |||
6218078c51 | |||
a9aae6b36c | |||
96fb2118d5 | |||
48fc0949cc | |||
7d270211ae | |||
a9f5b84c7f | |||
2d20f12335 | |||
45b53b8902 | |||
898b768b30 | |||
06aa1bb90f | |||
1f6078cf25 | |||
ba36ab9559 | |||
586c0a0579 | |||
209d7117fb | |||
3751d11a0b | |||
1af86f6afb | |||
4c77908bb4 | |||
952b208a01 | |||
40fb29ea2b | |||
c1081e3df0 | |||
4b60f7ddff | |||
75d8c4f5c0 | |||
16a7fcb79b | |||
8cd0137aed | |||
f455b12085 | |||
1a9057a175 | |||
0fcfb7b82b | |||
30f08ae48c | |||
8f1b65de59 | |||
d88f9f3b3e | |||
08e8d0f56f | |||
fb535ad6bb | |||
15efac520e | |||
dd5623ffbf | |||
7a6a0f364c | |||
93297b63b1 | |||
e1540390a8 | |||
71ba071160 | |||
af449161ff | |||
03aa11b412 | |||
5e272db8f5 | |||
827e68acf5 | |||
987ea1cb98 | |||
633ecb524e | |||
f19d8f7095 | |||
a20e3cd77e | |||
a7b6a67615 | |||
e7f05d76fa | |||
5cb57fb176 | |||
95bde7bb8a | |||
daa2329f8b | |||
b23710f89f | |||
277dda0dcb | |||
cf9134416c | |||
2425368c3a | |||
20c4d213d9 | |||
af9134ffb4 | |||
f65ddaa0f1 | |||
9580a21786 | |||
dfd17bdd88 | |||
0f48d221b4 | |||
8f57388cd3 | |||
0992587da5 | |||
138a6b1136 | |||
c6ec8317ac | |||
81c2ecc788 | |||
7abe5dc845 |
71
CHANGELOG.md
71
CHANGELOG.md
@ -1,3 +1,74 @@
|
||||
# 0.13.0 (2019-01-06)
|
||||
|
||||
## Version 0.13.0: Translations, Annotations, and Tor
|
||||
|
||||
I hope everyone had a happy New Year! There's been a couple new additions since last release, with [44 commits](https://github.com/omarroth/invidious/compare/0.12.0...0.13.0) from 9 contributors. It's been quite a year for the project, and I hope to continue improving the project into 2019! Starting off the new year:
|
||||
|
||||
## Translations
|
||||
|
||||
I'm happy to announce support for translations has been added with [`a160c64`](https://github.com/omarroth/invidious/a160c64). Currently, there is support for:
|
||||
|
||||
- Arabic (`ar`)
|
||||
- Dutch (`nl`)
|
||||
- English (`en-US`)
|
||||
- German (`de`)
|
||||
- Norwegian Bokmål (`nb_NO`)
|
||||
- Polish (`pl`)
|
||||
- Russian (`ru`)
|
||||
|
||||
Which you can change in your preferences under `Language`. You can also add `&hl=LANGUAGE` to the end of any request to translate it to your preferred language, for example https://invidio.us/?hl=ru. I'd like to say thank you again to everyone who has helped translate the site! I've mentioned this before, but I'm delighted that so many people find the project useful.
|
||||
|
||||
## Annotations
|
||||
|
||||
Recently, [YouTube announced that all annotations will be deleted on January 15th, 2019](https://support.google.com/youtube/answer/7342737). I believe that annotations have a very important place in YouTube's history, and [announced a project to archive them](https://www.reddit.com/r/DataHoarder/comments/aa6czg/youtube_annotation_archive/).
|
||||
|
||||
I expect annotations to be supported in the Invidious player once archiving is complete (see [#110](https://github.com/omarroth/invidious/issues/110) for details), and would also like to host them for other developers to use in their projects.
|
||||
|
||||
The code is available [here](https://github.com/omarroth/archive), and contains instructions for running a worker if you would like to contribute. There's much more information available in the announcement as well for anyone who is interested.
|
||||
|
||||
## Tor
|
||||
|
||||
I unfortunately missed the chance to mention this in the previous release, but I'm now happy to announce that you can now view Invidious through Tor at the following links:
|
||||
|
||||
kgg2m7yk5aybusll.onion
|
||||
axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4bzzsg2ii4fv2iid.onion
|
||||
|
||||
Invidious is well suited to use through Tor, as it does not require any JS and is fairly lightweight. I'd recommend looking [here](https://diasp.org/posts/10965196) and [here](https://www.reddit.com/r/TOR/comments/a3c1ak/you_can_now_watch_youtube_videos_anonymously_with/) for more details on how to use the onion links, and would like to say thank you to [/u/whonix-os](https://www.reddit.com/user/whonix-os) for suggesting it and providing support setting setting them up.
|
||||
|
||||
## Popular and Trending
|
||||
|
||||
You can now easily view videos trending on YouTube with [`a16f967`](https://github.com/omarroth/invidious/a16f967). It also provides support for viewing YouTube's various categories categories, such as `News`, `Gaming`, and `Music`. You can also change the `region` parameter to view trending in different countries, which should be made easier to use in the coming weeks.
|
||||
|
||||
A link to `/feed/popular` has also been added, which provides a list of videos sorted using the algorithm described [here](https://github.com/omarroth/invidious/issues/217#issuecomment-436503761). I think it better reflects what users watch on the site, but I'd like to hear peoples' thoughts on this and on how it could be improved.
|
||||
|
||||
## Finances
|
||||
|
||||
### Donations
|
||||
|
||||
- [Patreon](https://www.patreon.com/omarroth): \$64.63
|
||||
- [Liberapay](https://liberapay.com/omarroth) : \$30.05
|
||||
- Crypto : ~\$28.74 (converted from BCH, BTC)
|
||||
- Total : \$123.42
|
||||
|
||||
### Expenses
|
||||
|
||||
- invidious-load1 (nyc1) : \$10.00 (load balancer)
|
||||
- invidious-update1 (s-1vcpu-1gb) : \$5.00 (updates feeds)
|
||||
- invidious-node1 (s-1vcpu-1gb) : \$5.00 (web server)
|
||||
- invidious-node2 (s-1vcpu-1gb) : \$5.00 (web server)
|
||||
- invidious-node3 (s-1vcpu-1gb) : \$5.00 (web server)
|
||||
- invidious-node4 (s-1vcpu-1gb) : \$5.00 (web server)
|
||||
- invidious-db1 (s-4vcpu-8gb) : \$40.00 (database)
|
||||
- Total : \$75.00
|
||||
|
||||
### What will happen with what's left over?
|
||||
|
||||
I believe this is the first month that all expenses have been fully paid for by donations. Thank you! I expect to allocate the current amount for hardware to improve performance and for hosting annotation data, as mentioned above.
|
||||
|
||||
Anything that is left over is kept to continue hosting the project for as long as possible. Thank you again everyone!
|
||||
|
||||
I think that's everything for 2018. There's lots still planned, and I'm very excited for the future of this project!
|
||||
|
||||
# 0.12.0 (2018-12-06)
|
||||
|
||||
## Version 0.12.0: Accessibility, Privacy, Transparency
|
||||
|
141
README.md
141
README.md
@ -27,6 +27,13 @@ Patreon: https://patreon.com/omarroth
|
||||
BTC: 356DpZyMXu6rYd55Yqzjs29n79kGKWcYrY
|
||||
BCH: qq4ptclkzej5eza6a50et5ggc58hxsq5aylqut2npk
|
||||
|
||||
Onion links:
|
||||
|
||||
- kgg2m7yk5aybusll.onion
|
||||
- axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4bzzsg2ii4fv2iid.onion
|
||||
|
||||
[Alternative Invidious instances](https://github.com/omarroth/invidious/wiki/Invidious-Instances)
|
||||
|
||||
## Installation
|
||||
|
||||
### Docker:
|
||||
@ -52,71 +59,98 @@ $ docker volume rm invidious_postgresdata
|
||||
$ docker-compose build
|
||||
```
|
||||
|
||||
### Arch Linux:
|
||||
### Linux:
|
||||
|
||||
#### Install dependencies
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
$ sudo pacman -S shards crystal imagemagick librsvg
|
||||
# Arch Linux
|
||||
$ sudo pacman -S shards crystal imagemagick librsvg postgresql
|
||||
|
||||
# Setup PostgresSQL
|
||||
$ sudo systemctl enable postgresql
|
||||
$ sudo systemctl start postgresql
|
||||
$ sudo -i -u postgres
|
||||
$ createuser -s YOUR_USER_NAME
|
||||
$ createdb YOUR_USER_NAME
|
||||
$ exit
|
||||
|
||||
# Setup Invidious
|
||||
$ git clone https://github.com/omarroth/invidious
|
||||
$ cd invidious
|
||||
$ ./setup.sh
|
||||
$ shards
|
||||
$ crystal build src/invidious.cr --release
|
||||
```
|
||||
|
||||
### On Ubuntu:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
# 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
|
||||
$ sudo apt update
|
||||
# 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 librsvg2-dev postgresql imagemagick libsqlite3-dev
|
||||
```
|
||||
|
||||
# Setup PostgreSQL
|
||||
#### Add invidious user and clone repository
|
||||
|
||||
```bash
|
||||
$ useradd -m invidious
|
||||
$ sudo -i -u invidious
|
||||
$ git clone https://github.com/omarroth/invidious
|
||||
$ exit
|
||||
```
|
||||
|
||||
#### Setup PostgresSQL
|
||||
|
||||
```bash
|
||||
$ sudo systemctl enable postgresql
|
||||
$ sudo systemctl start postgresql
|
||||
$ sudo -i -u postgres
|
||||
$ createuser -s YOUR_USER_NAME_HERE
|
||||
$ createdb YOUR_USER_NAME_HERE
|
||||
$ psql -c "CREATE USER kemal WITH PASSWORD 'kemal';"
|
||||
$ createdb -O kemal invidious
|
||||
$ psql invidious < /home/invidious/invidious/config/sql/channels.sql
|
||||
$ psql invidious < /home/invidious/invidious/config/sql/videos.sql
|
||||
$ psql invidious < /home/invidious/invidious/config/sql/channel_videos.sql
|
||||
$ psql invidious < /home/invidious/invidious/config/sql/users.sql
|
||||
$ psql invidious < /home/invidious/invidious/config/sql/nonces.sql
|
||||
$ exit
|
||||
|
||||
# Setup Invidious
|
||||
$ git clone https://github.com/omarroth/invidious
|
||||
$ cd invidious
|
||||
$ ./setup.sh
|
||||
$ shards
|
||||
$ crystal build src/invidious.cr --release
|
||||
```
|
||||
|
||||
### On OSX:
|
||||
#### Setup Invidious
|
||||
|
||||
```bash
|
||||
$ sudo -i -u invidious
|
||||
$ cd invidious
|
||||
$ shards
|
||||
$ 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 invidious.service
|
||||
$ sudo systemctl start invidious.service
|
||||
```
|
||||
|
||||
### OSX:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
$ brew update
|
||||
$ brew install shards crystal-lang postgres imagemagick librsvg
|
||||
|
||||
# Setup Invidious
|
||||
# Clone repository and setup postgres database
|
||||
$ git clone https://github.com/omarroth/invidious
|
||||
$ cd invidious
|
||||
$ ./setup.sh
|
||||
$ brew services start postgresql
|
||||
$ psql -c "CREATE ROLE kemal WITH LOGIN PASSWORD 'kemal';"
|
||||
$ createdb invidious -U kemal
|
||||
$ psql invidious < config/sql/channels.sql
|
||||
$ psql invidious < config/sql/videos.sql
|
||||
$ psql invidious < config/sql/channel_videos.sql
|
||||
$ psql invidious < config/sql/users.sql
|
||||
$ psql invidious < config/sql/nonces.sql
|
||||
|
||||
# Setup Invidious
|
||||
$ shards
|
||||
$ crystal build src/invidious.cr --release
|
||||
```
|
||||
|
||||
## Update Invidious
|
||||
You can find information about how to update in the wiki: [Updating](https://github.com/omarroth/invidious/wiki/Updating).
|
||||
|
||||
## Usage:
|
||||
|
||||
```bash
|
||||
$ crystal build src/invidious.cr --release
|
||||
$ ./invidious -h
|
||||
Usage: invidious [arguments]
|
||||
-b HOST, --bind HOST Host to bind (defaults to 0.0.0.0)
|
||||
@ -126,13 +160,14 @@ Usage: invidious [arguments]
|
||||
--ssl-cert-file FILE SSL certificate file
|
||||
-h, --help Shows this help
|
||||
-t THREADS, --crawl-threads=THREADS
|
||||
Number of threads for crawling (default: 1)
|
||||
Number of threads for crawling YouTube (default: 0)
|
||||
-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)
|
||||
-v THREADS, --video-threads=THREADS
|
||||
Number of threads for refreshing videos (default: 1)
|
||||
Number of threads for refreshing videos (default: 0)
|
||||
-o OUTPUT, --output=OUTPUT Redirect output (default: STDOUT)
|
||||
```
|
||||
|
||||
Or for development:
|
||||
@ -142,13 +177,11 @@ $ curl -fsSLo- https://raw.githubusercontent.com/samueleaton/sentry/master/insta
|
||||
$ ./sentry
|
||||
```
|
||||
|
||||
## Extensions
|
||||
## Documentation
|
||||
[Documentation](https://github.com/omarroth/invidious/wiki) can be found in the wiki.
|
||||
|
||||
- [Alternate Tube Redirector](https://addons.mozilla.org/en-US/firefox/addon/alternate-tube-redirector/): Automatically open Youtube Videos on alternate sites like Invidious or Hooktube.
|
||||
- [Invidious Redirect](https://greasyfork.org/en/scripts/370461-invidious-redirect): Redirects Youtube URLs to Invidio.us (userscript)
|
||||
- [iPhone Redirector Shortcut](https://www.icloud.com/shortcuts/6bbf26d989cf4d07a5fe1626efbc0950): Automatically open YouTube videos in Invidious (iPhone shortcut)
|
||||
- [Youtube to Invidious](https://greasyfork.org/en/scripts/375264-youtube-to-invidious): Scan page for youtube embeds and urls and replace with Invidious (userscript)
|
||||
- [Invidious Downloader](https://github.com/erupete/InvidiousDownloader): Tampermonkey userscript for downloading videos or audio on Invidious (userscript)
|
||||
## Extensions
|
||||
Extensions for Invidious and for integrating Invidious into other projects [are in the wiki](https://github.com/omarroth/invidious/wiki/Extensions)
|
||||
|
||||
## Made with Invidious
|
||||
|
||||
@ -164,6 +197,18 @@ $ ./sentry
|
||||
4. Push to the branch (git push origin my-new-feature)
|
||||
5. Create a new Pull Request
|
||||
|
||||
## Contributors
|
||||
## Contact
|
||||
|
||||
- [omarroth](https://github.com/omarroth) - creator, maintainer
|
||||
Feel free to send an email to omarroth@protonmail.com or join our [Matrix Server](https://riot.im/app/#/room/#invidious:matrix.org), or #invidious on Freenode.
|
||||
|
||||
You can also view release notes on the [releases](https://github.com/omarroth/invidious/releases) page or in the CHANGELOG.md included in the repository.
|
||||
|
||||
## License
|
||||
|
||||
[](http://www.gnu.org/licenses/agpl-3.0.en.html)
|
||||
|
||||
Invidious is Free Software: You can use, study share and improve it at your
|
||||
will. Specifically you can redistribute and/or modify it under the terms of the
|
||||
[GNU Affero General Public License](https://www.gnu.org/licenses/agpl.html) as
|
||||
published by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
@ -1,3 +1,43 @@
|
||||
.channel-owner {
|
||||
background-color: #008BEC;
|
||||
color: #fff;
|
||||
border-radius: 9px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
.creator-heart-container {
|
||||
display: inline-block;
|
||||
padding: 0px 7px 6px 0px;
|
||||
margin: 0px -7px -4px 0px;
|
||||
}
|
||||
|
||||
.creator-heart {
|
||||
position: relative;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px none;
|
||||
}
|
||||
|
||||
.creator-heart-background-hearted {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 0px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.creator-heart-small-hearted {
|
||||
position: absolute;
|
||||
right: -7px;
|
||||
bottom: -4px;
|
||||
}
|
||||
|
||||
.creator-heart-small-container {
|
||||
position: relative;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
color: rgb(255, 0, 0);
|
||||
}
|
||||
|
||||
.h-box {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
@ -22,11 +62,13 @@ div {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
a.pure-button-primary {
|
||||
button.pure-button-primary,
|
||||
a.pure-button-primary, .channel-owner:hover {
|
||||
background-color: #a0a0a0;
|
||||
color: rgba(35, 35, 35, 1);
|
||||
}
|
||||
|
||||
button.pure-button-primary:hover,
|
||||
a.pure-button-primary:hover {
|
||||
background-color: rgba(0, 182, 240, 1);
|
||||
color: #fff;
|
||||
@ -262,8 +304,25 @@ img.thumbnail {
|
||||
|
||||
#player-container {
|
||||
position: relative;
|
||||
padding-bottom: 56.25%;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
padding-bottom: 55.25%;
|
||||
margin-left: 2em;
|
||||
margin-right: 2em;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
#progress-container {
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
background-color: #a0a0a0;
|
||||
color: rgba(35, 35, 35, 1);
|
||||
}
|
||||
|
||||
#download-progress {
|
||||
width: 0%;
|
||||
border-radius: 2px;
|
||||
height: 10px;
|
||||
background-color: rgba(0, 182, 240, 1);
|
||||
color: #fff;
|
||||
margin-top: 0.5em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
@ -4,6 +4,6 @@ a:active {
|
||||
}
|
||||
|
||||
a {
|
||||
color: #303030;
|
||||
color: #61809b;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
@ -50,3 +50,57 @@ function hide_youtube_replies(target) {
|
||||
target.innerHTML = "Show replies";
|
||||
target.setAttribute("onclick", "show_youtube_replies(this)");
|
||||
}
|
||||
|
||||
function download_video(target) {
|
||||
var title = target.getAttribute("data-title");
|
||||
var children = document.getElementById("download_widget").children;
|
||||
var progress = document.getElementById("download-progress");
|
||||
var url = "";
|
||||
|
||||
document.getElementById("progress-container").style.display = "";
|
||||
|
||||
for (i = 0; i < children.length; i++) {
|
||||
if (children[i].selected) {
|
||||
url = children[i].getAttribute("data-url");
|
||||
}
|
||||
}
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url);
|
||||
xhr.responseType = "arraybuffer";
|
||||
|
||||
xhr.onprogress = function(event) {
|
||||
if (event.lengthComputable) {
|
||||
progress.style.width = "" + (event.loaded / event.total)*100 + "%";
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onload = function(event) {
|
||||
if (event.currentTarget.status != 200) {
|
||||
console.log("Downloading " + title + " failed.")
|
||||
document.getElementById("progress-container").style.display = "none";
|
||||
progress.style.width = "0%";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var data = new Blob([xhr.response], {'type' : 'video/mp4'});
|
||||
var videoFile = window.URL.createObjectURL(data);
|
||||
|
||||
var link = document.createElement('a');
|
||||
link.href = videoFile;
|
||||
link.setAttribute('download', title);
|
||||
document.body.appendChild(link);
|
||||
|
||||
window.requestAnimationFrame(function() {
|
||||
var event = new MouseEvent('click');
|
||||
link.dispatchEvent(event);
|
||||
document.body.removeChild(link);
|
||||
});
|
||||
|
||||
document.getElementById("progress-container").style.display = "none";
|
||||
progress.style.width = "0%";
|
||||
};
|
||||
|
||||
xhr.send(null);
|
||||
}
|
@ -10,3 +10,4 @@ db:
|
||||
dbname: invidious
|
||||
full_refresh: false
|
||||
https_only: false
|
||||
domain: invidio.us
|
||||
|
@ -2,7 +2,6 @@ FROM postgres:10
|
||||
|
||||
ENV POSTGRES_USER postgres
|
||||
|
||||
ADD ./setup.sh /setup.sh
|
||||
ADD ./config/sql /config/sql
|
||||
ADD ./docker/entrypoint.postgres.sh /entrypoint.sh
|
||||
|
||||
|
@ -10,7 +10,14 @@ if [ ! -f /var/lib/postgresql/data/setupFinished ]; then
|
||||
sleep 5
|
||||
done
|
||||
>&2 echo "### importing table schemas"
|
||||
su postgres -c "/setup.sh" && touch /var/lib/postgresql/data/setupFinished
|
||||
su postgres -c 'createdb invidious'
|
||||
su postgres -c 'psql -c "CREATE USER kemal WITH PASSWORD '"'kemal'"'"'
|
||||
su postgres -c 'psql invidious < config/sql/channels.sql'
|
||||
su postgres -c 'psql invidious < config/sql/videos.sql'
|
||||
su postgres -c 'psql invidious < config/sql/channel_videos.sql'
|
||||
su postgres -c 'psql invidious < config/sql/users.sql'
|
||||
su postgres -c 'psql invidious < config/sql/nonces.sql'
|
||||
touch /var/lib/postgresql/data/setupFinished
|
||||
echo "### invidious database setup finished"
|
||||
exit
|
||||
fi
|
||||
|
19
invidious.service
Normal file
19
invidious.service
Normal file
@ -0,0 +1,19 @@
|
||||
[Unit]
|
||||
Description=Invidious (An alternative YouTube front-end)
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
RestartSec=2s
|
||||
Type=simple
|
||||
|
||||
User=invidious
|
||||
Group=invidious
|
||||
|
||||
WorkingDirectory=/home/invidious/invidious
|
||||
ExecStart=/home/invidious/invidious/invidious -o invidious.log
|
||||
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -265,9 +265,20 @@
|
||||
"`x` minutes": "`x` دقائق",
|
||||
"`x` seconds": "`x` ثوانى",
|
||||
"Fallback comments: ": "التعليقات المصاحبة",
|
||||
"Popular": "الشائع",
|
||||
"Popular": "لاكثر شعبية",
|
||||
"Top": "الأفضل",
|
||||
"About": "حول",
|
||||
"Rating: ": "التقييم",
|
||||
"Language: ": "اللغة"
|
||||
"Language: ": "اللغة",
|
||||
"Default": "الكل",
|
||||
"Music": "الاغانى",
|
||||
"Gaming": "الألعاب",
|
||||
"News": "الأخبار",
|
||||
"Movies": "الأفلام",
|
||||
"Download as: ": "تحميل كـ",
|
||||
"Download": "تحميل",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"Youtube permalink of the comment": "",
|
||||
"`x` marked it with a ❤": ""
|
||||
}
|
||||
|
553
locales/de.json
553
locales/de.json
@ -1,273 +1,284 @@
|
||||
{
|
||||
"`x` subscribers": "`x` Abonnenten",
|
||||
"`x` videos": "`x` Videos",
|
||||
"LIVE": "LIVE",
|
||||
"Shared `x` ago": "Vor `x` geteilt",
|
||||
"Unsubscribe": "Abbestellen",
|
||||
"Subscribe": "Abonnieren",
|
||||
"Login to subscribe to `x`": "Einloggen um `x` zu abonnieren",
|
||||
"View channel on YouTube": "Kanal auf YouTube anzeigen",
|
||||
"newest": "neueste",
|
||||
"oldest": "älteste",
|
||||
"popular": "beliebt",
|
||||
"Preview page": "Vorschau Seite",
|
||||
"Next page": "Nächste Seite",
|
||||
"Clear watch history?": "Verlauf löschen?",
|
||||
"Yes": "Ja",
|
||||
"No": "Nein",
|
||||
"Import and Export Data": "Import und Export Daten",
|
||||
"Import": "Importieren",
|
||||
"Import Invidious data": "Invidious Daten importieren",
|
||||
"Import YouTube subscriptions": "YouTube Abonnements importieren",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTube Abonnements importieren (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipe Abonnements importieren (.json)",
|
||||
"Import NewPipe data (.zip)": "NewPipe Daten importieren (.zip)",
|
||||
"Export": "Exportieren",
|
||||
"Export subscriptions as OPML": "Abonnements als OPML exportieren",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Abonnements als OPML exportieren (für NewPipe & FreeTube)",
|
||||
"Export data as JSON": "Daten als JSON exportieren",
|
||||
"Delete account?": "Account löschen?",
|
||||
"History": "Verlauf",
|
||||
"Previous page": "Vorherige Seite",
|
||||
"An alternative front-end to YouTube": "Eine alternative Oberfläche für YouTube",
|
||||
"JavaScript license information": "JavaScript Lizenzinformationen",
|
||||
"source": "Quelle",
|
||||
"Login": "Einloggen",
|
||||
"Login/Register": "Einloggen/Registrieren",
|
||||
"Login to Google": "In Google einloggen",
|
||||
"User ID:": "Benutzer ID:",
|
||||
"Password:": "Passwort:",
|
||||
"Time (h:mm:ss):": "Zeit (h:mm:ss):",
|
||||
"Text CAPTCHA": "Text CAPTCHA",
|
||||
"Image CAPTCHA": "Image CAPTCHA",
|
||||
"Sign In": "Einloggen",
|
||||
"Register": "Registrieren",
|
||||
"Email:": "Email:",
|
||||
"Google verification code:": "Google Bestätigungscode:",
|
||||
"Preferences": "Einstellungen",
|
||||
"Player preferences": "Playereinstellungen",
|
||||
"Always loop: ": "Immer wiederholen: ",
|
||||
"Autoplay: ": "Automatisch abspielen: ",
|
||||
"Autoplay next video: ": "nächstes Video automatisch abspielen: ",
|
||||
"Listen by default: ": "Nur Ton als Standard: ",
|
||||
"Default speed: ": "Standardgeschwindigkeit: ",
|
||||
"Preferred video quality: ": "Bevorzugte Videoqualität: ",
|
||||
"Player volume: ": "Playerlautstärke: ",
|
||||
"Default comments: ": "Standardkommentare: ",
|
||||
"youtube": "youtube",
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "Standarduntertitel: ",
|
||||
"Fallback captions: ": "Ersatzuntertitel: ",
|
||||
"Show related videos? ": "Ähnliche Videos anzeigen? ",
|
||||
"Visual preferences": "Anzeigeeinstellungen",
|
||||
"Dark mode: ": "Nachtmodus: ",
|
||||
"Thin mode: ": "Schlanker Modus: ",
|
||||
"Subscription preferences": "Abonnementeinstellungen",
|
||||
"Redirect homepage to feed: ": "Startseite zu Feed umleiten: ",
|
||||
"Number of videos shown in feed: ": "Anzahl von Videos die im Feed angezeigt werden: ",
|
||||
"Sort videos by: ": "Videos sortieren nach: ",
|
||||
"published": "veröffentlicht",
|
||||
"published - reverse": "veröffentlicht - invertiert",
|
||||
"alphabetically": "alphabetisch",
|
||||
"alphabetically - reverse": "alphabetisch - invertiert",
|
||||
"channel name": "Kanalname",
|
||||
"channel name - reverse": "Kanalname - invertiert",
|
||||
"Only show latest video from channel: ": "Nur neueste Videos des Kanals anzeigen: ",
|
||||
"Only show latest unwatched video from channel: ": "Nur neueste ungesehene Videos des Kanals anzeigen: ",
|
||||
"Only show unwatched: ": "Nur ungesehene anzeigen: ",
|
||||
"Only show notifications (if there are any): ": "Nur Benachrichtigungen anzeigen (wenn es welche gibt): ",
|
||||
"Data preferences": "Dateneinstellungen",
|
||||
"Clear watch history": "Verlauf löschen",
|
||||
"Import/Export data": "Daten im- exportieren",
|
||||
"Manage subscriptions": "Abonnements verwalten",
|
||||
"Watch history": "Verlauf",
|
||||
"Delete account": "Account löschen",
|
||||
"Save preferences": "Einstellungen speichern",
|
||||
"Subscription manager": "Abonnementverwaltung",
|
||||
"`x` subscriptions": "`x` Abonnements",
|
||||
"Import/Export": "Importieren/Exportieren",
|
||||
"unsubscribe": "abbestellen",
|
||||
"Subscriptions": "Abonnements",
|
||||
"`x` unseen notifications": "`x` ungesehene Benachrichtigungen",
|
||||
"search": "Suchen",
|
||||
"Sign out": "Abmelden",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Veröffentlicht unter AGPLv3 von Omar Roth.",
|
||||
"Source available here.": "Quellcode verfügbar hier.",
|
||||
"Liberapay: ": "Liberapay: ",
|
||||
"Patreon: ": "Patreon: ",
|
||||
"BTC: ": "BTC: ",
|
||||
"BCH: ": "BCH: ",
|
||||
"View JavaScript license information.": "Javascript Lizenzinformationen anzeigen.",
|
||||
"Trending": "Trending",
|
||||
"Watch video on Youtube": "Video auf YouTube ansehen",
|
||||
"Genre: ": "Genre: ",
|
||||
"License: ": "Lizenz: ",
|
||||
"Family friendly? ": "Familienfreundlich? ",
|
||||
"Wilson score: ": "Wilson-Score: ",
|
||||
"Engagement: ": "Engagement: ",
|
||||
"Whitelisted regions: ": "Erlaubte Regionen: ",
|
||||
"Blacklisted regions: ": "Unerlaubte Regionen: ",
|
||||
"Shared `x`": "Geteilt `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Hallo! Anscheinend haben Sie JavaScript deaktiviert. Klicken Sie hier um Kommentare anzuzeigen, beachten sie dass es etwas länger dauern kann um sie zu laden.",
|
||||
"View YouTube comments": "YouTube Kommentare anzeigen",
|
||||
"View more comments on Reddit": "Mehr Kommentare auf Reddit anzeigen",
|
||||
"View `x` comments": "`x` Kommentare anzeigen",
|
||||
"View Reddit comments": "Reddit Kommentare anzeigen",
|
||||
"Hide replies": "Antworten verstecken",
|
||||
"Show replies": "Antworten anzeigen",
|
||||
"Incorrect password": "Falsches Passwort",
|
||||
"Quota exceeded, try again in a few hours": "Kontingent überschritten, versuche es in ein paar Stunden erneut",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Login nicht möglich, stellen Sie sicher dass two-factor Authentifikation (Authentifizierung oder SMS) aktiviert ist.",
|
||||
"Invalid TFA code": "Ungültiger TFA Code",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Login fehlgeschlagen. Das kann daran liegen dass two-factor Authentifizierung in ihrem Account nicht aktiviert ist.",
|
||||
"Invalid answer": "Ungültige Antwort",
|
||||
"Invalid CAPTCHA": "Ungültiges CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA ist eine erforderliche Eingabe",
|
||||
"User ID is a required field": "Benutzer ID ist eine erforderliche Eingabe",
|
||||
"Password is a required field": "Passwort ist eine erforderliche Eingabe",
|
||||
"Invalid username or password": "Ungültiger Benutzername oder Passwort",
|
||||
"Please sign in using 'Sign in with Google'": "Bitte melden sie sich mit 'Mit Google anmelden' an",
|
||||
"Password cannot be empty": "Passwort darf nicht leer sein",
|
||||
"Password cannot be longer than 55 characters": "Passwort darf nicht länger als 55 Zeichen sein",
|
||||
"Please sign in": "Bitte anmelden",
|
||||
"Invidious Private Feed for `x`": "Invidious Persönlicher Feed für `x`",
|
||||
"channel:`x`": "Kanal:`x`",
|
||||
"Deleted or invalid channel": "Gelöschter oder ungültiger Kanal",
|
||||
"This channel does not exist.": "Dieser Kanal existiert nicht.",
|
||||
"Could not get channel info.": "Kanalinformationen konnten nicht geladen werden.",
|
||||
"Could not fetch comments": "Kommentare konnten nicht geladen werden",
|
||||
"View `x` replies": "Zeige `x` Antworten",
|
||||
"`x` ago": "vor `x`",
|
||||
"Load more": "Mehr laden",
|
||||
"`x` points": "`x` Punkte",
|
||||
"Could not create mix.": "Mix konnte nicht erstellt werden.",
|
||||
"Playlist is empty": "Playlist ist leer",
|
||||
"Invalid playlist.": "Ungültige Playlist.",
|
||||
"Playlist does not exist.": "Playlist existiert nicht.",
|
||||
"Could not pull trending pages.": "Trending Seiten konnten nicht geladen werden.",
|
||||
"Hidden field \"challenge\" is a required field": "Verstecktes Feld \"challenge\" ist eine erforderliche Eingabe",
|
||||
"Hidden field \"token\" is a required field": "Verstecktes Feld \"token\" ist eine erforderliche Eingabe",
|
||||
"Invalid challenge": "Ungültiger Test",
|
||||
"Invalid token": "Ungöltige Marke",
|
||||
"Invalid user": "Ungültiger Benutzer",
|
||||
"Token is expired, please try again": "Marke ist abgelaufen, bitte erneut versuchen",
|
||||
"English": "Englisch",
|
||||
"English (auto-generated)": "Englisch (automatisch erzeugt)",
|
||||
"Afrikaans": "Afrikaans",
|
||||
"Albanian": "Albanisch",
|
||||
"Amharic": "Amharisch",
|
||||
"Arabic": "Arabisch",
|
||||
"Armenian": "Armenisch",
|
||||
"Azerbaijani": "Aserbaidschanisch",
|
||||
"Bangla": "Bengalisch",
|
||||
"Basque": "Baskisch",
|
||||
"Belarusian": "Weißrussisch",
|
||||
"Bosnian": "Bosnisch",
|
||||
"Bulgarian": "Bulgarisch",
|
||||
"Burmese": "Burmesisch",
|
||||
"Catalan": "Katalanisch",
|
||||
"Cebuano": "Cebuano",
|
||||
"Chinese (Simplified)": "Chinesisch (vereinfacht)",
|
||||
"Chinese (Traditional)": "Chinesisch (traditionell)",
|
||||
"Corsican": "Korsisch",
|
||||
"Croatian": "Kroatisch",
|
||||
"Czech": "Tschechisch",
|
||||
"Danish": "Dänisch",
|
||||
"Dutch": "Niederländisch",
|
||||
"Esperanto": "Esperanto",
|
||||
"Estonian": "Estnisch",
|
||||
"Filipino": "Philippinisch",
|
||||
"Finnish": "Finnisch",
|
||||
"French": "Französisch",
|
||||
"Galician": "Galizisch",
|
||||
"Georgian": "Georgisch",
|
||||
"German": "Deutsch",
|
||||
"Greek": "Griechisch",
|
||||
"Gujarati": "Gujarati",
|
||||
"Haitian Creole": "Haitianisches Kreolisch",
|
||||
"Hausa": "Hausa",
|
||||
"Hawaiian": "Hawaiianisch",
|
||||
"Hebrew": "Hebräisch",
|
||||
"Hindi": "Hindi",
|
||||
"Hmong": "Hmong",
|
||||
"Hungarian": "Ungarisch",
|
||||
"Icelandic": "Isländisch",
|
||||
"Igbo": "Igbo",
|
||||
"Indonesian": "Indonesisch",
|
||||
"Irish": "Irisch",
|
||||
"Italian": "Italienisch",
|
||||
"Japanese": "Japanisch",
|
||||
"Javanese": "Javanisch",
|
||||
"Kannada": "Kannada",
|
||||
"Kazakh": "Kasachisch",
|
||||
"Khmer": "Khmer",
|
||||
"Korean": "Koreanisch",
|
||||
"Kurdish": "Kurdisch",
|
||||
"Kyrgyz": "Kirgisisch",
|
||||
"Lao": "Laotisch",
|
||||
"Latin": "Lateinisch",
|
||||
"Latvian": "Lettisch",
|
||||
"Lithuanian": "Litauisch",
|
||||
"Luxembourgish": "Luxemburgisch",
|
||||
"Macedonian": "Mazedonisch",
|
||||
"Malagasy": "Madagassisch",
|
||||
"Malay": "Malaiisch",
|
||||
"Malayalam": "Malayalam",
|
||||
"Maltese": "Maltesisch",
|
||||
"Maori": "Maori",
|
||||
"Marathi": "Marathi",
|
||||
"Mongolian": "Mongolisch",
|
||||
"Nepali": "Nepalesisch",
|
||||
"Norwegian": "Norwegisch",
|
||||
"Nyanja": "Nyanja",
|
||||
"Pashto": "Paschtunisch",
|
||||
"Persian": "Persisch",
|
||||
"Polish": "Polnisch",
|
||||
"Portuguese": "Portugiesisch",
|
||||
"Punjabi": "Pandschabi",
|
||||
"Romanian": "Rumänisch",
|
||||
"Russian": "Russisch",
|
||||
"Samoan": "Samoanisch",
|
||||
"Scottish Gaelic": "Schottisches Gälisch",
|
||||
"Serbian": "Serbisch",
|
||||
"Shona": "Schona",
|
||||
"Sindhi": "Sindhi",
|
||||
"Sinhala": "Singhalesisch",
|
||||
"Slovak": "Slowakisch",
|
||||
"Slovenian": "Slowenisch",
|
||||
"Somali": "Somali",
|
||||
"Southern Sotho": "Südliches Sotho",
|
||||
"Spanish": "Spanisch",
|
||||
"Spanish (Latin America)": "Spanisch (Lateinamerika)",
|
||||
"Sundanese": "Sundanesisch",
|
||||
"Swahili": "Suaheli",
|
||||
"Swedish": "Schwedisch",
|
||||
"Tajik": "Tadschikisch",
|
||||
"Tamil": "Tamilisch",
|
||||
"Telugu": "Telugu",
|
||||
"Thai": "Thailändisch",
|
||||
"Turkish": "Türkisch",
|
||||
"Ukrainian": "Ukrainisch",
|
||||
"Urdu": "Urdu",
|
||||
"Uzbek": "Usbekisch",
|
||||
"Vietnamese": "Vietnamesisch",
|
||||
"Welsh": "Walisisch",
|
||||
"Western Frisian": "Westfriesisch",
|
||||
"Xhosa": "Xhosa",
|
||||
"Yiddish": "Jiddisch",
|
||||
"Yoruba": "Joruba",
|
||||
"Zulu": "Zulu",
|
||||
"`x` years": "`x` Jahre",
|
||||
"`x` months": "`x` Monate",
|
||||
"`x` weeks": "`x` Wochen",
|
||||
"`x` days": "`x` Tage",
|
||||
"`x` hours": "`x` Stunden",
|
||||
"`x` minutes": "`x` Minuten",
|
||||
"`x` seconds": "`x` Sekunden",
|
||||
"Fallback comments: ": "",
|
||||
"Popular": "Populär",
|
||||
"Top": "",
|
||||
"About": "Über",
|
||||
"Rating: ": "Bewertung: ",
|
||||
"Language: ": "Sprache: "
|
||||
"`x` subscribers": "`x` Abonnenten",
|
||||
"`x` videos": "`x` Videos",
|
||||
"LIVE": "LIVE",
|
||||
"Shared `x` ago": "Vor `x` geteilt",
|
||||
"Unsubscribe": "Abbestellen",
|
||||
"Subscribe": "Abonnieren",
|
||||
"Login to subscribe to `x`": "Einloggen um `x` zu abonnieren",
|
||||
"View channel on YouTube": "Kanal auf YouTube anzeigen",
|
||||
"newest": "neueste",
|
||||
"oldest": "älteste",
|
||||
"popular": "beliebt",
|
||||
"Preview page": "Vorschau Seite",
|
||||
"Next page": "Nächste Seite",
|
||||
"Clear watch history?": "Verlauf löschen?",
|
||||
"Yes": "Ja",
|
||||
"No": "Nein",
|
||||
"Import and Export Data": "Import und Export Daten",
|
||||
"Import": "Importieren",
|
||||
"Import Invidious data": "Invidious Daten importieren",
|
||||
"Import YouTube subscriptions": "YouTube Abonnements importieren",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTube Abonnements importieren (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipe Abonnements importieren (.json)",
|
||||
"Import NewPipe data (.zip)": "NewPipe Daten importieren (.zip)",
|
||||
"Export": "Exportieren",
|
||||
"Export subscriptions as OPML": "Abonnements als OPML exportieren",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Abonnements als OPML exportieren (für NewPipe & FreeTube)",
|
||||
"Export data as JSON": "Daten als JSON exportieren",
|
||||
"Delete account?": "Account löschen?",
|
||||
"History": "Verlauf",
|
||||
"Previous page": "Vorherige Seite",
|
||||
"An alternative front-end to YouTube": "Eine alternative Oberfläche für YouTube",
|
||||
"JavaScript license information": "JavaScript Lizenzinformationen",
|
||||
"source": "Quelle",
|
||||
"Login": "Einloggen",
|
||||
"Login/Register": "Einloggen/Registrieren",
|
||||
"Login to Google": "In Google einloggen",
|
||||
"User ID:": "Benutzer ID:",
|
||||
"Password:": "Passwort:",
|
||||
"Time (h:mm:ss):": "Zeit (h:mm:ss):",
|
||||
"Text CAPTCHA": "Text CAPTCHA",
|
||||
"Image CAPTCHA": "Image CAPTCHA",
|
||||
"Sign In": "Einloggen",
|
||||
"Register": "Registrieren",
|
||||
"Email:": "Email:",
|
||||
"Google verification code:": "Google Bestätigungscode:",
|
||||
"Preferences": "Einstellungen",
|
||||
"Player preferences": "Playereinstellungen",
|
||||
"Always loop: ": "Immer wiederholen: ",
|
||||
"Autoplay: ": "Automatisch abspielen: ",
|
||||
"Autoplay next video: ": "nächstes Video automatisch abspielen: ",
|
||||
"Listen by default: ": "Nur Ton als Standard: ",
|
||||
"Default speed: ": "Standardgeschwindigkeit: ",
|
||||
"Preferred video quality: ": "Bevorzugte Videoqualität: ",
|
||||
"Player volume: ": "Playerlautstärke: ",
|
||||
"Default comments: ": "Standardkommentare: ",
|
||||
"youtube": "youtube",
|
||||
"reddit": "reddit",
|
||||
"Default captions: ": "Standarduntertitel: ",
|
||||
"Fallback captions: ": "Ersatzuntertitel: ",
|
||||
"Show related videos? ": "Ähnliche Videos anzeigen? ",
|
||||
"Visual preferences": "Anzeigeeinstellungen",
|
||||
"Dark mode: ": "Nachtmodus: ",
|
||||
"Thin mode: ": "Schlanker Modus: ",
|
||||
"Subscription preferences": "Abonnementeinstellungen",
|
||||
"Redirect homepage to feed: ": "Startseite zu Feed umleiten: ",
|
||||
"Number of videos shown in feed: ": "Anzahl von Videos die im Feed angezeigt werden: ",
|
||||
"Sort videos by: ": "Videos sortieren nach: ",
|
||||
"published": "veröffentlicht",
|
||||
"published - reverse": "veröffentlicht - invertiert",
|
||||
"alphabetically": "alphabetisch",
|
||||
"alphabetically - reverse": "alphabetisch - invertiert",
|
||||
"channel name": "Kanalname",
|
||||
"channel name - reverse": "Kanalname - invertiert",
|
||||
"Only show latest video from channel: ": "Nur neueste Videos des Kanals anzeigen: ",
|
||||
"Only show latest unwatched video from channel: ": "Nur neueste ungesehene Videos des Kanals anzeigen: ",
|
||||
"Only show unwatched: ": "Nur ungesehene anzeigen: ",
|
||||
"Only show notifications (if there are any): ": "Nur Benachrichtigungen anzeigen (wenn es welche gibt): ",
|
||||
"Data preferences": "Dateneinstellungen",
|
||||
"Clear watch history": "Verlauf löschen",
|
||||
"Import/Export data": "Daten im- exportieren",
|
||||
"Manage subscriptions": "Abonnements verwalten",
|
||||
"Watch history": "Verlauf",
|
||||
"Delete account": "Account löschen",
|
||||
"Save preferences": "Einstellungen speichern",
|
||||
"Subscription manager": "Abonnementverwaltung",
|
||||
"`x` subscriptions": "`x` Abonnements",
|
||||
"Import/Export": "Importieren/Exportieren",
|
||||
"unsubscribe": "abbestellen",
|
||||
"Subscriptions": "Abonnements",
|
||||
"`x` unseen notifications": "`x` ungesehene Benachrichtigungen",
|
||||
"search": "Suchen",
|
||||
"Sign out": "Abmelden",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Veröffentlicht unter AGPLv3 von Omar Roth.",
|
||||
"Source available here.": "Quellcode verfügbar hier.",
|
||||
"Liberapay: ": "Liberapay: ",
|
||||
"Patreon: ": "Patreon: ",
|
||||
"BTC: ": "BTC: ",
|
||||
"BCH: ": "BCH: ",
|
||||
"View JavaScript license information.": "Javascript Lizenzinformationen anzeigen.",
|
||||
"Trending": "Trending",
|
||||
"Watch video on Youtube": "Video auf YouTube ansehen",
|
||||
"Genre: ": "Genre: ",
|
||||
"License: ": "Lizenz: ",
|
||||
"Family friendly? ": "Familienfreundlich? ",
|
||||
"Wilson score: ": "Wilson-Score: ",
|
||||
"Engagement: ": "Engagement: ",
|
||||
"Whitelisted regions: ": "Erlaubte Regionen: ",
|
||||
"Blacklisted regions: ": "Unerlaubte Regionen: ",
|
||||
"Shared `x`": "Geteilt `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Hallo! Anscheinend haben Sie JavaScript deaktiviert. Klicken Sie hier um Kommentare anzuzeigen, beachten sie dass es etwas länger dauern kann um sie zu laden.",
|
||||
"View YouTube comments": "YouTube Kommentare anzeigen",
|
||||
"View more comments on Reddit": "Mehr Kommentare auf Reddit anzeigen",
|
||||
"View `x` comments": "`x` Kommentare anzeigen",
|
||||
"View Reddit comments": "Reddit Kommentare anzeigen",
|
||||
"Hide replies": "Antworten verstecken",
|
||||
"Show replies": "Antworten anzeigen",
|
||||
"Incorrect password": "Falsches Passwort",
|
||||
"Quota exceeded, try again in a few hours": "Kontingent überschritten, versuche es in ein paar Stunden erneut",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Login nicht möglich, stellen Sie sicher dass two-factor Authentifikation (Authentifizierung oder SMS) aktiviert ist.",
|
||||
"Invalid TFA code": "Ungültiger TFA Code",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Login fehlgeschlagen. Das kann daran liegen dass two-factor Authentifizierung in ihrem Account nicht aktiviert ist.",
|
||||
"Invalid answer": "Ungültige Antwort",
|
||||
"Invalid CAPTCHA": "Ungültiges CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA ist eine erforderliche Eingabe",
|
||||
"User ID is a required field": "Benutzer ID ist eine erforderliche Eingabe",
|
||||
"Password is a required field": "Passwort ist eine erforderliche Eingabe",
|
||||
"Invalid username or password": "Ungültiger Benutzername oder Passwort",
|
||||
"Please sign in using 'Sign in with Google'": "Bitte melden sie sich mit 'Mit Google anmelden' an",
|
||||
"Password cannot be empty": "Passwort darf nicht leer sein",
|
||||
"Password cannot be longer than 55 characters": "Passwort darf nicht länger als 55 Zeichen sein",
|
||||
"Please sign in": "Bitte anmelden",
|
||||
"Invidious Private Feed for `x`": "Invidious Persönlicher Feed für `x`",
|
||||
"channel:`x`": "Kanal:`x`",
|
||||
"Deleted or invalid channel": "Gelöschter oder ungültiger Kanal",
|
||||
"This channel does not exist.": "Dieser Kanal existiert nicht.",
|
||||
"Could not get channel info.": "Kanalinformationen konnten nicht geladen werden.",
|
||||
"Could not fetch comments": "Kommentare konnten nicht geladen werden",
|
||||
"View `x` replies": "Zeige `x` Antworten",
|
||||
"`x` ago": "vor `x`",
|
||||
"Load more": "Mehr laden",
|
||||
"`x` points": "`x` Punkte",
|
||||
"Could not create mix.": "Mix konnte nicht erstellt werden.",
|
||||
"Playlist is empty": "Playlist ist leer",
|
||||
"Invalid playlist.": "Ungültige Playlist.",
|
||||
"Playlist does not exist.": "Playlist existiert nicht.",
|
||||
"Could not pull trending pages.": "Trending Seiten konnten nicht geladen werden.",
|
||||
"Hidden field \"challenge\" is a required field": "Verstecktes Feld \"challenge\" ist eine erforderliche Eingabe",
|
||||
"Hidden field \"token\" is a required field": "Verstecktes Feld \"token\" ist eine erforderliche Eingabe",
|
||||
"Invalid challenge": "Ungültiger Test",
|
||||
"Invalid token": "Ungöltige Marke",
|
||||
"Invalid user": "Ungültiger Benutzer",
|
||||
"Token is expired, please try again": "Marke ist abgelaufen, bitte erneut versuchen",
|
||||
"English": "Englisch",
|
||||
"English (auto-generated)": "Englisch (automatisch erzeugt)",
|
||||
"Afrikaans": "Afrikaans",
|
||||
"Albanian": "Albanisch",
|
||||
"Amharic": "Amharisch",
|
||||
"Arabic": "Arabisch",
|
||||
"Armenian": "Armenisch",
|
||||
"Azerbaijani": "Aserbaidschanisch",
|
||||
"Bangla": "Bengalisch",
|
||||
"Basque": "Baskisch",
|
||||
"Belarusian": "Weißrussisch",
|
||||
"Bosnian": "Bosnisch",
|
||||
"Bulgarian": "Bulgarisch",
|
||||
"Burmese": "Burmesisch",
|
||||
"Catalan": "Katalanisch",
|
||||
"Cebuano": "Cebuano",
|
||||
"Chinese (Simplified)": "Chinesisch (vereinfacht)",
|
||||
"Chinese (Traditional)": "Chinesisch (traditionell)",
|
||||
"Corsican": "Korsisch",
|
||||
"Croatian": "Kroatisch",
|
||||
"Czech": "Tschechisch",
|
||||
"Danish": "Dänisch",
|
||||
"Dutch": "Niederländisch",
|
||||
"Esperanto": "Esperanto",
|
||||
"Estonian": "Estnisch",
|
||||
"Filipino": "Philippinisch",
|
||||
"Finnish": "Finnisch",
|
||||
"French": "Französisch",
|
||||
"Galician": "Galizisch",
|
||||
"Georgian": "Georgisch",
|
||||
"German": "Deutsch",
|
||||
"Greek": "Griechisch",
|
||||
"Gujarati": "Gujarati",
|
||||
"Haitian Creole": "Haitianisches Kreolisch",
|
||||
"Hausa": "Hausa",
|
||||
"Hawaiian": "Hawaiianisch",
|
||||
"Hebrew": "Hebräisch",
|
||||
"Hindi": "Hindi",
|
||||
"Hmong": "Hmong",
|
||||
"Hungarian": "Ungarisch",
|
||||
"Icelandic": "Isländisch",
|
||||
"Igbo": "Igbo",
|
||||
"Indonesian": "Indonesisch",
|
||||
"Irish": "Irisch",
|
||||
"Italian": "Italienisch",
|
||||
"Japanese": "Japanisch",
|
||||
"Javanese": "Javanisch",
|
||||
"Kannada": "Kannada",
|
||||
"Kazakh": "Kasachisch",
|
||||
"Khmer": "Khmer",
|
||||
"Korean": "Koreanisch",
|
||||
"Kurdish": "Kurdisch",
|
||||
"Kyrgyz": "Kirgisisch",
|
||||
"Lao": "Laotisch",
|
||||
"Latin": "Lateinisch",
|
||||
"Latvian": "Lettisch",
|
||||
"Lithuanian": "Litauisch",
|
||||
"Luxembourgish": "Luxemburgisch",
|
||||
"Macedonian": "Mazedonisch",
|
||||
"Malagasy": "Madagassisch",
|
||||
"Malay": "Malaiisch",
|
||||
"Malayalam": "Malayalam",
|
||||
"Maltese": "Maltesisch",
|
||||
"Maori": "Maori",
|
||||
"Marathi": "Marathi",
|
||||
"Mongolian": "Mongolisch",
|
||||
"Nepali": "Nepalesisch",
|
||||
"Norwegian": "Norwegisch",
|
||||
"Nyanja": "Nyanja",
|
||||
"Pashto": "Paschtunisch",
|
||||
"Persian": "Persisch",
|
||||
"Polish": "Polnisch",
|
||||
"Portuguese": "Portugiesisch",
|
||||
"Punjabi": "Pandschabi",
|
||||
"Romanian": "Rumänisch",
|
||||
"Russian": "Russisch",
|
||||
"Samoan": "Samoanisch",
|
||||
"Scottish Gaelic": "Schottisches Gälisch",
|
||||
"Serbian": "Serbisch",
|
||||
"Shona": "Schona",
|
||||
"Sindhi": "Sindhi",
|
||||
"Sinhala": "Singhalesisch",
|
||||
"Slovak": "Slowakisch",
|
||||
"Slovenian": "Slowenisch",
|
||||
"Somali": "Somali",
|
||||
"Southern Sotho": "Südliches Sotho",
|
||||
"Spanish": "Spanisch",
|
||||
"Spanish (Latin America)": "Spanisch (Lateinamerika)",
|
||||
"Sundanese": "Sundanesisch",
|
||||
"Swahili": "Suaheli",
|
||||
"Swedish": "Schwedisch",
|
||||
"Tajik": "Tadschikisch",
|
||||
"Tamil": "Tamilisch",
|
||||
"Telugu": "Telugu",
|
||||
"Thai": "Thailändisch",
|
||||
"Turkish": "Türkisch",
|
||||
"Ukrainian": "Ukrainisch",
|
||||
"Urdu": "Urdu",
|
||||
"Uzbek": "Usbekisch",
|
||||
"Vietnamese": "Vietnamesisch",
|
||||
"Welsh": "Walisisch",
|
||||
"Western Frisian": "Westfriesisch",
|
||||
"Xhosa": "Xhosa",
|
||||
"Yiddish": "Jiddisch",
|
||||
"Yoruba": "Joruba",
|
||||
"Zulu": "Zulu",
|
||||
"`x` years": "`x` Jahre",
|
||||
"`x` months": "`x` Monate",
|
||||
"`x` weeks": "`x` Wochen",
|
||||
"`x` days": "`x` Tage",
|
||||
"`x` hours": "`x` Stunden",
|
||||
"`x` minutes": "`x` Minuten",
|
||||
"`x` seconds": "`x` Sekunden",
|
||||
"Fallback comments: ": "",
|
||||
"Popular": "Populär",
|
||||
"Top": "",
|
||||
"About": "Über",
|
||||
"Rating: ": "Bewertung: ",
|
||||
"Language: ": "Sprache: ",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": "",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"Youtube permalink of the comment": "",
|
||||
"`x` marked it with a ❤": ""
|
||||
}
|
||||
|
@ -263,5 +263,16 @@
|
||||
"Top": "Top",
|
||||
"About": "About",
|
||||
"Rating: ": "Rating: ",
|
||||
"Language: ": "Language: "
|
||||
"Language: ": "Language: ",
|
||||
"Default": "Default",
|
||||
"Music": "Music",
|
||||
"Gaming": "Gaming",
|
||||
"News": "News",
|
||||
"Movies": "Movies",
|
||||
"Download": "Download",
|
||||
"Download as: ": "Download as: ",
|
||||
"%A %B %-d, %Y": "%A %B %-d, %Y",
|
||||
"(edited)": "(edited)",
|
||||
"Youtube permalink of the comment": "Youtube permalink of the comment",
|
||||
"`x` marked it with a ❤": "`x` marked it with a ❤"
|
||||
}
|
||||
|
278
locales/eu.json
Normal file
278
locales/eu.json
Normal file
@ -0,0 +1,278 @@
|
||||
{
|
||||
"`x` subscribers": "",
|
||||
"`x` videos": "",
|
||||
"LIVE": "",
|
||||
"Shared `x` ago": "",
|
||||
"Unsubscribe": "",
|
||||
"Subscribe": "Harpidetu",
|
||||
"Login to subscribe to `x`": "",
|
||||
"View channel on YouTube": "Ikusi kanala YouTuben",
|
||||
"newest": "berrienak",
|
||||
"oldest": "zaharrenak",
|
||||
"popular": "ospetsuenak",
|
||||
"Preview page": "Aurrebista orria",
|
||||
"Next page": "Hurrengo orria",
|
||||
"Clear watch history?": "Garbitu ikusitakoen historia?",
|
||||
"Yes": "Bai",
|
||||
"No": "Ez",
|
||||
"Import and Export Data": "Datuak inportatu eta esportatu",
|
||||
"Import": "Inportatu",
|
||||
"Import Invidious data": "Invidiouseko datuak inportatu",
|
||||
"Import YouTube subscriptions": "YouTubeko harpidetzak inportatu",
|
||||
"Import FreeTube subscriptions (.db)": "FreeTubeko harpidetzak inportatu (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "NewPipeko harpidetzak inportatu (.json)",
|
||||
"Import NewPipe data (.zip)": "NewPipeko datuak inportatu (.zip)",
|
||||
"Export": "Esportatu",
|
||||
"Export subscriptions as OPML": "Esportatu harpidetzak OPML bezala",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "",
|
||||
"Export data as JSON": "",
|
||||
"Delete account?": "Kontua ezabatu?",
|
||||
"History": "Historia",
|
||||
"Previous page": "Aurreko orria",
|
||||
"An alternative front-end to YouTube": "",
|
||||
"JavaScript license information": "",
|
||||
"source": "",
|
||||
"Login": "",
|
||||
"Login/Register": "",
|
||||
"Login to Google": "",
|
||||
"User ID:": "",
|
||||
"Password:": "",
|
||||
"Time (h:mm:ss):": "",
|
||||
"Text CAPTCHA": "",
|
||||
"Image CAPTCHA": "",
|
||||
"Sign In": "",
|
||||
"Register": "",
|
||||
"Email:": "",
|
||||
"Google verification code:": "",
|
||||
"Preferences": "",
|
||||
"Player preferences": "",
|
||||
"Always loop: ": "",
|
||||
"Autoplay: ": "",
|
||||
"Autoplay next video: ": "",
|
||||
"Listen by default: ": "",
|
||||
"Default speed: ": "",
|
||||
"Preferred video quality: ": "",
|
||||
"Player volume: ": "",
|
||||
"Default comments: ": "",
|
||||
"Default captions: ": "",
|
||||
"Fallback captions: ": "",
|
||||
"Show related videos? ": "",
|
||||
"Visual preferences": "",
|
||||
"Dark mode: ": "",
|
||||
"Thin mode: ": "",
|
||||
"Subscription preferences": "",
|
||||
"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): ": "",
|
||||
"Data preferences": "",
|
||||
"Clear watch history": "",
|
||||
"Import/Export data": "",
|
||||
"Manage subscriptions": "",
|
||||
"Watch history": "",
|
||||
"Delete account": "",
|
||||
"Save preferences": "",
|
||||
"Subscription manager": "",
|
||||
"`x` subscriptions": "",
|
||||
"Import/Export": "",
|
||||
"unsubscribe": "",
|
||||
"Subscriptions": "",
|
||||
"`x` unseen notifications": "",
|
||||
"search": "",
|
||||
"Sign out": "",
|
||||
"Released under the AGPLv3 by Omar Roth.": "",
|
||||
"Source available here.": "",
|
||||
"View JavaScript license information.": "",
|
||||
"Trending": "",
|
||||
"Watch video on Youtube": "",
|
||||
"Genre: ": "",
|
||||
"License: ": "",
|
||||
"Family friendly? ": "",
|
||||
"Wilson score: ": "",
|
||||
"Engagement: ": "",
|
||||
"Whitelisted regions: ": "",
|
||||
"Blacklisted regions: ": "",
|
||||
"Shared `x`": "",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "",
|
||||
"View YouTube comments": "",
|
||||
"View more comments on Reddit": "",
|
||||
"View `x` comments": "",
|
||||
"View Reddit comments": "",
|
||||
"Hide replies": "",
|
||||
"Show replies": "",
|
||||
"Incorrect password": "",
|
||||
"Quota exceeded, try again in a few hours": "",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "",
|
||||
"Invalid TFA code": "",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "",
|
||||
"Invalid answer": "",
|
||||
"Invalid CAPTCHA": "",
|
||||
"CAPTCHA is a required field": "",
|
||||
"User ID is a required field": "",
|
||||
"Password is a required field": "",
|
||||
"Invalid username or password": "",
|
||||
"Please sign in using 'Sign in with Google'": "",
|
||||
"Password cannot be empty": "",
|
||||
"Password cannot be longer than 55 characters": "",
|
||||
"Please sign in": "",
|
||||
"Invidious Private Feed for `x`": "",
|
||||
"channel:`x`": "",
|
||||
"Deleted or invalid channel": "",
|
||||
"This channel does not exist.": "",
|
||||
"Could not get channel info.": "",
|
||||
"Could not fetch comments": "",
|
||||
"View `x` replies": "",
|
||||
"`x` ago": "",
|
||||
"Load more": "",
|
||||
"`x` points": "",
|
||||
"Could not create mix.": "",
|
||||
"Playlist is empty": "",
|
||||
"Invalid playlist.": "",
|
||||
"Playlist does not exist.": "",
|
||||
"Could not pull trending pages.": "",
|
||||
"Hidden field \"challenge\" is a required field": "",
|
||||
"Hidden field \"token\" is a required field": "",
|
||||
"Invalid challenge": "",
|
||||
"Invalid token": "",
|
||||
"Invalid user": "",
|
||||
"Token is expired, please try again": "",
|
||||
"English": "",
|
||||
"English (auto-generated)": "",
|
||||
"Afrikaans": "",
|
||||
"Albanian": "",
|
||||
"Amharic": "",
|
||||
"Arabic": "",
|
||||
"Armenian": "",
|
||||
"Azerbaijani": "",
|
||||
"Bangla": "",
|
||||
"Basque": "",
|
||||
"Belarusian": "",
|
||||
"Bosnian": "",
|
||||
"Bulgarian": "",
|
||||
"Burmese": "",
|
||||
"Catalan": "",
|
||||
"Cebuano": "",
|
||||
"Chinese (Simplified)": "",
|
||||
"Chinese (Traditional)": "",
|
||||
"Corsican": "",
|
||||
"Croatian": "",
|
||||
"Czech": "",
|
||||
"Danish": "",
|
||||
"Dutch": "",
|
||||
"Esperanto": "",
|
||||
"Estonian": "",
|
||||
"Filipino": "",
|
||||
"Finnish": "",
|
||||
"French": "",
|
||||
"Galician": "",
|
||||
"Georgian": "",
|
||||
"German": "",
|
||||
"Greek": "",
|
||||
"Gujarati": "",
|
||||
"Haitian Creole": "",
|
||||
"Hausa": "",
|
||||
"Hawaiian": "",
|
||||
"Hebrew": "",
|
||||
"Hindi": "",
|
||||
"Hmong": "",
|
||||
"Hungarian": "",
|
||||
"Icelandic": "",
|
||||
"Igbo": "",
|
||||
"Indonesian": "",
|
||||
"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": "",
|
||||
"Nyanja": "",
|
||||
"Pashto": "",
|
||||
"Persian": "",
|
||||
"Polish": "",
|
||||
"Portuguese": "",
|
||||
"Punjabi": "",
|
||||
"Romanian": "",
|
||||
"Russian": "",
|
||||
"Samoan": "",
|
||||
"Scottish Gaelic": "",
|
||||
"Serbian": "",
|
||||
"Shona": "",
|
||||
"Sindhi": "",
|
||||
"Sinhala": "",
|
||||
"Slovak": "",
|
||||
"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": "",
|
||||
"`x` months": "",
|
||||
"`x` weeks": "",
|
||||
"`x` days": "",
|
||||
"`x` hours": "",
|
||||
"`x` minutes": "",
|
||||
"`x` seconds": "",
|
||||
"Fallback comments: ": "",
|
||||
"Popular": "",
|
||||
"Top": "",
|
||||
"About": "",
|
||||
"Rating: ": "",
|
||||
"Language: ": "",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": "",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"Youtube permalink of the comment": "",
|
||||
"`x` marked it with a ❤": ""
|
||||
}
|
278
locales/fr.json
Normal file
278
locales/fr.json
Normal file
@ -0,0 +1,278 @@
|
||||
{
|
||||
"`x` subscribers": "`x` souscripteurs",
|
||||
"`x` videos": "`x` vidéos",
|
||||
"LIVE": "LIVE",
|
||||
"Shared `x` ago": "Partagé il y a `x`",
|
||||
"Unsubscribe": "Se désabonner",
|
||||
"Subscribe": "S'abonner",
|
||||
"Login to subscribe to `x`": "Se connecter pour s'abonner à `x`",
|
||||
"View channel on YouTube": "Voir la chaîne sur YouTube",
|
||||
"newest": "récent",
|
||||
"oldest": "aînée",
|
||||
"popular": "appréciés",
|
||||
"Preview page": "Page de prévisualisation",
|
||||
"Next page": "Page suivante",
|
||||
"Clear watch history?": "L'histoire de la montre est claire?",
|
||||
"Yes": "Oui",
|
||||
"No": "Aucun",
|
||||
"Import and Export Data": "Importation et exportation de données",
|
||||
"Import": "Importation",
|
||||
"Import Invidious data": "Importation de données invalides",
|
||||
"Import YouTube subscriptions": "Importer des abonnements YouTube",
|
||||
"Import FreeTube subscriptions (.db)": "Importer des abonnements FreeTube (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importer des abonnements NewPipe (.json)",
|
||||
"Import NewPipe data (.zip)": "Importer des données NewPipe (.zip)",
|
||||
"Export": "Exporter",
|
||||
"Export subscriptions as OPML": "Exporter les abonnements comme OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Exporter les abonnements comme OPML (pour NewPipe & FreeTube)",
|
||||
"Export data as JSON": "Exporter les données au format JSON",
|
||||
"Delete account?": "Supprimer un compte ?",
|
||||
"History": "Histoire",
|
||||
"Previous page": "Page précédente",
|
||||
"An alternative front-end to YouTube": "Un frontal alternatif à YouTube",
|
||||
"JavaScript license information": "Informations sur la licence JavaScript",
|
||||
"source": "origine",
|
||||
"Login": "Connexion",
|
||||
"Login/Register": "Connexion/S'inscrire",
|
||||
"Login to Google": "Se connecter à Google",
|
||||
"User ID:": "ID utilisateur:",
|
||||
"Password:": "Mot de passe:",
|
||||
"Time (h:mm:ss):": "Temps (h:mm:ss):",
|
||||
"Text CAPTCHA": "Texte CAPTCHA",
|
||||
"Image CAPTCHA": "Image CAPTCHA",
|
||||
"Sign In": "S'identifier",
|
||||
"Register": "S'inscrire",
|
||||
"Email:": "Courriel:",
|
||||
"Google verification code:": "Code de vérification Google:",
|
||||
"Preferences": "Préférences",
|
||||
"Player preferences": "Joueur préférences",
|
||||
"Always loop: ": "Toujours en boucle: ",
|
||||
"Autoplay: ": "Autoplay: ",
|
||||
"Autoplay next video: ": "Lecture automatique de la vidéo suivante: ",
|
||||
"Listen by default: ": "Écouter par défaut: ",
|
||||
"Default speed: ": "Vitesse par défaut: ",
|
||||
"Preferred video quality: ": "Qualité vidéo préférée: ",
|
||||
"Player volume: ": "Volume de lecteur: ",
|
||||
"Default comments: ": "Commentaires par défaut: ",
|
||||
"Default captions: ": "Légendes par défaut: ",
|
||||
"Fallback captions: ": "Légendes de repli: ",
|
||||
"Show related videos? ": "Voir les vidéos liées à ce sujet? ",
|
||||
"Visual preferences": "Préférences visuelles",
|
||||
"Dark mode: ": "Mode sombre: ",
|
||||
"Thin mode: ": "Mode Thin: ",
|
||||
"Subscription preferences": "Préférences d'abonnement",
|
||||
"Redirect homepage to feed: ": "Rediriger la page d'accueil vers le flux: ",
|
||||
"Number of videos shown in feed: ": "Nombre de vidéos montrées dans le flux: ",
|
||||
"Sort videos by: ": "Trier les vidéos par: ",
|
||||
"published": "publié",
|
||||
"published - reverse": "publié - reverse",
|
||||
"alphabetically": "alphabétiquement",
|
||||
"alphabetically - reverse": "alphabétiquement - contraire",
|
||||
"channel name": "nom du canal",
|
||||
"channel name - reverse": "nom du canal - contraire",
|
||||
"Only show latest video from channel: ": "Afficher uniquement les dernières vidéos de la chaîne: ",
|
||||
"Only show latest unwatched video from channel: ": "Afficher uniquement les dernières vidéos non regardées de la chaîne: ",
|
||||
"Only show unwatched: ": "Afficher uniquement les images non surveillées: ",
|
||||
"Only show notifications (if there are any): ": "Afficher uniquement les notifications (s'il y en a): ",
|
||||
"Data preferences": "Préférences de données",
|
||||
"Clear watch history": "Historique clair de la montre",
|
||||
"Import/Export data": "Données d'importation/exportation",
|
||||
"Manage subscriptions": "Gérer les abonnements",
|
||||
"Watch history": "Historique des montres",
|
||||
"Delete account": "Supprimer un compte",
|
||||
"Save preferences": "Enregistrer les préférences",
|
||||
"Subscription manager": "Gestionnaire d'abonnement",
|
||||
"`x` subscriptions": "`x` abonnements",
|
||||
"Import/Export": "Importer/Exporter",
|
||||
"unsubscribe": "se désabonner",
|
||||
"Subscriptions": "Abonnements",
|
||||
"`x` unseen notifications": "`x` notifications invisibles",
|
||||
"search": "perquisition",
|
||||
"Sign out": "Déconnexion",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Publié sous l'AGPLv3 par Omar Roth.",
|
||||
"Source available here.": "Source disponible ici.",
|
||||
"View JavaScript license information.": "Voir les informations de licence JavaScript.",
|
||||
"Trending": "Tendances",
|
||||
"Watch video on Youtube": "Voir la vidéo sur Youtube",
|
||||
"Genre: ": "Genre: ",
|
||||
"License: ": "Licence: ",
|
||||
"Family friendly? ": "Convivialité familiale? ",
|
||||
"Wilson score: ": "Wilson marque: ",
|
||||
"Engagement: ": "Fiançailles: ",
|
||||
"Whitelisted regions: ": "Régions en liste blanche: ",
|
||||
"Blacklisted regions: ": "Régions sur liste noire: ",
|
||||
"Shared `x`": "Partagée `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Hi! On dirait que vous avez désactivé JavaScript. Cliquez ici pour voir les commentaires, gardez à l'esprit que le chargement peut prendre un peu plus de temps.",
|
||||
"View YouTube comments": "Voir les commentaires sur YouTube",
|
||||
"View more comments on Reddit": "Voir plus de commentaires sur Reddit",
|
||||
"View `x` comments": "Voir `x` commentaires",
|
||||
"View Reddit comments": "Voir Reddit commentaires",
|
||||
"Hide replies": "Masquer les réponses",
|
||||
"Show replies": "Afficher les réponses",
|
||||
"Incorrect password": "Mot de passe incorrect",
|
||||
"Quota exceeded, try again in a few hours": "Quota dépassé, réessayez dans quelques heures",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Si vous ne parvenez pas à vous connecter, assurez-vous que l'authentification à deux facteurs (Authenticator ou SMS) est activée.",
|
||||
"Invalid TFA code": "Code TFA invalide",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "La connexion a échoué. Cela peut être dû au fait que l'authentification à deux facteurs n'est pas activée sur votre compte.",
|
||||
"Invalid answer": "Réponse non valide",
|
||||
"Invalid CAPTCHA": "CAPTCHA invalide",
|
||||
"CAPTCHA is a required field": "CAPTCHA est un champ obligatoire",
|
||||
"User ID is a required field": "Utilisateur ID est un champ obligatoire",
|
||||
"Password is a required field": "Mot de passe est un champ obligatoire",
|
||||
"Invalid username or password": "Nom d'utilisateur ou mot de passe invalide",
|
||||
"Please sign in using 'Sign in with Google'": "Veuillez vous connecter en utilisant 'S'identifier avec Google'",
|
||||
"Password cannot be empty": "Le mot de passe ne peut pas être vide",
|
||||
"Password cannot be longer than 55 characters": "Le mot de passe ne doit pas comporter plus de 55 caractères.",
|
||||
"Please sign in": "Veuillez ouvrir une session",
|
||||
"Invidious Private Feed for `x`": "Flux privé Invidious pour `x`",
|
||||
"channel:`x`": "chenal:`x`",
|
||||
"Deleted or invalid channel": "Canal supprimé ou non valide",
|
||||
"This channel does not exist.": "Ce canal n'existe pas.",
|
||||
"Could not get channel info.": "Impossible d'obtenir des informations sur les chaînes.",
|
||||
"Could not fetch comments": "Impossible d'aller chercher les commentaires",
|
||||
"View `x` replies": "Voir `x` réponses",
|
||||
"`x` ago": "il y a `x`",
|
||||
"Load more": "Charger plus",
|
||||
"`x` points": "`x` points",
|
||||
"Could not create mix.": "Impossible de créer du mixage.",
|
||||
"Playlist is empty": "La liste de lecture est vide",
|
||||
"Invalid playlist.": "Liste de lecture invalide.",
|
||||
"Playlist does not exist.": "La liste de lecture n'existe pas.",
|
||||
"Could not pull trending pages.": "Impossible de tirer les pages de tendances.",
|
||||
"Hidden field \"challenge\" is a required field": "Champ caché \"contestation\" est un champ obligatoire",
|
||||
"Hidden field \"token\" is a required field": "Champ caché \"jeton\" est un champ obligatoire",
|
||||
"Invalid challenge": "Contestation non valide",
|
||||
"Invalid token": "Jeton non valide",
|
||||
"Invalid user": "Iutilisateur non valide",
|
||||
"Token is expired, please try again": "Le jeton est expiré, veuillez réessayer",
|
||||
"English": "Anglais",
|
||||
"English (auto-generated)": "Anglais (auto-généré)",
|
||||
"Afrikaans": "Afrikaans",
|
||||
"Albanian": "Albanais",
|
||||
"Amharic": "Amharique",
|
||||
"Arabic": "Arabe",
|
||||
"Armenian": "Arménien",
|
||||
"Azerbaijani": "Azerbaïdjanais",
|
||||
"Bangla": "Bangla",
|
||||
"Basque": "Basque",
|
||||
"Belarusian": "Belarusian",
|
||||
"Bosnian": "Bosnian",
|
||||
"Bulgarian": "Bulgarian",
|
||||
"Burmese": "Birman",
|
||||
"Catalan": "Catalan",
|
||||
"Cebuano": "Cebuano",
|
||||
"Chinese (Simplified)": "Chinois (Simplifié)",
|
||||
"Chinese (Traditional)": "Chinois (Traditionnel)",
|
||||
"Corsican": "Corse",
|
||||
"Croatian": "Croate",
|
||||
"Czech": "Tchèque",
|
||||
"Danish": "Danois",
|
||||
"Dutch": "Hollandais",
|
||||
"Esperanto": "Espéranto",
|
||||
"Estonian": "Estonien",
|
||||
"Filipino": "Philippin",
|
||||
"Finnish": "Finlandais",
|
||||
"French": "Français",
|
||||
"Galician": "Galicien",
|
||||
"Georgian": "Géorgien",
|
||||
"German": "Allemand",
|
||||
"Greek": "Grec",
|
||||
"Gujarati": "Gujarati",
|
||||
"Haitian Creole": "Créole Haïtien",
|
||||
"Hausa": "Haoussa",
|
||||
"Hawaiian": "Hawaïen",
|
||||
"Hebrew": "Hébraïque",
|
||||
"Hindi": "Hindi",
|
||||
"Hmong": "Hmong",
|
||||
"Hungarian": "Hongrois",
|
||||
"Icelandic": "Islandais",
|
||||
"Igbo": "Igbo",
|
||||
"Indonesian": "Indonésien",
|
||||
"Irish": "Irlandais",
|
||||
"Italian": "Italien",
|
||||
"Japanese": "Japonais",
|
||||
"Javanese": "Javanais",
|
||||
"Kannada": "Kannada",
|
||||
"Kazakh": "Kazakh",
|
||||
"Khmer": "Khmer",
|
||||
"Korean": "Coréen",
|
||||
"Kurdish": "Kurde",
|
||||
"Kyrgyz": "Kirghize",
|
||||
"Lao": "Lao",
|
||||
"Latin": "Latin",
|
||||
"Latvian": "Letton",
|
||||
"Lithuanian": "Lituanien",
|
||||
"Luxembourgish": "Luxembourgeois",
|
||||
"Macedonian": "Macédonien",
|
||||
"Malagasy": "Malgache",
|
||||
"Malay": "Malais",
|
||||
"Malayalam": "Malayalam",
|
||||
"Maltese": "Maltais",
|
||||
"Maori": "Maori",
|
||||
"Marathi": "Marathi",
|
||||
"Mongolian": "Mongol",
|
||||
"Nepali": "Népalais",
|
||||
"Norwegian": "Norvégien",
|
||||
"Nyanja": "Nyanja",
|
||||
"Pashto": "Pachtou",
|
||||
"Persian": "Persan",
|
||||
"Polish": "Polonais",
|
||||
"Portuguese": "Portugais",
|
||||
"Punjabi": "Punjabi",
|
||||
"Romanian": "Roumain",
|
||||
"Russian": "Russe",
|
||||
"Samoan": "Samoan",
|
||||
"Scottish Gaelic": "Eaélique Ècossais",
|
||||
"Serbian": "Serbe",
|
||||
"Shona": "Shona",
|
||||
"Sindhi": "Sindhi",
|
||||
"Sinhala": "Cinghalais",
|
||||
"Slovak": "Slovaque",
|
||||
"Slovenian": "Slovène",
|
||||
"Somali": "Somalien",
|
||||
"Southern Sotho": "Sotho du Sud",
|
||||
"Spanish": "Espagnol",
|
||||
"Spanish (Latin America)": "Espagnol (Amérique latine)",
|
||||
"Sundanese": "Sundanais",
|
||||
"Swahili": "Swahili",
|
||||
"Swedish": "Suédois",
|
||||
"Tajik": "Tajik",
|
||||
"Tamil": "Tamil",
|
||||
"Telugu": "Telugu",
|
||||
"Thai": "Thaï",
|
||||
"Turkish": "Turc",
|
||||
"Ukrainian": "Ukrainien",
|
||||
"Urdu": "Ourdou",
|
||||
"Uzbek": "Ouzbek",
|
||||
"Vietnamese": "Vietnamien",
|
||||
"Welsh": "Gallois",
|
||||
"Western Frisian": "Frison occidental",
|
||||
"Xhosa": "Xhosa",
|
||||
"Yiddish": "Yiddish",
|
||||
"Yoruba": "Yoruba",
|
||||
"Zulu": "Zoulou",
|
||||
"`x` years": "`x` ans",
|
||||
"`x` months": "`x` mois",
|
||||
"`x` weeks": "`x` semaines",
|
||||
"`x` days": "`x` jours",
|
||||
"`x` hours": "`x` heures",
|
||||
"`x` minutes": "`x` minutes",
|
||||
"`x` seconds": "`x` secondes",
|
||||
"Fallback comments: ": "Commentaires de repli: ",
|
||||
"Popular": "Populaire",
|
||||
"Top": "Haut",
|
||||
"About": "Sur",
|
||||
"Rating: ": "Évaluation: ",
|
||||
"Language: ": "Langue: ",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": "",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"Youtube permalink of the comment": "",
|
||||
"`x` marked it with a ❤": ""
|
||||
}
|
@ -1,267 +1,278 @@
|
||||
{
|
||||
"`x` subscribers": "`x` abonnenter",
|
||||
"`x` videos": "`x` videoer",
|
||||
"LIVE": "SANNTIDSVISNING",
|
||||
"Shared `x` ago": "Delt for `x` siden",
|
||||
"Unsubscribe": "Opphev abonnement",
|
||||
"Subscribe": "Abonner",
|
||||
"Login to subscribe to `x`": "Logg inn for å abonnere på `x`",
|
||||
"View channel on YouTube": "Vis kanal på YouTube",
|
||||
"newest": "nyeste",
|
||||
"oldest": "eldste",
|
||||
"popular": "populært",
|
||||
"Preview page": "Forhåndsvis side",
|
||||
"Next page": "Neste side",
|
||||
"Clear watch history?": "Tøm visningshistorikk?",
|
||||
"Yes": "Ja",
|
||||
"No": "Nei",
|
||||
"Import and Export Data": "Importer- og eksporter data",
|
||||
"Import": "Importer",
|
||||
"Import Invidious data": "Importer Invidious-data",
|
||||
"Import YouTube subscriptions": "Importer YouTube-abonnenter",
|
||||
"Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnenter (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnenter (.json)",
|
||||
"Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)",
|
||||
"Export": "Eksporter",
|
||||
"Export subscriptions as OPML": "Eksporter abonnenter som OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporter abonnenter som OPML (for NewPipe og FreeTube)",
|
||||
"Export data as JSON": "Eksporter data som JSON",
|
||||
"Delete account?": "Slett konto?",
|
||||
"History": "Historikk",
|
||||
"Previous page": "Forrige side",
|
||||
"An alternative front-end to YouTube": "En alternativ grenseflate for YouTube",
|
||||
"JavaScript license information": "JavaScript-lisensinformasjon",
|
||||
"source": "kilde",
|
||||
"Login": "Logg inn",
|
||||
"Login/Register": "Logg inn/registrer",
|
||||
"Login to Google": "Logg inn med Google",
|
||||
"User ID:": "Bruker-ID:",
|
||||
"Password:": "Passord:",
|
||||
"Time (h:mm:ss):": "Tid (h:mm:ss):",
|
||||
"Text CAPTCHA": "Tekst-CAPTCHA",
|
||||
"Image CAPTCHA": "Bilde-CAPTCHA",
|
||||
"Sign In": "Innlogging",
|
||||
"Register": "Registrer",
|
||||
"Email:": "E-post:",
|
||||
"Google verification code:": "Google-bekreftelseskode:",
|
||||
"Preferences": "Innstillinger",
|
||||
"Player preferences": "Avspillerinnstillinger",
|
||||
"Always loop: ": "Alltid gjenta: ",
|
||||
"Autoplay: ": "Autoavspilling: ",
|
||||
"Autoplay next video: ": "Autospill neste video: ",
|
||||
"Listen by default: ": "Lytt som forvalg: ",
|
||||
"Default speed: ": "Forvalgt hastighet: ",
|
||||
"Preferred video quality: ": "Foretrukket videokvalitet: ",
|
||||
"Player volume: ": "Avspillerlydstyrke: ",
|
||||
"Default comments: ": "Forvalgte kommentarer: ",
|
||||
"Default captions: ": "Forvalgte undertitler: ",
|
||||
"Fallback captions: ": "Tilbakefallsundertitler: ",
|
||||
"Show related videos? ": "Vis relaterte videoer? ",
|
||||
"Visual preferences": "Visuelle innstillinger",
|
||||
"Dark mode: ": "Mørk drakt: ",
|
||||
"Thin mode: ": "Tynt modus: ",
|
||||
"Subscription preferences": "Abonnementsinnstillinger",
|
||||
"Redirect homepage to feed: ": "Videresend hjemmeside til flyt: ",
|
||||
"Number of videos shown in feed: ": "Antall videoer å vise i flyt: ",
|
||||
"Sort videos by: ": "Sorter videoer etter: ",
|
||||
"published": "publisert",
|
||||
"published - reverse": "publisert - motsatt",
|
||||
"alphabetically": "alfabetisk",
|
||||
"alphabetically - reverse": "alfabetisk - motsatt",
|
||||
"channel name": "kanalnavn",
|
||||
"channel name - reverse": "kanalnavn - motsatt",
|
||||
"Only show latest video from channel: ": "Kun vis siste video fra kanal: ",
|
||||
"Only show latest unwatched video from channel: ": "Kun vis siste usette video fra kanal: ",
|
||||
"Only show unwatched: ": "Kun vis usette: ",
|
||||
"Only show notifications (if there are any): ": "Kun vis merknader (hvis det er noen): ",
|
||||
"Data preferences": "Datainnstillinger",
|
||||
"Clear watch history": "Tøm visningshistorikk",
|
||||
"Import/Export data": "Importer/eksporter data",
|
||||
"Manage subscriptions": "Behandle abonnementer",
|
||||
"Watch history": "Visningshistorikk",
|
||||
"Delete account": "Slett konto",
|
||||
"Save preferences": "Lagre innstillinger",
|
||||
"Subscription manager": "Abonnementsbehandler",
|
||||
"`x` subscriptions": "`x` abonnementer",
|
||||
"Import/Export": "Importer/eksporter",
|
||||
"unsubscribe": "opphev abonnement",
|
||||
"Subscriptions": "Abonnement",
|
||||
"`x` unseen notifications": "`x` usette merknader",
|
||||
"search": "søk",
|
||||
"Sign out": "Logg ut",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Utgitt med AGPLv3+lisens av Omar Roth.",
|
||||
"Source available here.": "Kildekode tilgjengelig her.",
|
||||
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
|
||||
"Trending": "Trendsettende",
|
||||
"Watch video on Youtube": "Vis video på YouTube",
|
||||
"Genre: ": "Sjanger: ",
|
||||
"License: ": "Lisens: ",
|
||||
"Family friendly? ": "Familievennlig? ",
|
||||
"Wilson score: ": "Wilson-poengsum: ",
|
||||
"Engagement: ": "Engasjement: ",
|
||||
"Whitelisted regions: ": "Hvitlistede regioner: ",
|
||||
"Blacklisted regions: ": "Svartelistede regioner: ",
|
||||
"Shared `x`": "Delt `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Hei. Det ser ut til at du har JavaScript avslått. Klikk her for å vise kommentarer, ha i minnet at innlasting tar lengre tid.",
|
||||
"View YouTube comments": "Vis YouTube-kommentarer",
|
||||
"View more comments on Reddit": "Vis flere kommenterer på Reddit",
|
||||
"View `x` comments": "Vis `x` kommentarer",
|
||||
"View Reddit comments": "Vis Reddit-kommentarer",
|
||||
"Hide replies": "Skjul svar",
|
||||
"Show replies": "Vis svar",
|
||||
"Incorrect password": "Feil passord",
|
||||
"Quota exceeded, try again in a few hours": "Kvote overskredet, prøv igjen om et par timer",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Kunne ikke logge inn, forsikre deg om at tofaktor-identitetsbekreftelse (Authenticator eller SMS) er skrudd på.",
|
||||
"Invalid TFA code": "Ugyldig tofaktorkode",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Innlogging mislyktes. Dette kan være fordi tofaktor-identitetsbekreftelse er skrudd av på kontoen din.",
|
||||
"Invalid answer": "Ugyldig svar",
|
||||
"Invalid CAPTCHA": "Ugyldig CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA er et påkrevd felt",
|
||||
"User ID is a required field": "Bruker-ID er et påkrevd felt",
|
||||
"Password is a required field": "Passord er et påkrevd felt",
|
||||
"Invalid username or password": "Ugyldig brukernavn eller passord",
|
||||
"Please sign in using 'Sign in with Google'": "Logg inn ved bruk av \"Google-innlogging\"",
|
||||
"Password cannot be empty": "Passordet kan ikke være tomt",
|
||||
"Password cannot be longer than 55 characters": "Passordet kan ikke være lengre enn 55 tegn",
|
||||
"Please sign in": "Logg inn",
|
||||
"Invidious Private Feed for `x`": "Ugyldig privat flyt for `x`",
|
||||
"channel:`x`": "kanal `x`",
|
||||
"Deleted or invalid channel": "Slettet eller ugyldig kanal",
|
||||
"This channel does not exist.": "Denne kanalen finnes ikke.",
|
||||
"Could not get channel info.": "Kunne ikke innhente kanalinfo.",
|
||||
"Could not fetch comments": "Kunne ikke hente kommentarer",
|
||||
"View `x` replies": "Vis `x` svar",
|
||||
"`x` ago": "`x` siden",
|
||||
"Load more": "Last inn flere",
|
||||
"`x` points": "`x` poeng",
|
||||
"Could not create mix.": "Kunne ikke opprette miks.",
|
||||
"Playlist is empty": "Spillelisten er tom",
|
||||
"Invalid playlist.": "Ugyldig spilleliste.",
|
||||
"Playlist does not exist.": "Spillelisten finnes ikke.",
|
||||
"Could not pull trending pages.": "Kunne ikke hente trendsettende sider.",
|
||||
"Hidden field \"challenge\" is a required field": "Skjult felt \"utfordring\" er et påkrevd felt",
|
||||
"Hidden field \"token\" is a required field": "Skjult felt \"symbol\" er et påkrevd felt",
|
||||
"Invalid challenge": "Ugyldig utfordring",
|
||||
"Invalid token": "Ugyldig symbol",
|
||||
"Invalid user": "Ugyldig bruker",
|
||||
"Token is expired, please try again": "Symbol utløpt, prøv igjen",
|
||||
"English": "Engelsk",
|
||||
"English (auto-generated)": "Engelsk (auto-generert)",
|
||||
"Afrikaans": "",
|
||||
"Albanian": "Albansk",
|
||||
"Amharic": "",
|
||||
"Arabic": "Arabisk",
|
||||
"Armenian": "Armensk",
|
||||
"Azerbaijani": "",
|
||||
"Bangla": "",
|
||||
"Basque": "",
|
||||
"Belarusian": "Hviterussisk",
|
||||
"Bosnian": "Bosnisk",
|
||||
"Bulgarian": "Bulgarsk",
|
||||
"Burmese": "Burmesisk",
|
||||
"Catalan": "Katalansk",
|
||||
"Cebuano": "",
|
||||
"Chinese (Simplified)": "",
|
||||
"Chinese (Traditional)": "",
|
||||
"Corsican": "",
|
||||
"Croatian": "",
|
||||
"Czech": "Tsjekkisk",
|
||||
"Danish": "Dansk",
|
||||
"Dutch": "",
|
||||
"Esperanto": "Esperanto",
|
||||
"Estonian": "",
|
||||
"Filipino": "",
|
||||
"Finnish": "Finsk",
|
||||
"French": "Fransk",
|
||||
"Galician": "",
|
||||
"Georgian": "",
|
||||
"German": "",
|
||||
"Greek": "",
|
||||
"Gujarati": "",
|
||||
"Haitian Creole": "",
|
||||
"Hausa": "",
|
||||
"Hawaiian": "",
|
||||
"Hebrew": "",
|
||||
"Hindi": "",
|
||||
"Hmong": "",
|
||||
"Hungarian": "Ungarsk",
|
||||
"Icelandic": "Islandsk",
|
||||
"Igbo": "",
|
||||
"Indonesian": "Indonesisk",
|
||||
"Irish": "Irsk",
|
||||
"Italian": "Italiensk",
|
||||
"Japanese": "Japansk",
|
||||
"Javanese": "",
|
||||
"Kannada": "",
|
||||
"Kazakh": "",
|
||||
"Khmer": "",
|
||||
"Korean": "",
|
||||
"Kurdish": "",
|
||||
"Kyrgyz": "",
|
||||
"Lao": "",
|
||||
"Latin": "",
|
||||
"Latvian": "",
|
||||
"Lithuanian": "",
|
||||
"Luxembourgish": "",
|
||||
"Macedonian": "",
|
||||
"Malagasy": "",
|
||||
"Malay": "",
|
||||
"Malayalam": "",
|
||||
"Maltese": "",
|
||||
"Maori": "",
|
||||
"Marathi": "",
|
||||
"Mongolian": "",
|
||||
"Nepali": "",
|
||||
"Norwegian": "Norsk bokmål",
|
||||
"Nyanja": "",
|
||||
"Pashto": "",
|
||||
"Persian": "",
|
||||
"Polish": "",
|
||||
"Portuguese": "",
|
||||
"Punjabi": "",
|
||||
"Romanian": "",
|
||||
"Russian": "Russisk",
|
||||
"Samoan": "",
|
||||
"Scottish Gaelic": "",
|
||||
"Serbian": "Serbisk",
|
||||
"Shona": "",
|
||||
"Sindhi": "",
|
||||
"Sinhala": "",
|
||||
"Slovak": "Slovakisk",
|
||||
"Slovenian": "Slovensk",
|
||||
"Somali": "Somali",
|
||||
"Southern Sotho": "",
|
||||
"Spanish": "Spansk",
|
||||
"Spanish (Latin America)": "",
|
||||
"Sundanese": "",
|
||||
"Swahili": "",
|
||||
"Swedish": "Svensk",
|
||||
"Tajik": "",
|
||||
"Tamil": "",
|
||||
"Telugu": "",
|
||||
"Thai": "",
|
||||
"Turkish": "Tyrkisk",
|
||||
"Ukrainian": "Ukrainsk",
|
||||
"Urdu": "",
|
||||
"Uzbek": "",
|
||||
"Vietnamese": "Vietnamesisk",
|
||||
"Welsh": "",
|
||||
"Western Frisian": "",
|
||||
"Xhosa": "",
|
||||
"Yiddish": "",
|
||||
"Yoruba": "",
|
||||
"Zulu": "",
|
||||
"`x` years": "`x` år",
|
||||
"`x` months": "`x` måneder",
|
||||
"`x` weeks": "`x` uker",
|
||||
"`x` days": "`x` dager",
|
||||
"`x` hours": "`x` timer",
|
||||
"`x` minutes": "`x` minutter",
|
||||
"`x` seconds": "`x` sekunder",
|
||||
"Fallback comments: ": "Tilbakefallskommentarer: ",
|
||||
"Popular": "Pupulært",
|
||||
"Top": "Topp",
|
||||
"About": "Om",
|
||||
"Rating: ": "Vurdering: ",
|
||||
"Language: ": "Språk: "
|
||||
"`x` subscribers": "`x` abonnenter",
|
||||
"`x` videos": "`x` videoer",
|
||||
"LIVE": "SANNTIDSVISNING",
|
||||
"Shared `x` ago": "Delt for `x` siden",
|
||||
"Unsubscribe": "Opphev abonnement",
|
||||
"Subscribe": "Abonner",
|
||||
"Login to subscribe to `x`": "Logg inn for å abonnere på `x`",
|
||||
"View channel on YouTube": "Vis kanal på YouTube",
|
||||
"newest": "nyeste",
|
||||
"oldest": "eldste",
|
||||
"popular": "populært",
|
||||
"Preview page": "Forhåndsvis side",
|
||||
"Next page": "Neste side",
|
||||
"Clear watch history?": "Tøm visningshistorikk?",
|
||||
"Yes": "Ja",
|
||||
"No": "Nei",
|
||||
"Import and Export Data": "Importer- og eksporter data",
|
||||
"Import": "Importer",
|
||||
"Import Invidious data": "Importer Invidious-data",
|
||||
"Import YouTube subscriptions": "Importer YouTube-abonnenter",
|
||||
"Import FreeTube subscriptions (.db)": "Importer FreeTube-abonnenter (.db)",
|
||||
"Import NewPipe subscriptions (.json)": "Importer NewPipe-abonnenter (.json)",
|
||||
"Import NewPipe data (.zip)": "Importer NewPipe-data (.zip)",
|
||||
"Export": "Eksporter",
|
||||
"Export subscriptions as OPML": "Eksporter abonnenter som OPML",
|
||||
"Export subscriptions as OPML (for NewPipe & FreeTube)": "Eksporter abonnenter som OPML (for NewPipe og FreeTube)",
|
||||
"Export data as JSON": "Eksporter data som JSON",
|
||||
"Delete account?": "Slett konto?",
|
||||
"History": "Historikk",
|
||||
"Previous page": "Forrige side",
|
||||
"An alternative front-end to YouTube": "En alternativ grenseflate for YouTube",
|
||||
"JavaScript license information": "JavaScript-lisensinformasjon",
|
||||
"source": "kilde",
|
||||
"Login": "Logg inn",
|
||||
"Login/Register": "Logg inn/registrer",
|
||||
"Login to Google": "Logg inn med Google",
|
||||
"User ID:": "Bruker-ID:",
|
||||
"Password:": "Passord:",
|
||||
"Time (h:mm:ss):": "Tid (h:mm:ss):",
|
||||
"Text CAPTCHA": "Tekst-CAPTCHA",
|
||||
"Image CAPTCHA": "Bilde-CAPTCHA",
|
||||
"Sign In": "Innlogging",
|
||||
"Register": "Registrer",
|
||||
"Email:": "E-post:",
|
||||
"Google verification code:": "Google-bekreftelseskode:",
|
||||
"Preferences": "Innstillinger",
|
||||
"Player preferences": "Avspillerinnstillinger",
|
||||
"Always loop: ": "Alltid gjenta: ",
|
||||
"Autoplay: ": "Autoavspilling: ",
|
||||
"Autoplay next video: ": "Autospill neste video: ",
|
||||
"Listen by default: ": "Lytt som forvalg: ",
|
||||
"Default speed: ": "Forvalgt hastighet: ",
|
||||
"Preferred video quality: ": "Foretrukket videokvalitet: ",
|
||||
"Player volume: ": "Avspillerlydstyrke: ",
|
||||
"Default comments: ": "Forvalgte kommentarer: ",
|
||||
"Default captions: ": "Forvalgte undertitler: ",
|
||||
"Fallback captions: ": "Tilbakefallsundertitler: ",
|
||||
"Show related videos? ": "Vis relaterte videoer? ",
|
||||
"Visual preferences": "Visuelle innstillinger",
|
||||
"Dark mode: ": "Mørk drakt: ",
|
||||
"Thin mode: ": "Tynt modus: ",
|
||||
"Subscription preferences": "Abonnementsinnstillinger",
|
||||
"Redirect homepage to feed: ": "Videresend hjemmeside til flyt: ",
|
||||
"Number of videos shown in feed: ": "Antall videoer å vise i flyt: ",
|
||||
"Sort videos by: ": "Sorter videoer etter: ",
|
||||
"published": "publisert",
|
||||
"published - reverse": "publisert - motsatt",
|
||||
"alphabetically": "alfabetisk",
|
||||
"alphabetically - reverse": "alfabetisk - motsatt",
|
||||
"channel name": "kanalnavn",
|
||||
"channel name - reverse": "kanalnavn - motsatt",
|
||||
"Only show latest video from channel: ": "Kun vis siste video fra kanal: ",
|
||||
"Only show latest unwatched video from channel: ": "Kun vis siste usette video fra kanal: ",
|
||||
"Only show unwatched: ": "Kun vis usette: ",
|
||||
"Only show notifications (if there are any): ": "Kun vis merknader (hvis det er noen): ",
|
||||
"Data preferences": "Datainnstillinger",
|
||||
"Clear watch history": "Tøm visningshistorikk",
|
||||
"Import/Export data": "Importer/eksporter data",
|
||||
"Manage subscriptions": "Behandle abonnementer",
|
||||
"Watch history": "Visningshistorikk",
|
||||
"Delete account": "Slett konto",
|
||||
"Save preferences": "Lagre innstillinger",
|
||||
"Subscription manager": "Abonnementsbehandler",
|
||||
"`x` subscriptions": "`x` abonnementer",
|
||||
"Import/Export": "Importer/eksporter",
|
||||
"unsubscribe": "opphev abonnement",
|
||||
"Subscriptions": "Abonnement",
|
||||
"`x` unseen notifications": "`x` usette merknader",
|
||||
"search": "søk",
|
||||
"Sign out": "Logg ut",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Utgitt med AGPLv3+lisens av Omar Roth.",
|
||||
"Source available here.": "Kildekode tilgjengelig her.",
|
||||
"View JavaScript license information.": "Vis JavaScript-lisensinfo.",
|
||||
"Trending": "Trendsettende",
|
||||
"Watch video on Youtube": "Vis video på YouTube",
|
||||
"Genre: ": "Sjanger: ",
|
||||
"License: ": "Lisens: ",
|
||||
"Family friendly? ": "Familievennlig? ",
|
||||
"Wilson score: ": "Wilson-poengsum: ",
|
||||
"Engagement: ": "Engasjement: ",
|
||||
"Whitelisted regions: ": "Hvitlistede regioner: ",
|
||||
"Blacklisted regions: ": "Svartelistede regioner: ",
|
||||
"Shared `x`": "Delt `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Hei. Det ser ut til at du har JavaScript avslått. Klikk her for å vise kommentarer, ha i minnet at innlasting tar lengre tid.",
|
||||
"View YouTube comments": "Vis YouTube-kommentarer",
|
||||
"View more comments on Reddit": "Vis flere kommenterer på Reddit",
|
||||
"View `x` comments": "Vis `x` kommentarer",
|
||||
"View Reddit comments": "Vis Reddit-kommentarer",
|
||||
"Hide replies": "Skjul svar",
|
||||
"Show replies": "Vis svar",
|
||||
"Incorrect password": "Feil passord",
|
||||
"Quota exceeded, try again in a few hours": "Kvote overskredet, prøv igjen om et par timer",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Kunne ikke logge inn, forsikre deg om at tofaktor-identitetsbekreftelse (Authenticator eller SMS) er skrudd på.",
|
||||
"Invalid TFA code": "Ugyldig tofaktorkode",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Innlogging mislyktes. Dette kan være fordi tofaktor-identitetsbekreftelse er skrudd av på kontoen din.",
|
||||
"Invalid answer": "Ugyldig svar",
|
||||
"Invalid CAPTCHA": "Ugyldig CAPTCHA",
|
||||
"CAPTCHA is a required field": "CAPTCHA er et påkrevd felt",
|
||||
"User ID is a required field": "Bruker-ID er et påkrevd felt",
|
||||
"Password is a required field": "Passord er et påkrevd felt",
|
||||
"Invalid username or password": "Ugyldig brukernavn eller passord",
|
||||
"Please sign in using 'Sign in with Google'": "Logg inn ved bruk av \"Google-innlogging\"",
|
||||
"Password cannot be empty": "Passordet kan ikke være tomt",
|
||||
"Password cannot be longer than 55 characters": "Passordet kan ikke være lengre enn 55 tegn",
|
||||
"Please sign in": "Logg inn",
|
||||
"Invidious Private Feed for `x`": "Ugyldig privat flyt for `x`",
|
||||
"channel:`x`": "kanal `x`",
|
||||
"Deleted or invalid channel": "Slettet eller ugyldig kanal",
|
||||
"This channel does not exist.": "Denne kanalen finnes ikke.",
|
||||
"Could not get channel info.": "Kunne ikke innhente kanalinfo.",
|
||||
"Could not fetch comments": "Kunne ikke hente kommentarer",
|
||||
"View `x` replies": "Vis `x` svar",
|
||||
"`x` ago": "`x` siden",
|
||||
"Load more": "Last inn flere",
|
||||
"`x` points": "`x` poeng",
|
||||
"Could not create mix.": "Kunne ikke opprette miks.",
|
||||
"Playlist is empty": "Spillelisten er tom",
|
||||
"Invalid playlist.": "Ugyldig spilleliste.",
|
||||
"Playlist does not exist.": "Spillelisten finnes ikke.",
|
||||
"Could not pull trending pages.": "Kunne ikke hente trendsettende sider.",
|
||||
"Hidden field \"challenge\" is a required field": "Skjult felt \"utfordring\" er et påkrevd felt",
|
||||
"Hidden field \"token\" is a required field": "Skjult felt \"symbol\" er et påkrevd felt",
|
||||
"Invalid challenge": "Ugyldig utfordring",
|
||||
"Invalid token": "Ugyldig symbol",
|
||||
"Invalid user": "Ugyldig bruker",
|
||||
"Token is expired, please try again": "Symbol utløpt, prøv igjen",
|
||||
"English": "Engelsk",
|
||||
"English (auto-generated)": "Engelsk (auto-generert)",
|
||||
"Afrikaans": "",
|
||||
"Albanian": "Albansk",
|
||||
"Amharic": "",
|
||||
"Arabic": "Arabisk",
|
||||
"Armenian": "Armensk",
|
||||
"Azerbaijani": "",
|
||||
"Bangla": "",
|
||||
"Basque": "",
|
||||
"Belarusian": "Hviterussisk",
|
||||
"Bosnian": "Bosnisk",
|
||||
"Bulgarian": "Bulgarsk",
|
||||
"Burmese": "Burmesisk",
|
||||
"Catalan": "Katalansk",
|
||||
"Cebuano": "",
|
||||
"Chinese (Simplified)": "",
|
||||
"Chinese (Traditional)": "",
|
||||
"Corsican": "",
|
||||
"Croatian": "",
|
||||
"Czech": "Tsjekkisk",
|
||||
"Danish": "Dansk",
|
||||
"Dutch": "",
|
||||
"Esperanto": "Esperanto",
|
||||
"Estonian": "",
|
||||
"Filipino": "",
|
||||
"Finnish": "Finsk",
|
||||
"French": "Fransk",
|
||||
"Galician": "",
|
||||
"Georgian": "",
|
||||
"German": "",
|
||||
"Greek": "",
|
||||
"Gujarati": "",
|
||||
"Haitian Creole": "",
|
||||
"Hausa": "",
|
||||
"Hawaiian": "",
|
||||
"Hebrew": "",
|
||||
"Hindi": "",
|
||||
"Hmong": "",
|
||||
"Hungarian": "Ungarsk",
|
||||
"Icelandic": "Islandsk",
|
||||
"Igbo": "",
|
||||
"Indonesian": "Indonesisk",
|
||||
"Irish": "Irsk",
|
||||
"Italian": "Italiensk",
|
||||
"Japanese": "Japansk",
|
||||
"Javanese": "",
|
||||
"Kannada": "",
|
||||
"Kazakh": "",
|
||||
"Khmer": "",
|
||||
"Korean": "",
|
||||
"Kurdish": "",
|
||||
"Kyrgyz": "",
|
||||
"Lao": "",
|
||||
"Latin": "",
|
||||
"Latvian": "",
|
||||
"Lithuanian": "",
|
||||
"Luxembourgish": "",
|
||||
"Macedonian": "",
|
||||
"Malagasy": "",
|
||||
"Malay": "",
|
||||
"Malayalam": "",
|
||||
"Maltese": "",
|
||||
"Maori": "",
|
||||
"Marathi": "",
|
||||
"Mongolian": "",
|
||||
"Nepali": "",
|
||||
"Norwegian": "Norsk bokmål",
|
||||
"Nyanja": "",
|
||||
"Pashto": "",
|
||||
"Persian": "",
|
||||
"Polish": "",
|
||||
"Portuguese": "",
|
||||
"Punjabi": "",
|
||||
"Romanian": "",
|
||||
"Russian": "Russisk",
|
||||
"Samoan": "",
|
||||
"Scottish Gaelic": "",
|
||||
"Serbian": "Serbisk",
|
||||
"Shona": "",
|
||||
"Sindhi": "",
|
||||
"Sinhala": "",
|
||||
"Slovak": "Slovakisk",
|
||||
"Slovenian": "Slovensk",
|
||||
"Somali": "Somali",
|
||||
"Southern Sotho": "",
|
||||
"Spanish": "Spansk",
|
||||
"Spanish (Latin America)": "",
|
||||
"Sundanese": "",
|
||||
"Swahili": "",
|
||||
"Swedish": "Svensk",
|
||||
"Tajik": "",
|
||||
"Tamil": "",
|
||||
"Telugu": "",
|
||||
"Thai": "",
|
||||
"Turkish": "Tyrkisk",
|
||||
"Ukrainian": "Ukrainsk",
|
||||
"Urdu": "",
|
||||
"Uzbek": "",
|
||||
"Vietnamese": "Vietnamesisk",
|
||||
"Welsh": "",
|
||||
"Western Frisian": "",
|
||||
"Xhosa": "",
|
||||
"Yiddish": "",
|
||||
"Yoruba": "",
|
||||
"Zulu": "",
|
||||
"`x` years": "`x` år",
|
||||
"`x` months": "`x` måneder",
|
||||
"`x` weeks": "`x` uker",
|
||||
"`x` days": "`x` dager",
|
||||
"`x` hours": "`x` timer",
|
||||
"`x` minutes": "`x` minutter",
|
||||
"`x` seconds": "`x` sekunder",
|
||||
"Fallback comments: ": "Tilbakefallskommentarer: ",
|
||||
"Popular": "Pupulært",
|
||||
"Top": "Topp",
|
||||
"About": "Om",
|
||||
"Rating: ": "Vurdering: ",
|
||||
"Language: ": "Språk: ",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": "",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"Youtube permalink of the comment": "",
|
||||
"`x` marked it with a ❤": ""
|
||||
}
|
||||
|
@ -263,5 +263,16 @@
|
||||
"Top": "",
|
||||
"About": "",
|
||||
"Rating: ": "",
|
||||
"Language: ": ""
|
||||
"Language: ": "",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": "",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"Youtube permalink of the comment": "",
|
||||
"`x` marked it with a ❤": ""
|
||||
}
|
||||
|
@ -263,5 +263,16 @@
|
||||
"Top": "",
|
||||
"About": "",
|
||||
"Rating: ": "",
|
||||
"Language: ": ""
|
||||
"Language: ": "",
|
||||
"Default": "",
|
||||
"Music": "",
|
||||
"Gaming": "",
|
||||
"News": "",
|
||||
"Movies": "",
|
||||
"Download": "",
|
||||
"Download as: ": "",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"Youtube permalink of the comment": "",
|
||||
"`x` marked it with a ❤": ""
|
||||
}
|
||||
|
553
locales/ru.json
553
locales/ru.json
@ -1,273 +1,284 @@
|
||||
{
|
||||
"`x` subscribers": "`x` подписчиков",
|
||||
"`x` videos": "`x` видео",
|
||||
"LIVE": "ПРЯМОЙ ЭФИР",
|
||||
"Shared `x` ago": "Опубликовано `x` назад",
|
||||
"Unsubscribe": "Отписаться",
|
||||
"Subscribe": "Подписаться",
|
||||
"Login to subscribe to `x`": "Войти, чтобы подписаться на `x`",
|
||||
"View channel on YouTube": "Канал на YouTube",
|
||||
"newest": "новые",
|
||||
"oldest": "старые",
|
||||
"popular": "популярные",
|
||||
"Preview page": "Предварительный просмотр",
|
||||
"Next page": "Следующая страница",
|
||||
"Clear watch history?": "Очистить историю просмотров?",
|
||||
"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": "История",
|
||||
"Previous page": "Предыдущая страница",
|
||||
"An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube",
|
||||
"JavaScript license information": "Лицензии JavaScript",
|
||||
"source": "источник",
|
||||
"Login": "Войти",
|
||||
"Login/Register": "Войти/Регистрация",
|
||||
"Login to Google": "Войти через Google",
|
||||
"User ID:": "ID пользователя:",
|
||||
"Password:": "Пароль:",
|
||||
"Time (h:mm:ss):": "Время (ч:мм:сс):",
|
||||
"Text CAPTCHA": "Текст капчи",
|
||||
"Image CAPTCHA": "Изображение капчи",
|
||||
"Sign In": "Войти",
|
||||
"Register": "Регистрация",
|
||||
"Email:": "Эл. почта:",
|
||||
"Google verification code:": "Код подтверждения Google:",
|
||||
"Preferences": "Настройки",
|
||||
"Player preferences": "Настройки проигрывателя",
|
||||
"Always loop: ": "Всегда повторять: ",
|
||||
"Autoplay: ": "Автовоспроизведение: ",
|
||||
"Autoplay next video: ": "Автовоспроизведение следующего видео: ",
|
||||
"Listen by default: ": "Режим \"только аудио\" по-умолчанию: ",
|
||||
"Default speed: ": "Скорость по-умолчанию: ",
|
||||
"Preferred video quality: ": "Предпочтительное качество видео: ",
|
||||
"Player volume: ": "Громкость воспроизведения: ",
|
||||
"Default comments: ": "Источник комментариев: ",
|
||||
"youtube": "YouTube",
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "Субтитры по-умолчанию: ",
|
||||
"Fallback captions: ": "Резервные субтитры: ",
|
||||
"Show related videos? ": "Показывать похожие видео? ",
|
||||
"Visual preferences": "Визуальные настройки",
|
||||
"Dark mode: ": "Темная тема: ",
|
||||
"Thin mode: ": "Облегченный режим: ",
|
||||
"Subscription preferences": "Настройки подписок",
|
||||
"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): ": "Отображать только оповещения (если есть): ",
|
||||
"Data preferences": "Настройки данных",
|
||||
"Clear watch history": "Очистить историю просмотра",
|
||||
"Import/Export data": "Импорт/Экспорт данных",
|
||||
"Manage subscriptions": "Управление подписками",
|
||||
"Watch history": "История просмотров",
|
||||
"Delete account": "Удалить аккаунт",
|
||||
"Save preferences": "Сохранить настройки",
|
||||
"Subscription manager": "Менеджер подписок",
|
||||
"`x` subscriptions": "`x` подписок",
|
||||
"Import/Export": "Импорт/Экспорт",
|
||||
"unsubscribe": "отписаться",
|
||||
"Subscriptions": "Подписки",
|
||||
"`x` unseen notifications": "`x` новых оповещений",
|
||||
"search": "поиск",
|
||||
"Sign out": "Выйти",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Распространяется Omar Roth по AGPLv3.",
|
||||
"Source available here.": "Исходный код доступен здесь.",
|
||||
"Liberapay: ": "Liberapay: ",
|
||||
"Patreon: ": "Patreon: ",
|
||||
"BTC: ": "BTC: ",
|
||||
"BCH: ": "BCH: ",
|
||||
"View JavaScript license information.": "Посмотреть лицензии JavaScript кода.",
|
||||
"Trending": "В тренде",
|
||||
"Watch video on Youtube": "Смотреть на YouTube",
|
||||
"Genre: ": "Жанр: ",
|
||||
"License: ": "Лицензия: ",
|
||||
"Family friendly? ": "Семейный просмотр: ",
|
||||
"Wilson score: ": "Рейтинг Вильсона: ",
|
||||
"Engagement: ": "Вовлеченность: ",
|
||||
"Whitelisted regions: ": "Доступно для: ",
|
||||
"Blacklisted regions: ": "Недоступно для: ",
|
||||
"Shared `x`": "Опубликовано `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Похоже, что у Вас отключен JavaScript. Нажмите сюда, чтобы увидеть комментарии (учтите, что они могут загружаться дольше).",
|
||||
"View YouTube comments": "Смотреть комментарии с YouTube",
|
||||
"View more comments on Reddit": "Больше комментариев на Reddit",
|
||||
"View `x` comments": "Показать `x` комментариев",
|
||||
"View Reddit comments": "Смотреть комментарии с Reddit",
|
||||
"Hide replies": "Скрыть ответы",
|
||||
"Show replies": "Показать ответы",
|
||||
"Incorrect password": "Неправильный пароль",
|
||||
"Quota exceeded, try again in a few hours": "Превышена квота, попробуйте снова через несколько часов",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Вход не выполнен, проверьте, не включена ли двухфакторная аутентификация.",
|
||||
"Invalid TFA code": "Неправильный TFA код",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Не удалось войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.",
|
||||
"Invalid answer": "Неверный ответ",
|
||||
"Invalid CAPTCHA": "Неверная капча",
|
||||
"CAPTCHA is a required field": "Необходимо ввести капчу",
|
||||
"User ID is a required field": "Необходимо ввести идентификатор пользователя",
|
||||
"Password is a required field": "Необходимо ввести пароль",
|
||||
"Invalid username or password": "Недопустимый пароль или имя пользователя",
|
||||
"Please sign in using 'Sign in with Google'": "Пожалуйста войдите через Google",
|
||||
"Password cannot be empty": "Пароль не может быть пустым",
|
||||
"Password cannot be longer than 55 characters": "Пароль не может быть длиннее 55 символов",
|
||||
"Please sign in": "Пожалуйста, войдите",
|
||||
"Invidious Private Feed for `x`": "Приватная лента Invidious для `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": "Показать `x` ответов",
|
||||
"`x` ago": "`x` назад",
|
||||
"Load more": "Загрузить больше",
|
||||
"`x` points": "`x` очков",
|
||||
"Could not create mix.": "Невозможно создать \"микс\".",
|
||||
"Playlist is empty": "Плейлист пуст",
|
||||
"Invalid playlist.": "Некорректный плейлист.",
|
||||
"Playlist does not exist.": "Плейлист не существует.",
|
||||
"Could not pull trending pages.": "Невозможно получить страницы \"в тренде\".",
|
||||
"Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле \"challenge\"",
|
||||
"Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле \"токен\"",
|
||||
"Invalid challenge": "Неправильный ответ в \"challenge\"",
|
||||
"Invalid token": "Неправильный токен",
|
||||
"Invalid user": "Недопустимое имя пользователя",
|
||||
"Token is expired, please try again": "Срок действия токена истек, попробуйте позже",
|
||||
"English": "Английский",
|
||||
"English (auto-generated)": "Английский (созданы автоматически)",
|
||||
"Afrikaans": "",
|
||||
"Albanian": "",
|
||||
"Amharic": "",
|
||||
"Arabic": "",
|
||||
"Armenian": "",
|
||||
"Azerbaijani": "",
|
||||
"Bangla": "",
|
||||
"Basque": "",
|
||||
"Belarusian": "",
|
||||
"Bosnian": "",
|
||||
"Bulgarian": "",
|
||||
"Burmese": "",
|
||||
"Catalan": "",
|
||||
"Cebuano": "",
|
||||
"Chinese (Simplified)": "",
|
||||
"Chinese (Traditional)": "",
|
||||
"Corsican": "",
|
||||
"Croatian": "",
|
||||
"Czech": "",
|
||||
"Danish": "",
|
||||
"Dutch": "",
|
||||
"Esperanto": "",
|
||||
"Estonian": "",
|
||||
"Filipino": "",
|
||||
"Finnish": "",
|
||||
"French": "",
|
||||
"Galician": "",
|
||||
"Georgian": "",
|
||||
"German": "",
|
||||
"Greek": "",
|
||||
"Gujarati": "",
|
||||
"Haitian Creole": "",
|
||||
"Hausa": "",
|
||||
"Hawaiian": "",
|
||||
"Hebrew": "",
|
||||
"Hindi": "",
|
||||
"Hmong": "",
|
||||
"Hungarian": "",
|
||||
"Icelandic": "",
|
||||
"Igbo": "",
|
||||
"Indonesian": "",
|
||||
"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": "",
|
||||
"Nyanja": "",
|
||||
"Pashto": "",
|
||||
"Persian": "",
|
||||
"Polish": "",
|
||||
"Portuguese": "",
|
||||
"Punjabi": "",
|
||||
"Romanian": "",
|
||||
"Russian": "",
|
||||
"Samoan": "",
|
||||
"Scottish Gaelic": "",
|
||||
"Serbian": "",
|
||||
"Shona": "",
|
||||
"Sindhi": "",
|
||||
"Sinhala": "",
|
||||
"Slovak": "",
|
||||
"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": "`x` лет",
|
||||
"`x` months": "`x` месяцев",
|
||||
"`x` weeks": "`x` недель",
|
||||
"`x` days": "`x` дней",
|
||||
"`x` hours": "`x` часов",
|
||||
"`x` minutes": "`x` минут",
|
||||
"`x` seconds": "`x` секунд",
|
||||
"Fallback comments: ": "Резервные комментарии: ",
|
||||
"Popular": "Популярное",
|
||||
"Top": "Топ",
|
||||
"About": "О сайте",
|
||||
"Rating: ": "Рейтинг: ",
|
||||
"Language: ": "Язык: "
|
||||
"`x` subscribers": "`x` подписчиков",
|
||||
"`x` videos": "`x` видео",
|
||||
"LIVE": "ПРЯМОЙ ЭФИР",
|
||||
"Shared `x` ago": "Опубликовано `x` назад",
|
||||
"Unsubscribe": "Отписаться",
|
||||
"Subscribe": "Подписаться",
|
||||
"Login to subscribe to `x`": "Войти, чтобы подписаться на `x`",
|
||||
"View channel on YouTube": "Канал на YouTube",
|
||||
"newest": "новые",
|
||||
"oldest": "старые",
|
||||
"popular": "популярные",
|
||||
"Preview page": "Предварительный просмотр",
|
||||
"Next page": "Следующая страница",
|
||||
"Clear watch history?": "Очистить историю просмотров?",
|
||||
"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": "История",
|
||||
"Previous page": "Предыдущая страница",
|
||||
"An alternative front-end to YouTube": "Альтернативный фронтенд для YouTube",
|
||||
"JavaScript license information": "Лицензии JavaScript",
|
||||
"source": "источник",
|
||||
"Login": "Войти",
|
||||
"Login/Register": "Войти/Регистрация",
|
||||
"Login to Google": "Войти через Google",
|
||||
"User ID:": "ID пользователя:",
|
||||
"Password:": "Пароль:",
|
||||
"Time (h:mm:ss):": "Время (ч:мм:сс):",
|
||||
"Text CAPTCHA": "Текст капчи",
|
||||
"Image CAPTCHA": "Изображение капчи",
|
||||
"Sign In": "Войти",
|
||||
"Register": "Регистрация",
|
||||
"Email:": "Эл. почта:",
|
||||
"Google verification code:": "Код подтверждения Google:",
|
||||
"Preferences": "Настройки",
|
||||
"Player preferences": "Настройки проигрывателя",
|
||||
"Always loop: ": "Всегда повторять: ",
|
||||
"Autoplay: ": "Автовоспроизведение: ",
|
||||
"Autoplay next video: ": "Автовоспроизведение следующего видео: ",
|
||||
"Listen by default: ": "Режим \"только аудио\" по-умолчанию: ",
|
||||
"Default speed: ": "Скорость по-умолчанию: ",
|
||||
"Preferred video quality: ": "Предпочтительное качество видео: ",
|
||||
"Player volume: ": "Громкость воспроизведения: ",
|
||||
"Default comments: ": "Источник комментариев: ",
|
||||
"youtube": "YouTube",
|
||||
"reddit": "Reddit",
|
||||
"Default captions: ": "Субтитры по-умолчанию: ",
|
||||
"Fallback captions: ": "Резервные субтитры: ",
|
||||
"Show related videos? ": "Показывать похожие видео? ",
|
||||
"Visual preferences": "Визуальные настройки",
|
||||
"Dark mode: ": "Темная тема: ",
|
||||
"Thin mode: ": "Облегченный режим: ",
|
||||
"Subscription preferences": "Настройки подписок",
|
||||
"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): ": "Отображать только оповещения (если есть): ",
|
||||
"Data preferences": "Настройки данных",
|
||||
"Clear watch history": "Очистить историю просмотра",
|
||||
"Import/Export data": "Импорт/Экспорт данных",
|
||||
"Manage subscriptions": "Управление подписками",
|
||||
"Watch history": "История просмотров",
|
||||
"Delete account": "Удалить аккаунт",
|
||||
"Save preferences": "Сохранить настройки",
|
||||
"Subscription manager": "Менеджер подписок",
|
||||
"`x` subscriptions": "`x` подписок",
|
||||
"Import/Export": "Импорт/Экспорт",
|
||||
"unsubscribe": "отписаться",
|
||||
"Subscriptions": "Подписки",
|
||||
"`x` unseen notifications": "`x` новых оповещений",
|
||||
"search": "поиск",
|
||||
"Sign out": "Выйти",
|
||||
"Released under the AGPLv3 by Omar Roth.": "Распространяется Omar Roth по AGPLv3.",
|
||||
"Source available here.": "Исходный код доступен здесь.",
|
||||
"Liberapay: ": "Liberapay: ",
|
||||
"Patreon: ": "Patreon: ",
|
||||
"BTC: ": "BTC: ",
|
||||
"BCH: ": "BCH: ",
|
||||
"View JavaScript license information.": "Посмотреть лицензии JavaScript кода.",
|
||||
"Trending": "В тренде",
|
||||
"Watch video on Youtube": "Смотреть на YouTube",
|
||||
"Genre: ": "Жанр: ",
|
||||
"License: ": "Лицензия: ",
|
||||
"Family friendly? ": "Семейный просмотр: ",
|
||||
"Wilson score: ": "Рейтинг Вильсона: ",
|
||||
"Engagement: ": "Вовлеченность: ",
|
||||
"Whitelisted regions: ": "Доступно для: ",
|
||||
"Blacklisted regions: ": "Недоступно для: ",
|
||||
"Shared `x`": "Опубликовано `x`",
|
||||
"Hi! Looks like you have JavaScript disabled. Click here to view comments, keep in mind it may take a bit longer to load.": "Похоже, что у Вас отключен JavaScript. Нажмите сюда, чтобы увидеть комментарии (учтите, что они могут загружаться дольше).",
|
||||
"View YouTube comments": "Смотреть комментарии с YouTube",
|
||||
"View more comments on Reddit": "Больше комментариев на Reddit",
|
||||
"View `x` comments": "Показать `x` комментариев",
|
||||
"View Reddit comments": "Смотреть комментарии с Reddit",
|
||||
"Hide replies": "Скрыть ответы",
|
||||
"Show replies": "Показать ответы",
|
||||
"Incorrect password": "Неправильный пароль",
|
||||
"Quota exceeded, try again in a few hours": "Превышена квота, попробуйте снова через несколько часов",
|
||||
"Unable to login, make sure two-factor authentication (Authenticator or SMS) is enabled.": "Вход не выполнен, проверьте, не включена ли двухфакторная аутентификация.",
|
||||
"Invalid TFA code": "Неправильный TFA код",
|
||||
"Login failed. This may be because two-factor authentication is not enabled on your account.": "Не удалось войти. Это может быть из-за того, что в вашем аккаунте не включена двухфакторная аутентификация.",
|
||||
"Invalid answer": "Неверный ответ",
|
||||
"Invalid CAPTCHA": "Неверная капча",
|
||||
"CAPTCHA is a required field": "Необходимо ввести капчу",
|
||||
"User ID is a required field": "Необходимо ввести идентификатор пользователя",
|
||||
"Password is a required field": "Необходимо ввести пароль",
|
||||
"Invalid username or password": "Недопустимый пароль или имя пользователя",
|
||||
"Please sign in using 'Sign in with Google'": "Пожалуйста войдите через Google",
|
||||
"Password cannot be empty": "Пароль не может быть пустым",
|
||||
"Password cannot be longer than 55 characters": "Пароль не может быть длиннее 55 символов",
|
||||
"Please sign in": "Пожалуйста, войдите",
|
||||
"Invidious Private Feed for `x`": "Приватная лента Invidious для `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": "Показать `x` ответов",
|
||||
"`x` ago": "`x` назад",
|
||||
"Load more": "Загрузить больше",
|
||||
"`x` points": "`x` очков",
|
||||
"Could not create mix.": "Невозможно создать \"микс\".",
|
||||
"Playlist is empty": "Плейлист пуст",
|
||||
"Invalid playlist.": "Некорректный плейлист.",
|
||||
"Playlist does not exist.": "Плейлист не существует.",
|
||||
"Could not pull trending pages.": "Невозможно получить страницы \"в тренде\".",
|
||||
"Hidden field \"challenge\" is a required field": "Необходимо заполнить скрытое поле \"challenge\"",
|
||||
"Hidden field \"token\" is a required field": "Необходимо заполнить скрытое поле \"токен\"",
|
||||
"Invalid challenge": "Неправильный ответ в \"challenge\"",
|
||||
"Invalid token": "Неправильный токен",
|
||||
"Invalid user": "Недопустимое имя пользователя",
|
||||
"Token is expired, please try again": "Срок действия токена истек, попробуйте позже",
|
||||
"English": "Английский",
|
||||
"English (auto-generated)": "Английский (созданы автоматически)",
|
||||
"Afrikaans": "Африкаанс",
|
||||
"Albanian": "Албанский",
|
||||
"Amharic": "Амхарский",
|
||||
"Arabic": "Арабский",
|
||||
"Armenian": "Армянский",
|
||||
"Azerbaijani": "Азербайджанский",
|
||||
"Bangla": "",
|
||||
"Basque": "",
|
||||
"Belarusian": "",
|
||||
"Bosnian": "",
|
||||
"Bulgarian": "",
|
||||
"Burmese": "",
|
||||
"Catalan": "",
|
||||
"Cebuano": "",
|
||||
"Chinese (Simplified)": "",
|
||||
"Chinese (Traditional)": "",
|
||||
"Corsican": "",
|
||||
"Croatian": "",
|
||||
"Czech": "",
|
||||
"Danish": "",
|
||||
"Dutch": "",
|
||||
"Esperanto": "",
|
||||
"Estonian": "",
|
||||
"Filipino": "",
|
||||
"Finnish": "",
|
||||
"French": "",
|
||||
"Galician": "",
|
||||
"Georgian": "",
|
||||
"German": "",
|
||||
"Greek": "",
|
||||
"Gujarati": "",
|
||||
"Haitian Creole": "",
|
||||
"Hausa": "",
|
||||
"Hawaiian": "",
|
||||
"Hebrew": "",
|
||||
"Hindi": "",
|
||||
"Hmong": "",
|
||||
"Hungarian": "",
|
||||
"Icelandic": "",
|
||||
"Igbo": "",
|
||||
"Indonesian": "",
|
||||
"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": "",
|
||||
"Nyanja": "",
|
||||
"Pashto": "",
|
||||
"Persian": "",
|
||||
"Polish": "",
|
||||
"Portuguese": "",
|
||||
"Punjabi": "",
|
||||
"Romanian": "",
|
||||
"Russian": "",
|
||||
"Samoan": "",
|
||||
"Scottish Gaelic": "",
|
||||
"Serbian": "",
|
||||
"Shona": "",
|
||||
"Sindhi": "",
|
||||
"Sinhala": "",
|
||||
"Slovak": "",
|
||||
"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": "`x` лет",
|
||||
"`x` months": "`x` месяцев",
|
||||
"`x` weeks": "`x` недель",
|
||||
"`x` days": "`x` дней",
|
||||
"`x` hours": "`x` часов",
|
||||
"`x` minutes": "`x` минут",
|
||||
"`x` seconds": "`x` секунд",
|
||||
"Fallback comments: ": "Резервные комментарии: ",
|
||||
"Popular": "Популярное",
|
||||
"Top": "Топ",
|
||||
"About": "О сайте",
|
||||
"Rating: ": "Рейтинг: ",
|
||||
"Language: ": "Язык: ",
|
||||
"Default": "По-умолчанию",
|
||||
"Music": "Музыка",
|
||||
"Gaming": "Игры",
|
||||
"News": "Новости",
|
||||
"Movies": "Фильмы",
|
||||
"Download": "Скачать",
|
||||
"Download as: ": "Скачать как: ",
|
||||
"%A %B %-d, %Y": "",
|
||||
"(edited)": "",
|
||||
"Youtube permalink of the comment": "",
|
||||
"`x` marked it with a ❤": ""
|
||||
}
|
||||
|
10
setup.sh
10
setup.sh
@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
createdb invidious
|
||||
#createuser kemal
|
||||
psql -c "CREATE USER kemal WITH PASSWORD 'kemal';"
|
||||
psql invidious < config/sql/channels.sql
|
||||
psql invidious < config/sql/videos.sql
|
||||
psql invidious < config/sql/channel_videos.sql
|
||||
psql invidious < config/sql/users.sql
|
||||
psql invidious < config/sql/nonces.sql
|
@ -1,5 +1,5 @@
|
||||
name: invidious
|
||||
version: 0.12.0
|
||||
version: 0.13.1
|
||||
|
||||
authors:
|
||||
- Omar Roth <omarroth@hotmail.com>
|
||||
@ -19,6 +19,6 @@ dependencies:
|
||||
sqlite3:
|
||||
github: crystal-lang/crystal-sqlite3
|
||||
|
||||
crystal: 0.27.0
|
||||
crystal: 0.27.1
|
||||
|
||||
license: AGPLv3
|
||||
|
62
spec/helpers_spec.cr
Normal file
62
spec/helpers_spec.cr
Normal file
@ -0,0 +1,62 @@
|
||||
require "kemal"
|
||||
require "pg"
|
||||
require "spec"
|
||||
require "yaml"
|
||||
require "../src/invidious/helpers/*"
|
||||
require "../src/invidious/channels"
|
||||
require "../src/invidious/playlists"
|
||||
require "../src/invidious/search"
|
||||
|
||||
describe "Helpers" do
|
||||
describe "#produce_channel_videos_url" do
|
||||
it "correctly produces url for requesting page `x` of a channel's videos" do
|
||||
produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw").should eq("/browse_ajax?continuation=4qmFsgI8EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4&gl=US&hl=en")
|
||||
|
||||
produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", sort_by: "popular").should eq("/browse_ajax?continuation=4qmFsgJCEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJkVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0V4R0FFJTNE&gl=US&hl=en")
|
||||
|
||||
produce_channel_videos_url(ucid: "UCXuqSBlHAE6Xw-yeJA0Tunw", page: 20).should eq("/browse_ajax?continuation=4qmFsgJEEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaKEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQUFlZ0l5TUElM0QlM0Q%3D&gl=US&hl=en")
|
||||
|
||||
produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", auto_generated: true).should eq("/browse_ajax?continuation=4qmFsgJIEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaLEVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQTJlZ294TlRRNU5qQXpOelE1&gl=US&hl=en")
|
||||
|
||||
produce_channel_videos_url(ucid: "UC-9-kyTW8ZkZNDHQJ6FgpwQ", page: 20, auto_generated: true, sort_by: "popular").should eq("/browse_ajax?continuation=4qmFsgJOEhhVQy05LWt5VFc4WmtaTkRIUUo2Rmdwd1EaMkVnWjJhV1JsYjNNd0FqZ0JZQUZxQUxnQkFDQTJlZ294TkRrNU5Ea3hOelE1R0FFJTNE&gl=US&hl=en")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#produce_channel_search_url" do
|
||||
it "correctly produces token for searching a specific channel" do
|
||||
produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "", 100).should eq("/browse_ajax?continuation=4qmFsgI-EhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaIEVnWnpaV0Z5WTJnd0FqZ0JZQUZxQUxnQkFIb0RNVEF3WgA%3D&gl=US&hl=en")
|
||||
|
||||
produce_channel_search_url("UCXuqSBlHAE6Xw-yeJA0Tunw", "По ожиशुपतिरपि子而時ஸ்றீனி", 0).should eq("/browse_ajax?continuation=4qmFsgJZEhhVQ1h1cVNCbEhBRTZYdy15ZUpBMFR1bncaJEVnWnpaV0Z5WTJnd0FqZ0JZQUZxQUxnQkFIb0JNQSUzRCUzRFoX0J_QviDQvtC20LjgpLbgpYHgpKrgpKTgpL_gpLDgpKrgpL_lrZDogIzmmYLgrrjgr43grrHgr4Dgrqngrr8%3D&gl=US&hl=en")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#produce_playlist_url" do
|
||||
it "correctly produces url for requesting index `x` of a playlist" do
|
||||
produce_playlist_url("UUCla9fZca4I7KagBtgRGnOw", 0).should eq("/browse_ajax?continuation=4qmFsgIsEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoOZWdaUVZEcERRVUUlM0Q%3D&gl=US&hl=en")
|
||||
|
||||
produce_playlist_url("UCCla9fZca4I7KagBtgRGnOw", 0).should eq("/browse_ajax?continuation=4qmFsgIsEhpWTFVVQ2xhOWZaY2E0STdLYWdCdGdSR25PdxoOZWdaUVZEcERRVUUlM0Q%3D&gl=US&hl=en")
|
||||
|
||||
produce_playlist_url("PLt5AfwLFPxWLNVixpe1w3fi6lE2OTq0ET", 0).should eq("/browse_ajax?continuation=4qmFsgI2EiRWTFBMdDVBZndMRlB4V0xOVml4cGUxdzNmaTZsRTJPVHEwRVQaDmVnWlFWRHBEUVVFJTNE&gl=US&hl=en")
|
||||
|
||||
produce_playlist_url("PLt5AfwLFPxWLNVixpe1w3fi6lE2OTq0ET", 10000).should eq("/browse_ajax?continuation=4qmFsgI0EiRWTFBMdDVBZndMRlB4V0xOVml4cGUxdzNmaTZsRTJPVHEwRVQaDGVnZFFWRHBEU2tKUA%3D%3D&gl=US&hl=en")
|
||||
|
||||
produce_playlist_url("PL55713C70BA91BD6E", 0).should eq("/browse_ajax?continuation=4qmFsgImEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoOZWdaUVZEcERRVUUlM0Q%3D&gl=US&hl=en")
|
||||
|
||||
produce_playlist_url("PL55713C70BA91BD6E", 10000).should eq("/browse_ajax?continuation=4qmFsgIkEhRWTFBMNTU3MTNDNzBCQTkxQkQ2RRoMZWdkUVZEcERTa0pQ&gl=US&hl=en")
|
||||
end
|
||||
end
|
||||
|
||||
describe "#produce_search_params" do
|
||||
it "correctly produces token for searching with specified filters" do
|
||||
produce_search_params.should eq("CAASAhAB")
|
||||
|
||||
produce_search_params(sort: "upload_date", content_type: "video").should eq("CAISAhAB")
|
||||
|
||||
produce_search_params(content_type: "playlist").should eq("CAASAhAD")
|
||||
|
||||
produce_search_params(sort: "date", content_type: "video", features: ["hd", "cc", "purchased", "hdr"]).should eq("CAISCxABIAEwAUgByAEB")
|
||||
|
||||
produce_search_params(content_type: "channel").should eq("CAASAhAC")
|
||||
end
|
||||
end
|
||||
end
|
280
src/invidious.cr
280
src/invidious.cr
@ -16,6 +16,7 @@
|
||||
|
||||
require "detect_language"
|
||||
require "digest/md5"
|
||||
require "file_utils"
|
||||
require "kemal"
|
||||
require "openssl/hmac"
|
||||
require "option_parser"
|
||||
@ -35,6 +36,8 @@ channel_threads = CONFIG.channel_threads
|
||||
feed_threads = CONFIG.feed_threads
|
||||
video_threads = CONFIG.video_threads
|
||||
|
||||
logger = Invidious::LogHandler.new
|
||||
|
||||
Kemal.config.extra_options do |parser|
|
||||
parser.banner = "Usage: invidious [arguments]"
|
||||
parser.on("-t THREADS", "--crawl-threads=THREADS", "Number of threads for crawling YouTube (default: #{crawl_threads})") do |number|
|
||||
@ -69,6 +72,10 @@ Kemal.config.extra_options do |parser|
|
||||
exit
|
||||
end
|
||||
end
|
||||
parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: STDOUT)") do |output|
|
||||
FileUtils.mkdir_p(File.dirname(output))
|
||||
logger = Invidious::LogHandler.new(File.open(output, mode: "a"))
|
||||
end
|
||||
end
|
||||
|
||||
Kemal::CLI.new
|
||||
@ -92,6 +99,7 @@ LOCALES = {
|
||||
"ar" => load_locale("ar"),
|
||||
"de" => load_locale("de"),
|
||||
"en-US" => load_locale("en-US"),
|
||||
"fr" => load_locale("fr"),
|
||||
"nb_NO" => load_locale("nb_NO"),
|
||||
"nl" => load_locale("nl"),
|
||||
"pl" => load_locale("pl"),
|
||||
@ -100,17 +108,17 @@ LOCALES = {
|
||||
|
||||
crawl_threads.times do
|
||||
spawn do
|
||||
crawl_videos(PG_DB)
|
||||
crawl_videos(PG_DB, logger)
|
||||
end
|
||||
end
|
||||
|
||||
refresh_channels(PG_DB, channel_threads, CONFIG.full_refresh)
|
||||
refresh_channels(PG_DB, logger, channel_threads, CONFIG.full_refresh)
|
||||
|
||||
refresh_feeds(PG_DB, feed_threads)
|
||||
refresh_feeds(PG_DB, logger, feed_threads)
|
||||
|
||||
video_threads.times do |i|
|
||||
spawn do
|
||||
refresh_videos(PG_DB)
|
||||
refresh_videos(PG_DB, logger)
|
||||
end
|
||||
end
|
||||
|
||||
@ -118,6 +126,8 @@ top_videos = [] of Video
|
||||
spawn do
|
||||
pull_top_videos(CONFIG, PG_DB) do |videos|
|
||||
top_videos = videos
|
||||
sleep 1.minutes
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
|
||||
@ -125,6 +135,8 @@ popular_videos = [] of ChannelVideo
|
||||
spawn do
|
||||
pull_popular_videos(PG_DB) do |videos|
|
||||
popular_videos = videos
|
||||
sleep 1.minutes
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
|
||||
@ -294,7 +306,7 @@ get "/watch" do |env|
|
||||
next env.redirect "/watch?v=#{ex.message}"
|
||||
rescue ex
|
||||
error_message = ex.message
|
||||
STDOUT << id << " : " << ex.message << "\n"
|
||||
logger.write("#{id} : #{ex.message}\n")
|
||||
next templated "error"
|
||||
end
|
||||
|
||||
@ -364,12 +376,12 @@ get "/watch" do |env|
|
||||
video.description = replace_links(video.description)
|
||||
description = video.short_description
|
||||
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"]?)
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
host_params = env.request.query_params
|
||||
host_params.delete_all("v")
|
||||
|
||||
if video.info["hlsvp"]?
|
||||
hlsvp = video.info["hlsvp"]
|
||||
if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
|
||||
hlsvp = video.player_response["streamingData"]["hlsManifestUrl"].as_s
|
||||
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url)
|
||||
end
|
||||
|
||||
@ -392,9 +404,7 @@ get "/watch" do |env|
|
||||
rvs << HTTP::Params.parse(rv).to_h
|
||||
end
|
||||
|
||||
# rating = (video.likes.to_f/(video.likes.to_f + video.dislikes.to_f) * 4 + 1)
|
||||
rating = video.info["avg_rating"].to_f64
|
||||
|
||||
engagement = ((video.dislikes.to_f + video.likes.to_f)/video.views * 100)
|
||||
|
||||
playability_status = video.player_response["playabilityStatus"]?
|
||||
@ -466,12 +476,12 @@ get "/embed/:id" do |env|
|
||||
video.description = replace_links(video.description)
|
||||
description = video.short_description
|
||||
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"]?)
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
host_params = env.request.query_params
|
||||
host_params.delete_all("v")
|
||||
|
||||
if video.info["hlsvp"]?
|
||||
hlsvp = video.info["hlsvp"]
|
||||
if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
|
||||
hlsvp = video.player_response["streamingData"]["hlsManifestUrl"].as_s
|
||||
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url)
|
||||
end
|
||||
|
||||
@ -552,14 +562,16 @@ get "/opensearch.xml" do |env|
|
||||
locale = LOCALES[env.get("locale").as(String)]?
|
||||
env.response.content_type = "application/opensearchdescription+xml"
|
||||
|
||||
host = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
|
||||
XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||
xml.element("OpenSearchDescription", xmlns: "http://a9.com/-/spec/opensearch/1.1/") do
|
||||
xml.element("ShortName") { xml.text "Invidious" }
|
||||
xml.element("LongName") { xml.text "Invidious Search" }
|
||||
xml.element("Description") { xml.text "Search for videos, channels, and playlists on Invidious" }
|
||||
xml.element("InputEncoding") { xml.text "UTF-8" }
|
||||
xml.element("Image", width: 48, height: 48, type: "image/x-icon") { xml.text "https://invidio.us/favicon.ico" }
|
||||
xml.element("Url", type: "text/html", method: "get", template: "https://invidio.us/search?q={searchTerms}")
|
||||
xml.element("Image", width: 48, height: 48, type: "image/x-icon") { xml.text "#{host}/favicon.ico" }
|
||||
xml.element("Url", type: "text/html", method: "get", template: "#{host}/search?q={searchTerms}")
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -802,7 +814,7 @@ post "/login" do |env|
|
||||
|
||||
if challenge_results[0][-1][0].as_a?
|
||||
# Prefer Authenticator app and SMS over unsupported protocols
|
||||
if challenge_results[0][-1][0][0][8] != 6 || challenge_results[0][-1][0][0][8] != 9
|
||||
if challenge_results[0][-1][0][0][8] != 6 && challenge_results[0][-1][0][0][8] != 9
|
||||
tfa = challenge_results[0][-1][0].as_a.select { |auth_type| auth_type[8] == 6 || auth_type[8] == 9 }[0]
|
||||
select_challenge = "[#{challenge_results[0][-1][0].as_a.index(tfa).not_nil!}]"
|
||||
|
||||
@ -1021,7 +1033,7 @@ post "/login" do |env|
|
||||
view_name = "subscriptions_#{sha256(user.email)[0..7]}"
|
||||
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS \
|
||||
SELECT * FROM channel_videos WHERE \
|
||||
ucid = ANY ((SELECT subscriptions FROM users WHERE email = '#{user.email}')::text[]) \
|
||||
ucid = ANY ((SELECT subscriptions FROM users WHERE email = E'#{user.email.gsub("'", "\\'")}')::text[]) \
|
||||
ORDER BY published DESC;")
|
||||
|
||||
if Kemal.config.ssl || CONFIG.https_only
|
||||
@ -1116,21 +1128,21 @@ post "/preferences" do |env|
|
||||
listen = listen == "on"
|
||||
|
||||
speed = env.params.body["speed"]?.try &.as(String).to_f?
|
||||
speed ||= 1.0
|
||||
speed ||= DEFAULT_USER_PREFERENCES.speed
|
||||
|
||||
quality = env.params.body["quality"]?.try &.as(String)
|
||||
quality ||= "hd720"
|
||||
quality ||= DEFAULT_USER_PREFERENCES.quality
|
||||
|
||||
volume = env.params.body["volume"]?.try &.as(String).to_i?
|
||||
volume ||= 100
|
||||
volume ||= DEFAULT_USER_PREFERENCES.volume
|
||||
|
||||
comments_0 = env.params.body["comments_0"]?.try &.as(String) || "youtube"
|
||||
comments_1 = env.params.body["comments_1"]?.try &.as(String) || ""
|
||||
comments_0 = env.params.body["comments_0"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[0]
|
||||
comments_1 = env.params.body["comments_1"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.comments[1]
|
||||
comments = [comments_0, comments_1]
|
||||
|
||||
captions_0 = env.params.body["captions_0"]?.try &.as(String) || ""
|
||||
captions_1 = env.params.body["captions_1"]?.try &.as(String) || ""
|
||||
captions_2 = env.params.body["captions_2"]?.try &.as(String) || ""
|
||||
captions_0 = env.params.body["captions_0"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[0]
|
||||
captions_1 = env.params.body["captions_1"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[1]
|
||||
captions_2 = env.params.body["captions_2"]?.try &.as(String) || DEFAULT_USER_PREFERENCES.captions[2]
|
||||
captions = [captions_0, captions_1, captions_2]
|
||||
|
||||
related_videos = env.params.body["related_videos"]?.try &.as(String)
|
||||
@ -1142,7 +1154,7 @@ post "/preferences" do |env|
|
||||
redirect_feed = redirect_feed == "on"
|
||||
|
||||
locale = env.params.body["locale"]?.try &.as(String)
|
||||
locale ||= "en-US"
|
||||
locale ||= DEFAULT_USER_PREFERENCES.locale
|
||||
|
||||
dark_mode = env.params.body["dark_mode"]?.try &.as(String)
|
||||
dark_mode ||= "off"
|
||||
@ -1153,10 +1165,10 @@ post "/preferences" do |env|
|
||||
thin_mode = thin_mode == "on"
|
||||
|
||||
max_results = env.params.body["max_results"]?.try &.as(String).to_i?
|
||||
max_results ||= 40
|
||||
max_results ||= DEFAULT_USER_PREFERENCES.max_results
|
||||
|
||||
sort = env.params.body["sort"]?.try &.as(String)
|
||||
sort ||= "published"
|
||||
sort ||= DEFAULT_USER_PREFERENCES.sort
|
||||
|
||||
latest_only = env.params.body["latest_only"]?.try &.as(String)
|
||||
latest_only ||= "off"
|
||||
@ -1367,7 +1379,7 @@ get "/subscription_manager" do |env|
|
||||
subscriptions.sort_by! { |channel| channel.author.downcase }
|
||||
|
||||
if action_takeout
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"]?)
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
|
||||
if format == "json"
|
||||
env.response.content_type = "application/json"
|
||||
@ -1915,7 +1927,7 @@ get "/feed/channel/:ucid" do |env|
|
||||
videos, count = get_60_videos(ucid, page, auto_generated)
|
||||
videos.select! { |video| !video.paid }
|
||||
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"]?)
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
path = env.request.path
|
||||
|
||||
feed = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||
@ -2037,7 +2049,7 @@ get "/feed/private" do |env|
|
||||
videos = videos[0..max_results]
|
||||
end
|
||||
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"]?)
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
path = env.request.path
|
||||
query = env.request.query.not_nil!
|
||||
|
||||
@ -2084,7 +2096,7 @@ get "/feed/playlist/:plid" do |env|
|
||||
|
||||
plid = env.params.url["plid"]
|
||||
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"]?)
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
path = env.request.path
|
||||
|
||||
client = make_client(YT_URL)
|
||||
@ -2134,6 +2146,16 @@ get "/c/:user" do |env|
|
||||
env.redirect anchor["href"]
|
||||
end
|
||||
|
||||
# Legacy endpoint for /user/:username
|
||||
get "/profile" do |env|
|
||||
user = env.params.query["user"]?
|
||||
if !user
|
||||
env.redirect "/"
|
||||
else
|
||||
env.redirect "/user/#{user}"
|
||||
end
|
||||
end
|
||||
|
||||
get "/user/:user" do |env|
|
||||
user = env.params.url["user"]
|
||||
env.redirect "/channel/#{user}"
|
||||
@ -2170,7 +2192,7 @@ get "/channel/:ucid" do |env|
|
||||
end
|
||||
|
||||
if !auto_generated
|
||||
if author.includes? " "
|
||||
if author.includes?(" ") || author.includes?("-")
|
||||
env.set "search", "channel:#{ucid} "
|
||||
else
|
||||
env.set "search", "channel:#{author.downcase} "
|
||||
@ -2240,7 +2262,11 @@ get "/api/v1/captions/:id" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
next response
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
next JSON.parse(response).to_pretty_json
|
||||
else
|
||||
next response
|
||||
end
|
||||
end
|
||||
|
||||
env.response.content_type = "text/vtt"
|
||||
@ -2346,13 +2372,24 @@ get "/api/v1/comments/:id" do |env|
|
||||
if format == "json"
|
||||
reddit_thread = JSON.parse(reddit_thread.to_json).as_h
|
||||
reddit_thread["comments"] = JSON.parse(comments.to_json)
|
||||
next reddit_thread.to_json
|
||||
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
next reddit_thread.to_pretty_json
|
||||
else
|
||||
next reddit_thread.to_json
|
||||
end
|
||||
else
|
||||
next {
|
||||
response = {
|
||||
"title" => reddit_thread.title,
|
||||
"permalink" => reddit_thread.permalink,
|
||||
"contentHtml" => content_html,
|
||||
}.to_json
|
||||
}
|
||||
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
next response.to_pretty_json
|
||||
else
|
||||
next response.to_json
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -2363,6 +2400,9 @@ get "/api/v1/insights/:id" do |env|
|
||||
id = env.params.url["id"]
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
error_message = {"error" => "YouTube has removed publicly-available analytics."}.to_json
|
||||
halt env, status_code: 503, response: error_message
|
||||
|
||||
client = make_client(YT_URL)
|
||||
headers = HTTP::Headers.new
|
||||
html = client.get("/watch?v=#{id}&gl=US&hl=en&disable_polymer=1")
|
||||
@ -2429,14 +2469,20 @@ get "/api/v1/insights/:id" do |env|
|
||||
avg_view_duration_seconds = html_content.xpath_node(%q(//div[@id="stats-chart-tab-watch-time"]/span/span[2])).not_nil!.content
|
||||
avg_view_duration_seconds = decode_length_seconds(avg_view_duration_seconds)
|
||||
|
||||
{
|
||||
response = {
|
||||
"viewCount" => view_count,
|
||||
"timeWatchedText" => time_watched,
|
||||
"subscriptionsDriven" => subscriptions_driven,
|
||||
"shares" => shares,
|
||||
"avgViewDurationSeconds" => avg_view_duration_seconds,
|
||||
"graphData" => graph_data,
|
||||
}.to_json
|
||||
}
|
||||
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
next response.to_pretty_json
|
||||
else
|
||||
next response.to_json
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v1/videos/:id" do |env|
|
||||
@ -2520,12 +2566,13 @@ get "/api/v1/videos/:id" do |env|
|
||||
json.field "isListed", video.info["is_listed"] == "1"
|
||||
end
|
||||
|
||||
if video.info["hlsvp"]?
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"]?)
|
||||
if video.player_response["streamingData"]?.try &.["hlsManifestUrl"]?
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
|
||||
host_params = env.request.query_params
|
||||
host_params.delete_all("v")
|
||||
|
||||
hlsvp = video.info["hlsvp"]
|
||||
hlsvp = video.player_response["streamingData"]["hlsManifestUrl"].as_s
|
||||
hlsvp = hlsvp.gsub("https://manifest.googlevideo.com", host_url)
|
||||
|
||||
json.field "hlsUrl", hlsvp
|
||||
@ -2641,12 +2688,18 @@ get "/api/v1/videos/:id" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
video_info
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(video_info).to_pretty_json
|
||||
else
|
||||
video_info
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v1/trending" do |env|
|
||||
locale = LOCALES[env.get("locale").as(String)]?
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
region = env.params.query["region"]?
|
||||
trending_type = env.params.query["type"]?
|
||||
|
||||
@ -2686,13 +2739,18 @@ get "/api/v1/trending" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
videos
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(videos).to_pretty_json
|
||||
else
|
||||
videos
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v1/popular" do |env|
|
||||
locale = LOCALES[env.get("locale").as(String)]?
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
videos = JSON.build do |json|
|
||||
json.array do
|
||||
popular_videos.each do |video|
|
||||
@ -2715,13 +2773,18 @@ get "/api/v1/popular" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
videos
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(videos).to_pretty_json
|
||||
else
|
||||
videos
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v1/top" do |env|
|
||||
locale = LOCALES[env.get("locale").as(String)]?
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
videos = JSON.build do |json|
|
||||
json.array do
|
||||
top_videos.each do |video|
|
||||
@ -2751,8 +2814,11 @@ get "/api/v1/top" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
env.response.content_type = "application/json"
|
||||
videos
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(videos).to_pretty_json
|
||||
else
|
||||
videos
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v1/channels/:ucid" do |env|
|
||||
@ -2949,7 +3015,11 @@ get "/api/v1/channels/:ucid" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
channel_info
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(channel_info).to_pretty_json
|
||||
else
|
||||
channel_info
|
||||
end
|
||||
end
|
||||
|
||||
["/api/v1/channels/:ucid/videos", "/api/v1/channels/videos/:ucid"].each do |route|
|
||||
@ -3014,7 +3084,11 @@ end
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(result).to_pretty_json
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -3115,7 +3189,11 @@ get "/api/v1/channels/search/:ucid" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
response
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(response).to_pretty_json
|
||||
else
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v1/search" do |env|
|
||||
@ -3240,7 +3318,11 @@ get "/api/v1/search" do |env|
|
||||
end
|
||||
end
|
||||
|
||||
response
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(response).to_pretty_json
|
||||
else
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v1/playlists/:plid" do |env|
|
||||
@ -3339,7 +3421,11 @@ get "/api/v1/playlists/:plid" do |env|
|
||||
}.to_json
|
||||
end
|
||||
|
||||
response
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(response).to_pretty_json
|
||||
else
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/v1/mixes/:rdid" do |env|
|
||||
@ -3413,7 +3499,11 @@ get "/api/v1/mixes/:rdid" do |env|
|
||||
}.to_json
|
||||
end
|
||||
|
||||
response
|
||||
if env.params.query["pretty"]? && env.params.query["pretty"] == "1"
|
||||
JSON.parse(response).to_pretty_json
|
||||
else
|
||||
response
|
||||
end
|
||||
end
|
||||
|
||||
get "/api/manifest/dash/id/videoplayback" do |env|
|
||||
@ -3536,7 +3626,8 @@ get "/api/manifest/hls_variant/*" do |env|
|
||||
env.response.content_type = "application/x-mpegURL"
|
||||
env.response.headers.add("Access-Control-Allow-Origin", "*")
|
||||
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
|
||||
manifest = manifest.body
|
||||
manifest.gsub("https://www.youtube.com", host_url)
|
||||
end
|
||||
@ -3549,7 +3640,7 @@ get "/api/manifest/hls_playlist/*" do |env|
|
||||
halt env, status_code: manifest.status_code
|
||||
end
|
||||
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, env.request.headers["Host"])
|
||||
host_url = make_host_url(Kemal.config.ssl || CONFIG.https_only, CONFIG.domain)
|
||||
|
||||
manifest = manifest.body.gsub("https://www.youtube.com", host_url)
|
||||
manifest = manifest.gsub(/https:\/\/r\d---.{11}\.c\.youtube\.com/, host_url)
|
||||
@ -3561,6 +3652,40 @@ get "/api/manifest/hls_playlist/*" do |env|
|
||||
manifest
|
||||
end
|
||||
|
||||
# YouTube /videoplayback links expire after 6 hours,
|
||||
# so we have a mechanism here to redirect to the latest version
|
||||
get "/latest_version" do |env|
|
||||
id = env.params.query["id"]?
|
||||
itag = env.params.query["itag"]?
|
||||
|
||||
local = env.params.query["local"]?
|
||||
local ||= "false"
|
||||
local = local == "true"
|
||||
|
||||
if !id || !itag
|
||||
halt env, status_code: 400
|
||||
end
|
||||
|
||||
video = get_video(id, PG_DB, proxies)
|
||||
|
||||
fmt_stream = video.fmt_stream(decrypt_function)
|
||||
adaptive_fmts = video.adaptive_fmts(decrypt_function)
|
||||
|
||||
urls = (fmt_stream + adaptive_fmts).select { |fmt| fmt["itag"] == itag }
|
||||
if urls.empty?
|
||||
halt env, status_code: 404
|
||||
elsif urls.size > 1
|
||||
halt env, status_code: 409
|
||||
end
|
||||
|
||||
url = urls[0]["url"]
|
||||
if local
|
||||
url = URI.parse(url).full_path.not_nil!
|
||||
end
|
||||
|
||||
env.redirect url
|
||||
end
|
||||
|
||||
options "/videoplayback" do |env|
|
||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
env.response.headers["Access-Control-Allow-Methods"] = "GET, OPTIONS"
|
||||
@ -3624,9 +3749,23 @@ get "/videoplayback" do |env|
|
||||
host = "https://r#{fvip}---#{mn}.googlevideo.com"
|
||||
url = "/videoplayback?#{query_params.to_s}"
|
||||
|
||||
headers = env.request.headers
|
||||
headers.delete("Host")
|
||||
headers.delete("Cookie")
|
||||
headers.delete("User-Agent")
|
||||
headers.delete("Referer")
|
||||
|
||||
region = query_params["region"]?
|
||||
client = make_client(URI.parse(host), proxies, region)
|
||||
response = client.head(url)
|
||||
|
||||
response = HTTP::Client::Response.new(403)
|
||||
loop do
|
||||
begin
|
||||
client = make_client(URI.parse(host), proxies, region)
|
||||
response = client.head(url, headers)
|
||||
break
|
||||
rescue ex
|
||||
end
|
||||
end
|
||||
|
||||
if response.headers["Location"]?
|
||||
url = URI.parse(response.headers["Location"])
|
||||
@ -3644,12 +3783,6 @@ get "/videoplayback" do |env|
|
||||
halt env, status_code: 403
|
||||
end
|
||||
|
||||
headers = env.request.headers
|
||||
headers.delete("Host")
|
||||
headers.delete("Cookie")
|
||||
headers.delete("User-Agent")
|
||||
headers.delete("Referer")
|
||||
|
||||
client = make_client(URI.parse(host), proxies, region)
|
||||
client.get(url, headers) do |response|
|
||||
env.response.status_code = response.status_code
|
||||
@ -3800,12 +3933,21 @@ error 404 do |env|
|
||||
halt env, status_code: 302
|
||||
end
|
||||
|
||||
error_message = "404 Page not found"
|
||||
templated "error"
|
||||
env.response.headers["Location"] = "/"
|
||||
halt env, status_code: 302
|
||||
end
|
||||
|
||||
error 500 do |env|
|
||||
error_message = "500 Server error"
|
||||
error_message = <<-END_HTML
|
||||
Looks like you've found a bug in Invidious. Feel free to open a new issue
|
||||
<a href="https://github.com/omarroth/invidious/issues/github.com/omarroth/invidious">
|
||||
here
|
||||
</a>
|
||||
or send an email to
|
||||
<a href="mailto:omarroth@protonmail.com">
|
||||
omarroth@protonmail.com
|
||||
</a>.
|
||||
END_HTML
|
||||
templated "error"
|
||||
end
|
||||
|
||||
@ -3835,6 +3977,8 @@ public_folder "assets"
|
||||
Kemal.config.powered_by_header = false
|
||||
add_handler FilteredCompressHandler.new
|
||||
add_handler DenyFrame.new
|
||||
add_handler APIHandler.new
|
||||
add_context_storage_type(User)
|
||||
|
||||
Kemal.config.logger = logger
|
||||
Kemal.run
|
||||
|
@ -202,50 +202,58 @@ def produce_channel_videos_url(ucid, page = 1, auto_generated = nil, sort_by = "
|
||||
timestamp = seed - (page - 1).months
|
||||
|
||||
page = "#{timestamp.to_unix}"
|
||||
switch = "\x36"
|
||||
switch = 0x36
|
||||
else
|
||||
page = "#{page}"
|
||||
switch = "\x00"
|
||||
switch = 0x00
|
||||
end
|
||||
|
||||
meta = "\x12\x06videos"
|
||||
meta += "\x30\x02"
|
||||
meta += "\x38\x01"
|
||||
meta += "\x60\x01"
|
||||
meta += "\x6a\x00"
|
||||
meta += "\xb8\x01\x00"
|
||||
meta += "\x20#{switch}"
|
||||
meta += "\x7a"
|
||||
meta += page.size.to_u8.unsafe_chr
|
||||
meta += page
|
||||
meta = IO::Memory.new
|
||||
meta.write(Bytes[0x12, 0x06])
|
||||
meta.print("videos")
|
||||
|
||||
meta.write(Bytes[0x30, 0x02])
|
||||
meta.write(Bytes[0x38, 0x01])
|
||||
meta.write(Bytes[0x60, 0x01])
|
||||
meta.write(Bytes[0x6a, 0x00])
|
||||
meta.write(Bytes[0xb8, 0x01, 0x00])
|
||||
|
||||
meta.write(Bytes[0x20, switch, 0x7a, page.size])
|
||||
meta.print(page)
|
||||
|
||||
case sort_by
|
||||
when "newest"
|
||||
# Empty tags can be omitted
|
||||
# meta += "\x18\x00"
|
||||
# meta.write(Bytes[0x18,0x00])
|
||||
when "popular"
|
||||
meta += "\x18\x01"
|
||||
meta.write(Bytes[0x18, 0x01])
|
||||
when "oldest"
|
||||
meta += "\x18\x02"
|
||||
meta.write(Bytes[0x18, 0x02])
|
||||
end
|
||||
|
||||
meta = Base64.urlsafe_encode(meta)
|
||||
meta.rewind
|
||||
meta = Base64.urlsafe_encode(meta.to_slice)
|
||||
meta = URI.escape(meta)
|
||||
|
||||
continuation = "\x12"
|
||||
continuation += ucid.size.to_u8.unsafe_chr
|
||||
continuation += ucid
|
||||
continuation += "\x1a"
|
||||
continuation += meta.size.to_u8.unsafe_chr
|
||||
continuation += meta
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x12, ucid.size])
|
||||
continuation.print(ucid)
|
||||
|
||||
continuation = continuation.size.to_u8.unsafe_chr + continuation
|
||||
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
||||
continuation.write(Bytes[0x1a, meta.size])
|
||||
continuation.print(meta)
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation)
|
||||
continuation = URI.escape(continuation)
|
||||
continuation.rewind
|
||||
continuation = continuation.gets_to_end
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}&gl=US&hl=en"
|
||||
wrapper = IO::Memory.new
|
||||
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
|
||||
wrapper.print(continuation)
|
||||
wrapper.rewind
|
||||
|
||||
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
|
||||
wrapper = URI.escape(wrapper)
|
||||
|
||||
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
|
||||
|
||||
return url
|
||||
end
|
||||
|
@ -67,7 +67,7 @@ def fetch_youtube_comments(id, continuation, proxies, format, locale)
|
||||
itct = body.match(/itct=(?<itct>[^"]+)"/).not_nil!["itct"]
|
||||
ctoken = body.match(/'COMMENTS_TOKEN': "(?<ctoken>[^"]+)"/)
|
||||
|
||||
if body.match(/<meta itemprop="regionsAllowed" content="">/)
|
||||
if body.match(/<meta itemprop="regionsAllowed" content="">/) && !body.match(/player-age-gate-content\">/)
|
||||
bypass_channel = Channel({String, HTTPClient, HTTP::Headers} | Nil).new
|
||||
|
||||
proxies.each do |proxy_region, list|
|
||||
@ -79,7 +79,7 @@ def fetch_youtube_comments(id, continuation, proxies, format, locale)
|
||||
proxy_headers["Cookie"] = response.cookies.add_request_headers(headers)["cookie"]
|
||||
proxy_html = response.body
|
||||
|
||||
if !proxy_html.match(/<meta itemprop="regionsAllowed" content="">/)
|
||||
if !proxy_html.match(/<meta itemprop="regionsAllowed" content="">/) && !proxy_html.match(/player-age-gate-content\">/)
|
||||
bypass_channel.send({proxy_html, proxy_client, proxy_headers})
|
||||
else
|
||||
bypass_channel.send(nil)
|
||||
@ -159,6 +159,8 @@ def fetch_youtube_comments(id, continuation, proxies, format, locale)
|
||||
json.field "commentCount", comment_count
|
||||
end
|
||||
|
||||
json.field "videoId", id
|
||||
|
||||
json.field "comments" do
|
||||
json.array do
|
||||
contents.as_a.each do |node|
|
||||
@ -209,7 +211,14 @@ def fetch_youtube_comments(id, continuation, proxies, format, locale)
|
||||
json.field "authorUrl", ""
|
||||
end
|
||||
|
||||
published = decode_date(node_comment["publishedTimeText"]["runs"][0]["text"].as_s.rchop(" (edited)"))
|
||||
published_text = node_comment["publishedTimeText"]["runs"][0]["text"].as_s
|
||||
published = decode_date(published_text.rchop(" (edited)"))
|
||||
|
||||
if published_text.includes?(" (edited)")
|
||||
json.field "isEdited", true
|
||||
else
|
||||
json.field "isEdited", false
|
||||
end
|
||||
|
||||
json.field "content", content
|
||||
json.field "contentHtml", content_html
|
||||
@ -217,6 +226,17 @@ def fetch_youtube_comments(id, continuation, proxies, format, locale)
|
||||
json.field "publishedText", translate(locale, "`x` ago", recode_date(published))
|
||||
json.field "likeCount", node_comment["likeCount"]
|
||||
json.field "commentId", node_comment["commentId"]
|
||||
json.field "authorIsChannelOwner", node_comment["authorIsChannelOwner"]
|
||||
|
||||
if node_comment["actionButtons"]["commentActionButtonsRenderer"]["creatorHeart"]?
|
||||
hearth_data = node_comment["actionButtons"]["commentActionButtonsRenderer"]["creatorHeart"]["creatorHeartRenderer"]["creatorThumbnail"]
|
||||
json.field "creatorHeart" do
|
||||
json.object do
|
||||
json.field "creatorThumbnail", hearth_data["thumbnails"][-1]["url"]
|
||||
json.field "creatorName", hearth_data["accessibility"]["accessibilityData"]["label"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if node_replies && !response["commentRepliesContinuation"]?
|
||||
reply_count = node_replies["moreText"]["simpleText"].as_s.delete("View all reply replies,")
|
||||
@ -227,7 +247,8 @@ def fetch_youtube_comments(id, continuation, proxies, format, locale)
|
||||
reply_count ||= 1
|
||||
end
|
||||
|
||||
continuation = node_replies["continuations"].as_a[0]["nextContinuationData"]["continuation"].as_s
|
||||
continuation = node_replies["continuations"]?.try &.as_a[0]["nextContinuationData"]["continuation"].as_s
|
||||
continuation ||= ""
|
||||
|
||||
json.field "replies" do
|
||||
json.object do
|
||||
@ -270,7 +291,7 @@ end
|
||||
|
||||
def fetch_reddit_comments(id)
|
||||
client = make_client(REDDIT_URL)
|
||||
headers = HTTP::Headers{"User-Agent" => "web:invidio.us:v0.12.0 (by /u/omarroth)"}
|
||||
headers = HTTP::Headers{"User-Agent" => "web:invidio.us:v0.13.1 (by /u/omarroth)"}
|
||||
|
||||
query = "(url:3D#{id}%20OR%20url:#{id})%20(site:youtube.com%20OR%20site:youtu.be)"
|
||||
search_results = client.get("/search.json?q=#{query}", headers)
|
||||
@ -325,12 +346,31 @@ def template_youtube_comments(comments, locale)
|
||||
<div class="pure-u-20-24 pure-u-md-22-24">
|
||||
<p>
|
||||
<b>
|
||||
<a href="#{child["authorUrl"]}">#{child["author"]}</a>
|
||||
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{child["author"]}</a>
|
||||
</b>
|
||||
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
|
||||
#{translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64)))}
|
||||
<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)))} #{child["isEdited"] == true ? translate(locale, "(edited)") : ""}</span>
|
||||
|
|
||||
<a href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{translate(locale, "Youtube permalink of the comment")}">[YT]</a>
|
||||
|
|
||||
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
|
||||
END_HTML
|
||||
|
||||
if child["creatorHeart"]?
|
||||
creator_thumbnail = "/ggpht#{URI.parse(child["creatorHeart"]["creatorThumbnail"].as_s).full_path}"
|
||||
html += <<-END_HTML
|
||||
<span class="creator-heart-container" title="#{translate(locale, "`x` marked it with a ❤", child["creatorHeart"]["creatorName"].as_s)}">
|
||||
<div class="creator-heart">
|
||||
<img class="creator-heart-background-hearted" src="#{creator_thumbnail}"></img>
|
||||
<div class="creator-heart-small-hearted">
|
||||
<div class="creator-heart-small-container">🖤</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html += <<-END_HTML
|
||||
</p>
|
||||
#{replies_html}
|
||||
</div>
|
||||
@ -488,10 +528,14 @@ def content_to_comment_html(content)
|
||||
|
||||
text = %(<a href="#{url}">#{text}</a>)
|
||||
elsif watch_endpoint = run["navigationEndpoint"]["watchEndpoint"]?
|
||||
length_seconds = watch_endpoint["startTimeSeconds"].as_i
|
||||
length_seconds = watch_endpoint["startTimeSeconds"]?
|
||||
video_id = watch_endpoint["videoId"].as_s
|
||||
|
||||
text = %(<a href="javascript:void(0)" onclick="player.currentTime(#{length_seconds})">#{text}</a>)
|
||||
if length_seconds
|
||||
text = %(<a href="javascript:void(0)" onclick="player.currentTime(#{length_seconds})">#{text}</a>)
|
||||
else
|
||||
text = %(<a href="/watch?v=#{video_id}">#{text}</a>)
|
||||
end
|
||||
elsif url = run["navigationEndpoint"]["commandMetadata"]?.try &.["webCommandMetadata"]["url"].as_s
|
||||
text = %(<a href="#{url}">#{text}</a>)
|
||||
end
|
||||
|
@ -1,21 +1,21 @@
|
||||
class Config
|
||||
YAML.mapping({
|
||||
crawl_threads: Int32,
|
||||
channel_threads: Int32,
|
||||
feed_threads: Int32,
|
||||
video_threads: Int32,
|
||||
db: NamedTuple(
|
||||
user: String,
|
||||
crawl_threads: Int32, # Number of threads to use for finding new videos from YouTube (used to populate "top" page)
|
||||
channel_threads: Int32, # Number of threads to use for crawling videos from channels (for updating subscriptions)
|
||||
feed_threads: Int32, # Number of threads to use for updating feeds
|
||||
video_threads: Int32, # Number of threads to use for updating videos in cache (mostly non-functional)
|
||||
db: NamedTuple( # Database configuration
|
||||
user: String,
|
||||
password: String,
|
||||
host: String,
|
||||
port: Int32,
|
||||
dbname: String,
|
||||
),
|
||||
dl_api_key: String?,
|
||||
https_only: Bool?,
|
||||
hmac_key: String?,
|
||||
full_refresh: Bool,
|
||||
domain: String?,
|
||||
dl_api_key: String?, # DetectLanguage API Key (used to filter non-English results from "top" page), mostly non-functional
|
||||
https_only: Bool?, # Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
||||
hmac_key: String?, # HMAC signing key for CSRF tokens
|
||||
full_refresh: Bool, # Used for crawling channels: threads should check all videos uploaded by a channel
|
||||
domain: String, # Domain to be used for links to resources on the site where an absolute URL is required
|
||||
})
|
||||
end
|
||||
|
||||
@ -43,6 +43,18 @@ class FilteredCompressHandler < Kemal::Handler
|
||||
end
|
||||
end
|
||||
|
||||
class APIHandler < Kemal::Handler
|
||||
only ["/api/v1/*"]
|
||||
|
||||
def call(env)
|
||||
return call_next env unless only_match? env
|
||||
|
||||
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
call_next env
|
||||
end
|
||||
end
|
||||
|
||||
class DenyFrame < Kemal::Handler
|
||||
exclude ["/embed/*"]
|
||||
|
||||
|
35
src/invidious/helpers/logger.cr
Normal file
35
src/invidious/helpers/logger.cr
Normal file
@ -0,0 +1,35 @@
|
||||
require "logger"
|
||||
|
||||
class Invidious::LogHandler < Kemal::BaseLogHandler
|
||||
def initialize(@io : IO = STDOUT)
|
||||
end
|
||||
|
||||
def call(context : HTTP::Server::Context)
|
||||
time = Time.now
|
||||
call_next(context)
|
||||
elapsed_text = elapsed_text(Time.now - time)
|
||||
|
||||
@io << time << ' ' << context.response.status_code << ' ' << context.request.method << ' ' << context.request.resource << ' ' << elapsed_text << '\n'
|
||||
|
||||
if @io.is_a? File
|
||||
@io.flush
|
||||
end
|
||||
|
||||
context
|
||||
end
|
||||
|
||||
def write(message : String)
|
||||
@io << message
|
||||
|
||||
if @io.is_a? File
|
||||
@io.flush
|
||||
end
|
||||
end
|
||||
|
||||
private def elapsed_text(elapsed)
|
||||
millis = elapsed.total_milliseconds
|
||||
return "#{millis.round(2)}ms" if millis >= 1
|
||||
|
||||
"#{(millis * 1000).round(2)}µs"
|
||||
end
|
||||
end
|
@ -205,8 +205,6 @@ def make_host_url(ssl, host)
|
||||
scheme = "http://"
|
||||
end
|
||||
|
||||
host ||= "invidio.us"
|
||||
|
||||
return "#{scheme}#{host}"
|
||||
end
|
||||
|
||||
@ -284,7 +282,7 @@ def write_var_int(value : Int)
|
||||
end
|
||||
end
|
||||
|
||||
return bytes
|
||||
return Slice.new(bytes.to_unsafe, bytes.size)
|
||||
end
|
||||
|
||||
def sha256(text)
|
||||
|
@ -1,4 +1,4 @@
|
||||
def crawl_videos(db)
|
||||
def crawl_videos(db, logger)
|
||||
ids = Deque(String).new
|
||||
random = Random.new
|
||||
|
||||
@ -21,7 +21,7 @@ def crawl_videos(db)
|
||||
id = ids[0]
|
||||
video = get_video(id, db)
|
||||
rescue ex
|
||||
STDOUT << id << " : " << ex.message << "\n"
|
||||
logger.write("#{id} : #{ex.message}\n")
|
||||
next
|
||||
ensure
|
||||
ids.delete(id)
|
||||
@ -46,7 +46,7 @@ def crawl_videos(db)
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_channels(db, max_threads = 1, full_refresh = false)
|
||||
def refresh_channels(db, logger, max_threads = 1, full_refresh = false)
|
||||
max_channel = Channel(Int32).new
|
||||
|
||||
spawn do
|
||||
@ -73,7 +73,7 @@ def refresh_channels(db, max_threads = 1, full_refresh = false)
|
||||
|
||||
db.exec("UPDATE channels SET updated = $1, author = $2 WHERE id = $3", Time.now, channel.author, id)
|
||||
rescue ex
|
||||
STDOUT << id << " : " << ex.message << "\n"
|
||||
logger.write("#{id} : #{ex.message}\n")
|
||||
end
|
||||
|
||||
active_channel.send(true)
|
||||
@ -86,7 +86,7 @@ def refresh_channels(db, max_threads = 1, full_refresh = false)
|
||||
max_channel.send(max_threads)
|
||||
end
|
||||
|
||||
def refresh_videos(db)
|
||||
def refresh_videos(db, logger)
|
||||
loop do
|
||||
db.query("SELECT id FROM videos ORDER BY updated") do |rs|
|
||||
rs.each do
|
||||
@ -94,7 +94,7 @@ def refresh_videos(db)
|
||||
id = rs.read(String)
|
||||
video = get_video(id, db)
|
||||
rescue ex
|
||||
STDOUT << id << " : " << ex.message << "\n"
|
||||
logger.write("#{id} : #{ex.message}\n")
|
||||
next
|
||||
end
|
||||
end
|
||||
@ -104,7 +104,7 @@ def refresh_videos(db)
|
||||
end
|
||||
end
|
||||
|
||||
def refresh_feeds(db, max_threads = 1)
|
||||
def refresh_feeds(db, logger, max_threads = 1)
|
||||
max_channel = Channel(Int32).new
|
||||
|
||||
spawn do
|
||||
@ -129,7 +129,7 @@ def refresh_feeds(db, max_threads = 1)
|
||||
begin
|
||||
db.exec("REFRESH MATERIALIZED VIEW #{view_name}")
|
||||
rescue ex
|
||||
STDOUT << "REFRESH " << email << " : " << ex.message << "\n"
|
||||
logger.write("REFRESH #{email} : #{ex.message}\n")
|
||||
end
|
||||
|
||||
active_channel.send(true)
|
||||
|
@ -126,32 +126,37 @@ def produce_playlist_url(id, index)
|
||||
end
|
||||
ucid = "VL" + id
|
||||
|
||||
meta = [0x08_u8] + write_var_int(index)
|
||||
meta = Slice.new(meta.to_unsafe, meta.size)
|
||||
meta = Base64.urlsafe_encode(meta, false)
|
||||
meta = IO::Memory.new
|
||||
meta.write(Bytes[0x08])
|
||||
meta.write(write_var_int(index))
|
||||
|
||||
meta.rewind
|
||||
meta = Base64.urlsafe_encode(meta.to_slice, false)
|
||||
meta = "PT:#{meta}"
|
||||
|
||||
wrapped = "\x7a"
|
||||
wrapped += meta.bytes.size.unsafe_chr
|
||||
wrapped += meta
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x7a, meta.size])
|
||||
continuation.print(meta)
|
||||
|
||||
wrapped = Base64.urlsafe_encode(wrapped)
|
||||
meta = URI.escape(wrapped)
|
||||
continuation.rewind
|
||||
meta = Base64.urlsafe_encode(continuation.to_slice)
|
||||
meta = URI.escape(meta)
|
||||
|
||||
continuation = "\x12"
|
||||
continuation += ucid.size.unsafe_chr
|
||||
continuation += ucid
|
||||
continuation += "\x1a"
|
||||
continuation += meta.bytes.size.unsafe_chr
|
||||
continuation += meta
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x12, ucid.size])
|
||||
continuation.print(ucid)
|
||||
continuation.write(Bytes[0x1a, meta.size])
|
||||
continuation.print(meta)
|
||||
|
||||
continuation = continuation.size.to_u8.unsafe_chr + continuation
|
||||
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
||||
wrapper = IO::Memory.new
|
||||
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
|
||||
wrapper.print(continuation)
|
||||
wrapper.rewind
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation)
|
||||
continuation = URI.escape(continuation)
|
||||
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
|
||||
wrapper = URI.escape(wrapper)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}"
|
||||
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
|
||||
|
||||
return url
|
||||
end
|
||||
|
@ -203,36 +203,45 @@ end
|
||||
def produce_channel_search_url(ucid, query, page)
|
||||
page = "#{page}"
|
||||
|
||||
meta = "\x12\x06search"
|
||||
meta += "\x30\x02"
|
||||
meta += "\x38\x01"
|
||||
meta += "\x60\x01"
|
||||
meta += "\x6a\x00"
|
||||
meta += "\xb8\x01\x00"
|
||||
meta += "\x7a"
|
||||
meta += page.size.unsafe_chr
|
||||
meta += page
|
||||
meta = IO::Memory.new
|
||||
meta.write(Bytes[0x12, 0x06])
|
||||
meta.print("search")
|
||||
|
||||
meta = Base64.urlsafe_encode(meta)
|
||||
meta.write(Bytes[0x30, 0x02])
|
||||
meta.write(Bytes[0x38, 0x01])
|
||||
meta.write(Bytes[0x60, 0x01])
|
||||
meta.write(Bytes[0x6a, 0x00])
|
||||
meta.write(Bytes[0xb8, 0x01, 0x00])
|
||||
|
||||
meta.write(Bytes[0x7a, page.size])
|
||||
meta.print(page)
|
||||
|
||||
meta.rewind
|
||||
meta = Base64.urlsafe_encode(meta.to_slice)
|
||||
meta = URI.escape(meta)
|
||||
|
||||
continuation = "\x12"
|
||||
continuation += ucid.size.unsafe_chr
|
||||
continuation += ucid
|
||||
continuation += "\x1a"
|
||||
continuation += meta.size.unsafe_chr
|
||||
continuation += meta
|
||||
continuation += "\x5a"
|
||||
continuation += query.size.unsafe_chr
|
||||
continuation += query
|
||||
continuation = IO::Memory.new
|
||||
continuation.write(Bytes[0x12, ucid.size])
|
||||
continuation.print(ucid)
|
||||
|
||||
continuation = continuation.size.unsafe_chr + continuation
|
||||
continuation = "\xe2\xa9\x85\xb2\x02" + continuation
|
||||
continuation.write(Bytes[0x1a, meta.size])
|
||||
continuation.print(meta)
|
||||
|
||||
continuation = Base64.urlsafe_encode(continuation)
|
||||
continuation = URI.escape(continuation)
|
||||
continuation.write(Bytes[0x5a, query.size])
|
||||
continuation.print(query)
|
||||
|
||||
url = "/browse_ajax?continuation=#{continuation}"
|
||||
continuation.rewind
|
||||
continuation = continuation.gets_to_end
|
||||
|
||||
wrapper = IO::Memory.new
|
||||
wrapper.write(Bytes[0xe2, 0xa9, 0x85, 0xb2, 0x02, continuation.size])
|
||||
wrapper.print(continuation)
|
||||
wrapper.rewind
|
||||
|
||||
wrapper = Base64.urlsafe_encode(wrapper.to_slice)
|
||||
wrapper = URI.escape(wrapper)
|
||||
|
||||
url = "/browse_ajax?continuation=#{wrapper}&gl=US&hl=en"
|
||||
|
||||
return url
|
||||
end
|
||||
|
@ -79,36 +79,36 @@ class Preferences
|
||||
autoplay: Bool,
|
||||
continue: {
|
||||
type: Bool,
|
||||
default: false,
|
||||
default: DEFAULT_USER_PREFERENCES.continue,
|
||||
},
|
||||
listen: {
|
||||
type: Bool,
|
||||
default: false,
|
||||
default: DEFAULT_USER_PREFERENCES.listen,
|
||||
},
|
||||
speed: Float32,
|
||||
quality: String,
|
||||
volume: Int32,
|
||||
comments: {
|
||||
type: Array(String),
|
||||
default: ["youtube", ""],
|
||||
default: DEFAULT_USER_PREFERENCES.comments,
|
||||
converter: StringToArray,
|
||||
},
|
||||
captions: {
|
||||
type: Array(String),
|
||||
default: ["", "", ""],
|
||||
default: DEFAULT_USER_PREFERENCES.captions,
|
||||
},
|
||||
redirect_feed: {
|
||||
type: Bool,
|
||||
default: false,
|
||||
default: DEFAULT_USER_PREFERENCES.redirect_feed,
|
||||
},
|
||||
related_videos: {
|
||||
type: Bool,
|
||||
default: true,
|
||||
default: DEFAULT_USER_PREFERENCES.related_videos,
|
||||
},
|
||||
dark_mode: Bool,
|
||||
thin_mode: {
|
||||
type: Bool,
|
||||
default: false,
|
||||
default: DEFAULT_USER_PREFERENCES.thin_mode,
|
||||
},
|
||||
max_results: Int32,
|
||||
sort: String,
|
||||
@ -116,11 +116,11 @@ class Preferences
|
||||
unseen_only: Bool,
|
||||
notifications_only: {
|
||||
type: Bool,
|
||||
default: false,
|
||||
default: DEFAULT_USER_PREFERENCES.notifications_only,
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: "en-US",
|
||||
default: DEFAULT_USER_PREFERENCES.locale,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
@ -137,7 +137,7 @@ BYPASS_REGIONS = {
|
||||
}
|
||||
|
||||
VIDEO_THUMBNAILS = {
|
||||
{name: "maxres", host: "invidio.us", url: "maxres", height: 720, width: 1280},
|
||||
{name: "maxres", host: "#{CONFIG.domain}", url: "maxres", height: 720, width: 1280},
|
||||
{name: "maxresdefault", host: "i.ytimg.com", url: "maxresdefault", height: 720, width: 1280},
|
||||
{name: "sddefault", host: "i.ytimg.com", url: "sddefault", height: 480, width: 640},
|
||||
{name: "high", host: "i.ytimg.com", url: "hqdefault", height: 360, width: 480},
|
||||
@ -633,6 +633,10 @@ def fetch_video(id, proxies, region)
|
||||
end
|
||||
end
|
||||
|
||||
if info["errorcode"]?.try &.== "2"
|
||||
raise "Video unavailable."
|
||||
end
|
||||
|
||||
title = info["title"]
|
||||
author = info["author"]
|
||||
ucid = info["ucid"]
|
||||
@ -649,6 +653,10 @@ def fetch_video(id, proxies, region)
|
||||
dislikes = dislikes.try &.content.delete(",").try &.to_i?
|
||||
dislikes ||= 0
|
||||
|
||||
avg_rating = (likes.to_f/(likes.to_f + dislikes.to_f) * 4 + 1)
|
||||
avg_rating = avg_rating.nan? ? 0.0 : avg_rating
|
||||
info["avg_rating"] = "#{avg_rating}"
|
||||
|
||||
description = html.xpath_node(%q(//p[@id="eow-description"]))
|
||||
description = description ? description.to_xml : ""
|
||||
|
||||
@ -666,13 +674,20 @@ def fetch_video(id, proxies, region)
|
||||
genre = html.xpath_node(%q(//meta[@itemprop="genre"])).try &.["content"]
|
||||
genre ||= ""
|
||||
|
||||
genre_url = html.xpath_node(%(//a[text()="#{genre}"])).try &.["href"]
|
||||
genre_url = html.xpath_node(%(//ul[contains(@class, "watch-info-tag-list")]/li/a[text()="#{genre}"])).try &.["href"]
|
||||
|
||||
# Sometimes YouTube tries to link to invalid/missing channels, so we fix that here
|
||||
case genre
|
||||
when "Education"
|
||||
genre_url = "/channel/UCdxpofrI-dO6oYfsqHDHphw"
|
||||
when "Gaming"
|
||||
genre_url = "/channel/UCOpNcN46UbXVtpKMrmU4Abg"
|
||||
when "Movies"
|
||||
genre_url = "/channel/UClgRkhTL3_hImCAmdLfDE4g"
|
||||
when "Education"
|
||||
# Education channel is linked but does not exist
|
||||
genre_url = "/channel/UC3yA8nDwraeOfnYfBWun83g"
|
||||
when "Nonprofits & Activism"
|
||||
genre_url = "/channel/UCfFyYRYslvuhwMDnx6KjUvw"
|
||||
when "Trailers"
|
||||
genre_url = "/channel/UClgRkhTL3_hImCAmdLfDE4g"
|
||||
end
|
||||
genre_url ||= ""
|
||||
|
||||
@ -710,6 +725,7 @@ end
|
||||
def process_video_params(query, preferences)
|
||||
autoplay = query["autoplay"]?.try &.to_i?
|
||||
continue = query["continue"]?.try &.to_i?
|
||||
related_videos = query["related_videos"]?
|
||||
listen = query["listen"]? && (query["listen"] == "true" || query["listen"] == "1").to_unsafe
|
||||
preferred_captions = query["subtitles"]?.try &.split(",").map { |a| a.downcase }
|
||||
quality = query["quality"]?
|
||||
@ -722,6 +738,7 @@ def process_video_params(query, preferences)
|
||||
# region ||= preferences.region
|
||||
autoplay ||= preferences.autoplay.to_unsafe
|
||||
continue ||= preferences.continue.to_unsafe
|
||||
related_videos ||= preferences.related_videos.to_unsafe
|
||||
listen ||= preferences.listen.to_unsafe
|
||||
preferred_captions ||= preferences.captions
|
||||
quality ||= preferences.quality
|
||||
@ -730,17 +747,19 @@ def process_video_params(query, preferences)
|
||||
volume ||= preferences.volume
|
||||
end
|
||||
|
||||
autoplay ||= 0
|
||||
continue ||= 0
|
||||
listen ||= 0
|
||||
preferred_captions ||= [] of String
|
||||
quality ||= "hd720"
|
||||
speed ||= 1
|
||||
video_loop ||= 0
|
||||
volume ||= 100
|
||||
autoplay ||= DEFAULT_USER_PREFERENCES.autoplay.to_unsafe
|
||||
continue ||= DEFAULT_USER_PREFERENCES.continue.to_unsafe
|
||||
related_videos ||= DEFAULT_USER_PREFERENCES.related_videos.to_unsafe
|
||||
listen ||= DEFAULT_USER_PREFERENCES.listen.to_unsafe
|
||||
preferred_captions ||= DEFAULT_USER_PREFERENCES.captions
|
||||
quality ||= DEFAULT_USER_PREFERENCES.quality
|
||||
speed ||= DEFAULT_USER_PREFERENCES.speed
|
||||
video_loop ||= DEFAULT_USER_PREFERENCES.video_loop.to_unsafe
|
||||
volume ||= DEFAULT_USER_PREFERENCES.volume
|
||||
|
||||
autoplay = autoplay == 1
|
||||
continue = continue == 1
|
||||
related_videos = related_videos == 1
|
||||
listen = listen == 1
|
||||
video_loop = video_loop == 1
|
||||
|
||||
@ -778,6 +797,7 @@ def process_video_params(query, preferences)
|
||||
quality: quality,
|
||||
raw: raw,
|
||||
region: region,
|
||||
related_videos: related_videos,
|
||||
speed: speed,
|
||||
video_end: video_end,
|
||||
video_loop: video_loop,
|
||||
|
@ -14,30 +14,8 @@
|
||||
</div>
|
||||
|
||||
<div class="h-box">
|
||||
<% if user %>
|
||||
<% if subscriptions.includes? ucid %>
|
||||
<p>
|
||||
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary"
|
||||
href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Unsubscribe") %> | <%= number_to_short_text(sub_count) %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% else %>
|
||||
<p>
|
||||
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary"
|
||||
href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Subscribe") %> | <%= number_to_short_text(sub_count) %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p>
|
||||
<a id="subscribe" class="pure-button pure-button-primary"
|
||||
href="/login?referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Login to subscribe to `x`", author) %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% end %>
|
||||
<% sub_count_text = number_to_short_text(sub_count) %>
|
||||
<%= rendered "components/subscribe_widget" %>
|
||||
</div>
|
||||
|
||||
<div class="pure-g h-box">
|
||||
@ -94,43 +72,6 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById("subscribe")["href"] = "javascript:void(0)"
|
||||
|
||||
function subscribe() {
|
||||
var url = "/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
subscribe_button.onclick = unsubscribe;
|
||||
subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe") %> | <%= number_to_short_text(sub_count) %></b>'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unsubscribe() {
|
||||
var url = "/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
subscribe_button.onclick = subscribe;
|
||||
subscribe_button.innerHTML = '<b><%= translate(locale, "Subscribe") %> | <%= number_to_short_text(sub_count) %></b>'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<% sub_count_text = number_to_short_text(sub_count) %>
|
||||
<%= rendered "components/subscribe_widget_script" %>
|
||||
</script>
|
||||
|
@ -11,7 +11,7 @@
|
||||
<% else %>
|
||||
<% if params[:listen] %>
|
||||
<% audio_streams.each_with_index do |fmt, i| %>
|
||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>">
|
||||
<source src="/latest_version?id=<%= video.id %>&itag=<%= fmt["itag"] %>" type='<%= fmt["type"] %>' label="<%= fmt["bitrate"] %>k" selected="<%= i == 0 ? true : false %>">
|
||||
<% end %>
|
||||
<% else %>
|
||||
<% if params[:quality] == "dash" %>
|
||||
@ -19,9 +19,9 @@
|
||||
<% end %>
|
||||
<% fmt_stream.each_with_index do |fmt, i| %>
|
||||
<% if params[:quality] %>
|
||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= params[:quality] == fmt["label"].split(" - ")[0] %>">
|
||||
<source src="/latest_version?id=<%= video.id %>&itag=<%= fmt["itag"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= params[:quality] == fmt["label"].split(" - ")[0] %>">
|
||||
<% else %>
|
||||
<source src="<%= fmt["url"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= i == 0 ? true : false %>">
|
||||
<source src="/latest_version?id=<%= video.id %>&itag=<%= fmt["itag"] %>" type='<%= fmt["type"] %>' label="<%= fmt["label"] %>" selected="<%= i == 0 ? true : false %>">
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@ -114,6 +114,29 @@ var player = videojs("player", options, function() {
|
||||
});
|
||||
});
|
||||
|
||||
player.on('error', function(event) {
|
||||
if (player.error().code === 2 || player.error().code === 4) {
|
||||
setInterval(setTimeout(function (event) {
|
||||
console.log("An error occured in the player, reloading...");
|
||||
|
||||
var currentTime = player.currentTime();
|
||||
var playbackRate = player.playbackRate();
|
||||
var paused = player.paused();
|
||||
|
||||
player.load();
|
||||
if (currentTime > 0.5) {
|
||||
currentTime -= 0.5;
|
||||
}
|
||||
player.currentTime(currentTime);
|
||||
player.playbackRate(playbackRate);
|
||||
|
||||
if (!paused) {
|
||||
player.play();
|
||||
}
|
||||
}, 5000), 5000);
|
||||
}
|
||||
});
|
||||
|
||||
player.share(shareOptions);
|
||||
|
||||
<% if params[:video_start] > 0 || params[:video_end] > 0 %>
|
||||
|
@ -8,7 +8,7 @@
|
||||
<script src="/js/videojs-markers.min.js"></script>
|
||||
<script src="/js/videojs-share.min.js"></script>
|
||||
<script src="/js/videojs-http-streaming.min.js"></script>
|
||||
<% if env.get?("user") && env.get("user").as(User).preferences.quality == "dash" %>
|
||||
<% if params[:quality] == "dash" %>
|
||||
<script src="/js/dash.mediaplayer.min.js"></script>
|
||||
<script src="/js/videojs-dash.min.js"></script>
|
||||
<script src="/js/videojs-contrib-quality-levels.min.js"></script>
|
||||
|
24
src/invidious/views/components/subscribe_widget.ecr
Normal file
24
src/invidious/views/components/subscribe_widget.ecr
Normal file
@ -0,0 +1,24 @@
|
||||
<% if user %>
|
||||
<% if subscriptions.includes? ucid %>
|
||||
<p>
|
||||
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary"
|
||||
href="/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% else %>
|
||||
<p>
|
||||
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary"
|
||||
href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p>
|
||||
<a id="subscribe" class="pure-button pure-button-primary"
|
||||
href="/login?referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Login to subscribe to `x`", author) %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% end %>
|
74
src/invidious/views/components/subscribe_widget_script.ecr
Normal file
74
src/invidious/views/components/subscribe_widget_script.ecr
Normal file
@ -0,0 +1,74 @@
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
if (subscribe_button.getAttribute('onclick')) {
|
||||
subscribe_button["href"] = "javascript:void(0)";
|
||||
}
|
||||
|
||||
function subscribe(timeouts = 0) {
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
|
||||
if (timeouts > 10) {
|
||||
console.log("Failed to subscribe.");
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "/subscription_ajax?action_create_subscription_to_channel=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
var fallback = subscribe_button.innerHTML;
|
||||
subscribe_button.onclick = unsubscribe;
|
||||
subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe") %> | <%= sub_count_text %></b>'
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status != 200) {
|
||||
subscribe_button.onclick = subscribe;
|
||||
subscribe_button.innerHTML = fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
console.log("Subscribing timed out.");
|
||||
|
||||
subscribe(timeouts + 1);
|
||||
};
|
||||
}
|
||||
|
||||
function unsubscribe(timeouts = 0) {
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
|
||||
if (timeouts > 10) {
|
||||
console.log("Failed to subscribe");
|
||||
return;
|
||||
}
|
||||
|
||||
var url = "/subscription_ajax?action_remove_subscriptions=1&c=<%= ucid %>&referer=<%= env.get("current_page") %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
var fallback = subscribe_button.innerHTML;
|
||||
subscribe_button.onclick = subscribe;
|
||||
subscribe_button.innerHTML = '<b><%= translate(locale, "Subscribe") %> | <%= sub_count_text %></b>'
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status != 200) {
|
||||
subscribe_button.onclick = unsubscribe;
|
||||
subscribe_button.innerHTML = fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
console.log("Unsubscribing timed out.");
|
||||
|
||||
unsubscribe(timeouts + 1);
|
||||
};
|
||||
}
|
@ -147,7 +147,11 @@ function update_value(element) {
|
||||
</div>
|
||||
|
||||
<div class="pure-control-group">
|
||||
<label for="latest_only">Only show latest <% if user.preferences.unseen_only %>unwatched<% end %> video from channel: </label>
|
||||
<% if user.preferences.unseen_only %>
|
||||
<label for="latest_only"><%= translate(locale, "Only show latest unwatched video from channel: ") %></label>
|
||||
<% else %>
|
||||
<label for="latest_only"><%= translate(locale, "Only show latest video from channel: ") %></label>
|
||||
<% end %>
|
||||
<input name="latest_only" id="latest_only" type="checkbox" <% if user.preferences.latest_only %>checked<% end %>>
|
||||
</div>
|
||||
|
||||
|
@ -53,6 +53,34 @@
|
||||
<div class="pure-u-1 pure-u-md-1-5">
|
||||
<div class="h-box">
|
||||
<p><a href="https://www.youtube.com/watch?v=<%= video.id %>"><%= translate(locale, "Watch video on Youtube") %></a></p>
|
||||
|
||||
<form class="pure-form pure-form-stacked">
|
||||
<div class="pure-control-group">
|
||||
<label for="download_widget"><%= translate(locale, "Download as: ") %></label>
|
||||
<select style="width:100%" name="download_widget" id="download_widget">
|
||||
<% video_streams.each do |option| %>
|
||||
<option data-url="/latest_version?id=<%= video.id %>&itag=<%= option["itag"] %>&local=true"><%= option["quality_label"] %> - <%= option["type"].split(";")[0] %> @ <%= option["fps"] %>fps - video only</option>
|
||||
<% end %>
|
||||
<% audio_streams.each do |option| %>
|
||||
<option data-url="/latest_version?id=<%= video.id %>&itag=<%= option["itag"] %>&local=true"><%= option["type"].split(";")[0] %> @ <%= option["bitrate"] %>k - audio only</option>
|
||||
<% end %>
|
||||
<% fmt_stream.each do |option| %>
|
||||
<option data-url="/latest_version?id=<%= video.id %>&itag=<%= option["itag"] %>&local=true"><%= itag_to_metadata?(option["itag"]).try &.["height"]? || "~240" %>p - <%= option["type"].split(";")[0] %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="progress-container" style="width:100%; display:none">
|
||||
<div id="download-progress">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" data-title="<%= video.title.dump_unquoted %>-<%= video.id %>.mp4" onclick="download_video(this)"
|
||||
class="pure-button pure-button-primary">
|
||||
<b><%= translate(locale, "Download") %></b>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p><i class="icon ion-ios-eye"></i> <%= number_with_separator(video.views) %></p>
|
||||
<p><i class="icon ion-ios-thumbs-up"></i> <%= number_with_separator(video.likes) %></p>
|
||||
<p><i class="icon ion-ios-thumbs-down"></i> <%= number_with_separator(video.dislikes) %></p>
|
||||
@ -82,37 +110,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 <% if preferences && !preferences.related_videos && !plid %>pure-u-md-4-5<% else %>pure-u-md-3-5<% end %>">
|
||||
<div class="pure-u-1 <% if params[:related_videos] || plid %>pure-u-md-3-5<% else %>pure-u-md-4-5<% end %>">
|
||||
<div class="h-box">
|
||||
<p>
|
||||
<a href="/channel/<%= video.ucid %>">
|
||||
<h3><%= video.author %></h3>
|
||||
</a>
|
||||
</p>
|
||||
<% if user %>
|
||||
<% if subscriptions.includes? video.ucid %>
|
||||
<p>
|
||||
<a id="subscribe" onclick="unsubscribe()" class="pure-button pure-button-primary"
|
||||
href="/subscription_ajax?action_remove_subscriptions=1&c=<%= video.ucid %>&referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Unsubscribe") %> | <%= video.sub_count_text %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% else %>
|
||||
<p>
|
||||
<a id="subscribe" onclick="subscribe()" class="pure-button pure-button-primary"
|
||||
href="/subscription_ajax?action_create_subscription_to_channel=1&c=<%= video.ucid %>&referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Subscribe") %> | <%= video.sub_count_text %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p>
|
||||
<a id="subscribe" class="pure-button pure-button-primary"
|
||||
href="/login?referer=<%= env.get("current_page") %>">
|
||||
<b><%= translate(locale, "Login to subscribe to `x`", video.author) %></b>
|
||||
</a>
|
||||
</p>
|
||||
<% end %>
|
||||
<% ucid = video.ucid %>
|
||||
<% author = video.author %>
|
||||
<% sub_count_text = video.sub_count_text %>
|
||||
<%= rendered "components/subscribe_widget" %>
|
||||
<p>
|
||||
<b><%= translate(locale, "Shared `x`", video.published.to_s("%B %-d, %Y")) %></b>
|
||||
</p>
|
||||
@ -133,14 +141,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% if preferences && preferences.related_videos || plid %>
|
||||
<% if params[:related_videos] || plid %>
|
||||
<div class="pure-u-1 pure-u-md-1-5">
|
||||
<% if plid %>
|
||||
<div id="playlist" class="h-box">
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if !preferences || preferences && preferences.related_videos %>
|
||||
<% if params[:related_videos] %>
|
||||
<div class="h-box">
|
||||
|
||||
<% if !rvs.empty? %>
|
||||
@ -179,7 +187,7 @@
|
||||
<script>
|
||||
<% if !rvs.empty? && !plid && params[:continue] %>
|
||||
player.on('ended', function() {
|
||||
window.location.replace("/watch?v="
|
||||
location.assign("/watch?v="
|
||||
+ "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>"
|
||||
+ "&continue=1"
|
||||
<% if params[:listen] %>
|
||||
@ -198,7 +206,7 @@ player.on('ended', function() {
|
||||
function continue_autoplay(target) {
|
||||
if (target.checked) {
|
||||
player.on('ended', function() {
|
||||
window.location.replace("/watch?v="
|
||||
location.assign("/watch?v="
|
||||
+ "<%= rvs.select { |rv| rv["id"]? }[0]?.try &.["id"] %>"
|
||||
+ "&continue=1"
|
||||
<% if params[:listen] %>
|
||||
@ -224,52 +232,22 @@ function number_with_separator(val) {
|
||||
return val;
|
||||
}
|
||||
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
if (subscribe_button.getAttribute('onclick')) {
|
||||
subscribe_button["href"] = "javascript:void(0)";
|
||||
}
|
||||
<% ucid = video.ucid %>
|
||||
<% author = video.author %>
|
||||
<% sub_count_text = video.sub_count_text %>
|
||||
<%= rendered "components/subscribe_widget_script" %>
|
||||
|
||||
function subscribe() {
|
||||
var url = "/subscription_ajax?action_create_subscription_to_channel=1&c=<%= video.ucid %>&referer=<%= env.get("current_page") %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
subscribe_button.onclick = unsubscribe;
|
||||
subscribe_button.innerHTML = '<b><%= translate(locale, "Unsubscribe") %> | <%= video.sub_count_text %></b>'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unsubscribe() {
|
||||
var url = "/subscription_ajax?action_remove_subscriptions=1&c=<%= video.ucid %>&referer=<%= env.get("current_page") %>";
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "json";
|
||||
xhr.timeout = 20000;
|
||||
xhr.open("GET", url, true);
|
||||
xhr.send();
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState == 4) {
|
||||
if (xhr.status == 200) {
|
||||
subscribe_button = document.getElementById("subscribe");
|
||||
subscribe_button.onclick = subscribe;
|
||||
subscribe_button.innerHTML = '<b><%= translate(locale, "Subscribe") %> | <%= video.sub_count_text %></b>'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
<% if plid %>
|
||||
function get_playlist() {
|
||||
function get_playlist(timeouts = 0) {
|
||||
playlist = document.getElementById("playlist");
|
||||
|
||||
if (timeouts > 10) {
|
||||
console.log("Failed to pull playlist");
|
||||
playlist.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
playlist.innerHTML = ' \
|
||||
<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3> \
|
||||
<hr>'
|
||||
@ -295,7 +273,7 @@ function get_playlist() {
|
||||
|
||||
if (xhr.response.nextVideo) {
|
||||
player.on('ended', function() {
|
||||
window.location.replace("/watch?v="
|
||||
location.assign("/watch?v="
|
||||
+ xhr.response.nextVideo
|
||||
+ "&list=<%= plid %>"
|
||||
<% if params[:listen] %>
|
||||
@ -323,15 +301,22 @@ function get_playlist() {
|
||||
comments = document.getElementById("playlist");
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3><hr>';
|
||||
get_playlist();
|
||||
get_playlist(timeouts + 1);
|
||||
};
|
||||
}
|
||||
|
||||
get_playlist();
|
||||
<% end %>
|
||||
|
||||
function get_reddit_comments() {
|
||||
function get_reddit_comments(timeouts = 0) {
|
||||
comments = document.getElementById("comments");
|
||||
|
||||
if (timeouts > 10) {
|
||||
console.log("Failed to pull comments");
|
||||
comments.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
var fallback = comments.innerHTML;
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
@ -382,12 +367,19 @@ function get_reddit_comments() {
|
||||
xhr.ontimeout = function() {
|
||||
console.log("Pulling comments timed out.");
|
||||
|
||||
get_reddit_comments();
|
||||
get_reddit_comments(timeouts + 1);
|
||||
};
|
||||
}
|
||||
|
||||
function get_youtube_comments() {
|
||||
function get_youtube_comments(timeouts = 0) {
|
||||
comments = document.getElementById("comments");
|
||||
|
||||
if (timeouts > 10) {
|
||||
console.log("Failed to pull comments");
|
||||
comments.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
var fallback = comments.innerHTML;
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
@ -438,7 +430,7 @@ function get_youtube_comments() {
|
||||
|
||||
comments.innerHTML =
|
||||
'<h3><center class="loading"><i class="icon ion-ios-refresh"></i></center></h3>';
|
||||
get_youtube_comments();
|
||||
get_youtube_comments(timeouts + 1);
|
||||
};
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user