Compare commits

...

90 commits

Author SHA1 Message Date
7d4b1c18ba
add support for an external videoplayback proxy loaded from invidious-companion
Some checks failed
Invidious CI / build (push) Failing after 29s
2024-12-14 14:52:12 -03:00
8831288ca1
Merge remote-tracking branch 'companion/invidious-companion' 2024-12-14 14:51:40 -03:00
041d6cc9d3
adapt actions to git.nadeko.net
Some checks failed
Invidious CI / build (push) Has been cancelled
2024-12-09 16:57:28 -03:00
79859100a8
dockerfile: use x86-64-v2 instead to support more CPUs
I used x86-64-v3 because my CPU supports it, but I doubt this is going
to make Invidious any faster of most of the operations.
2024-11-26 19:31:48 -03:00
47ef5dfe4c
Overwirte CONFIG.po_token and CONFIG.visitor_data by the tokens stored
on redis

This approach is better to prevent conflicts with the upstream
repository.
2024-11-16 12:27:10 -03:00
13e00e674b
Remove server side generated tokens (448007e5ba) 2024-11-16 12:10:51 -03:00
3615bbd893
Remove user supplied po_token and visitor_data 2024-11-16 12:07:05 -03:00
9b9efc6841
Merge remote-tracking branch 'upstream/master' 2024-11-13 21:14:26 -03:00
036ab6ef65
Docker: Install tzdata in Dockerfile 2024-11-10 00:02:33 -03:00
c27a703544
Merge remote-tracking branch 'upstream/master' 2024-11-09 23:42:52 -03:00
5a75ef7f94
Remove old code that is done on the Openresty side 2024-11-09 23:37:58 -03:00
91c9cd45a4
Update CI 2024-11-09 23:37:58 -03:00
b953dc1ce7
Videos: Add support for OpenGraph videos
To support OpenGraph clients like Discord and other platforms able to
pull the video from the OpenGraph metadata.
2024-11-09 23:37:58 -03:00
70dc1a9f11
Tokens: Better logging 2024-10-31 21:38:59 -03:00
fc910b43ba
External Proxies: Adapt it to use a NamedTuple 2024-10-31 21:38:40 -03:00
67998d1f36
Revert "External Proxies: Rotate between proxies with balance enabled"
This reverts commit 26bee068eb.
It's broken and it doesn't work when a proxy comes back up.
2024-10-31 21:26:03 -03:00
e2276ace1b
Merge remote-tracking branch 'upstream/master' into master 2024-10-31 20:25:33 -03:00
c61b2963ac
Videos: Fix audio tracks language.
Video will only return the default language. The rest of the audio
tracks are deleted since they will not be used.
2024-10-30 13:14:54 -03:00
26bee068eb
External Proxies: Rotate between proxies with balance enabled
Closes #17
2024-10-30 01:59:08 -03:00
486c5845cd
Config: Also reload env variables 2024-10-30 01:57:06 -03:00
6f10a7c67e
Use POST requests for /videoplayback requests 2024-10-29 19:02:05 -03:00
67d7b78ac9
Config: Reload configuration on modification
It detects changes on the config.yml automtically if invidious is
running on linux. If not, the configuration can be reloaded using
`kill -s HUP $(pidof invidious)` or any other tool that sends a SIGHUP
signal to the invidious process.

