Compare commits
90 commits
4041ee16f4
...
7d4b1c18ba
Author | SHA1 | Date | |
---|---|---|---|
7d4b1c18ba | |||
8831288ca1 | |||
041d6cc9d3 | |||
79859100a8 | |||
47ef5dfe4c | |||
13e00e674b | |||
3615bbd893 | |||
9b9efc6841 | |||
036ab6ef65 | |||
c27a703544 | |||
5a75ef7f94 | |||
91c9cd45a4 | |||
b953dc1ce7 | |||
70dc1a9f11 | |||
fc910b43ba | |||
67998d1f36 | |||
e2276ace1b | |||
c61b2963ac | |||
26bee068eb | |||
486c5845cd | |||
6f10a7c67e | |||
67d7b78ac9 | |||
3afac4d842 | |||
448007e5ba | |||
3cc0dbca01 | |||
c3e8721051 | |||
cf5028d09a | |||
eb2670fe49 | |||
976e1ccf5a | |||
fee2acc666 | |||
b5ab49e8e8 | |||
65f3bbcb10 | |||
|
fb3ecdad9a | ||
5357c83e00 | |||
8dc0a67be3 | |||
d61043edea | |||
3111158a7c | |||
cf6c3a7b5b | |||
2f5a555ea7 | |||
472dd8663d | |||
dc2aba106c | |||
eff8673efc | |||
b1f25a69ad | |||
d5b8b0b19c | |||
|
b3e6aaddab | ||
33ffafb9e3 | |||
8f46bd5751 | |||
cae8cbdda8 | |||
100ecff0b3 | |||
5ced7694fe | |||
ae937d8339 | |||
4a2877f28b | |||
|
e476dbe25b | ||
2f8ef155c8 | |||
6c2626cf05 | |||
72150ae676 | |||
b7430c5a5a | |||
ddfb8e7d93 | |||
|
b0cd6587bd | ||
c7b8f470d8 | |||
762fa5214d | |||
d50990ea15 | |||
067dcbef5e | |||
a7e9602ccd | |||
a74057bb7a | |||
9a7b6976ff | |||
6e1e3e9554 | |||
ad591f3c32 | |||
|
8665a69fee | ||
|
d1051efd6e | ||
03bf4592ce | |||
d641bcbf5d | |||
2027a35e5a | |||
|
65d9468911 | ||
|
389a2a4a4d | ||
|
b60e056f96 | ||
|
532d92bb7a | ||
|
24f878e6f6 | ||
|
a9fc84bc14 | ||
|
7e680c692f | ||
|
1eb28edfb3 | ||
|
efcd94ffbe | ||
|
780f9df7d3 | ||
|
4d11c324b0 | ||
|
57f8bfb965 | ||
|
6acabc5bff | ||
|
9d0ab0a83c | ||
|
4164159057 | ||
|
30d858bc8b | ||
|
e98aafa4b5 |
53 changed files with 1354 additions and 502 deletions
63
.forgejo/workflows/ci.yml
Normal file
63
.forgejo/workflows/ci.yml
Normal file
|
@ -0,0 +1,63 @@
|
|||
name: 'Invidious CI'
|
||||
|
||||
on:
|
||||
# workflow_dispatch:
|
||||
# inputs: {}
|
||||
schedule:
|
||||
- cron: '0 7 * * 0'
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "experimental"
|
||||
- "experimental2"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: runner
|
||||
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
|
||||
- name: Set up cache
|
||||
uses: https://code.forgejo.org/actions/cache@v3
|
||||
id: ccache-restore
|
||||
with:
|
||||
path: |
|
||||
./lib
|
||||
key: ${{ runner.os }}-invidious-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-invidious-
|
||||
|
||||
- name: Create cache directory
|
||||
if: steps.ccache-restore.outputs.cache-hit != 'true'
|
||||
run: mkdir -p ./lib
|
||||
|
||||
- uses: https://code.forgejo.org/docker/setup-buildx-action@v3
|
||||
name: Setup Docker BuildX system
|
||||
|
||||
- name: Login to Docker Container Registry
|
||||
uses: https://code.forgejo.org/docker/login-action@v3.1.0
|
||||
with:
|
||||
registry: git.nadeko.net
|
||||
username: ${{ secrets.USERNAME }}
|
||||
password: ${{ secrets.TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: https://github.com/docker/metadata-action@v5
|
||||
with:
|
||||
images: git.nadeko.net/fijxu/invidious-with-companion
|
||||
tags: |
|
||||
type=sha,format=short,prefix={{date 'YYYY.MM.DD'}}-,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'master') }}
|
||||
|
||||
- uses: https://code.forgejo.org/docker/build-push-action@v6
|
||||
name: Build images
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
build-args: |
|
||||
"release=1"
|
2
Makefile
2
Makefile
|
@ -51,7 +51,7 @@ get-libs:
|
|||
|
||||
# TODO: add support for ARM64 via cross-compilation
|
||||
invidious: get-libs
|
||||
crystal build src/invidious.cr $(FLAGS) --progress --stats --error-trace
|
||||
crystal build src/invidious.cr $(FLAGS) --progress --stats --error-trace --mcpu=x86-64-v3
|
||||
|
||||
|
||||
run: invidious
|
||||
|
|
|
@ -121,6 +121,7 @@ body a.channel-owner {
|
|||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.2vh;
|
||||
}
|
||||
|
||||
.feed-menu-item {
|
||||
|
@ -460,31 +461,68 @@ p.video-data { margin: 0; font-weight: bold; font-size: 80%; }
|
|||
*/
|
||||
|
||||
footer {
|
||||
margin-top: auto;
|
||||
color: #919191;
|
||||
margin-top: 2.5em;
|
||||
padding: 1.5em 0;
|
||||
text-align: center;
|
||||
max-height: 30vh;
|
||||
}
|
||||
|
||||
.light-theme footer {
|
||||
color: #7c7c7c;
|
||||
#footer-content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.dark-theme footer {
|
||||
color: #adadad;
|
||||
#footer-content-container > hr {
|
||||
margin: 0;
|
||||
color: rgb(241, 241, 241);
|
||||
}
|
||||
|
||||
.light-theme footer a {
|
||||
color: #7c7c7c !important;
|
||||
.footer-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.dark-theme footer a {
|
||||
color: #adadad !important;
|
||||
footer a {
|
||||
color: #919191;
|
||||
}
|
||||
|
||||
footer span {
|
||||
margin: 4px 0;
|
||||
display: block;
|
||||
.footer-content #footer-custom-text > b {
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.footer-section {
|
||||
margin-right: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.footer-section-list {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.footer-section-item {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.footer-footer .left {
|
||||
float: left
|
||||
}
|
||||
|
||||
.footer-footer .right {
|
||||
float: right;
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.footer-right .right a {
|
||||
color: #919191
|
||||
}
|
||||
|
||||
@media screen and (max-width: 929px) {
|
||||
#footer-custom-text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* keyframes */
|
||||
|
@ -550,6 +588,23 @@ span > select {
|
|||
color: #565d64;
|
||||
}
|
||||
|
||||
.light-theme footer {
|
||||
color: #7a7a7a;
|
||||
background: #f2f2f2;
|
||||
}
|
||||
|
||||
.light-theme #footer-content-container > hr {
|
||||
color: rgb(241, 241, 241);
|
||||
}
|
||||
|
||||
.light-theme footer a {
|
||||
color: #7c7c7c !important;
|
||||
}
|
||||
|
||||
.light-theme footer #footer-custom-text > b {
|
||||
color: #565D64;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.no-theme a:hover,
|
||||
.no-theme a:active,
|
||||
|
@ -585,17 +640,22 @@ span > select {
|
|||
color: #303030;
|
||||
}
|
||||
|
||||
.light-theme .pure-menu-heading {
|
||||
color: #565d64;
|
||||
}
|
||||
|
||||
.no-theme footer {
|
||||
background: #f2f2f2;
|
||||
color: #7c7c7c;
|
||||
}
|
||||
|
||||
.no-theme footer #footer-custom-text > b {
|
||||
color: #565D64;
|
||||
}
|
||||
|
||||
.no-theme footer a {
|
||||
color: #7c7c7c !important;
|
||||
}
|
||||
|
||||
.light-theme .pure-menu-heading {
|
||||
color: #565d64;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -631,7 +691,7 @@ span > select {
|
|||
}
|
||||
|
||||
body.dark-theme {
|
||||
background-color: rgba(35, 35, 35, 1);
|
||||
background-color: rgba(18, 18, 18, 1);
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
|
@ -658,6 +718,20 @@ body.dark-theme {
|
|||
color: inherit;
|
||||
}
|
||||
|
||||
|
||||
.dark-theme footer {
|
||||
background: #16191a;
|
||||
}
|
||||
|
||||
.dark-theme #footer-content-container > hr {
|
||||
color: #313131;
|
||||
}
|
||||
|
||||
.dark-theme .footer-content #footer-custom-text > b {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.no-theme a:hover,
|
||||
.no-theme a:active,
|
||||
|
@ -685,7 +759,7 @@ body.dark-theme {
|
|||
}
|
||||
|
||||
body.no-theme {
|
||||
background-color: rgba(35, 35, 35, 1);
|
||||
background-color: rgba(18, 18, 18, 1);
|
||||
color: #f0f0f0;
|
||||
}
|
||||
|
||||
|
@ -713,11 +787,12 @@ body.dark-theme {
|
|||
}
|
||||
|
||||
.no-theme footer {
|
||||
color: #adadad;
|
||||
background: #16191a;
|
||||
color: #313131;
|
||||
}
|
||||
|
||||
.no-theme footer a {
|
||||
color: #adadad !important;
|
||||
.no-theme footer #footer-custom-text > b {
|
||||
color: #ccc;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -803,6 +878,17 @@ h1, h2, h3, h4, h5, p,
|
|||
/* Center the "invidious" logo on the search page */
|
||||
#logo > h1 { text-align: center; }
|
||||
|
||||
#footer_buffer {
|
||||
margin-top: 50vh;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 450px) {
|
||||
#footer_buffer {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* IE11 fixes */
|
||||
:-ms-input-placeholder { color: #888; }
|
||||
|
||||
|
|
|
@ -50,6 +50,9 @@ videojs.Vhs.xhr.beforeRequest = function(options) {
|
|||
return options;
|
||||
};
|
||||
|
||||
videojs.Vhs.GOAL_BUFFER_LENGTH = 40;
|
||||
videojs.Vhs.MAX_GOAL_BUFFER_LENGTH = 80;
|
||||
|
||||
var player = videojs('player', options);
|
||||
|
||||
player.on('error', function () {
|
||||
|
@ -350,12 +353,8 @@ if (video_data.params.save_player_pos) {
|
|||
const rememberedTime = get_video_time();
|
||||
let lastUpdated = 0;
|
||||
|
||||
if(!hasTimeParam) {
|
||||
if (rememberedTime >= video_data.length_seconds - 20)
|
||||
set_seconds_after_start(0);
|
||||
else
|
||||
set_seconds_after_start(rememberedTime);
|
||||
}
|
||||
if(!hasTimeParam) set_seconds_after_start(rememberedTime);
|
||||
if (rememberedTime >= video_data.length_seconds - 20 && !hasTimeParam) set_seconds_after_start(0);
|
||||
|
||||
player.on('timeupdate', function () {
|
||||
const raw = player.currentTime();
|
||||
|
|
|
@ -461,9 +461,8 @@ jobs:
|
|||
##
|
||||
enable: true
|
||||
|
||||
|
||||
# -----------------------------
|
||||
# Miscellaneous
|
||||
# Instance customization
|
||||
# -----------------------------
|
||||
|
||||
##
|
||||
|
@ -475,6 +474,78 @@ jobs:
|
|||
##
|
||||
#banner:
|
||||
|
||||
##
|
||||
## custom text displayed at the bottom of every page within Invidious' footer. This can
|
||||
## used for instance announcements, e.g
|
||||
##
|
||||
## When unset Invidious defaults to some text that describes what Invidious is. See
|
||||
## localization key default_invidious_footer_text
|
||||
##
|
||||
## Accepted values: any string. HTML is accepted.
|
||||
## Default: <none>
|
||||
##
|
||||
#footer:
|
||||
|
||||
##
|
||||
## Source code URL. If your instance is running a modified source
|
||||
## code, you MUST publish it somewhere and set this option.
|
||||
##
|
||||
## Accepted values: a string
|
||||
## Default: <none>
|
||||
##
|
||||
#modified_source_code_url: ""
|
||||
|
||||
##
|
||||
## Email to contact the instance maintainer; used in a mailto: link within the footer.
|
||||
##
|
||||
## Accepted values: string
|
||||
## Default: <none>
|
||||
##
|
||||
# instance_maintainer_email:
|
||||
|
||||
##
|
||||
## Link to the terms of service of the instance (if any)
|
||||
##
|
||||
## Displayed within the instance section of the footer
|
||||
##
|
||||
## Accepted values: String (link)
|
||||
## Default: <none>
|
||||
##
|
||||
# footer_instance_tos_link:
|
||||
|
||||
##
|
||||
## Link to the privacy-policy of the instance (if any)
|
||||
##
|
||||
## Displayed within the instance section of the footer
|
||||
##
|
||||
## Accepted values: String (link)
|
||||
## Default: <none>
|
||||
##
|
||||
# footer_instance_privacy_policy_link:
|
||||
|
||||
##
|
||||
## Instance donation URL. If your instance has a donation option.
|
||||
## you can add it here so it will be present in the footer along
|
||||
## with the donation link for the project itself.
|
||||
##
|
||||
## Accepted values: a string
|
||||
## Default: <none>
|
||||
##
|
||||
#footer_instance_donate_link: ""
|
||||
|
||||
##
|
||||
## Custom fields to be displayed within the footer's instance section
|
||||
##
|
||||
## Accepted values: A nested array mapping field name and links together.
|
||||
## IE: [ ["field1", "https://example.com/1"], ["field2", "https://example.com/2"] ]
|
||||
## Default: <none>
|
||||
##
|
||||
# footer_instance_section_custom_fields: []
|
||||
|
||||
# -----------------------------
|
||||
# Miscellaneous
|
||||
# -----------------------------
|
||||
|
||||
##
|
||||
## Subscribe to channels using PubSubHub (Google PubSubHubbub service).
|
||||
## PubSubHub allows Invidious to be instantly notified when a new video
|
||||
|
@ -528,15 +599,6 @@ hmac_key: "CHANGE_ME!!"
|
|||
##
|
||||
#cache_annotations: false
|
||||
|
||||
##
|
||||
## Source code URL. If your instance is running a modified source
|
||||
## code, you MUST publish it somewhere and set this option.
|
||||
##
|
||||
## Accepted values: a string
|
||||
## Default: <none>
|
||||
##
|
||||
#modified_source_code_url: ""
|
||||
|
||||
##
|
||||
## Maximum custom playlist length limit.
|
||||
##
|
||||
|
@ -545,6 +607,7 @@ hmac_key: "CHANGE_ME!!"
|
|||
##
|
||||
#playlist_length_limit: 500
|
||||
|
||||
|
||||
#########################################
|
||||
#
|
||||
# Default user preferences
|
||||
|
@ -877,7 +940,7 @@ default_user_preferences:
|
|||
## Default: true
|
||||
##
|
||||
#vr_mode: true
|
||||
|
||||
|
||||
##
|
||||
## Save the playback position
|
||||
## Allow to continue watching at the previous position when
|
||||
|
@ -985,3 +1048,9 @@ default_user_preferences:
|
|||
## Default: false
|
||||
##
|
||||
#extend_desc: false
|
||||
|
||||
# redis_url: 127.0.0.1:6379
|
||||
# redis_socket: /var/run/valkey/valkey.sock
|
||||
# donation_url: "https://example.com/donate"
|
||||
# contact_url: "https://example.com/contact"
|
||||
# home_domain: "https://example.com/
|
||||
|
|
6
config/migrate-scripts/migrate-db-8bc91ce.sh
Normal file
6
config/migrate-scripts/migrate-db-8bc91ce.sh
Normal file
|
@ -0,0 +1,6 @@
|
|||
CREATE INDEX channel_videos_ucid_published_idx
|
||||
ON public.channel_videos
|
||||
USING btree
|
||||
(ucid COLLATE pg_catalog."default", published);
|
||||
|
||||
DROP INDEX channel_videos_ucid_idx;
|
|
@ -19,12 +19,12 @@ CREATE TABLE IF NOT EXISTS public.channel_videos
|
|||
|
||||
GRANT ALL ON TABLE public.channel_videos TO current_user;
|
||||
|
||||
-- Index: public.channel_videos_ucid_idx
|
||||
-- Index: public.channel_videos_ucid_published_idx
|
||||
|
||||
-- DROP INDEX public.channel_videos_ucid_idx;
|
||||
-- DROP INDEX public.channel_videos_ucid_published_idx;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx
|
||||
CREATE INDEX IF NOT EXISTS channel_videos_ucid_published_idx
|
||||
ON public.channel_videos
|
||||
USING btree
|
||||
(ucid COLLATE pg_catalog."default");
|
||||
(ucid COLLATE pg_catalog."default", published);
|
||||
|
||||
|
|
62
crystal_formatters.py
Normal file
62
crystal_formatters.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
import lldb
|
||||
|
||||
class CrystalArraySyntheticProvider:
|
||||
def __init__(self, valobj, internal_dict):
|
||||
self.valobj = valobj
|
||||
self.buffer = None
|
||||
self.size = 0
|
||||
|
||||
def update(self):
|
||||
if self.valobj.type.is_pointer:
|
||||
self.valobj = self.valobj.Dereference()
|
||||
self.size = int(self.valobj.child[0].value)
|
||||
self.type = self.valobj.type
|
||||
self.buffer = self.valobj.child[3]
|
||||
|
||||
def num_children(self):
|
||||
size = 0 if self.size is None else self.size
|
||||
return size
|
||||
|
||||
def get_child_index(self, name):
|
||||
try:
|
||||
return int(name.lstrip('[').rstrip(']'))
|
||||
except:
|
||||
return -1
|
||||
|
||||
def get_child_at_index(self,index):
|
||||
if index >= self.size:
|
||||
return None
|
||||
try:
|
||||
elementType = self.buffer.type.GetPointeeType()
|
||||
offset = elementType.size * index
|
||||
return self.buffer.CreateChildAtOffset('[' + str(index) + ']', offset, elementType)
|
||||
except Exception as e:
|
||||
print('Got exception %s' % (str(e)))
|
||||
return None
|
||||
|
||||
def findType(name, module):
|
||||
cachedTypes = module.GetTypes()
|
||||
for idx in range(cachedTypes.GetSize()):
|
||||
type = cachedTypes.GetTypeAtIndex(idx)
|
||||
if type.name == name:
|
||||
return type
|
||||
return None
|
||||
|
||||
|
||||
def CrystalString_SummaryProvider(value, dict):
|
||||
error = lldb.SBError()
|
||||
if value.TypeIsPointerType():
|
||||
value = value.Dereference()
|
||||
process = value.GetTarget().GetProcess()
|
||||
byteSize = int(value.child[0].value)
|
||||
len = int(value.child[1].value)
|
||||
len = byteSize or len
|
||||
strAddr = value.child[2].load_addr
|
||||
val = process.ReadCStringFromMemory(strAddr, len + 1, error)
|
||||
return '"%s"' % val
|
||||
|
||||
|
||||
def __lldb_init_module(debugger, dict):
|
||||
debugger.HandleCommand(r'type synthetic add -l crystal_formatters.CrystalArraySyntheticProvider -x "^Array\(.+\)(\s*\**)?" -w Crystal')
|
||||
debugger.HandleCommand(r'type summary add -F crystal_formatters.CrystalString_SummaryProvider -x "^(String|\(String \| Nil\))(\s*\**)?$" -w Crystal')
|
||||
debugger.HandleCommand(r'type category enable Crystal')
|
|
@ -1,55 +1,56 @@
|
|||
# Warning: This docker-compose file is made for development purposes.
|
||||
# Using it will build an image from the locally cloned repository.
|
||||
#
|
||||
# If you want to use Invidious in production, see the docker-compose.yml file provided
|
||||
# in the installation documentation: https://docs.invidious.io/installation/
|
||||
# Docker compose file for inv.nadeko.net
|
||||
|
||||
version: "3"
|
||||
services:
|
||||
|
||||
invidious:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile
|
||||
valkey:
|
||||
image: valkey/valkey:7.2-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
volumes:
|
||||
- "./valkey:/data"
|
||||
|
||||
invidious-refresher:
|
||||
image: git.nadeko.net/fijxu/invidious:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./config/config-refresher.yml:/etc/invidious/config.yml:ro
|
||||
- ./logs:/var/log/invidious:rw
|
||||
- /var/run/postgresql/.s.PGSQL.5432:/var/run/postgresql/.s.PGSQL.5432:rw
|
||||
environment:
|
||||
# Please read the following file for a comprehensive list of all available
|
||||
# configuration options and their associated syntax:
|
||||
# https://github.com/iv-org/invidious/blob/master/config/config.example.yml
|
||||
INVIDIOUS_CONFIG: |
|
||||
db:
|
||||
dbname: invidious
|
||||
user: kemal
|
||||
password: kemal
|
||||
host: invidious-db
|
||||
port: 5432
|
||||
check_tables: true
|
||||
# external_port:
|
||||
# domain:
|
||||
# https_only: false
|
||||
# statistics_enabled: false
|
||||
hmac_key: "CHANGE_ME!!"
|
||||
INVIDIOUS_CONFIG_FILE: /etc/invidious/config.yml
|
||||
depends_on:
|
||||
- valkey
|
||||
healthcheck:
|
||||
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
|
||||
invidious-db:
|
||||
image: docker.io/library/postgres:14
|
||||
|
||||
invidious:
|
||||
image: git.nadeko.net/fijxu/invidious:latest
|
||||
restart: unless-stopped
|
||||
deploy:
|
||||
replicas: 8
|
||||
volumes:
|
||||
- ./config/config.yml:/etc/invidious/config.yml:ro
|
||||
- ./logs:/var/log/invidious:rw
|
||||
- /var/run/postgresql/.s.PGSQL.5432:/var/run/postgresql/.s.PGSQL.5432:rw
|
||||
environment:
|
||||
INVIDIOUS_CONFIG_FILE: /etc/invidious/config.yml
|
||||
depends_on:
|
||||
- valkey
|
||||
healthcheck:
|
||||
test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/trending || exit 1
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
|
||||
|
||||
invidious-nginx:
|
||||
image: nginx:latest
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- postgresdata:/var/lib/postgresql/data
|
||||
- ./config/sql:/config/sql
|
||||
- ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh
|
||||
environment:
|
||||
POSTGRES_DB: invidious
|
||||
POSTGRES_USER: kemal
|
||||
POSTGRES_PASSWORD: kemal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||
|
||||
volumes:
|
||||
postgresdata:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
depends_on:
|
||||
- invidious
|
||||
ports:
|
||||
- "127.0.0.1:10011:3000"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM crystallang/crystal:1.12.2-alpine AS builder
|
||||
FROM crystallang/crystal:1.14.0-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache sqlite-static yaml-static
|
||||
|
||||
|
@ -20,10 +20,10 @@ COPY ./assets/ ./assets/
|
|||
COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
|
||||
|
||||
RUN crystal spec --warnings all \
|
||||
--link-flags "-lxml2 -llzma"
|
||||
--link-flags "-lxml2 -llzma"
|
||||
RUN if [[ "${release}" == 1 ]] ; then \
|
||||
crystal build ./src/invidious.cr \
|
||||
--release \
|
||||
--release --mcpu=x86-64-v2 \
|
||||
--static --warnings all \
|
||||
--link-flags "-lxml2 -llzma"; \
|
||||
else \
|
||||
|
|
60
kubernetes/values.yaml
Normal file
60
kubernetes/values.yaml
Normal file
|
@ -0,0 +1,60 @@
|
|||
name: invidious
|
||||
|
||||
image:
|
||||
repository: quay.io/invidious/invidious
|
||||
tag: latest
|
||||
pullPolicy: Always
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
autoscaling:
|
||||
enabled: false
|
||||
minReplicas: 1
|
||||
maxReplicas: 16
|
||||
targetCPUUtilizationPercentage: 50
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 3000
|
||||
#loadBalancerIP:
|
||||
|
||||
resources: {}
|
||||
#requests:
|
||||
# cpu: 100m
|
||||
# memory: 64Mi
|
||||
#limits:
|
||||
# cpu: 800m
|
||||
# memory: 512Mi
|
||||
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
fsGroup: 1000
|
||||
|
||||
# See https://github.com/bitnami/charts/tree/master/bitnami/postgresql
|
||||
postgresql:
|
||||
image:
|
||||
tag: 13
|
||||
auth:
|
||||
username: kemal
|
||||
password: kemal
|
||||
database: invidious
|
||||
primary:
|
||||
initdb:
|
||||
username: kemal
|
||||
password: kemal
|
||||
scriptsConfigMap: invidious-postgresql-init
|
||||
|
||||
# Adapted from ../config/config.yml
|
||||
config:
|
||||
channel_threads: 1
|
||||
db:
|
||||
user: kemal
|
||||
password: kemal
|
||||
host: invidious-postgresql
|
||||
port: 5432
|
||||
dbname: invidious
|
||||
full_refresh: false
|
||||
https_only: false
|
||||
domain:
|
|
@ -164,6 +164,7 @@
|
|||
"unsubscribe": "unsubscribe",
|
||||
"revoke": "revoke",
|
||||
"Subscriptions": "Subscriptions",
|
||||
"History": "History",
|
||||
"subscriptions_unseen_notifs_count": "{{count}} unseen notification",
|
||||
"subscriptions_unseen_notifs_count_plural": "{{count}} unseen notifications",
|
||||
"search": "search",
|
||||
|
@ -460,18 +461,37 @@
|
|||
"search_filters_sort_option_views": "View count",
|
||||
"search_filters_apply_button": "Apply selected filters",
|
||||
"Current version: ": "Current version: ",
|
||||
"footer_current_version_modified": "Current version (modified): ",
|
||||
"next_steps_error_message": "After which you should try to: ",
|
||||
"next_steps_error_message_refresh": "Refresh",
|
||||
"next_steps_error_message_go_to_youtube": "Go to YouTube",
|
||||
"footer_donate_page": "Donate",
|
||||
"footer_documentation": "Documentation",
|
||||
"footer_source_code": "Source code",
|
||||
"footer_original_source_code": "Original source code",
|
||||
"footer_modfied_source_code": "Modified source code",
|
||||
"default_invidious_footer_text": "A free and open source frontend for Youtube that that respects your privacy! Now you can watch videos (ad-free), subscribe to channels, create playlist and much more all without the prying eyes of Google!",
|
||||
"footer_navigation_section_header": "Navigation",
|
||||
"footer_home_link": "Home",
|
||||
"footer_project_information_section_header": "Invidious",
|
||||
"footer_project_homepage_link": "Project Homepage",
|
||||
"footer_source_code_link": "Source Code",
|
||||
"footer_issue_tracker_link": "Issue tracker",
|
||||
"footer_public_instances_link": "Public instances",
|
||||
"footer_donate_link": "Donate",
|
||||
"footer_matrix_link": "Matrix",
|
||||
"footer_support_section_header": "Support",
|
||||
"footer_contact_link": "Contact Instance Maintainer",
|
||||
"footer_report_bug_link": "Report a bug",
|
||||
"footer_faq_link": "FAQ",
|
||||
"footer_instance_section_header": "Instance",
|
||||
"footer_instance_section_header_modified_source": "Instance (Modified)",
|
||||
"footer_instance_section_modified_source_code": "Instance Source Code",
|
||||
"footer_instance_section_tos": "Terms of Service",
|
||||
"footer_instance_section_privacy_policy": "Privacy Policy",
|
||||
"footer_instance_section_donate": "Donate (Instance)",
|
||||
"footer_licences_link": "Licences",
|
||||
"footer_privacy_policy_link": "Privacy",
|
||||
"adminprefs_modified_source_code_url_label": "URL to modified source code repository",
|
||||
"none": "none",
|
||||
"videoinfo_started_streaming_x_ago": "Started streaming `x` ago",
|
||||
"videoinfo_watch_on_youTube": "Watch on YouTube",
|
||||
"videoinfo_watch_on_materialious": "Watch on Materialious",
|
||||
"videoinfo_youTube_embed_link": "Embed",
|
||||
"videoinfo_invidious_embed_link": "Embed Link",
|
||||
"download_subtitles": "Subtitles - `x` (.vtt)",
|
||||
|
@ -498,5 +518,7 @@
|
|||
"toggle_theme": "Toggle Theme",
|
||||
"carousel_slide": "Slide {{current}} of {{total}}",
|
||||
"carousel_skip": "Skip the Carousel",
|
||||
"carousel_go_to": "Go to slide `x`"
|
||||
"carousel_go_to": "Go to slide `x`",
|
||||
"footer_contact_url": "Contact the Administrator"
|
||||
|
||||
}
|
||||
|
|
|
@ -116,6 +116,7 @@
|
|||
"unsubscribe": "desuscribirse",
|
||||
"revoke": "revocar",
|
||||
"Subscriptions": "Suscripciones",
|
||||
"History": "Historial",
|
||||
"search": "buscar",
|
||||
"Log out": "Cerrar la sesión",
|
||||
"Released under the AGPLv3 on Github.": "Publicado bajo la AGPLv3 en GitHub.",
|
||||
|
@ -389,6 +390,7 @@
|
|||
"search_filters_features_option_purchased": "Comprado",
|
||||
"search_filters_features_option_three_sixty": "360°",
|
||||
"videoinfo_watch_on_youTube": "Ver en YouTube",
|
||||
"videoinfo_watch_on_materialious": "Ver en Materialious",
|
||||
"preferences_save_player_pos_label": "Guardar posición de reproducción: ",
|
||||
"generic_views_count_0": "{{count}} visualización",
|
||||
"generic_views_count_1": "{{count}} visualizaciones",
|
||||
|
@ -513,5 +515,6 @@
|
|||
"The Popular feed has been disabled by the administrator.": "El feed Popular ha sido desactivado por el administrador.",
|
||||
"carousel_slide": "Diapositiva {{current}} de {{total}}",
|
||||
"carousel_skip": "Saltar el carrusel",
|
||||
"carousel_go_to": "Ir a la diapositiva `x`"
|
||||
"carousel_go_to": "Ir a la diapositiva `x`",
|
||||
"footer_contact_url": "Contactar al Administrador"
|
||||
}
|
||||
|
|
18
nginx.conf
Normal file
18
nginx.conf
Normal file
|
@ -0,0 +1,18 @@
|
|||
user www-data;
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
http {
|
||||
server {
|
||||
listen 3000;
|
||||
listen [::]:3000;
|
||||
access_log off;
|
||||
location / {
|
||||
resolver 127.0.0.11;
|
||||
set $backend "invidious";
|
||||
proxy_pass http://$backend:3000;
|
||||
proxy_http_version 1.1; # to keep alive
|
||||
proxy_set_header Connection ""; # to keep alive
|
||||
}
|
||||
}
|
||||
}
|
11
shard.lock
11
shard.lock
|
@ -20,6 +20,9 @@ shards:
|
|||
git: https://github.com/crystal-loot/exception_page.git
|
||||
version: 0.2.2
|
||||
|
||||
inotify:
|
||||
git: https://github.com/petoem/inotify.cr.git
|
||||
version: 1.0.3
|
||||
http_proxy:
|
||||
git: https://github.com/mamantoha/http_proxy.git
|
||||
version: 0.10.3
|
||||
|
@ -36,6 +39,10 @@ shards:
|
|||
git: https://github.com/will/crystal-pg.git
|
||||
version: 0.28.0
|
||||
|
||||
pool:
|
||||
git: https://github.com/ysbaddaden/pool.git
|
||||
version: 0.2.4
|
||||
|
||||
protodec:
|
||||
git: https://github.com/iv-org/protodec.git
|
||||
version: 0.1.5
|
||||
|
@ -44,6 +51,10 @@ shards:
|
|||
git: https://github.com/luislavena/radix.git
|
||||
version: 0.4.1
|
||||
|
||||
redis:
|
||||
git: https://github.com/stefanwille/crystal-redis.git
|
||||
version: 2.9.1
|
||||
|
||||
spectator:
|
||||
git: https://github.com/icy-arctic-fox/spectator.git
|
||||
version: 0.10.6
|
||||
|
|
|
@ -27,6 +27,11 @@ dependencies:
|
|||
athena-negotiation:
|
||||
github: athena-framework/negotiation
|
||||
version: ~> 0.1.1
|
||||
redis:
|
||||
github: stefanwille/crystal-redis
|
||||
inotify:
|
||||
github: petoem/inotify.cr
|
||||
version: 1.0.3
|
||||
http_proxy:
|
||||
github: mamantoha/http_proxy
|
||||
version: ~> 0.10.3
|
||||
|
|
|
@ -32,6 +32,8 @@ require "xml"
|
|||
require "yaml"
|
||||
require "compress/zip"
|
||||
require "protodec/utils"
|
||||
require "redis"
|
||||
require "inotify"
|
||||
|
||||
require "./invidious/database/*"
|
||||
require "./invidious/database/migrations/*"
|
||||
|
@ -58,15 +60,35 @@ end
|
|||
# Simple alias to make code easier to read
|
||||
alias IV = Invidious
|
||||
|
||||
CONFIG = Config.load
|
||||
CONFIG = Config.load
|
||||
|
||||
Signal::HUP.trap do
|
||||
Config.reload
|
||||
end
|
||||
|
||||
{% if flag?(:linux) %}
|
||||
if CONFIG.reload_config_automatically
|
||||
Inotify.watch("config/config.yml") do |event|
|
||||
Config.reload
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
|
||||
HMAC_KEY = CONFIG.hmac_key
|
||||
|
||||
PG_DB = DB.open CONFIG.database_url
|
||||
ARCHIVE_URL = URI.parse("https://archive.org")
|
||||
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
||||
REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||
YT_URL = URI.parse("https://www.youtube.com")
|
||||
HOST_URL = make_host_url(Kemal.config)
|
||||
PG_DB = DB.open CONFIG.database_url
|
||||
REDIS_DB = Redis::PooledClient.new(unixsocket: CONFIG.redis_socket || nil, url: CONFIG.redis_url || nil)
|
||||
|
||||
if REDIS_DB.ping
|
||||
puts "Connected to redis"
|
||||
end
|
||||
ARCHIVE_URL = URI.parse("https://archive.org")
|
||||
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
|
||||
REDDIT_URL = URI.parse("https://www.reddit.com")
|
||||
YT_URL = URI.parse("https://www.youtube.com")
|
||||
PUBSUB_HOST_URL = CONFIG.pubsub_domain
|
||||
HOST_URL = make_host_url(Kemal.config)
|
||||
EXT_VIDEOP_LIST = gen_videoplayback_proxy_list()
|
||||
|
||||
CHARS_SAFE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
TEST_IDS = {"AgbeGFYluEA", "BaW_jenozKc", "a9LDPn-MO4I", "ddFvjfvPnqk", "iqKdEhx-dD4"}
|
||||
|
@ -108,14 +130,6 @@ Kemal.config.extra_options do |parser|
|
|||
exit
|
||||
end
|
||||
end
|
||||
parser.on("-f THREADS", "--feed-threads=THREADS", "Number of threads for refreshing feeds (default: #{CONFIG.feed_threads})") do |number|
|
||||
begin
|
||||
CONFIG.feed_threads = number.to_i
|
||||
rescue ex
|
||||
puts "THREADS must be integer"
|
||||
exit
|
||||
end
|
||||
end
|
||||
parser.on("-o OUTPUT", "--output=OUTPUT", "Redirect output (default: #{CONFIG.output})") do |output|
|
||||
CONFIG.output = output
|
||||
end
|
||||
|
@ -176,10 +190,6 @@ if CONFIG.channel_threads > 0
|
|||
Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB)
|
||||
end
|
||||
|
||||
if CONFIG.feed_threads > 0
|
||||
Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB)
|
||||
end
|
||||
|
||||
if CONFIG.statistics_enabled
|
||||
Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE)
|
||||
end
|
||||
|
@ -199,6 +209,14 @@ Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
|
|||
|
||||
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
|
||||
|
||||
if !CONFIG.external_videoplayback_proxy.empty?
|
||||
Invidious::Jobs.register Invidious::Jobs::CheckExternalProxy.new
|
||||
end
|
||||
|
||||
if CONFIG.refresh_tokens
|
||||
Invidious::Jobs.register Invidious::Jobs::RefreshTokens.new
|
||||
end
|
||||
|
||||
Invidious::Jobs.start_all
|
||||
|
||||
def popular_videos
|
||||
|
|
|
@ -251,8 +251,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
|||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Inserted, updating subscriptions")
|
||||
if CONFIG.enable_user_notifications
|
||||
Invidious::Database::Users.add_notification(video)
|
||||
else
|
||||
Invidious::Database::Users.feed_needs_update(video)
|
||||
# else
|
||||
# Invidious::Database::Users.feed_needs_update(video)
|
||||
end
|
||||
else
|
||||
LOGGER.trace("fetch_channel: #{ucid} : video #{video_id} : Updated")
|
||||
|
@ -287,8 +287,8 @@ def fetch_channel(ucid, pull_all_videos : Bool)
|
|||
if was_insert
|
||||
if CONFIG.enable_user_notifications
|
||||
Invidious::Database::Users.add_notification(video)
|
||||
else
|
||||
Invidious::Database::Users.feed_needs_update(video)
|
||||
# else
|
||||
# Invidious::Database::Users.feed_needs_update(video)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,7 @@ struct ConfigPreferences
|
|||
property quality : String = "hd720"
|
||||
property quality_dash : String = "auto"
|
||||
property default_home : String? = "Popular"
|
||||
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists"]
|
||||
property feed_menu : Array(String) = ["Popular", "Trending", "Subscriptions", "Playlists", "History"]
|
||||
property automatic_instance_redirect : Bool = false
|
||||
property region : String = "US"
|
||||
property related_videos : Bool = true
|
||||
|
@ -82,8 +82,6 @@ class Config
|
|||
# Time interval between two executions of the job that crawls channel videos (subscriptions update).
|
||||
@[YAML::Field(converter: Preferences::TimeSpanConverter)]
|
||||
property channel_refresh_interval : Time::Span = 30.minutes
|
||||
# Number of threads to use for updating feeds
|
||||
property feed_threads : Int32 = 1
|
||||
# Log file path or STDOUT
|
||||
property output : String = "STDOUT"
|
||||
# Default log level, valid YAML values are ints and strings, see src/invidious/helpers/logger.cr
|
||||
|
@ -96,6 +94,10 @@ class Config
|
|||
# Database configuration using 12-Factor "Database URL" syntax
|
||||
@[YAML::Field(converter: Preferences::URIConverter)]
|
||||
property database_url : URI = URI.parse("")
|
||||
property redis_url : String?
|
||||
property redis_socket : String?
|
||||
# Use polling to keep decryption function up to date
|
||||
property decrypt_polling : Bool = false
|
||||
# Used for crawling channels: threads should check all videos uploaded by a channel
|
||||
property full_refresh : Bool = false
|
||||
|
||||
|
@ -104,10 +106,20 @@ class Config
|
|||
|
||||
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
|
||||
property https_only : Bool?
|
||||
# Enable or disable CSP
|
||||
property csp : Bool? = true
|
||||
# HMAC signing key for CSRF tokens and verifying pubsub subscriptions
|
||||
property hmac_key : String = ""
|
||||
# Domain to be used for links to resources on the site where an absolute URL is required
|
||||
property domain : String?
|
||||
# Materialious redirects
|
||||
property materialious_domain : String?
|
||||
# Alternative domains. You can add other domains, like TOR and I2P addresses
|
||||
property alternative_domains : Array(String) = [] of String
|
||||
property donation_url : String?
|
||||
property contact_url : String?
|
||||
property home_domain : String?
|
||||
|
||||
# Subscribe to channels using PubSubHubbub (requires domain, hmac_key)
|
||||
property use_pubsub_feeds : Bool | Int32 = false
|
||||
property popular_enabled : Bool = true
|
||||
|
@ -124,8 +136,6 @@ class Config
|
|||
property check_tables : Bool = false
|
||||
# Cache annotations requested from IA, will not cache empty annotations or annotations that only contain cards
|
||||
property cache_annotations : Bool = false
|
||||
# Optional banner to be displayed along top of page for announcements, etc.
|
||||
property banner : String? = nil
|
||||
# Enables 'Strict-Transport-Security'. Ensure that `domain` and all subdomains are served securely
|
||||
property hsts : Bool? = true
|
||||
# Disable proxying server-wide: options: 'dash', 'livestreams', 'downloads', 'local'
|
||||
|
@ -133,9 +143,23 @@ class Config
|
|||
# Enable the user notifications for all users
|
||||
property enable_user_notifications : Bool = true
|
||||
|
||||
# Optional banner to be displayed along top of page for announcements, etc.
|
||||
property banner : String? = nil
|
||||
# Optional footer text to be displayed within Invidious' footer. Can be used for maintainer contact info, etc.
|
||||
property footer : String? = nil
|
||||
# Email to contact the instance maintainer. This is used within the footer as an mailto link.
|
||||
property instance_maintainer_email : String? = nil
|
||||
# URL to the modified source code to be easily AGPL compliant
|
||||
# Will display in the footer, next to the main source code link
|
||||
# Will display in the footer
|
||||
property modified_source_code_url : String? = nil
|
||||
# Link to the terms of service of the instance (if any). Will be displayed in the footer.
|
||||
property footer_instance_tos_link : String? = nil
|
||||
# Link to the privacy policy of the instance (if any). Will be displayed in the footer.
|
||||
property footer_instance_privacy_policy_link : String? = nil
|
||||
# Instance donation URL displayed in the "Instance" section of the footer
|
||||
property footer_instance_donate_link : String? = nil
|
||||
# Custom fields to be displayed within the footer's instance section
|
||||
property footer_instance_section_custom_fields : Array(Array(String)) = [] of Array(String)
|
||||
|
||||
# Connect to YouTube over 'ipv6', 'ipv4'. Will sometimes resolve fix issues with rate-limiting (see https://github.com/ytdl-org/youtube-dl/issues/21729)
|
||||
@[YAML::Field(converter: Preferences::FamilyConverter)]
|
||||
|
@ -174,6 +198,32 @@ class Config
|
|||
# Playlist length limit
|
||||
property playlist_length_limit : Int32 = 500
|
||||
|
||||
# The max resolution the Instance can offer
|
||||
property max_dash_resolution : Int32?
|
||||
|
||||
# List of names of the backends
|
||||
property backends : Array(String) = [] of String
|
||||
# Character used to separate the backend number from the description/note
|
||||
# of the backend
|
||||
property backends_delimiter : String = "|"
|
||||
|
||||
# External videoplayback proxies list. They should include `https://`
|
||||
# at the start of the URI
|
||||
property external_videoplayback_proxy : Array(NamedTuple(url: String, balance: Bool)) = [] of NamedTuple(url: String, balance: Bool)
|
||||
|
||||
# Job to refresh tokens from a Redis compatible DB
|
||||
property refresh_tokens : Bool = true
|
||||
|
||||
property pubsub_domain : String = ""
|
||||
|
||||
property ignore_user_tokens : Bool = false
|
||||
|
||||
property server_id_cookie_name : String = "INVIDIOUS_SERVER_ID"
|
||||
|
||||
{% if flag?(:linux) %}
|
||||
property reload_config_automatically : Bool = true
|
||||
{% end %}
|
||||
|
||||
def disabled?(option)
|
||||
case disabled = CONFIG.disable_proxy
|
||||
when Bool
|
||||
|
@ -189,6 +239,64 @@ class Config
|
|||
end
|
||||
end
|
||||
|
||||
def self.reload
|
||||
LOGGER.info("Config: Reloading configuration")
|
||||
# Load config from file or YAML string env var
|
||||
env_config_file = "INVIDIOUS_CONFIG_FILE"
|
||||
env_config_yaml = "INVIDIOUS_CONFIG"
|
||||
|
||||
config_file = ENV.has_key?(env_config_file) ? ENV.fetch(env_config_file) : "config/config.yml"
|
||||
config_yaml = ENV.has_key?(env_config_yaml) ? ENV.fetch(env_config_yaml) : File.read(config_file)
|
||||
|
||||
begin
|
||||
config = Config.from_yaml(config_yaml)
|
||||
rescue ex
|
||||
LOGGER.error("Config: Error when reloading configuration: '#{ex.message}'")
|
||||
config = CONFIG
|
||||
end
|
||||
|
||||
# TODO: Preserve old config and don't exit on fail
|
||||
{% for ivar in Config.instance_vars %}
|
||||
CONFIG.{{ivar}} = config.{{ivar}}
|
||||
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
|
||||
|
||||
if ENV.has_key?({{env_id}})
|
||||
env_value = ENV.fetch({{env_id}})
|
||||
success = false
|
||||
|
||||
# Use YAML converter if specified
|
||||
{% ann = ivar.annotation(::YAML::Field) %}
|
||||
{% if ann && ann[:converter] %}
|
||||
CONFIG.{{ivar.id}} = {{ann[:converter]}}.from_yaml(YAML::ParseContext.new, YAML::Nodes.parse(ENV.fetch({{env_id}})).nodes[0])
|
||||
success = true
|
||||
|
||||
# Use regular YAML parser otherwise
|
||||
{% else %}
|
||||
{% ivar_types = ivar.type.union? ? ivar.type.union_types : [ivar.type] %}
|
||||
# Sort types to avoid parsing nulls and numbers as strings
|
||||
{% ivar_types = ivar_types.sort_by { |ivar_type| ivar_type == Nil ? 0 : ivar_type == Int32 ? 1 : 2 } %}
|
||||
{{ivar_types}}.each do |ivar_type|
|
||||
if !success
|
||||
begin
|
||||
CONFIG.{{ivar.id}} = ivar_type.from_yaml(env_value)
|
||||
success = true
|
||||
rescue
|
||||
# nop
|
||||
end
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
|
||||
# Exit on fail
|
||||
if !success
|
||||
LOGGER.error("Config: Error when reloading environment variables for the configuration, exiting (fixme!)")
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
LOGGER.info("Config: Reload successfull")
|
||||
end
|
||||
|
||||
def self.load
|
||||
# Load config from file or YAML string env var
|
||||
env_config_file = "INVIDIOUS_CONFIG_FILE"
|
||||
|
|
|
@ -154,15 +154,17 @@ module Invidious::Database::Users
|
|||
# Update (misc)
|
||||
# -------------------
|
||||
|
||||
def feed_needs_update(video : ChannelVideo)
|
||||
request = <<-SQL
|
||||
UPDATE users
|
||||
SET feed_needs_update = true
|
||||
WHERE $1 = ANY(subscriptions)
|
||||
SQL
|
||||
# Feeds never need update. PubSubHubBub is the one that sends videos to
|
||||
# invidious.
|
||||
# def feed_needs_update(video : ChannelVideo)
|
||||
# request = <<-SQL
|
||||
# UPDATE users
|
||||
# SET feed_needs_update = true
|
||||
# WHERE $1 = ANY(subscriptions)
|
||||
# SQL
|
||||
|
||||
PG_DB.exec(request, video.ucid)
|
||||
end
|
||||
# PG_DB.exec(request, video.ucid)
|
||||
# end
|
||||
|
||||
def update_preferences(user : User)
|
||||
request = <<-SQL
|
||||
|
|
|
@ -10,7 +10,8 @@ module Invidious::Database::Videos
|
|||
ON CONFLICT (id) DO NOTHING
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, video.id, video.info.to_json, video.updated)
|
||||
REDIS_DB.set(video.id, video.info.to_json, ex: 14400)
|
||||
REDIS_DB.set(video.id + ":time", video.updated, ex: 14400)
|
||||
end
|
||||
|
||||
def delete(id)
|
||||
|
@ -19,7 +20,8 @@ module Invidious::Database::Videos
|
|||
WHERE id = $1
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, id)
|
||||
REDIS_DB.del(id)
|
||||
REDIS_DB.del(id + ":time")
|
||||
end
|
||||
|
||||
def delete_expired
|
||||
|
@ -47,6 +49,14 @@ module Invidious::Database::Videos
|
|||
WHERE id = $1
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, id, as: Video)
|
||||
if ((info = REDIS_DB.get(id)) && (time = REDIS_DB.get(id + ":time")))
|
||||
return Video.new({
|
||||
id: id,
|
||||
info: JSON.parse(info).as_h,
|
||||
updated: Time.parse(time, "%Y-%m-%d %H:%M:%S %z", Time::Location::UTC),
|
||||
})
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,8 +48,9 @@ module JSON::Serializable
|
|||
end
|
||||
end
|
||||
|
||||
macro templated(_filename, template = "template", navbar_search = true)
|
||||
macro templated(_filename, template = "template", navbar_search = true, buffer_footer = false)
|
||||
navbar_search = {{navbar_search}}
|
||||
buffer_footer = {{buffer_footer}}
|
||||
|
||||
{{ filename = "src/invidious/views/" + _filename + ".ecr" }}
|
||||
{{ layout = "src/invidious/views/" + template + ".ecr" }}
|
||||
|
|
36
src/invidious/helpers/redis_tokens.cr
Normal file
36
src/invidious/helpers/redis_tokens.cr
Normal file
|
@ -0,0 +1,36 @@
|
|||
module Tokens
|
||||
extend self
|
||||
@@po_token : String | Nil
|
||||
@@visitor_data : String | Nil
|
||||
|
||||
def refresh_tokens
|
||||
@@po_token = REDIS_DB.get("invidious:po_token")
|
||||
@@visitor_data = REDIS_DB.get("invidious:visitor_data")
|
||||
if !@@po_token.nil? && !@@visitor_data.nil?
|
||||
set_tokens
|
||||
LOGGER.debug("RefreshTokens: Successfully updated po_token and visitor_data")
|
||||
else
|
||||
LOGGER.warn("RefreshTokens: Tokens are empty!")
|
||||
end
|
||||
LOGGER.trace("RefreshTokens: Tokens are:")
|
||||
LOGGER.trace("RefreshTokens: po_token: #{CONFIG.po_token}")
|
||||
LOGGER.trace("RefreshTokens: visitor_data: #{CONFIG.visitor_data}")
|
||||
end
|
||||
|
||||
def set_tokens
|
||||
CONFIG.po_token = @@po_token
|
||||
CONFIG.visitor_data = @@visitor_data
|
||||
end
|
||||
|
||||
def get_tokens
|
||||
return {@@po_token, @@visitor_data}
|
||||
end
|
||||
|
||||
def get_po_token
|
||||
return @@po_token
|
||||
end
|
||||
|
||||
def get_visitor_data
|
||||
return @@visitor_data
|
||||
end
|
||||
end
|
|
@ -294,7 +294,7 @@ def subscribe_pubsub(topic, key)
|
|||
signature = "#{time}:#{nonce}"
|
||||
|
||||
body = {
|
||||
"hub.callback" => "#{HOST_URL}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}",
|
||||
"hub.callback" => "#{PUBSUB_HOST_URL}/feed/webhook/v1:#{time}:#{nonce}:#{OpenSSL::HMAC.hexdigest(:sha1, key, signature)}",
|
||||
"hub.topic" => "https://www.youtube.com/xml/feeds/videos.xml?#{topic}",
|
||||
"hub.verify" => "async",
|
||||
"hub.mode" => "subscribe",
|
||||
|
@ -384,6 +384,20 @@ def parse_link_endpoint(endpoint : JSON::Any, text : String, video_id : String)
|
|||
return text
|
||||
end
|
||||
|
||||
# Generates a list of external videoplayback proxies for
|
||||
# CSP
|
||||
def gen_videoplayback_proxy_list
|
||||
if !CONFIG.external_videoplayback_proxy.empty?
|
||||
external_videoplayback_proxy = ""
|
||||
CONFIG.external_videoplayback_proxy.each do |proxy|
|
||||
external_videoplayback_proxy += " #{proxy[:url]}"
|
||||
end
|
||||
else
|
||||
external_videoplayback_proxy = ""
|
||||
end
|
||||
return external_videoplayback_proxy
|
||||
end
|
||||
|
||||
def encrypt_ecb_without_salt(data, key)
|
||||
cipher = OpenSSL::Cipher.new("aes-128-ecb")
|
||||
cipher.encrypt
|
||||
|
|
|
@ -4,6 +4,30 @@ module Invidious::HttpServer
|
|||
module Utils
|
||||
extend self
|
||||
|
||||
@@proxy_alive : String = ""
|
||||
|
||||
def check_external_proxy
|
||||
CONFIG.external_videoplayback_proxy.each do |proxy|
|
||||
begin
|
||||
response = HTTP::Client.get("#{proxy[:url]}/health")
|
||||
if response.status_code == 200
|
||||
@@proxy_alive = proxy[:url]
|
||||
LOGGER.debug("CheckExternalProxy: Proxy set to: '#{proxy[:url]}'")
|
||||
break
|
||||
end
|
||||
rescue
|
||||
LOGGER.debug("CheckExternalProxy: Proxy '#{proxy[:url]}' is not available")
|
||||
end
|
||||
end
|
||||
if @@proxy_alive.empty?
|
||||
LOGGER.warn("CheckExternalProxy: No proxies alive! Using own server proxy")
|
||||
end
|
||||
end
|
||||
|
||||
def get_external_proxy
|
||||
return @@proxy_alive
|
||||
end
|
||||
|
||||
def proxy_video_url(raw_url : String, *, region : String? = nil, absolute : Bool = false)
|
||||
url = URI.parse(raw_url)
|
||||
|
||||
|
@ -14,7 +38,11 @@ module Invidious::HttpServer
|
|||
url.query_params = params
|
||||
|
||||
if absolute
|
||||
return "#{HOST_URL}#{url.request_target}"
|
||||
if !@@proxy_alive.empty?
|
||||
return "#{@@proxy_alive}#{url.request_target}"
|
||||
else
|
||||
return "#{HOST_URL}#{url.request_target}"
|
||||
end
|
||||
else
|
||||
return url.request_target
|
||||
end
|
||||
|
|
13
src/invidious/jobs/check_external_proxy.cr
Normal file
13
src/invidious/jobs/check_external_proxy.cr
Normal file
|
@ -0,0 +1,13 @@
|
|||
class Invidious::Jobs::CheckExternalProxy < Invidious::Jobs::BaseJob
|
||||
def initialize
|
||||
end
|
||||
|
||||
def begin
|
||||
loop do
|
||||
HttpServer::Utils.check_external_proxy
|
||||
LOGGER.info("CheckExternalProxy: Done, sleeping for 10 seconds")
|
||||
sleep 10.seconds
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,75 +0,0 @@
|
|||
class Invidious::Jobs::RefreshFeedsJob < Invidious::Jobs::BaseJob
|
||||
private getter db : DB::Database
|
||||
|
||||
def initialize(@db)
|
||||
end
|
||||
|
||||
def begin
|
||||
max_fibers = CONFIG.feed_threads
|
||||
active_fibers = 0
|
||||
active_channel = ::Channel(Bool).new
|
||||
|
||||
loop do
|
||||
db.query("SELECT email FROM users WHERE feed_needs_update = true OR feed_needs_update IS NULL") do |rs|
|
||||
rs.each do
|
||||
email = rs.read(String)
|
||||
view_name = "subscriptions_#{sha256(email)}"
|
||||
|
||||
if active_fibers >= max_fibers
|
||||
if active_channel.receive
|
||||
active_fibers -= 1
|
||||
end
|
||||
end
|
||||
|
||||
active_fibers += 1
|
||||
spawn do
|
||||
begin
|
||||
# Drop outdated views
|
||||
column_array = Invidious::Database.get_column_array(db, view_name)
|
||||
ChannelVideo.type_array.each_with_index do |name, i|
|
||||
if name != column_array[i]?
|
||||
LOGGER.info("RefreshFeedsJob: DROP MATERIALIZED VIEW #{view_name}")
|
||||
db.exec("DROP MATERIALIZED VIEW #{view_name}")
|
||||
raise "view does not exist"
|
||||
end
|
||||
end
|
||||
|
||||
if !db.query_one("SELECT pg_get_viewdef('#{view_name}')", as: String).includes? "WHERE ((cv.ucid = ANY (u.subscriptions))"
|
||||
LOGGER.info("RefreshFeedsJob: Materialized view #{view_name} is out-of-date, recreating...")
|
||||
db.exec("DROP MATERIALIZED VIEW #{view_name}")
|
||||
end
|
||||
|
||||
db.exec("REFRESH MATERIALIZED VIEW #{view_name}")
|
||||
db.exec("UPDATE users SET feed_needs_update = false WHERE email = $1", email)
|
||||
rescue ex
|
||||
# Rename old views
|
||||
begin
|
||||
legacy_view_name = "subscriptions_#{sha256(email)[0..7]}"
|
||||
|
||||
db.exec("SELECT * FROM #{legacy_view_name} LIMIT 0")
|
||||
LOGGER.info("RefreshFeedsJob: RENAME MATERIALIZED VIEW #{legacy_view_name}")
|
||||
db.exec("ALTER MATERIALIZED VIEW #{legacy_view_name} RENAME TO #{view_name}")
|
||||
rescue ex
|
||||
begin
|
||||
# While iterating through, we may have an email stored from a deleted account
|
||||
if db.query_one?("SELECT true FROM users WHERE email = $1", email, as: Bool)
|
||||
LOGGER.info("RefreshFeedsJob: CREATE #{view_name}")
|
||||
db.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(email)}")
|
||||
db.exec("UPDATE users SET feed_needs_update = false WHERE email = $1", email)
|
||||
end
|
||||
rescue ex
|
||||
LOGGER.error("RefreshFeedJobs: REFRESH #{email} : #{ex.message}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
active_channel.send(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
sleep 5.seconds
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
end
|
13
src/invidious/jobs/refresh_tokens.cr
Normal file
13
src/invidious/jobs/refresh_tokens.cr
Normal file
|
@ -0,0 +1,13 @@
|
|||
class Invidious::Jobs::RefreshTokens < Invidious::Jobs::BaseJob
|
||||
def initialize
|
||||
end
|
||||
|
||||
def begin
|
||||
loop do
|
||||
Tokens.refresh_tokens
|
||||
LOGGER.info("RefreshTokens: Done, sleeping for 5 seconds")
|
||||
sleep 5.seconds
|
||||
Fiber.yield
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,6 +30,8 @@ class Invidious::Jobs::SubscribeToFeedsJob < Invidious::Jobs::BaseJob
|
|||
spawn do
|
||||
begin
|
||||
response = subscribe_pubsub(ucid, hmac_key)
|
||||
LOGGER.debug("SubscribeToFeedsJob: Subscribed to #{ucid}.")
|
||||
LOGGER.trace("SubscribeToFeedsJob: response.body: #{response.body}")
|
||||
|
||||
if response.status_code >= 400
|
||||
LOGGER.error("SubscribeToFeedsJob: #{ucid} : #{response.body}")
|
||||
|
|
|
@ -123,10 +123,8 @@ module Invidious::Routes::Account
|
|||
return error_template(400, ex)
|
||||
end
|
||||
|
||||
view_name = "subscriptions_#{sha256(user.email)}"
|
||||
Invidious::Database::Users.delete(user)
|
||||
Invidious::Database::SessionIDs.delete(email: user.email)
|
||||
PG_DB.exec("DROP MATERIALIZED VIEW #{view_name}")
|
||||
|
||||
env.request.cookies.each do |cookie|
|
||||
cookie.expires = Time.utc(1990, 1, 1)
|
||||
|
|
|
@ -32,34 +32,38 @@ module Invidious::Routes::API::Manifest
|
|||
haltf env, status_code: response.status_code
|
||||
end
|
||||
|
||||
manifest = response.body
|
||||
|
||||
manifest = manifest.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
||||
url = baseurl.lchop("<BaseURL>")
|
||||
url = url.rchop("</BaseURL>")
|
||||
|
||||
if local
|
||||
uri = URI.parse(url)
|
||||
url = "#{HOST_URL}#{uri.request_target}host/#{uri.host}/"
|
||||
end
|
||||
|
||||
# Proxy URLs for video playback on invidious.
|
||||
# Other API clients can get the original URLs by omiting `local=true`.
|
||||
manifest = response.body.gsub(/<BaseURL>[^<]+<\/BaseURL>/) do |baseurl|
|
||||
url = baseurl.lchop("<BaseURL>").rchop("</BaseURL>")
|
||||
url = HttpServer::Utils.proxy_video_url(url, absolute: true) if local
|
||||
"<BaseURL>#{url}</BaseURL>"
|
||||
end
|
||||
|
||||
return manifest
|
||||
end
|
||||
|
||||
adaptive_fmts = video.adaptive_fmts
|
||||
|
||||
# Ditto, only proxify URLs if `local=true` is used
|
||||
if local
|
||||
adaptive_fmts.each do |fmt|
|
||||
fmt["url"] = JSON::Any.new("#{HOST_URL}#{URI.parse(fmt["url"].as_s).request_target}")
|
||||
video.adaptive_fmts.each do |fmt|
|
||||
fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s, absolute: true))
|
||||
end
|
||||
end
|
||||
|
||||
audio_streams = video.audio_streams.sort_by { |stream| {stream["bitrate"].as_i} }.reverse!
|
||||
video_streams = video.video_streams.sort_by { |stream| {stream["width"].as_i, stream["fps"].as_i} }.reverse!
|
||||
|
||||
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
|
||||
if CONFIG.max_dash_resolution
|
||||
video_streams.reject! do |z|
|
||||
(z["height"].as_i > CONFIG.max_dash_resolution.not_nil!) if z["height"]?
|
||||
end
|
||||
end
|
||||
|
||||
audio_streams.reject! do |z|
|
||||
z if z.dig?("audioTrack", "audioIsDefault") == false
|
||||
end
|
||||
|
||||
manifest = XML.build(indent: " ", encoding: "UTF-8") do |xml|
|
||||
xml.element("MPD", "xmlns": "urn:mpeg:dash:schema:mpd:2011",
|
||||
"profiles": "urn:mpeg:dash:profile:full:2011", minBufferTime: "PT1.5S", type: "static",
|
||||
|
@ -182,8 +186,9 @@ module Invidious::Routes::API::Manifest
|
|||
manifest = response.body
|
||||
|
||||
if local
|
||||
manifest = manifest.gsub(/^https:\/\/\w+---.{11}\.c\.youtube\.com[^\n]*/m) do |match|
|
||||
path = URI.parse(match).path
|
||||
manifest = manifest.gsub(/^https:\/\/[^\n"]*/m) do |match|
|
||||
uri = URI.parse(match)
|
||||
path = uri.path
|
||||
|
||||
path = path.lchop("/videoplayback/")
|
||||
path = path.rchop("/")
|
||||
|
@ -212,9 +217,15 @@ module Invidious::Routes::API::Manifest
|
|||
raw_params["fvip"] = fvip["fvip"]
|
||||
end
|
||||
|
||||
raw_params["local"] = "true"
|
||||
raw_params["host"] = uri.host.not_nil!
|
||||
|
||||
"#{HOST_URL}/videoplayback?#{raw_params}"
|
||||
proxy = Invidious::HttpServer::Utils.get_external_proxy
|
||||
|
||||
if !proxy.empty?
|
||||
"#{proxy}/videoplayback?#{raw_params}"
|
||||
else
|
||||
"#{HOST_URL}/videoplayback?#{raw_params}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -263,59 +263,60 @@ module Invidious::Routes::API::V1::Videos
|
|||
|
||||
annotations = ""
|
||||
|
||||
case source
|
||||
when "archive"
|
||||
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
|
||||
annotations = cached_annotation.annotations
|
||||
else
|
||||
index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
|
||||
# case source
|
||||
# when "archive"
|
||||
# if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
|
||||
# annotations = cached_annotation.annotations
|
||||
# else
|
||||
# index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
|
||||
|
||||
# IA doesn't handle leading hyphens,
|
||||
# so we use https://archive.org/details/youtubeannotations_64
|
||||
if index == "62"
|
||||
index = "64"
|
||||
id = id.sub(/^-/, 'A')
|
||||
end
|
||||
# # IA doesn't handle leading hyphens,
|
||||
# # so we use https://archive.org/details/youtubeannotations_64
|
||||
# if index == "62"
|
||||
# index = "64"
|
||||
# id = id.sub(/^-/, 'A')
|
||||
# end
|
||||
|
||||
file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
|
||||
# file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
|
||||
|
||||
location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
||||
# location = make_client(ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
||||
|
||||
if !location.headers["Location"]?
|
||||
env.response.status_code = location.status_code
|
||||
end
|
||||
# if !location.headers["Location"]?
|
||||
# env.response.status_code = location.status_code
|
||||
# end
|
||||
|
||||
response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"]))
|
||||
# response = make_client(URI.parse(location.headers["Location"]), &.get(location.headers["Location"]))
|
||||
|
||||
if response.body.empty?
|
||||
haltf env, 404
|
||||
end
|
||||
# if response.body.empty?
|
||||
# haltf env, 404
|
||||
# end
|
||||
|
||||
if response.status_code != 200
|
||||
haltf env, response.status_code
|
||||
end
|
||||
# if response.status_code != 200
|
||||
# haltf env, response.status_code
|
||||
# end
|
||||
|
||||
annotations = response.body
|
||||
# annotations = response.body
|
||||
|
||||
cache_annotation(id, annotations)
|
||||
end
|
||||
else # "youtube"
|
||||
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
|
||||
# cache_annotation(id, annotations)
|
||||
# end
|
||||
# else # "youtube"
|
||||
# response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
|
||||
|
||||
if response.status_code != 200
|
||||
haltf env, response.status_code
|
||||
end
|
||||
# if response.status_code != 200
|
||||
# haltf env, response.status_code
|
||||
# end
|
||||
|
||||
annotations = response.body
|
||||
end
|
||||
# annotations = response.body
|
||||
# end
|
||||
|
||||
etag = sha256(annotations)[0, 16]
|
||||
if env.request.headers["If-None-Match"]?.try &.== etag
|
||||
haltf env, 304
|
||||
else
|
||||
env.response.headers["ETag"] = etag
|
||||
annotations
|
||||
end
|
||||
# etag = sha256(annotations)[0, 16]
|
||||
# if env.request.headers["If-None-Match"]?.try &.== etag
|
||||
# haltf env, 304
|
||||
# else
|
||||
# env.response.headers["ETag"] = etag
|
||||
# annotations
|
||||
# end
|
||||
annotations
|
||||
end
|
||||
|
||||
def self.comments(env)
|
||||
|
|
|
@ -43,9 +43,9 @@ module Invidious::Routes::BeforeAll
|
|||
"style-src 'self' 'unsafe-inline'",
|
||||
"img-src 'self' data:",
|
||||
"font-src 'self' data:",
|
||||
"connect-src 'self'",
|
||||
"connect-src 'self'" + EXT_VIDEOP_LIST,
|
||||
"manifest-src 'self'",
|
||||
"media-src 'self' blob:" + extra_media_csp,
|
||||
"media-src 'self' blob:" + extra_media_csp + EXT_VIDEOP_LIST,
|
||||
"child-src 'self' blob:",
|
||||
"frame-src 'self'",
|
||||
"frame-ancestors " + frame_ancestors,
|
||||
|
|
|
@ -154,7 +154,7 @@ module Invidious::Routes::Channels
|
|||
items.each(&.author = "")
|
||||
|
||||
selected_tab = Frontend::ChannelPage::TabsAvailable::Playlists
|
||||
templated "channel"
|
||||
templated "channel", buffer_footer: true
|
||||
end
|
||||
|
||||
def self.podcasts(env)
|
||||
|
|
|
@ -157,10 +157,12 @@ module Invidious::Routes::Embed
|
|||
adaptive_fmts = video.adaptive_fmts
|
||||
|
||||
if params.local
|
||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||
end
|
||||
|
||||
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||
|
||||
video_streams = video.video_streams
|
||||
audio_streams = video.audio_streams
|
||||
|
||||
|
@ -206,6 +208,12 @@ module Invidious::Routes::Embed
|
|||
env.response.headers["Content-Security-Policy"]
|
||||
.gsub("media-src", "media-src #{companion_base_url}")
|
||||
.gsub("connect-src", "connect-src #{companion_base_url}")
|
||||
if external_videoplayback_proxy = video.invidious_companion.dig?("external_videoplayback_proxy").try &.as_s
|
||||
env.response.headers["Content-Security-Policy"] =
|
||||
env.response.headers["Content-Security-Policy"]
|
||||
.gsub("media-src #{companion_base_url}", "media-src #{companion_base_url} #{external_videoplayback_proxy}")
|
||||
.gsub("connect-src #{companion_base_url}", "connect-src #{companion_base_url} #{external_videoplayback_proxy}")
|
||||
end
|
||||
end
|
||||
|
||||
rendered "embed"
|
||||
|
|
|
@ -450,8 +450,8 @@ module Invidious::Routes::Feeds
|
|||
if was_insert
|
||||
if CONFIG.enable_user_notifications
|
||||
Invidious::Database::Users.add_notification(video)
|
||||
else
|
||||
Invidious::Database::Users.feed_needs_update(video)
|
||||
# else
|
||||
# Invidious::Database::Users.feed_needs_update(video)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -60,7 +60,13 @@ module Invidious::Routes::Login
|
|||
sid = Base64.urlsafe_encode(Random::Secure.random_bytes(32))
|
||||
Invidious::Database::SessionIDs.insert(sid, email)
|
||||
|
||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||
# Checks if there is any alternative domain, like a second domain name,
|
||||
# TOR or I2P address
|
||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
|
||||
else
|
||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||
end
|
||||
else
|
||||
return error_template(401, "Wrong username or password")
|
||||
end
|
||||
|
@ -160,10 +166,13 @@ module Invidious::Routes::Login
|
|||
Invidious::Database::Users.insert(user)
|
||||
Invidious::Database::SessionIDs.insert(sid, email)
|
||||
|
||||
view_name = "subscriptions_#{sha256(user.email)}"
|
||||
PG_DB.exec("CREATE MATERIALIZED VIEW #{view_name} AS #{MATERIALIZED_VIEW_SQL.call(user.email)}")
|
||||
|
||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||
# Checks if there is any alternative domain, like a second domain name,
|
||||
# TOR or I2P address
|
||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.alternative_domains[alt], sid)
|
||||
else
|
||||
env.response.cookies["SID"] = Invidious::User::Cookies.sid(CONFIG.domain, sid)
|
||||
end
|
||||
|
||||
if env.request.cookies["PREFS"]?
|
||||
user.preferences = env.get("preferences").as(Preferences)
|
||||
|
|
|
@ -23,6 +23,12 @@ module Invidious::Routes::Misc
|
|||
else
|
||||
env.redirect "/feed/popular"
|
||||
end
|
||||
when "History"
|
||||
if user
|
||||
env.redirect "/feed/history"
|
||||
else
|
||||
env.redirect "/feed/popular"
|
||||
end
|
||||
else
|
||||
templated "search_homepage", navbar_search: false
|
||||
end
|
||||
|
|
|
@ -8,7 +8,7 @@ module Invidious::Routes::PreferencesRoute
|
|||
|
||||
preferences = env.get("preferences").as(Preferences)
|
||||
|
||||
templated "user/preferences"
|
||||
templated "user/preferences", buffer_footer: true
|
||||
end
|
||||
|
||||
def self.update(env)
|
||||
|
@ -103,7 +103,7 @@ module Invidious::Routes::PreferencesRoute
|
|||
default_home = env.params.body["default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
|
||||
|
||||
feed_menu = [] of String
|
||||
4.times do |index|
|
||||
5.times do |index|
|
||||
option = env.params.body["feed_menu[#{index}]"]?.try &.as(String) || ""
|
||||
if !option.empty?
|
||||
feed_menu << option
|
||||
|
@ -191,7 +191,7 @@ module Invidious::Routes::PreferencesRoute
|
|||
CONFIG.default_user_preferences.default_home = env.params.body["admin_default_home"]?.try &.as(String) || CONFIG.default_user_preferences.default_home
|
||||
|
||||
admin_feed_menu = [] of String
|
||||
4.times do |index|
|
||||
5.times do |index|
|
||||
option = env.params.body["admin_feed_menu[#{index}]"]?.try &.as(String) || ""
|
||||
if !option.empty?
|
||||
admin_feed_menu << option
|
||||
|
@ -224,7 +224,13 @@ module Invidious::Routes::PreferencesRoute
|
|||
File.write("config/config.yml", CONFIG.to_yaml)
|
||||
end
|
||||
else
|
||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
||||
# Checks if there is any alternative domain, like a second domain name,
|
||||
# TOR or I2P address
|
||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
|
||||
else
|
||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
||||
end
|
||||
end
|
||||
|
||||
env.redirect referer
|
||||
|
@ -259,7 +265,13 @@ module Invidious::Routes::PreferencesRoute
|
|||
preferences.dark_mode = "dark"
|
||||
end
|
||||
|
||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
||||
# Checks if there is any alternative domain, like a second domain name,
|
||||
# TOR or I2P address
|
||||
if alt = CONFIG.alternative_domains.index(env.request.headers["Host"])
|
||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.alternative_domains[alt], preferences)
|
||||
else
|
||||
env.response.cookies["PREFS"] = Invidious::User::Cookies.prefs(CONFIG.domain, preferences)
|
||||
end
|
||||
end
|
||||
|
||||
if redirect
|
||||
|
|
|
@ -3,6 +3,11 @@ module Invidious::Routes::VideoPlayback
|
|||
def self.get_video_playback(env)
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
query_params = env.params.query
|
||||
array = UInt8[0x78, 0]
|
||||
protobuf = Bytes.new(array.size)
|
||||
array.each_with_index do |byte, index|
|
||||
protobuf[index] = byte
|
||||
end
|
||||
|
||||
fvip = query_params["fvip"]? || "3"
|
||||
mns = query_params["mn"]?.try &.split(",")
|
||||
|
@ -96,7 +101,7 @@ module Invidious::Routes::VideoPlayback
|
|||
end
|
||||
|
||||
begin
|
||||
client.get(url, headers) do |resp|
|
||||
client.post(url, headers, protobuf) do |resp|
|
||||
resp.headers.each do |key, value|
|
||||
if !RESPONSE_HEADERS_BLACKLIST.includes?(key.downcase)
|
||||
env.response.headers[key] = value
|
||||
|
@ -147,7 +152,7 @@ module Invidious::Routes::VideoPlayback
|
|||
headers["Range"] = "bytes=#{chunk_start}-#{chunk_end}"
|
||||
|
||||
begin
|
||||
client.get(url, headers) do |resp|
|
||||
client.post(url, headers, protobuf) do |resp|
|
||||
if first_chunk
|
||||
if !env.request.headers["Range"]? && resp.status_code == 206
|
||||
env.response.status_code = 200
|
||||
|
@ -299,7 +304,16 @@ module Invidious::Routes::VideoPlayback
|
|||
end
|
||||
|
||||
if local
|
||||
url = URI.parse(url).request_target.not_nil!
|
||||
external_proxy = Invidious::HttpServer::Utils.get_external_proxy
|
||||
if !external_proxy.empty?
|
||||
url = URI.parse(url)
|
||||
external_proxy = URI.parse(external_proxy)
|
||||
url.host = external_proxy.host
|
||||
url.port = external_proxy.port
|
||||
url = url.to_s
|
||||
else
|
||||
url = URI.parse(url).request_target.not_nil!
|
||||
end
|
||||
url += "&title=#{URI.encode_www_form(title, space_to_plus: false)}" if title
|
||||
end
|
||||
|
||||
|
|
|
@ -120,14 +120,35 @@ module Invidious::Routes::Watch
|
|||
fmt_stream = video.fmt_stream
|
||||
adaptive_fmts = video.adaptive_fmts
|
||||
|
||||
if params.local
|
||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(URI.parse(fmt["url"].as_s).request_target) }
|
||||
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
|
||||
if CONFIG.max_dash_resolution
|
||||
adaptive_fmts.reject! do |z|
|
||||
(z["height"].as_i > CONFIG.max_dash_resolution.not_nil!) if z["height"]?
|
||||
end
|
||||
end
|
||||
|
||||
if params.local
|
||||
fmt_stream.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||
end
|
||||
|
||||
# Always proxy DASH streams, otherwise youtube CORS headers will prevent playback
|
||||
adaptive_fmts.each { |fmt| fmt["url"] = JSON::Any.new(HttpServer::Utils.proxy_video_url(fmt["url"].as_s)) }
|
||||
|
||||
video_streams = video.video_streams
|
||||
audio_streams = video.audio_streams
|
||||
|
||||
# Removes all the resolutions with a height higher than CONFIG.max_dash_resolution
|
||||
if CONFIG.max_dash_resolution
|
||||
video_streams.reject! do |z|
|
||||
(z["height"].as_i > CONFIG.max_dash_resolution.not_nil!) if z["height"]?
|
||||
end
|
||||
end
|
||||
|
||||
# Removes non default audio tracks
|
||||
audio_streams.reject! do |z|
|
||||
z if z.dig?("audioTrack", "audioIsDefault") == false
|
||||
end
|
||||
|
||||
# Older videos may not have audio sources available.
|
||||
# We redirect here so they're not unplayable
|
||||
if audio_streams.empty? && !video.live_now
|
||||
|
@ -190,11 +211,23 @@ module Invidious::Routes::Watch
|
|||
captions: video.captions
|
||||
)
|
||||
|
||||
begin
|
||||
video_url = fmt_stream[0]["url"].to_s
|
||||
rescue
|
||||
video_url = nil
|
||||
end
|
||||
|
||||
if companion_base_url = video.invidious_companion.try &.["baseUrl"].as_s
|
||||
env.response.headers["Content-Security-Policy"] =
|
||||
env.response.headers["Content-Security-Policy"]
|
||||
.gsub("media-src", "media-src #{companion_base_url}")
|
||||
.gsub("connect-src", "connect-src #{companion_base_url}")
|
||||
if external_videoplayback_proxy = video.invidious_companion.dig?("external_videoplayback_proxy").try &.as_s
|
||||
env.response.headers["Content-Security-Policy"] =
|
||||
env.response.headers["Content-Security-Policy"]
|
||||
.gsub("media-src #{companion_base_url}", "media-src #{companion_base_url} #{external_videoplayback_proxy}")
|
||||
.gsub("connect-src #{companion_base_url}", "connect-src #{companion_base_url} #{external_videoplayback_proxy}")
|
||||
end
|
||||
end
|
||||
|
||||
templated "watch"
|
||||
|
|
|
@ -37,18 +37,18 @@ module Invidious::Search
|
|||
|
||||
# Search inside of user subscriptions
|
||||
def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo)
|
||||
view_name = "subscriptions_#{sha256(user.email)}"
|
||||
|
||||
return PG_DB.query_all("
|
||||
SELECT id,title,published,updated,ucid,author,length_seconds
|
||||
FROM (
|
||||
SELECT *,
|
||||
to_tsvector(#{view_name}.title) ||
|
||||
to_tsvector(#{view_name}.author)
|
||||
as document
|
||||
FROM #{view_name}
|
||||
) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;",
|
||||
query.text, (query.page - 1) * 20,
|
||||
SELECT cv.*,
|
||||
to_tsvector(cv.title) ||
|
||||
to_tsvector(cv.author) AS document
|
||||
FROM channel_videos cv
|
||||
JOIN users ON cv.ucid = any(users.subscriptions)
|
||||
WHERE users.email = $1 AND published > now() - interval '1 month'
|
||||
ORDER BY published
|
||||
) v_search WHERE v_search.document @@ plainto_tsquery($2) LIMIT 20 OFFSET $3;",
|
||||
user.email, query.text, (query.page - 1) * 20,
|
||||
as: ChannelVideo
|
||||
)
|
||||
end
|
||||
|
|
|
@ -6,17 +6,22 @@ struct Invidious::User
|
|||
|
||||
# Note: we use ternary operator because the two variables
|
||||
# used in here are not booleans.
|
||||
SECURE = (Kemal.config.ssl || CONFIG.https_only) ? true : false
|
||||
@@secure = (Kemal.config.ssl || CONFIG.https_only) ? true : false
|
||||
|
||||
# Session ID (SID) cookie
|
||||
# Parameter "domain" comes from the global config
|
||||
def sid(domain : String?, sid) : HTTP::Cookie
|
||||
# Not secure if it's being accessed from I2P
|
||||
# Browsers expect the domain to include https. On I2P there is no HTTPS
|
||||
if domain.not_nil!.split(".").last == "i2p"
|
||||
@@secure = false
|
||||
end
|
||||
return HTTP::Cookie.new(
|
||||
name: "SID",
|
||||
domain: domain,
|
||||
value: sid,
|
||||
expires: Time.utc + 2.years,
|
||||
secure: SECURE,
|
||||
secure: @@secure,
|
||||
http_only: true,
|
||||
samesite: HTTP::Cookie::SameSite::Lax
|
||||
)
|
||||
|
@ -25,12 +30,17 @@ struct Invidious::User
|
|||
# Preferences (PREFS) cookie
|
||||
# Parameter "domain" comes from the global config
|
||||
def prefs(domain : String?, preferences : Preferences) : HTTP::Cookie
|
||||
# Not secure if it's being accessed from I2P
|
||||
# Browsers expect the domain to include https. On I2P there is no HTTPS
|
||||
if domain.not_nil!.split(".").last == "i2p"
|
||||
@@secure = false
|
||||
end
|
||||
return HTTP::Cookie.new(
|
||||
name: "PREFS",
|
||||
domain: domain,
|
||||
value: URI.encode_www_form(preferences.to_json),
|
||||
expires: Time.utc + 2.years,
|
||||
secure: SECURE,
|
||||
secure: @@secure,
|
||||
http_only: false,
|
||||
samesite: HTTP::Cookie::SameSite::Lax
|
||||
)
|
||||
|
|
|
@ -27,7 +27,6 @@ def get_subscription_feed(user, max_results = 40, page = 1)
|
|||
offset = (page - 1) * limit
|
||||
|
||||
notifications = Invidious::Database::Users.select_notifications(user)
|
||||
view_name = "subscriptions_#{sha256(user.email)}"
|
||||
|
||||
if user.preferences.notifications_only && !notifications.empty?
|
||||
# Only show notifications
|
||||
|
@ -53,33 +52,39 @@ def get_subscription_feed(user, max_results = 40, page = 1)
|
|||
# Show latest video from a channel that a user hasn't watched
|
||||
# "unseen_only" isn't really correct here, more accurate would be "unwatched_only"
|
||||
|
||||
if user.watched.empty?
|
||||
values = "'{}'"
|
||||
else
|
||||
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
|
||||
end
|
||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY ucid, published DESC", as: ChannelVideo)
|
||||
# "SELECT cv.* FROM channel_videos cv JOIN users ON cv.ucid = any(users.subscriptions) WHERE users.email = $1 AND published > now() - interval '1 month' ORDER BY published DESC"
|
||||
# "SELECT DISTINCT ON (cv.ucid) cv.* FROM channel_videos cv JOIN users ON cv.ucid = any(users.subscriptions) WHERE users.email = ? AND NOT cv.id = any(users.watched) AND published > now() - interval '1 month' ORDER BY ucid, published DESC"
|
||||
videos = PG_DB.query_all("SELECT DISTINCT ON (cv.ucid) cv.* " \
|
||||
"FROM channel_videos cv " \
|
||||
"JOIN users ON cv.ucid = any(users.subscriptions) " \
|
||||
"WHERE users.email = $1 AND NOT cv.id = any(users.watched) AND published > now() - interval '1 month' " \
|
||||
"ORDER BY ucid, published DESC", user.email, as: ChannelVideo)
|
||||
else
|
||||
# Show latest video from each channel
|
||||
|
||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} ORDER BY ucid, published DESC", as: ChannelVideo)
|
||||
videos = PG_DB.query_all("SELECT DISTINCT ON (cv.ucid) cv.* " \
|
||||
"FROM channel_videos cv " \
|
||||
"JOIN users ON cv.ucid = any(users.subscriptions) " \
|
||||
"WHERE users.email = $1 AND published > now() - interval '1 month' " \
|
||||
"ORDER BY ucid, published DESC", user.email, as: ChannelVideo)
|
||||
end
|
||||
|
||||
videos.sort_by!(&.published).reverse!
|
||||
else
|
||||
if user.preferences.unseen_only
|
||||
# Only show unwatched
|
||||
|
||||
if user.watched.empty?
|
||||
values = "'{}'"
|
||||
else
|
||||
values = "VALUES #{user.watched.map { |id| %(('#{id}')) }.join(",")}"
|
||||
end
|
||||
videos = PG_DB.query_all("SELECT * FROM #{view_name} WHERE NOT id = ANY (#{values}) ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
|
||||
videos = PG_DB.query_all("SELECT cv.* " \
|
||||
"FROM channel_videos cv " \
|
||||
"JOIN users ON cv.ucid = any(users.subscriptions) " \
|
||||
"WHERE users.email = $1 AND NOT cv.id = any(users.watched) AND published > now() - interval '1 month' " \
|
||||
"ORDER BY published DESC LIMIT $2 OFFSET $3", user.email, limit, offset, as: ChannelVideo)
|
||||
else
|
||||
# Sort subscriptions as normal
|
||||
|
||||
videos = PG_DB.query_all("SELECT * FROM #{view_name} ORDER BY published DESC LIMIT $1 OFFSET $2", limit, offset, as: ChannelVideo)
|
||||
videos = PG_DB.query_all("SELECT cv.* " \
|
||||
"FROM channel_videos cv " \
|
||||
"JOIN users ON cv.ucid = any(users.subscriptions) " \
|
||||
"WHERE users.email = $1 AND published > now() - interval '1 month' " \
|
||||
"ORDER BY published DESC LIMIT $2 OFFSET $3", user.email, limit, offset, as: ChannelVideo)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -309,7 +309,7 @@ def get_video(id, refresh = true, region = nil, force_refresh = false)
|
|||
video.schema_version != Video::SCHEMA_VERSION # cache control
|
||||
begin
|
||||
video = fetch_video(id, region)
|
||||
Invidious::Database::Videos.update(video)
|
||||
Invidious::Database::Videos.insert(video)
|
||||
rescue ex
|
||||
Invidious::Database::Videos.delete(id)
|
||||
raise ex
|
||||
|
|
|
@ -54,6 +54,11 @@ def extract_video_info(video_id : String)
|
|||
# Init client config for the API
|
||||
client_config = YoutubeAPI::ClientConfig.new
|
||||
|
||||
redis_po_token, redis_visitor_data = Tokens.get_tokens
|
||||
|
||||
po_token = redis_po_token || CONFIG.po_token
|
||||
visitor_data = redis_visitor_data || CONFIG.visitor_data
|
||||
|
||||
# Fetch data from the player endpoint
|
||||
player_response = YoutubeAPI.player(video_id: video_id, params: "2AMB", client_config: client_config)
|
||||
|
||||
|
@ -189,11 +194,10 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
|
|||
end
|
||||
|
||||
video_details = player_response.dig?("videoDetails")
|
||||
if !(microformat = player_response.dig?("microformat", "playerMicroformatRenderer"))
|
||||
microformat = {} of String => JSON::Any
|
||||
end
|
||||
microformat = player_response.dig?("microformat", "playerMicroformatRenderer")
|
||||
|
||||
raise BrokenTubeException.new("videoDetails") if !video_details
|
||||
raise BrokenTubeException.new("microformat") if !microformat
|
||||
|
||||
# Basic video infos
|
||||
|
||||
|
@ -239,7 +243,7 @@ def parse_video_info(video_id : String, player_response : Hash(String, JSON::Any
|
|||
.try &.as_a.map &.as_s || [] of String
|
||||
|
||||
allow_ratings = video_details["allowRatings"]?.try &.as_bool
|
||||
family_friendly = microformat["isFamilySafe"]?.try &.as_bool
|
||||
family_friendly = microformat["isFamilySafe"].try &.as_bool
|
||||
is_listed = video_details["isCrawlable"]?.try &.as_bool
|
||||
is_upcoming = video_details["isUpcoming"]?.try &.as_bool
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="feed-menu">
|
||||
<% feed_menu = env.get("preferences").as(Preferences).feed_menu.dup %>
|
||||
<% if !env.get?("user") %>
|
||||
<% feed_menu.reject! {|item| {"Subscriptions", "Playlists"}.includes? item} %>
|
||||
<% feed_menu.reject! {|item| {"Subscriptions", "Playlists", "History"}.includes? item} %>
|
||||
<% end %>
|
||||
<% feed_menu.each do |feed| %>
|
||||
<a href="/feed/<%= feed.downcase %>" class="feed-menu-item pure-menu-heading">
|
||||
|
|
|
@ -9,12 +9,11 @@
|
|||
<div class="pure-u-1-3">
|
||||
<h3>
|
||||
<a href="/subscription_manager"><%= translate(locale, "Manage subscriptions") %></a>
|
||||
<br>
|
||||
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:center">
|
||||
<a href="/feed/history"><%= translate(locale, "Watch history") %></a>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<h3 style="text-align:right">
|
||||
|
|
|
@ -1,159 +1,146 @@
|
|||
<%
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
dark_mode = env.get("preferences").as(Preferences).dark_mode
|
||||
current_backend = env.request.cookies[CONFIG.server_id_cookie_name]?.try &.value
|
||||
%>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<%= locale %>">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<%= yield_content "header" %>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="manifest" href="/site.webmanifest?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=<%= ASSET_COMMIT %>" color="#575757">
|
||||
<meta name="msapplication-TileColor" content="#575757">
|
||||
<meta name="theme-color" content="#575757">
|
||||
<link title="Invidious" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml">
|
||||
<link rel="stylesheet" href="/css/pure-min.css?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>">
|
||||
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
</head>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<%= yield_content "header" %>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="manifest" href="/site.webmanifest?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="mask-icon" href="/safari-pinned-tab.svg?v=<%= ASSET_COMMIT %>" color="#575757">
|
||||
<meta name="msapplication-TileColor" content="#575757">
|
||||
<meta name="theme-color" content="#575757">
|
||||
<link title="Invidious" type="application/opensearchdescription+xml" rel="search" href="/opensearch.xml">
|
||||
<link rel="stylesheet" href="/css/pure-min.css?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="stylesheet" href="/css/grids-responsive-min.css?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="stylesheet" href="/css/ionicons.min.css?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="stylesheet" href="/css/default.css?v=<%= ASSET_COMMIT %>">
|
||||
<link rel="stylesheet" href="/css/carousel.css?v=<%= ASSET_COMMIT %>">
|
||||
<script src="/js/_helpers.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
</head>
|
||||
|
||||
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
|
||||
<span style="display:none" id="dark_mode_pref"><%= dark_mode %></span>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-xl-20-24" id="contents">
|
||||
<div class="pure-g navbar h-box">
|
||||
<% if navbar_search %>
|
||||
<div class="pure-u-1 pure-u-md-4-24">
|
||||
<a href="/" class="index-link pure-menu-heading">Invidious</a>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-12-24 searchbar">
|
||||
<% autofocus = false %><%= rendered "components/search_box" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<body class="<%= dark_mode.blank? ? "no" : dark_mode %>-theme">
|
||||
<span style="display:none" id="dark_mode_pref"><%= dark_mode %></span>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-xl-20-24" id="contents">
|
||||
<div class="pure-g navbar h-box">
|
||||
<% if navbar_search %>
|
||||
<div class="pure-u-1 pure-u-md-4-24">
|
||||
<a href="/" class="index-link pure-menu-heading">Invidious</a>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-12-24 searchbar">
|
||||
<% autofocus = false %><%= rendered "components/search_box" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
||||
<% if env.get? "user" %>
|
||||
<div class="pure-u-1-4">
|
||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
|
||||
<% if dark_mode == "dark" %>
|
||||
<i class="icon ion-ios-sunny"></i>
|
||||
<% else %>
|
||||
<i class="icon ion-ios-moon"></i>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
|
||||
<% notification_count = env.get("user").as(Invidious::User).notifications.size %>
|
||||
<% if CONFIG.enable_user_notifications && notification_count > 0 %>
|
||||
<span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i>
|
||||
<% else %>
|
||||
<i class="icon ion-ios-notifications-outline"></i>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<i class="icon ion-ios-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
<% if env.get("preferences").as(Preferences).show_nick %>
|
||||
<div class="pure-u-1-4" style="overflow: hidden; white-space: nowrap;">
|
||||
<span id="user_name"><%= HTML.escape(env.get("user").as(Invidious::User).email) %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="pure-u-1-4">
|
||||
<form action="/signout?referer=<%= env.get?("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<a class="pure-menu-heading" href="#">
|
||||
<input style="all:unset" type="submit" value="<%= translate(locale, "Log out") %>">
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="pure-u-1-3">
|
||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
|
||||
<% if dark_mode == "dark" %>
|
||||
<i class="icon ion-ios-sunny"></i>
|
||||
<% else %>
|
||||
<i class="icon ion-ios-moon"></i>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<i class="icon ion-ios-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
<% if CONFIG.login_enabled %>
|
||||
<div class="pure-u-1-3">
|
||||
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<%= translate(locale, "Log in") %>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="pure-u-1 pure-u-md-8-24 user-field">
|
||||
<% if env.get? "user" %>
|
||||
<div class="pure-u-1-4">
|
||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
|
||||
<% if dark_mode == "dark" %>
|
||||
<i class="icon ion-ios-sunny"></i>
|
||||
<% else %>
|
||||
<i class="icon ion-ios-moon"></i>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a id="notification_ticker" title="<%= translate(locale, "Subscriptions") %>" href="/feed/subscriptions" class="pure-menu-heading">
|
||||
<% notification_count = env.get("user").as(Invidious::User).notifications.size %>
|
||||
<% if CONFIG.enable_user_notifications && notification_count > 0 %>
|
||||
<span id="notification_count"><%= notification_count %></span> <i class="icon ion-ios-notifications"></i>
|
||||
<% else %>
|
||||
<i class="icon ion-ios-notifications-outline"></i>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-4">
|
||||
<a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<i class="icon ion-ios-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
<% if env.get("preferences").as(Preferences).show_nick %>
|
||||
<div class="pure-u-1-4" style="overflow: hidden; white-space: nowrap;">
|
||||
<span id="user_name"><%= HTML.escape(env.get("user").as(Invidious::User).email) %></span>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="pure-u-1-4">
|
||||
<form action="/signout?referer=<%= env.get?("current_page") %>" method="post">
|
||||
<input type="hidden" name="csrf_token" value="<%= HTML.escape(env.get?("csrf_token").try &.as(String) || "") %>">
|
||||
<a class="pure-menu-heading" href="#">
|
||||
<input style="all:unset" type="submit" value="<%= translate(locale, "Log out") %>">
|
||||
</a>
|
||||
</form>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="pure-u-1-3">
|
||||
<a id="toggle_theme" href="/toggle_theme?referer=<%= env.get?("current_page") %>" class="pure-menu-heading" title="<%= translate(locale, "toggle_theme") %>">
|
||||
<% if dark_mode == "dark" %>
|
||||
<i class="icon ion-ios-sunny"></i>
|
||||
<% else %>
|
||||
<i class="icon ion-ios-moon"></i>
|
||||
<% end %>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pure-u-1-3">
|
||||
<a title="<%= translate(locale, "Preferences") %>" href="/preferences?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<i class="icon ion-ios-cog"></i>
|
||||
</a>
|
||||
</div>
|
||||
<% if CONFIG.login_enabled %>
|
||||
<div class="pure-u-1-3">
|
||||
<a href="/login?referer=<%= env.get?("current_page") %>" class="pure-menu-heading">
|
||||
<%= translate(locale, "Log in") %>
|
||||
</a>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% if !CONFIG.backends.empty? %>
|
||||
<div class="h-box">
|
||||
<b>Switch Backend:</b>
|
||||
<% CONFIG.backends.each do | backend | %>
|
||||
<% backend = backend.split(CONFIG.backends_delimiter) %>
|
||||
<% if current_backend == backend[0] %>
|
||||
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="text-decoration-line: underline; display: inline-block;">
|
||||
Backend<%= HTML.escape(backend[0]) %>
|
||||
<% if backend.size == 2 %>
|
||||
<%= HTML.escape(backend[1]) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</a> <span> | </span>
|
||||
<% else %>
|
||||
<a href="/switchbackend?backend_id=<%= backend[0] %>" style="display: inline-block;">
|
||||
Backend<%= HTML.escape(backend[0]) %>
|
||||
<% if backend.size == 2 %>
|
||||
<%= HTML.escape(backend[1]) %>
|
||||
<% end %>
|
||||
</a> <span> | </span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if CONFIG.banner %>
|
||||
<div class="h-box">
|
||||
<h3><%= CONFIG.banner %></h3>
|
||||
</div>
|
||||
<% if CONFIG.banner %>
|
||||
<div class="h-box">
|
||||
<h3><%= CONFIG.banner %></h3>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= content %>
|
||||
|
||||
<% if buffer_footer %>
|
||||
<div id="footer_buffer"></div>
|
||||
<% end %>
|
||||
|
||||
<%= content %>
|
||||
|
||||
<footer>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<span>
|
||||
<i class="icon ion-logo-github"></i>
|
||||
<% if CONFIG.modified_source_code_url %>
|
||||
<a href="https://github.com/iv-org/invidious"><%= translate(locale, "footer_original_source_code") %></a> /
|
||||
<a href="<%= CONFIG.modified_source_code_url %>"><%= translate(locale, "footer_modfied_source_code") %></a>
|
||||
<% else %>
|
||||
<a href="https://github.com/iv-org/invidious"><%= translate(locale, "footer_source_code") %></a>
|
||||
<% end %>
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon ion-ios-paper"></i>
|
||||
<a href="https://github.com/iv-org/documentation"><%= translate(locale, "footer_documentation") %></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<span>
|
||||
<a href="https://github.com/iv-org/invidious/blob/master/LICENSE"><%= translate(locale, "Released under the AGPLv3 on Github.") %></a>
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon ion-logo-javascript"></i>
|
||||
<a rel="jslicense" href="/licenses"><%= translate(locale, "View JavaScript license information.") %></a>
|
||||
</span>
|
||||
<span>
|
||||
<i class="icon ion-ios-paper"></i>
|
||||
<a href="/privacy"><%= translate(locale, "View privacy policy.") %></a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="pure-u-1 pure-u-md-1-3">
|
||||
<span>
|
||||
<i class="icon ion-ios-wallet"></i>
|
||||
<a href="https://invidious.io/donate/"><%= translate(locale, "footer_donate_page") %></a>
|
||||
</span>
|
||||
<span><%= translate(locale, "Current version: ") %> <%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %></span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script src="/js/handlers.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
|
@ -172,6 +159,177 @@
|
|||
<script src="/js/notifications.js?v=<%= ASSET_COMMIT %>"></script>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
<footer class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||
<div class="h-box pure-u-1 pure-u-md-20-24" id="footer-content-container">
|
||||
<div class="pure-u-1 footer-content">
|
||||
<div class="footer-section pure-u-1-4" id="footer-custom-text">
|
||||
<b>Invidious</b>
|
||||
<% if CONFIG.footer %>
|
||||
<p><%=CONFIG.footer%></p>
|
||||
<% else %>
|
||||
<p><%=translate(locale, "default_invidious_footer_text")%></p>
|
||||
<% end %>
|
||||
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<b class="footer-section-header"><%= translate(locale, "footer_navigation_section_header")%></b>
|
||||
<ul class="pure-menu-list footer-section-list">
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="/" title="<%= translate(locale, "footer_home_link")%>">
|
||||
<%= translate(locale, "footer_home_link") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="/feed/popular" title="<%= translate(locale, "Popular")%>">
|
||||
<%= translate(locale, "Popular") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="/feed/trending" title="<%= translate(locale, "Trending")%>" style="">
|
||||
<%= translate(locale, "Trending") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="/search" title="<%= translate(locale, "Search")%>">
|
||||
<%= translate(locale, "Search") %>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="footer-section">
|
||||
<b class="footer-section-header">Invidious</b>
|
||||
<ul class="pure-menu-list footer-section-list">
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="https://invidious.io" title="<%= translate(locale, "footer_project_homepage_link")%>">
|
||||
<%= translate(locale, "footer_project_homepage_link") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="https://github.com/iv-org/invidious" title="<%= translate(locale, "footer_source_code_link")%>">
|
||||
<%= translate(locale, "footer_source_code_link") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="https://github.com/iv-org/invidious/issues" title="<%= translate(locale, "footer_issue_tracker_link")%>" style="">
|
||||
<%= translate(locale, "footer_issue_tracker_link") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="https://instances.invidious.io" title="<%= translate(locale, "footer_public_instances_link")%>">
|
||||
<%= translate(locale, "footer_public_instances_link") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="https://invidious.io/donate" title="<%= translate(locale, "footer_donate_link")%>">
|
||||
<%= translate(locale, "footer_donate_link") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="https://matrix.to/#/#invidious:matrix.org" title="<%= translate(locale, "footer_matrix_link")%>">
|
||||
<%= translate(locale, "footer_matrix_link") %>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
<% if CONFIG.instance_maintainer_email || CONFIG.modified_source_code_url || CONFIG.footer_instance_tos_link || CONFIG.footer_instance_privacy_policy_link %>
|
||||
<div class="footer-section">
|
||||
<b class="footer-section-header">
|
||||
<% if CONFIG.modified_source_code_url %>
|
||||
<%= translate(locale, "footer_instance_section_header_modified_source")%>
|
||||
<% else %>
|
||||
<%= translate(locale, "footer_instance_section_header")%>
|
||||
<% end %>
|
||||
</b>
|
||||
<ul class="pure-menu-list footer-section-list">
|
||||
<% if CONFIG.instance_maintainer_email %>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="<%=HTML.escape("mailto:#{CONFIG.instance_maintainer_email.not_nil!}")%>" title="<%= translate(locale, "footer_contact_link")%>">
|
||||
<%= translate(locale, "footer_contact_link") %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if CONFIG.modified_source_code_url %>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="<%=HTML.escape(CONFIG.modified_source_code_url.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_modified_source_code")%>">
|
||||
<%= translate(locale, "footer_instance_section_modified_source_code") %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if CONFIG.footer_instance_tos_link %>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="<%=HTML.escape(CONFIG.footer_instance_tos_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_tos")%>">
|
||||
<%= translate(locale, "footer_instance_section_tos") %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if CONFIG.footer_instance_privacy_policy_link %>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="<%=HTML.escape(CONFIG.footer_instance_privacy_policy_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_privacy_policy")%>">
|
||||
<%= translate(locale, "footer_instance_section_privacy_policy") %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<% if CONFIG.footer_instance_donate_link %>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="<%=HTML.escape(CONFIG.footer_instance_donate_link.not_nil!)%>" title="<%= translate(locale, "footer_instance_section_donate")%>">
|
||||
<%= translate(locale, "footer_instance_section_donate") %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
|
||||
<% CONFIG.footer_instance_section_custom_fields.each do | field | %>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="<%=HTML.escape(field[1])%>" title="<%= HTML.escape(field[0]) %>">
|
||||
<%= HTML.escape(field[0]) %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="footer-section">
|
||||
<b class="footer-section-header"><%= translate(locale, "footer_support_section_header")%></b>
|
||||
<ul class="pure-menu-list footer-section-list">
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="https://github.com/iv-org/invidious/issues/new" title="<%= translate(locale, "footer_report_bug_link")%>">
|
||||
<%= translate(locale, "footer_report_bug_link") %>
|
||||
</a>
|
||||
</li>
|
||||
<li class="pure-menu-item footer-section-item">
|
||||
<a href="#" title="<%= translate(locale, "footer_faq_link")%>" style="">
|
||||
<%= translate(locale, "footer_faq_link") %>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<div class="footer-footer">
|
||||
<div class="box">You are currently using Backend: <%= current_backend %></div>
|
||||
<span class="left">
|
||||
<% if CONFIG.modified_source_code_url %>
|
||||
<%= translate(locale, "footer_current_version_modified") %>
|
||||
<% else %>
|
||||
<%= translate(locale, "Current version: ") %>
|
||||
<% end %>
|
||||
|
||||
<%= CURRENT_VERSION %>-<%= CURRENT_COMMIT %> @ <%= CURRENT_BRANCH %>
|
||||
</span>
|
||||
<div class="right">
|
||||
<a href="/privacy" title="<%= translate(locale, "footer_privacy_policy_link")%>"><%= translate(locale, "footer_privacy_policy_link") %></a>
|
||||
<span> | </span>
|
||||
<a href="/licenses" title="<%= translate(locale, "footer_licences_link")%>"><%= translate(locale, "footer_licences_link") %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pure-u-1 pure-u-md-2-24"></div>
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -68,6 +68,8 @@
|
|||
<%= translate(locale, "Sign In") %>/<%= translate(locale, "Register") %>
|
||||
</button>
|
||||
<% end %>
|
||||
<p>Please, do not use your E-mail address!. If you lost your password there is no way to recover it using a Password Recovery E-mail. If you lose your password, your should <a href="https://nadeko.net/contact">contact the admin</a> instead or just create a new account (If you don't care about your subscriptions, history and playlists.)</p>
|
||||
<p>
|
||||
</fieldset>
|
||||
</form>
|
||||
<% end %>
|
||||
|
|
|
@ -170,7 +170,7 @@
|
|||
</div>
|
||||
|
||||
<% if env.get?("user") %>
|
||||
<% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists"} %>
|
||||
<% feed_options = {"", "Popular", "Trending", "Subscriptions", "Playlists", "History"} %>
|
||||
<% else %>
|
||||
<% feed_options = {"", "Popular", "Trending"} %>
|
||||
<% end %>
|
||||
|
|
|
@ -13,11 +13,13 @@
|
|||
<meta property="og:image" content="<%= HOST_URL %>/vi/<%= video.id %>/maxres.jpg">
|
||||
<meta property="og:description" content="<%= HTML.escape(video.short_description) %>">
|
||||
<meta property="og:type" content="video.other">
|
||||
<meta property="og:video:url" content="<%= HOST_URL %>/embed/<%= video.id %>">
|
||||
<meta property="og:video:secure_url" content="<%= HOST_URL %>/embed/<%= video.id %>">
|
||||
<meta property="og:video:type" content="text/html">
|
||||
<meta property="og:video:width" content="1280">
|
||||
<meta property="og:video:height" content="720">
|
||||
<!-- This shouldn't be empty, ever. -->
|
||||
<meta property="og:video" content="<%= video_url %>">
|
||||
<meta property="og:video:url" content="<%= video_url %>">
|
||||
<meta property="og:video:secure_url" content="<%= video_url %>">
|
||||
<meta property="og:video:type" content="video/mp4">
|
||||
<meta property="og:video:width" content="640">
|
||||
<meta property="og:video:height" content="360">
|
||||
<meta name="twitter:card" content="player">
|
||||
<meta name="twitter:url" content="<%= HOST_URL %>/watch?v=<%= video.id %>">
|
||||
<meta name="twitter:title" content="<%= title %>">
|
||||
|
@ -127,6 +129,20 @@ we're going to need to do it here in order to allow for translations.
|
|||
(<a id="link-yt-embed" rel="noreferrer noopener" data-base-url="<%= link_yt_embed %>" href="<%= link_yt_embed %>"><%= translate(locale, "videoinfo_youTube_embed_link") %></a>)
|
||||
</span>
|
||||
|
||||
<% if CONFIG.materialious_domain %>
|
||||
<p id="watch-on-materialious">
|
||||
<%-
|
||||
link_materialious_watch = URI.new(scheme: "https", host: "#{CONFIG.materialious_domain}", path: "/watch", query: "v=#{video.id}")
|
||||
|
||||
if !plid.nil? && !continuation.nil?
|
||||
link_materialious_param = URI::Params{"list" => [plid], "index" => [continuation.to_s]}
|
||||
link_materialious_watch = IV::HttpServer::Utils.add_params_to_url(link_materialious_watch, link_materialious_param)
|
||||
end
|
||||
-%>
|
||||
<a id="link-materialious-watch" data-base-url="<%= link_materialious_watch %>" href="<%= link_materialious_watch %>"><%= translate(locale, "videoinfo_watch_on_materialious") %></a>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<p id="watch-on-another-invidious-instance">
|
||||
<%- link_iv_other = IV::Frontend::Misc.redirect_url(env) -%>
|
||||
<a id="link-iv-other" data-base-url="<%= link_iv_other %>" href="<%= link_iv_other %>"><%= translate(locale, "Switch Invidious Instance") %></a>
|
||||
|
|
|
@ -29,7 +29,6 @@ module YoutubeAPI
|
|||
WebEmbeddedPlayer
|
||||
WebMobile
|
||||
WebScreenEmbed
|
||||
WebCreator
|
||||
|
||||
Android
|
||||
AndroidEmbeddedPlayer
|
||||
|
@ -81,14 +80,6 @@ module YoutubeAPI
|
|||
os_version: WINDOWS_VERSION,
|
||||
platform: "DESKTOP",
|
||||
},
|
||||
ClientType::WebCreator => {
|
||||
name: "WEB_CREATOR",
|
||||
name_proto: "62",
|
||||
version: "1.20240918.03.00",
|
||||
os_name: "Windows",
|
||||
os_version: WINDOWS_VERSION,
|
||||
platform: "DESKTOP",
|
||||
},
|
||||
|
||||
# Android
|
||||
|
||||
|
@ -464,7 +455,7 @@ module YoutubeAPI
|
|||
video_id : String,
|
||||
*, # Force the following parameters to be passed by name
|
||||
params : String,
|
||||
client_config : ClientConfig | Nil = nil
|
||||
client_config : ClientConfig | Nil = nil,
|
||||
)
|
||||
# Playback context, separate because it can be different between clients
|
||||
playback_ctx = {
|
||||
|
@ -609,7 +600,7 @@ module YoutubeAPI
|
|||
def _post_json(
|
||||
endpoint : String,
|
||||
data : Hash,
|
||||
client_config : ClientConfig | Nil
|
||||
client_config : ClientConfig | Nil,
|
||||
) : Hash(String, JSON::Any)
|
||||
# Use the default client config if nil is passed
|
||||
client_config ||= DEFAULT_CLIENT_CONFIG
|
||||
|
|
Loading…
Reference in a new issue