Closes #16
2024-10-28 13:37:06 -03:00
3afac4d842
Tokens: Option to disable user tokens. 2024-10-25 10:36:20 -03:00
448007e5ba
Tokens: Server side generated tokens.
#18
2024-10-17 23:44:30 -03:00
3cc0dbca01
PubSub: Use external domain for pubsub feeds 2024-10-17 17:02:12 -03:00
c3e8721051
External Proxies: Proxyfi HLS Playlists 2024-10-14 17:57:52 -03:00
cf5028d09a
Videos: Completly disable annotations due to archive.org being down
Closes #15
2024-10-13 23:47:57 -03:00
eb2670fe49
Tokens: Refresh po_token and visitor_data every 5 seconds
Closes #11
2024-10-13 15:57:51 -03:00
976e1ccf5a
External Proxies: Proxyfi HD720 2024-10-13 15:19:49 -03:00
fee2acc666
Videos: Increase video cache to 4 hours 2024-10-12 02:59:36 -03:00
b5ab49e8e8
Feat: Experimental support for potoken inside redis
Using https://git.nadeko.net/Fijxu/youtube-po-token-generator
2024-10-12 02:04:14 -03:00
65f3bbcb10
External Proxies: Use list of external videoplayback proxies 2024-10-11 13:50:42 -03:00
Samantaz Fox
fb3ecdad9a
Videos: Fix missing host parameter on playback URLs when local=true 2024-10-09 16:15:50 +02:00
5357c83e00
CI: Experimental branches for testing builds 2024-10-10 18:19:29 -03:00
8dc0a67be3
Feat: User supplied po_token and visitor_data 2024-10-11 16:50:21 -03:00
d61043edea
Small try. 2024-10-10 15:07:33 -03:00
3111158a7c
Feeds: Get rid of feed_needs_update() since it appears to be unused 2024-10-09 18:09:23 -03:00
cf6c3a7b5b
Revert "use WEB_CREATOR when po_token with WEB_EMBED as a fallback (#4928)"
This reverts commit d9df90b5e3.
2024-10-08 19:53:35 -03:00
2f5a555ea7
Merge remote-tracking branch 'upstream/master' 2024-10-08 19:22:53 -03:00
472dd8663d
VideoJS: Increase buffer 2024-10-08 18:59:01 -03:00
dc2aba106c
Backends: Use backend switcher to indicate the current backend in use. 2024-10-08 18:59:01 -03:00
eff8673efc
Feat: Experimental support for external videoplayback proxies 2024-10-08 18:59:01 -03:00
b1f25a69ad
Logger: Add color support for different log levels 2024-10-08 18:59:01 -03:00
d5b8b0b19c
SigHelper: Reconnect to signature helper 2024-10-08 18:59:00 -03:00
Emilien Devos
b3e6aaddab
decrease buffer seconds for saving bandwidth 2024-10-08 16:54:19 -03:00
33ffafb9e3
Feat: backend supports with cookies 2024-10-08 16:54:18 -03:00
8f46bd5751
Feat: Add resolution limit 2024-09-15 01:06:33 -03:00
cae8cbdda8
Revert "retreive potoken for bypass restrictions"
This reverts commit b0cd6587bd.
2024-09-14 19:06:52 -03:00
100ecff0b3
Config: Support for alternative domains 2024-09-14 18:59:12 -03:00
5ced7694fe
Merge remote-tracking branch 'upstream/master' 2024-08-24 15:56:36 -04:00
ae937d8339
Use x86-64-v3 Microarch 2024-08-22 22:30:01 -04:00
4a2877f28b
Merge remote-tracking branch 'upstream/limit-feeds-materialized-views' 2024-08-19 18:29:25 -04:00
Emilien Devos
e476dbe25b limit feeds and delete materialized views 2024-08-14 19:38:54 +02:00
2f8ef155c8
Merge remote-tracking branch 'upstream/master' 2024-08-13 15:30:54 -04:00
6c2626cf05
Remove old DECRYPT_FUNCTION var 2024-08-10 16:28:31 -04:00
72150ae676
Merge branch 'potoken-config' 2024-08-10 16:17:48 -04:00
b7430c5a5a
Merge branch 'sig_helper' 2024-08-10 16:17:09 -04:00
ddfb8e7d93
Views: Add "Watch on Materialious" link on videos. 2024-07-21 13:01:00 -04:00
Emilien Devos
b0cd6587bd
retreive potoken for bypass restrictions
Signed-off-by: Fijxu <fijxu@nadeko.net>
2024-07-21 13:01:00 -04:00
c7b8f470d8
Set the video time to 0 if the video has been watched ALMOST completly 2024-07-21 13:00:58 -04:00
762fa5214d
Use Docker Valkey instead of passing a socket 2024-07-21 13:00:27 -04:00
d50990ea15
Use Valkey instead of Redis for video cache 2024-07-21 13:00:27 -04:00
067dcbef5e
Options for donation and contact links 2024-07-21 13:00:27 -04:00
a7e9602ccd
Execute jobs only on master branch, better tags for images 2024-07-21 13:00:27 -04:00
a74057bb7a
Use full path for OpenGraph og:image 2024-07-21 13:00:26 -04:00
9a7b6976ff
Only execute action on changes inside specific folders 2024-07-21 13:00:26 -04:00
6e1e3e9554
docker-compose.yml for my instances 2024-07-21 13:00:26 -04:00
ad591f3c32
Automated invidious docker builds 2024-07-21 13:00:26 -04:00
Emilien Devos
8665a69fee
limit feeds and delete materialized views 2024-07-21 13:00:26 -04:00
sf.nadeko.net ~root
d1051efd6e
Add History feed menu 2024-07-21 13:00:26 -04:00
03bf4592ce
Add history tab in feed menu 2024-07-21 13:00:26 -04:00
d641bcbf5d
Use legit User-Agent instead of Crystal User-Agent. 2024-07-21 13:00:26 -04:00
2027a35e5a
test123 2024-07-21 13:00:25 -04:00
sf.nadeko.net ~root
65d9468911
Add some links and change some things in the CSS 2024-07-21 13:00:25 -04:00
Emilien Devos
389a2a4a4d
use redis for video cache
Signed-off-by: zzls Selfhost <root@selfhost.zzls.xyz>
2024-07-21 13:00:25 -04:00
syeopite
b60e056f96
Update uptime logic to handle updown.io response 2024-07-21 13:00:25 -04:00
syeopite
532d92bb7a
Fix invalid logic for instance uptime comparison 2024-07-21 13:00:25 -04:00
syeopite
24f878e6f6
Use HTTP::Client directly in instance list job
The HTTP::Client created via `make_client` is affected by the
force_resolve configuration option. However, api.invidious.io
does not support ipv6 and as such any request with ipv6 to
api.invidious.io will instead raise.

Directly calling the HTTP::Client will ignore the force_resolve option
allowing requests to go through ipv4 when needed.
2024-07-21 13:00:25 -04:00
syeopite
a9fc84bc14
Refactor instance fetching logic into separate job 2024-07-21 13:00:25 -04:00
syeopite
7e680c692f
Remove preferences and login link from footer 2024-07-21 13:00:25 -04:00
syeopite
1eb28edfb3
Add modified disclaimer to version tag 2024-07-21 13:00:25 -04:00
syeopite
efcd94ffbe
Typo 2024-07-21 13:00:24 -04:00
syeopite
780f9df7d3
Add config option for instance donation link
Co-authored-by: Arya K <arya@projectsegfau.lt>
2024-07-21 13:00:24 -04:00
syeopite
4d11c324b0
Add "Instance" section to footer 2024-07-21 13:00:24 -04:00
syeopite
57f8bfb965
Add config to add custom text in the footer
Co-authored-by: Aural Glow <125497673+auralglow@users.noreply.github.com>
2024-07-21 13:00:24 -04:00
syeopite
6acabc5bff
Add new instance customization section in config 2024-07-21 13:00:24 -04:00
syeopite
9d0ab0a83c
Add Invidious version to footer 2024-07-21 13:00:24 -04:00
syeopite
4164159057
Use instances.invidious.io instead of redirect
Co-authored-by: TheFrenchGhosty <47571719+TheFrenchGhosty@users.noreply.github.com>
2024-07-21 13:00:24 -04:00
syeopite
30d858bc8b
Update locales/en-US.json
Co-authored-by: Samantaz Fox <coding@samantaz.fr>
2024-07-21 13:00:24 -04:00
syeopite
e98aafa4b5
Extract and implement footer overhaul from #2215 2024-07-21 13:00:24 -04:00
53 changed files with 1354 additions and 502 deletions

63
.forgejo/workflows/ci.yml Normal file
View 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"

View file

@ -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

View file

@ -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; }

View file

@ -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();

View file

@ -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/

View 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;

View file

@ -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
View 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')

View file

@ -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"

View file

@ -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
View 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:

View file

@ -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"
}

View file

@ -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
View 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
}
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View 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

View file

@ -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

View file

@ -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" }}

View 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

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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}")

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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"

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -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
)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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">

View file

@ -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">

View file

@ -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>&nbsp;/
<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>

View file

@ -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 %>

View file

@ -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 %>

View file

@ -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>

View file

@ -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