mirror of
https://github.com/iv-org/invidious.git
synced 2026-10-20 12:48:05 +05:30
Compare commits
27
Commits
821365cf71
...
efcfb1ef03
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efcfb1ef03 | ||
|
|
57cc097022 | ||
|
|
41d044692c | ||
|
|
907fe54aab | ||
|
|
7286c6dea4 | ||
|
|
b91e517c36 | ||
|
|
dc1011d74e | ||
|
|
20bcaa0e1d | ||
|
|
381c6e8c92 | ||
|
|
1454eada06 | ||
|
|
c88230067b | ||
|
|
3012b9c0e3 | ||
|
|
248c315b4e | ||
|
|
e4ad826f1f | ||
|
|
9f8bc9e2c4 | ||
|
|
b052ced0b4 | ||
|
|
47756e0d1e | ||
|
|
f3e6d6d84e | ||
|
|
7e613cf228 | ||
|
|
049d591d29 | ||
|
|
b3a3f3a6df | ||
|
|
a625015187 | ||
|
|
34173a3d84 | ||
|
|
4daa142844 | ||
|
|
eff609d0cd | ||
|
|
9c8c37ea86 | ||
|
|
c756c35268 |
No files matched your search
+22
-9
@@ -5,20 +5,26 @@
|
||||
# Exclude assigns for ECR files
|
||||
Lint/UselessAssign:
|
||||
Excluded:
|
||||
- src/invidious.cr
|
||||
- src/invidious/helpers/errors.cr
|
||||
- src/invidious/routes/**/*.cr
|
||||
- src/invidious.cr
|
||||
- src/invidious/helpers/errors.cr
|
||||
- src/invidious/routes/*.cr
|
||||
- src/invidious/views/**/**
|
||||
|
||||
# Exclude unused arguments for ECR files
|
||||
Lint/UnusedArgument:
|
||||
Excluded:
|
||||
- src/invidious/views/**/**
|
||||
|
||||
# Ignore false negative (if !db.query_one?...)
|
||||
Lint/UnreachableCode:
|
||||
Excluded:
|
||||
- src/invidious/database/base.cr
|
||||
- src/invidious/database/base.cr
|
||||
|
||||
# Ignore shadowed variable `key` (it works for now, and that's
|
||||
# a sensitive part of the code)
|
||||
Lint/ShadowingOuterLocalVar:
|
||||
Excluded:
|
||||
- src/invidious/helpers/tokens.cr
|
||||
- src/invidious/helpers/tokens.cr
|
||||
|
||||
Lint/NotNil:
|
||||
Enabled: false
|
||||
@@ -27,7 +33,6 @@ Lint/SpecFilename:
|
||||
Excluded:
|
||||
- spec/parsers_helper.cr
|
||||
|
||||
|
||||
#
|
||||
# Style
|
||||
#
|
||||
@@ -44,6 +49,15 @@ Style/RedundantNext:
|
||||
Style/ParenthesesAroundCondition:
|
||||
Enabled: false
|
||||
|
||||
# Disable this as it's really useful for functions and variables
|
||||
# that belong to a same module, struct or class, and it can be confused
|
||||
# by a global function.
|
||||
# This is subject to change with https://github.com/iv-org/invidious/pull/6046
|
||||
# where there will not be more global variables or functions, well, that's
|
||||
# the goal.
|
||||
Style/RedundantSelf:
|
||||
Enabled: false
|
||||
|
||||
# This requires a rewrite of most data structs (and their usage) in Invidious.
|
||||
Naming/QueryBoolMethods:
|
||||
Enabled: false
|
||||
@@ -56,12 +70,11 @@ Naming/BlockParameterName:
|
||||
|
||||
# Hides TODO comment warnings.
|
||||
#
|
||||
# Call `bin/ameba --only Documentation/DocumentationAdmonition` to
|
||||
# Call `bin/ameba --only Documentation/Admonition` to
|
||||
# list them
|
||||
Documentation/DocumentationAdmonition:
|
||||
Documentation/Admonition:
|
||||
Enabled: false
|
||||
|
||||
|
||||
#
|
||||
# Metrics
|
||||
#
|
||||
|
||||
@@ -4,11 +4,17 @@ updates:
|
||||
directory: "/docker"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
assignees:
|
||||
- "Fijxu"
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
assignees:
|
||||
- "Fijxu"
|
||||
- package-ecosystem: "nix"
|
||||
directory: /nix
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
assignees:
|
||||
- "Fijxu"
|
||||
@@ -15,8 +15,8 @@ on:
|
||||
- .github/ISSUE_TEMPLATE/*
|
||||
- kubernetes/**
|
||||
# Nix development files that are not used here.
|
||||
- "*.nix"
|
||||
- flake.lock
|
||||
- .envrc
|
||||
- "nix/**"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
@@ -89,7 +89,7 @@ jobs:
|
||||
|
||||
# https://github.com/marketplace/actions/docker-manifest-create-action
|
||||
- name: Create and push manifest
|
||||
uses: int128/docker-manifest-create-action@v2.26.0
|
||||
uses: int128/docker-manifest-create-action@v2
|
||||
with:
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:master
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
|
||||
# https://github.com/marketplace/actions/docker-manifest-create-action
|
||||
- name: Create and push manifest
|
||||
uses: int128/docker-manifest-create-action@v2.26.0
|
||||
uses: int128/docker-manifest-create-action@v2
|
||||
with:
|
||||
push: true
|
||||
tags: quay.io/invidious/invidious:latest
|
||||
|
||||
+11
-11
@@ -25,8 +25,8 @@ on:
|
||||
- kubernetes/**
|
||||
|
||||
# Nix development files that are not used here.
|
||||
- "*.nix"
|
||||
- flake.flake
|
||||
- .envrc
|
||||
- "nix/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -49,13 +49,14 @@ jobs:
|
||||
- 1.18.2
|
||||
- 1.19.2
|
||||
- 1.20.3
|
||||
- 1.21.0
|
||||
- 1.21.1
|
||||
- 1.22.0
|
||||
include:
|
||||
- crystal: nightly
|
||||
stable: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
@@ -65,7 +66,7 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1.9.2
|
||||
uses: crystal-lang/install-crystal@v1.9.3
|
||||
with:
|
||||
crystal: ${{ matrix.crystal }}
|
||||
|
||||
@@ -103,7 +104,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Build Docker
|
||||
run: docker compose build
|
||||
@@ -131,22 +132,21 @@ jobs:
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Install Crystal
|
||||
id: lint_step_install_crystal
|
||||
uses: crystal-lang/install-crystal@v1.9.2
|
||||
uses: crystal-lang/install-crystal@v1.9.3
|
||||
with:
|
||||
crystal: 1.20.3
|
||||
crystal: 1.22.0
|
||||
|
||||
- name: Cache Shards
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
./lib
|
||||
./bin
|
||||
key: shards-${{ hashFiles('shard.lock') }}-${{ steps.lint_step_install_crystal.outputs.crystal }}
|
||||
|
||||
- name: Install Shards
|
||||
@@ -164,4 +164,4 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Run Ameba linter
|
||||
run: bin/ameba
|
||||
uses: crystal-ameba/github-action@v1
|
||||
@@ -37,8 +37,9 @@ permissions:
|
||||
jobs:
|
||||
prepare-release:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: master
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
days-before-stale: 730
|
||||
days-before-pr-stale: -1
|
||||
days-before-close: 60
|
||||
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 30 days because it has not had recent activity and is much likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
||||
stale-issue-message: 'This issue has been automatically marked as stale and will be closed in 60 days because it has not had recent activity and is most likely outdated. If you think this issue is still relevant and applicable, you just have to post a comment and it will be unmarked.'
|
||||
stale-issue-label: "stale"
|
||||
ascending: true
|
||||
# Exempt the following types of issues from being staled
|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
[submodule "mocks"]
|
||||
path = mocks
|
||||
url = ../mocks
|
||||
url = https://github.com/iv-org/mocks.git
|
||||
@@ -6,6 +6,7 @@ RELEASE := 1
|
||||
STATIC := 0
|
||||
|
||||
NO_DBG_SYMBOLS := 0
|
||||
SKIP_VERSION_CHECK := 0
|
||||
|
||||
FLAGS ?=
|
||||
|
||||
@@ -27,6 +28,9 @@ ifeq ($(API_ONLY), 1)
|
||||
FLAGS += -Dapi_only
|
||||
endif
|
||||
|
||||
ifeq ($(SKIP_VERSION_CHECK), 1)
|
||||
FLAGS += -Dskip_version_check
|
||||
endif
|
||||
|
||||
# -----------------------
|
||||
# Main
|
||||
@@ -103,11 +107,12 @@ help:
|
||||
@echo ""
|
||||
@echo "Build options available for this Makefile:"
|
||||
@echo ""
|
||||
@echo " RELEASE Make a release build (Default: 1)"
|
||||
@echo " STATIC Link libraries statically (Default: 0)"
|
||||
@echo " RELEASE Make a release build (Default: 1)"
|
||||
@echo " STATIC Link libraries statically (Default: 0)"
|
||||
@echo ""
|
||||
@echo " API_ONLY Build invidious without a GUI (Default: 0)"
|
||||
@echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)"
|
||||
@echo " API_ONLY Build invidious without a GUI (Default: 0)"
|
||||
@echo " NO_DBG_SYMBOLS Strip debug symbols (Default: 0)"
|
||||
@echo " SKIP_VERSION_CHECK Skips the Crystal compiler version check (Default: 0)"
|
||||
|
||||
|
||||
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
# https://github.com/openssl/openssl/releases/tag/openssl-3.6.3
|
||||
ARG OPENSSL_VERSION='3.6.3'
|
||||
ARG OPENSSL_SHA256='243a86649cf6f23eeb6a2ff2456e09e5d77dd9018a54d3d96b0c6bdd6ba6c7f1'
|
||||
# https://github.com/openssl/openssl/releases/tag/openssl-3.6.5
|
||||
ARG OPENSSL_VERSION='3.6.5'
|
||||
ARG OPENSSL_SHA256='a2157c2830efdec3788939b00c9b0638306d3f0bbb76dc4832ee503bb397df98'
|
||||
|
||||
FROM 84codes/crystal:1.21.0-alpine AS dependabot-crystal
|
||||
FROM 84codes/crystal:1.22.0-alpine AS dependabot-crystal
|
||||
|
||||
# We compile openssl ourselves due to a memory leak in how crystal interacts
|
||||
# with openssl
|
||||
|
||||
+1
-1
Submodule mocks updated: b55d58dea9...3dee503b6b.
Generated
+3
-3
@@ -20,11 +20,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1786599213,
|
||||
"narHash": "sha256-yNJd40f11EzXBjSByCB7IPpeFFAdeoSKKM67dGkfFoU=",
|
||||
"lastModified": 1791260994,
|
||||
"narHash": "sha256-Miqqk/ammqnTUxaoCyvtwoPeLbI1ksFyLxi/CazZpWY=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "0e251e24a4f24e036a084b6b4b2d2491af4167f4",
|
||||
"rev": "151fa4e8ddfdd8dd25d945ad94ed54a13de9f6e4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+56
-23
@@ -44,6 +44,7 @@ import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
@@ -52,6 +53,10 @@ DEFAULT_MODEL = "deepseek/deepseek-v4-flash-0731"
|
||||
REPO_URL = "https://github.com/iv-org/invidious"
|
||||
CHANGELOG_FILE = "CHANGELOG.md"
|
||||
SHARD_FILE = "shard.yml"
|
||||
LLM_ATTEMPTS = 3
|
||||
# Per-attempt wall-clock limit. The socket timeout alone is not enough, as
|
||||
# OpenRouter keeps idle connections alive by trickling whitespace.
|
||||
LLM_DEADLINE = 600
|
||||
|
||||
|
||||
def die(msg):
|
||||
@@ -212,29 +217,57 @@ def call_llm(base_url, api_key, model, prompt):
|
||||
],
|
||||
"temperature": 0.3,
|
||||
}
|
||||
req = urllib.request.Request(
|
||||
url, data=json.dumps(payload).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
# OpenRouter-specific attribution headers; ignored by other APIs.
|
||||
"HTTP-Referer": REPO_URL,
|
||||
"X-Title": "Invidious release changelog",
|
||||
}, method="POST")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=180) as resp:
|
||||
body = json.loads(resp.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
die(f"LLM API HTTP {exc.code}: {exc.read().decode('utf-8', 'replace')}")
|
||||
except urllib.error.URLError as exc:
|
||||
die(f"LLM API request failed: {exc}")
|
||||
try:
|
||||
content = body["choices"][0]["message"]["content"].strip()
|
||||
except (KeyError, IndexError):
|
||||
die(f"unexpected LLM API response: {json.dumps(body)[:1000]}")
|
||||
if not content:
|
||||
die("LLM API returned empty content")
|
||||
return content
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
# OpenRouter-specific attribution headers; ignored by other APIs.
|
||||
"HTTP-Referer": REPO_URL,
|
||||
"X-Title": "Invidious release changelog",
|
||||
}
|
||||
error = None
|
||||
for attempt in range(1, LLM_ATTEMPTS + 1):
|
||||
if error:
|
||||
print(f"LLM attempt {attempt - 1}/{LLM_ATTEMPTS} failed: {error}; "
|
||||
"retrying...", file=sys.stderr)
|
||||
time.sleep(10 * (attempt - 1))
|
||||
req = urllib.request.Request(url, data=data, headers=headers,
|
||||
method="POST")
|
||||
try:
|
||||
deadline = time.monotonic() + LLM_DEADLINE
|
||||
raw = b""
|
||||
with urllib.request.urlopen(req, timeout=180) as resp:
|
||||
while chunk := resp.read1(8192):
|
||||
raw += chunk
|
||||
if time.monotonic() > deadline:
|
||||
raise TimeoutError(f"no answer after {LLM_DEADLINE}s")
|
||||
body = json.loads(raw.decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
error = f"HTTP {exc.code}: {exc.read().decode('utf-8', 'replace')}"
|
||||
# Client errors (bad key, unknown model...) won't fix themselves.
|
||||
if exc.code < 500 and exc.code != 429:
|
||||
break
|
||||
continue
|
||||
except (urllib.error.URLError, TimeoutError) as exc:
|
||||
error = f"request failed: {exc}"
|
||||
continue
|
||||
except json.JSONDecodeError as exc:
|
||||
error = f"invalid JSON response: {exc}"
|
||||
continue
|
||||
try:
|
||||
choice = body["choices"][0]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
error = f"unexpected response: {json.dumps(body)[:1000]}"
|
||||
continue
|
||||
# Providers may return a choice with null content (upstream error,
|
||||
# all tokens spent on reasoning...); report why instead of crashing.
|
||||
content = ((choice.get("message") or {}).get("content") or "").strip()
|
||||
if content:
|
||||
return content
|
||||
error = ("empty content (finish_reason="
|
||||
f"{choice.get('finish_reason')!r}, "
|
||||
f"error={json.dumps(choice.get('error') or body.get('error'))})")
|
||||
die(f"LLM API failed after {attempt} attempt(s): {error}")
|
||||
|
||||
|
||||
def generate_prose(prs, version, base_url, api_key, model):
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ version: 2.0
|
||||
shards:
|
||||
ameba:
|
||||
git: https://github.com/crystal-ameba/ameba.git
|
||||
version: 1.6.1
|
||||
version: 1.7.0
|
||||
|
||||
athena-negotiation:
|
||||
git: https://github.com/athena-framework/negotiation.git
|
||||
@@ -42,7 +42,7 @@ shards:
|
||||
|
||||
spectator:
|
||||
git: https://github.com/icy-arctic-fox/spectator.git
|
||||
version: 0.10.6
|
||||
version: 0.12.4
|
||||
|
||||
sqlite3:
|
||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||
|
||||
@@ -8,9 +8,11 @@ authors:
|
||||
targets:
|
||||
invidious:
|
||||
main: src/invidious.cr
|
||||
ameba:
|
||||
main: lib/ameba/bin/ameba.cr
|
||||
|
||||
description: |
|
||||
Invidious is an alternative front-end to YouTube
|
||||
Invidious is an alternative front-end to YouTube
|
||||
|
||||
dependencies:
|
||||
pg:
|
||||
@@ -35,12 +37,12 @@ dependencies:
|
||||
development_dependencies:
|
||||
spectator:
|
||||
github: icy-arctic-fox/spectator
|
||||
version: ~> 0.10.4
|
||||
version: ~> 0.12.4
|
||||
ameba:
|
||||
github: crystal-ameba/ameba
|
||||
version: ~> 1.6.1
|
||||
version: ~> 1.7.0
|
||||
|
||||
crystal: ">= 1.10.0, < 2.0.0"
|
||||
crystal: ">= 1.14.1, < 2.0.0"
|
||||
|
||||
license: AGPL-3.0-only
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ end
|
||||
|
||||
# Get relative file path to a file within the static_assets_handler folder
|
||||
macro get_file_path(basename)
|
||||
"spec/http_server/handlers/static_assets_handler/#{ {{basename}} }"
|
||||
"spec/http_server/handlers/static_assets_handler/#{ {{ basename }} }"
|
||||
end
|
||||
|
||||
Spectator.describe StaticAssetsHandler do
|
||||
|
||||
@@ -8,21 +8,21 @@ end
|
||||
|
||||
def csv_sample
|
||||
return <<-CSV
|
||||
Kanal-ID,Kanal-URL,Kanaltitel
|
||||
UC0hHW5Y08ggq-9kbrGgWj0A,http://www.youtube.com/channel/UC0hHW5Y08ggq-9kbrGgWj0A,Matias Marolla
|
||||
UC0vBXGSyV14uvJ4hECDOl0Q,http://www.youtube.com/channel/UC0vBXGSyV14uvJ4hECDOl0Q,Techquickie
|
||||
UC1sELGmy5jp5fQUugmuYlXQ,http://www.youtube.com/channel/UC1sELGmy5jp5fQUugmuYlXQ,Minecraft
|
||||
UC9kFnwdCRrX7oTjqKd6-tiQ,http://www.youtube.com/channel/UC9kFnwdCRrX7oTjqKd6-tiQ,LUMOX - Topic
|
||||
UCBa659QWEk1AI4Tg--mrJ2A,http://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A,Tom Scott
|
||||
UCGu6_XQ64rXPR6nuitMQE_A,http://www.youtube.com/channel/UCGu6_XQ64rXPR6nuitMQE_A,Callcenter Fun
|
||||
UCGwu0nbY2wSkW8N-cghnLpA,http://www.youtube.com/channel/UCGwu0nbY2wSkW8N-cghnLpA,Jaiden Animations
|
||||
UCQ0OvZ54pCFZwsKxbltg_tg,http://www.youtube.com/channel/UCQ0OvZ54pCFZwsKxbltg_tg,Methos
|
||||
UCRE6itj4Jte4manQEu3Y7OA,http://www.youtube.com/channel/UCRE6itj4Jte4manQEu3Y7OA,Chipflake
|
||||
UCRLc6zsv_d0OEBO8OOkz-DA,http://www.youtube.com/channel/UCRLc6zsv_d0OEBO8OOkz-DA,Kegy
|
||||
UCSl5Uxu2LyaoAoMMGp6oTJA,http://www.youtube.com/channel/UCSl5Uxu2LyaoAoMMGp6oTJA,Atomic Shrimp
|
||||
UCXuqSBlHAE6Xw-yeJA0Tunw,http://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw,Linus Tech Tips
|
||||
UCZ5XnGb-3t7jCkXdawN2tkA,http://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA,Discord
|
||||
CSV
|
||||
Kanal-ID,Kanal-URL,Kanaltitel
|
||||
UC0hHW5Y08ggq-9kbrGgWj0A,http://www.youtube.com/channel/UC0hHW5Y08ggq-9kbrGgWj0A,Matias Marolla
|
||||
UC0vBXGSyV14uvJ4hECDOl0Q,http://www.youtube.com/channel/UC0vBXGSyV14uvJ4hECDOl0Q,Techquickie
|
||||
UC1sELGmy5jp5fQUugmuYlXQ,http://www.youtube.com/channel/UC1sELGmy5jp5fQUugmuYlXQ,Minecraft
|
||||
UC9kFnwdCRrX7oTjqKd6-tiQ,http://www.youtube.com/channel/UC9kFnwdCRrX7oTjqKd6-tiQ,LUMOX - Topic
|
||||
UCBa659QWEk1AI4Tg--mrJ2A,http://www.youtube.com/channel/UCBa659QWEk1AI4Tg--mrJ2A,Tom Scott
|
||||
UCGu6_XQ64rXPR6nuitMQE_A,http://www.youtube.com/channel/UCGu6_XQ64rXPR6nuitMQE_A,Callcenter Fun
|
||||
UCGwu0nbY2wSkW8N-cghnLpA,http://www.youtube.com/channel/UCGwu0nbY2wSkW8N-cghnLpA,Jaiden Animations
|
||||
UCQ0OvZ54pCFZwsKxbltg_tg,http://www.youtube.com/channel/UCQ0OvZ54pCFZwsKxbltg_tg,Methos
|
||||
UCRE6itj4Jte4manQEu3Y7OA,http://www.youtube.com/channel/UCRE6itj4Jte4manQEu3Y7OA,Chipflake
|
||||
UCRLc6zsv_d0OEBO8OOkz-DA,http://www.youtube.com/channel/UCRLc6zsv_d0OEBO8OOkz-DA,Kegy
|
||||
UCSl5Uxu2LyaoAoMMGp6oTJA,http://www.youtube.com/channel/UCSl5Uxu2LyaoAoMMGp6oTJA,Atomic Shrimp
|
||||
UCXuqSBlHAE6Xw-yeJA0Tunw,http://www.youtube.com/channel/UCXuqSBlHAE6Xw-yeJA0Tunw,Linus Tech Tips
|
||||
UCZ5XnGb-3t7jCkXdawN2tkA,http://www.youtube.com/channel/UCZ5XnGb-3t7jCkXdawN2tkA,Discord
|
||||
CSV
|
||||
end
|
||||
|
||||
Spectator.describe Invidious::User::Import do
|
||||
|
||||
+23
-2
@@ -50,6 +50,27 @@ require "./invidious/routes/**"
|
||||
require "./invidious/jobs/base_job"
|
||||
require "./invidious/jobs/*"
|
||||
|
||||
# Show a nice message in case the current Crystal compiler version is unsupoported```
|
||||
# encouraging the user compiling Invidious to update it```
|
||||
# version.
|
||||
{% begin %}
|
||||
# `-Dskip_version_check` compile-time flag, for development purposes
|
||||
# or weird installations that lack the `shard.yml` file when compiling
|
||||
# Invidious.
|
||||
{% if !flag?(:skip_version_check) %}
|
||||
{% if file_exists?("#{__DIR__}/../shard.yml") %}
|
||||
{% shard_yml = read_file("#{__DIR__}/../shard.yml") %}
|
||||
{% crystal_constraint = shard_yml.split('\n').find(&.starts_with?("crystal:")) %}
|
||||
{% minimum_version = crystal_constraint.split('"')[1].split(",").first.split(">=").last.strip %}
|
||||
{% if compare_versions(Crystal::VERSION, minimum_version) < 0 %}
|
||||
{{ raise "Crystal version #{minimum_version} or newer is required to build Invidious. \
|
||||
You currently have Crystal version #{Crystal::VERSION} installed. \
|
||||
We recommend that you install it following the way that the team behind the Crystal compiler itself recommend for your OS, see: https://crystal-lang.org/install/" }}
|
||||
{% end %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
# Declare the base namespace for invidious
|
||||
module Invidious
|
||||
end
|
||||
@@ -102,7 +123,7 @@ Kemal.config.extra_options do |parser|
|
||||
parser.on("-c THREADS", "--channel-threads=THREADS", "Number of threads for refreshing channels (default: #{CONFIG.channel_threads})") do |number|
|
||||
begin
|
||||
CONFIG.channel_threads = number.to_i
|
||||
rescue ex
|
||||
rescue
|
||||
puts "THREADS must be integer"
|
||||
exit
|
||||
end
|
||||
@@ -110,7 +131,7 @@ Kemal.config.extra_options do |parser|
|
||||
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
|
||||
rescue
|
||||
puts "THREADS must be integer"
|
||||
exit
|
||||
end
|
||||
|
||||
@@ -93,7 +93,7 @@ struct ChannelVideo
|
||||
def to_tuple
|
||||
{% begin %}
|
||||
{
|
||||
{{@type.instance_vars.map(&.name).splat}}
|
||||
{{ @type.instance_vars.map(&.name).splat }}
|
||||
}
|
||||
{% end %}
|
||||
end
|
||||
@@ -107,7 +107,7 @@ class ChannelRedirect < Exception
|
||||
end
|
||||
|
||||
def get_batch_channels(channels)
|
||||
finished_channel = Channel(String | Nil).new
|
||||
finished_channel = Channel(String?).new
|
||||
max_threads = 10
|
||||
|
||||
spawn do
|
||||
@@ -125,7 +125,7 @@ def get_batch_channels(channels)
|
||||
begin
|
||||
get_channel(ucid)
|
||||
finished_channel.send(ucid)
|
||||
rescue ex
|
||||
rescue
|
||||
finished_channel.send(nil)
|
||||
ensure
|
||||
active_channel.send(nil)
|
||||
|
||||
@@ -15,7 +15,7 @@ module Invidious::Comments
|
||||
|
||||
if url.host.nil? || url.host.not_nil!.ends_with?("youtube.com") || url.host.not_nil!.ends_with?("youtu.be")
|
||||
if url.host.try &.ends_with? "youtu.be"
|
||||
url = "/watch?v=#{url.path.lstrip('/')}#{url.query_params}"
|
||||
anchor["href"] = "/watch?v=#{url.path.lstrip('/')}#{url.query_params}"
|
||||
else
|
||||
if url.path == "/redirect"
|
||||
params = HTTP::Params.parse(url.query.not_nil!)
|
||||
@@ -27,7 +27,7 @@ module Invidious::Comments
|
||||
elsif url.to_s == "#"
|
||||
begin
|
||||
length_seconds = decode_length_seconds(anchor.content)
|
||||
rescue ex
|
||||
rescue
|
||||
length_seconds = decode_time(anchor.content)
|
||||
end
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ struct ConfigPreferences
|
||||
def to_tuple
|
||||
{% begin %}
|
||||
{
|
||||
{{(@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat}}
|
||||
{{ (@type.instance_vars.map { |var| "#{var.name}: #{var.name}".id }).splat }}
|
||||
}
|
||||
{% end %}
|
||||
end
|
||||
@@ -237,14 +237,14 @@ class Config
|
||||
{% for ivar in Config.instance_vars %}
|
||||
{% env_id = "INVIDIOUS_#{ivar.id.upcase}" %}
|
||||
|
||||
if ENV.has_key?({{env_id}})
|
||||
env_value = ENV.fetch({{env_id}})
|
||||
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])
|
||||
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
|
||||
@@ -252,10 +252,10 @@ class Config
|
||||
{% 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|
|
||||
{{ ivar_types }}.each do |ivar_type|
|
||||
if !success
|
||||
begin
|
||||
config.{{ivar.id}} = ivar_type.from_yaml(env_value)
|
||||
config.{{ ivar.id }} = ivar_type.from_yaml(env_value)
|
||||
success = true
|
||||
rescue
|
||||
# nop
|
||||
@@ -266,14 +266,14 @@ class Config
|
||||
|
||||
# Exit on fail
|
||||
if !success
|
||||
puts %(Config.{{ivar.id}} failed to parse #{env_value} as {{ivar.type}})
|
||||
puts %(Config.{{ ivar.id }} failed to parse #{env_value} as {{ ivar.type }})
|
||||
exit(1)
|
||||
end
|
||||
end
|
||||
|
||||
# Warn when any config attribute is set to "CHANGE_ME!!"
|
||||
if config.{{ivar.id}} == "CHANGE_ME!!"
|
||||
puts "Config: The value of '#{ {{ivar.stringify}} }' needs to be changed!!"
|
||||
if config.{{ ivar.id }} == "CHANGE_ME!!"
|
||||
puts "Config: The value of '#{ {{ ivar.stringify }} }' needs to be changed!!"
|
||||
exit(1)
|
||||
end
|
||||
{% end %}
|
||||
|
||||
@@ -8,7 +8,7 @@ module Invidious::Database::Annotations
|
||||
INSERT INTO annotations
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, id, annotations)
|
||||
end
|
||||
@@ -17,7 +17,7 @@ module Invidious::Database::Annotations
|
||||
request = <<-SQL
|
||||
SELECT * FROM annotations
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, id, as: Annotation)
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ module Invidious::Database
|
||||
# Create table if it doesn't exist
|
||||
begin
|
||||
PG_DB.exec("SELECT * FROM #{table_name} LIMIT 0")
|
||||
rescue ex
|
||||
rescue
|
||||
LOGGER.info("check_table: check_table: CREATE TABLE #{table_name}")
|
||||
|
||||
PG_DB.using_connection do |conn|
|
||||
|
||||
@@ -16,13 +16,13 @@ module Invidious::Database::Channels
|
||||
request = <<-SQL
|
||||
INSERT INTO channels
|
||||
VALUES (#{arg_array(channel_array)})
|
||||
SQL
|
||||
SQL
|
||||
|
||||
if update_on_conflict
|
||||
request += <<-SQL
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET author = $2, updated = $3
|
||||
SQL
|
||||
SQL
|
||||
end
|
||||
|
||||
PG_DB.exec(request, args: channel_array)
|
||||
@@ -37,7 +37,7 @@ module Invidious::Database::Channels
|
||||
UPDATE channels
|
||||
SET updated = now(), author = $1, deleted = false
|
||||
WHERE id = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, author, id)
|
||||
end
|
||||
@@ -47,7 +47,7 @@ module Invidious::Database::Channels
|
||||
UPDATE channels
|
||||
SET subscribed = now()
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, id)
|
||||
end
|
||||
@@ -57,7 +57,7 @@ module Invidious::Database::Channels
|
||||
UPDATE channels
|
||||
SET updated = now(), deleted = true
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, id)
|
||||
end
|
||||
@@ -70,7 +70,7 @@ module Invidious::Database::Channels
|
||||
request = <<-SQL
|
||||
SELECT * FROM channels
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, id, as: InvidiousChannel)
|
||||
end
|
||||
@@ -81,7 +81,7 @@ module Invidious::Database::Channels
|
||||
request = <<-SQL
|
||||
SELECT * FROM channels
|
||||
WHERE id = ANY($1)
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_all(request, ids, as: InvidiousChannel)
|
||||
end
|
||||
@@ -112,7 +112,7 @@ module Invidious::Database::ChannelVideos
|
||||
SET title = $2, published = $3, updated = $4, ucid = $5,
|
||||
author = $6, length_seconds = $7, live_now = $8, #{last_items}
|
||||
RETURNING (xmax=0) AS was_insert
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one(request, *video.to_tuple, as: Bool)
|
||||
end
|
||||
@@ -128,7 +128,7 @@ module Invidious::Database::ChannelVideos
|
||||
SELECT * FROM channel_videos
|
||||
WHERE id = ANY($1)
|
||||
ORDER BY published DESC
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_all(request, ids, as: ChannelVideo)
|
||||
end
|
||||
@@ -139,7 +139,7 @@ module Invidious::Database::ChannelVideos
|
||||
WHERE ucid = $1 AND published > $2
|
||||
ORDER BY published DESC
|
||||
LIMIT 15
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_all(request, ucid, since, as: ChannelVideo)
|
||||
end
|
||||
@@ -151,7 +151,7 @@ module Invidious::Database::ChannelVideos
|
||||
WHERE ucid IN (SELECT channel FROM (SELECT UNNEST(subscriptions) AS channel FROM users) AS d
|
||||
GROUP BY channel ORDER BY COUNT(channel) DESC LIMIT 40)
|
||||
ORDER BY ucid, published DESC
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_all(request, as: ChannelVideo)
|
||||
end
|
||||
|
||||
@@ -4,27 +4,27 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
CREATE TABLE IF NOT EXISTS public.channels
|
||||
(
|
||||
id text NOT NULL,
|
||||
author text,
|
||||
updated timestamp with time zone,
|
||||
deleted boolean,
|
||||
subscribed timestamp with time zone,
|
||||
CONSTRAINT channels_id_key UNIQUE (id)
|
||||
);
|
||||
SQL
|
||||
CREATE TABLE IF NOT EXISTS public.channels
|
||||
(
|
||||
id text NOT NULL,
|
||||
author text,
|
||||
updated timestamp with time zone,
|
||||
deleted boolean,
|
||||
subscribed timestamp with time zone,
|
||||
CONSTRAINT channels_id_key UNIQUE (id)
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON TABLE public.channels TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON TABLE public.channels TO current_user;
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
CREATE INDEX IF NOT EXISTS channels_id_idx
|
||||
ON public.channels
|
||||
USING btree
|
||||
(id COLLATE pg_catalog."default");
|
||||
SQL
|
||||
CREATE INDEX IF NOT EXISTS channels_id_idx
|
||||
ON public.channels
|
||||
USING btree
|
||||
(id COLLATE pg_catalog."default");
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,25 +4,25 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
CREATE UNLOGGED TABLE IF NOT EXISTS public.videos
|
||||
(
|
||||
id text NOT NULL,
|
||||
info text,
|
||||
updated timestamp with time zone,
|
||||
CONSTRAINT videos_pkey PRIMARY KEY (id)
|
||||
);
|
||||
SQL
|
||||
CREATE UNLOGGED TABLE IF NOT EXISTS public.videos
|
||||
(
|
||||
id text NOT NULL,
|
||||
info text,
|
||||
updated timestamp with time zone,
|
||||
CONSTRAINT videos_pkey PRIMARY KEY (id)
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON TABLE public.videos TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON TABLE public.videos TO current_user;
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS id_idx
|
||||
ON public.videos
|
||||
USING btree
|
||||
(id COLLATE pg_catalog."default");
|
||||
SQL
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS id_idx
|
||||
ON public.videos
|
||||
USING btree
|
||||
(id COLLATE pg_catalog."default");
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,32 +4,32 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
CREATE TABLE IF NOT EXISTS public.channel_videos
|
||||
(
|
||||
id text NOT NULL,
|
||||
title text,
|
||||
published timestamp with time zone,
|
||||
updated timestamp with time zone,
|
||||
ucid text,
|
||||
author text,
|
||||
length_seconds integer,
|
||||
live_now boolean,
|
||||
premiere_timestamp timestamp with time zone,
|
||||
views bigint,
|
||||
CONSTRAINT channel_videos_id_key UNIQUE (id)
|
||||
);
|
||||
SQL
|
||||
CREATE TABLE IF NOT EXISTS public.channel_videos
|
||||
(
|
||||
id text NOT NULL,
|
||||
title text,
|
||||
published timestamp with time zone,
|
||||
updated timestamp with time zone,
|
||||
ucid text,
|
||||
author text,
|
||||
length_seconds integer,
|
||||
live_now boolean,
|
||||
premiere_timestamp timestamp with time zone,
|
||||
views bigint,
|
||||
CONSTRAINT channel_videos_id_key UNIQUE (id)
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON TABLE public.channel_videos TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON TABLE public.channel_videos TO current_user;
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx
|
||||
ON public.channel_videos
|
||||
USING btree
|
||||
(ucid COLLATE pg_catalog."default");
|
||||
SQL
|
||||
CREATE INDEX IF NOT EXISTS channel_videos_ucid_idx
|
||||
ON public.channel_videos
|
||||
USING btree
|
||||
(ucid COLLATE pg_catalog."default");
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,31 +4,31 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
CREATE TABLE IF NOT EXISTS public.users
|
||||
(
|
||||
updated timestamp with time zone,
|
||||
notifications text[],
|
||||
subscriptions text[],
|
||||
email text NOT NULL,
|
||||
preferences text,
|
||||
password text,
|
||||
token text,
|
||||
watched text[],
|
||||
feed_needs_update boolean,
|
||||
CONSTRAINT users_email_key UNIQUE (email)
|
||||
);
|
||||
SQL
|
||||
CREATE TABLE IF NOT EXISTS public.users
|
||||
(
|
||||
updated timestamp with time zone,
|
||||
notifications text[],
|
||||
subscriptions text[],
|
||||
email text NOT NULL,
|
||||
preferences text,
|
||||
password text,
|
||||
token text,
|
||||
watched text[],
|
||||
feed_needs_update boolean,
|
||||
CONSTRAINT users_email_key UNIQUE (email)
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON TABLE public.users TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON TABLE public.users TO current_user;
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx
|
||||
ON public.users
|
||||
USING btree
|
||||
(lower(email) COLLATE pg_catalog."default");
|
||||
SQL
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx
|
||||
ON public.users
|
||||
USING btree
|
||||
(lower(email) COLLATE pg_catalog."default");
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,25 +4,25 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
CREATE TABLE IF NOT EXISTS public.session_ids
|
||||
(
|
||||
id text NOT NULL,
|
||||
email text,
|
||||
issued timestamp with time zone,
|
||||
CONSTRAINT session_ids_pkey PRIMARY KEY (id)
|
||||
);
|
||||
SQL
|
||||
CREATE TABLE IF NOT EXISTS public.session_ids
|
||||
(
|
||||
id text NOT NULL,
|
||||
email text,
|
||||
issued timestamp with time zone,
|
||||
CONSTRAINT session_ids_pkey PRIMARY KEY (id)
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON TABLE public.session_ids TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON TABLE public.session_ids TO current_user;
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
CREATE INDEX IF NOT EXISTS session_ids_id_idx
|
||||
ON public.session_ids
|
||||
USING btree
|
||||
(id COLLATE pg_catalog."default");
|
||||
SQL
|
||||
CREATE INDEX IF NOT EXISTS session_ids_id_idx
|
||||
ON public.session_ids
|
||||
USING btree
|
||||
(id COLLATE pg_catalog."default");
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,24 +4,24 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
CREATE TABLE IF NOT EXISTS public.nonces
|
||||
(
|
||||
nonce text,
|
||||
expire timestamp with time zone,
|
||||
CONSTRAINT nonces_id_key UNIQUE (nonce)
|
||||
);
|
||||
SQL
|
||||
CREATE TABLE IF NOT EXISTS public.nonces
|
||||
(
|
||||
nonce text,
|
||||
expire timestamp with time zone,
|
||||
CONSTRAINT nonces_id_key UNIQUE (nonce)
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON TABLE public.nonces TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON TABLE public.nonces TO current_user;
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
CREATE INDEX IF NOT EXISTS nonces_nonce_idx
|
||||
ON public.nonces
|
||||
USING btree
|
||||
(nonce COLLATE pg_catalog."default");
|
||||
SQL
|
||||
CREATE INDEX IF NOT EXISTS nonces_nonce_idx
|
||||
ON public.nonces
|
||||
USING btree
|
||||
(nonce COLLATE pg_catalog."default");
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,17 +4,17 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
CREATE TABLE IF NOT EXISTS public.annotations
|
||||
(
|
||||
id text NOT NULL,
|
||||
annotations xml,
|
||||
CONSTRAINT annotations_id_key UNIQUE (id)
|
||||
);
|
||||
SQL
|
||||
CREATE TABLE IF NOT EXISTS public.annotations
|
||||
(
|
||||
id text NOT NULL,
|
||||
annotations xml,
|
||||
CONSTRAINT annotations_id_key UNIQUE (id)
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON TABLE public.annotations TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON TABLE public.annotations TO current_user;
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,33 +5,33 @@ module Invidious::Database::Migrations
|
||||
def up(conn : DB::Connection)
|
||||
if !privacy_type_exists?(conn)
|
||||
conn.exec <<-SQL
|
||||
CREATE TYPE public.privacy AS ENUM
|
||||
(
|
||||
'Public',
|
||||
'Unlisted',
|
||||
'Private'
|
||||
);
|
||||
SQL
|
||||
CREATE TYPE public.privacy AS ENUM
|
||||
(
|
||||
'Public',
|
||||
'Unlisted',
|
||||
'Private'
|
||||
);
|
||||
SQL
|
||||
end
|
||||
|
||||
conn.exec <<-SQL
|
||||
CREATE TABLE IF NOT EXISTS public.playlists
|
||||
(
|
||||
title text,
|
||||
id text primary key,
|
||||
author text,
|
||||
description text,
|
||||
video_count integer,
|
||||
created timestamptz,
|
||||
updated timestamptz,
|
||||
privacy privacy,
|
||||
index int8[]
|
||||
);
|
||||
SQL
|
||||
CREATE TABLE IF NOT EXISTS public.playlists
|
||||
(
|
||||
title text,
|
||||
id text primary key,
|
||||
author text,
|
||||
description text,
|
||||
video_count integer,
|
||||
created timestamptz,
|
||||
updated timestamptz,
|
||||
privacy privacy,
|
||||
index int8[]
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON public.playlists TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON public.playlists TO current_user;
|
||||
SQL
|
||||
end
|
||||
|
||||
private def privacy_type_exists?(conn : DB::Connection) : Bool
|
||||
@@ -42,7 +42,7 @@ module Invidious::Database::Migrations
|
||||
WHERE pg_namespace.nspname = 'public'
|
||||
AND pg_type.typname = 'privacy'
|
||||
LIMIT 1;
|
||||
SQL
|
||||
SQL
|
||||
|
||||
!conn.query_one?(request, as: Int32).nil?
|
||||
end
|
||||
|
||||
@@ -4,24 +4,24 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
CREATE TABLE IF NOT EXISTS public.playlist_videos
|
||||
(
|
||||
title text,
|
||||
id text,
|
||||
author text,
|
||||
ucid text,
|
||||
length_seconds integer,
|
||||
published timestamptz,
|
||||
plid text references playlists(id),
|
||||
index int8,
|
||||
live_now boolean,
|
||||
PRIMARY KEY (index,plid)
|
||||
);
|
||||
SQL
|
||||
CREATE TABLE IF NOT EXISTS public.playlist_videos
|
||||
(
|
||||
title text,
|
||||
id text,
|
||||
author text,
|
||||
ucid text,
|
||||
length_seconds integer,
|
||||
published timestamptz,
|
||||
plid text references playlists(id),
|
||||
index int8,
|
||||
live_now boolean,
|
||||
PRIMARY KEY (index,plid)
|
||||
);
|
||||
SQL
|
||||
|
||||
conn.exec <<-SQL
|
||||
GRANT ALL ON TABLE public.playlist_videos TO current_user;
|
||||
SQL
|
||||
GRANT ALL ON TABLE public.playlist_videos TO current_user;
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,8 +4,8 @@ module Invidious::Database::Migrations
|
||||
|
||||
def up(conn : DB::Connection)
|
||||
conn.exec <<-SQL
|
||||
ALTER TABLE public.videos SET UNLOGGED;
|
||||
SQL
|
||||
ALTER TABLE public.videos SET UNLOGGED;
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -44,6 +44,6 @@ class Invidious::Database::Migrator
|
||||
id bigserial PRIMARY KEY,
|
||||
version bigint NOT NULL
|
||||
)
|
||||
SQL
|
||||
SQL
|
||||
end
|
||||
end
|
||||
@@ -12,7 +12,7 @@ module Invidious::Database::Nonces
|
||||
INSERT INTO nonces
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, nonce, expire)
|
||||
end
|
||||
@@ -21,7 +21,7 @@ module Invidious::Database::Nonces
|
||||
request = <<-SQL
|
||||
DELETE FROM nonces *
|
||||
WHERE expire < now()
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request)
|
||||
end
|
||||
@@ -35,7 +35,7 @@ module Invidious::Database::Nonces
|
||||
UPDATE nonces
|
||||
SET expire = $1
|
||||
WHERE nonce = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, Time.utc(1990, 1, 1), nonce)
|
||||
end
|
||||
@@ -48,7 +48,7 @@ module Invidious::Database::Nonces
|
||||
request = <<-SQL
|
||||
SELECT * FROM nonces
|
||||
WHERE nonce = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, nonce, as: {String, Time})
|
||||
end
|
||||
|
||||
@@ -16,7 +16,7 @@ module Invidious::Database::Playlists
|
||||
request = <<-SQL
|
||||
INSERT INTO playlists
|
||||
VALUES (#{arg_array(playlist_array)})
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, args: playlist_array)
|
||||
end
|
||||
@@ -27,7 +27,7 @@ module Invidious::Database::Playlists
|
||||
request = <<-SQL
|
||||
DELETE FROM playlists *
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, id)
|
||||
end
|
||||
@@ -41,7 +41,7 @@ module Invidious::Database::Playlists
|
||||
UPDATE playlists
|
||||
SET title = $1, privacy = $2, description = $3, updated = $4
|
||||
WHERE id = $5
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, title, privacy, description, updated, id)
|
||||
end
|
||||
@@ -51,7 +51,7 @@ module Invidious::Database::Playlists
|
||||
UPDATE playlists
|
||||
SET description = $1
|
||||
WHERE id = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, description, id)
|
||||
end
|
||||
@@ -61,7 +61,7 @@ module Invidious::Database::Playlists
|
||||
UPDATE playlists
|
||||
SET subscribed = now()
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, id)
|
||||
end
|
||||
@@ -73,7 +73,7 @@ module Invidious::Database::Playlists
|
||||
video_count = cardinality(index) + 1,
|
||||
updated = now()
|
||||
WHERE id = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, index, id)
|
||||
end
|
||||
@@ -85,7 +85,7 @@ module Invidious::Database::Playlists
|
||||
video_count = cardinality(index) - 1,
|
||||
updated = now()
|
||||
WHERE id = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, index, id)
|
||||
end
|
||||
@@ -98,7 +98,7 @@ module Invidious::Database::Playlists
|
||||
request = <<-SQL
|
||||
SELECT * FROM playlists
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, id, as: InvidiousPlaylist)
|
||||
end
|
||||
@@ -107,7 +107,7 @@ module Invidious::Database::Playlists
|
||||
request = <<-SQL
|
||||
SELECT * FROM playlists
|
||||
WHERE author = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_all(request, author, as: InvidiousPlaylist)
|
||||
end
|
||||
@@ -121,7 +121,7 @@ module Invidious::Database::Playlists
|
||||
SELECT * FROM playlists
|
||||
WHERE author = $1 AND id LIKE 'IV%'
|
||||
ORDER BY created
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_all(request, email, as: InvidiousPlaylist)
|
||||
end
|
||||
@@ -131,7 +131,7 @@ module Invidious::Database::Playlists
|
||||
SELECT * FROM playlists
|
||||
WHERE author = $1 AND id NOT LIKE 'IV%'
|
||||
ORDER BY created
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_all(request, email, as: InvidiousPlaylist)
|
||||
end
|
||||
@@ -141,7 +141,7 @@ module Invidious::Database::Playlists
|
||||
SELECT id,title FROM playlists
|
||||
WHERE author = $1 AND id LIKE 'IV%'
|
||||
ORDER BY title
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_all(request, email, as: {String, String})
|
||||
end
|
||||
@@ -155,7 +155,7 @@ module Invidious::Database::Playlists
|
||||
request = <<-SQL
|
||||
SELECT id FROM playlists
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, id, as: String).nil?
|
||||
end
|
||||
@@ -165,7 +165,7 @@ module Invidious::Database::Playlists
|
||||
request = <<-SQL
|
||||
SELECT count(*) FROM playlists
|
||||
WHERE author = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, author, as: Int64) || 0_i64
|
||||
end
|
||||
@@ -189,7 +189,7 @@ module Invidious::Database::PlaylistVideos
|
||||
request = <<-SQL
|
||||
INSERT INTO playlist_videos
|
||||
VALUES (#{arg_array(video_array)})
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, args: video_array)
|
||||
end
|
||||
@@ -198,7 +198,7 @@ module Invidious::Database::PlaylistVideos
|
||||
request = <<-SQL
|
||||
DELETE FROM playlist_videos *
|
||||
WHERE index = $1 AND plid = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, index, plid)
|
||||
end
|
||||
@@ -207,7 +207,7 @@ module Invidious::Database::PlaylistVideos
|
||||
request = <<-SQL
|
||||
DELETE FROM playlist_videos *
|
||||
WHERE plid = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, plid)
|
||||
end
|
||||
@@ -223,7 +223,7 @@ module Invidious::Database::PlaylistVideos
|
||||
ORDER BY array_position($2, index)
|
||||
LIMIT $3
|
||||
OFFSET $4
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_all(request, plid, index, limit, offset, as: PlaylistVideo)
|
||||
end
|
||||
@@ -233,7 +233,7 @@ module Invidious::Database::PlaylistVideos
|
||||
SELECT index FROM playlist_videos
|
||||
WHERE plid = $1 AND id = $2
|
||||
LIMIT 1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, plid, vid, as: Int64)
|
||||
end
|
||||
@@ -244,7 +244,7 @@ module Invidious::Database::PlaylistVideos
|
||||
WHERE plid = $1
|
||||
ORDER BY array_position($2, index)
|
||||
LIMIT 1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, plid, index, as: String)
|
||||
end
|
||||
@@ -255,7 +255,7 @@ module Invidious::Database::PlaylistVideos
|
||||
WHERE plid = $1
|
||||
ORDER BY array_position($2, index)
|
||||
LIMIT $3
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_all(request, plid, index, limit, as: String)
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ module Invidious::Database::SessionIDs
|
||||
request = <<-SQL
|
||||
INSERT INTO session_ids
|
||||
VALUES ($1, $2, now())
|
||||
SQL
|
||||
SQL
|
||||
|
||||
request += " ON CONFLICT (id) DO NOTHING" if handle_conflicts
|
||||
|
||||
@@ -26,7 +26,7 @@ module Invidious::Database::SessionIDs
|
||||
request = <<-SQL
|
||||
DELETE FROM session_ids *
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, sid)
|
||||
end
|
||||
@@ -35,7 +35,7 @@ module Invidious::Database::SessionIDs
|
||||
request = <<-SQL
|
||||
DELETE FROM session_ids *
|
||||
WHERE email = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, email)
|
||||
end
|
||||
@@ -44,7 +44,7 @@ module Invidious::Database::SessionIDs
|
||||
request = <<-SQL
|
||||
DELETE FROM session_ids *
|
||||
WHERE id = $1 AND email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, sid, email)
|
||||
end
|
||||
@@ -57,7 +57,7 @@ module Invidious::Database::SessionIDs
|
||||
request = <<-SQL
|
||||
SELECT email FROM session_ids
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_one?(request, sid, as: String)
|
||||
end
|
||||
@@ -67,7 +67,7 @@ module Invidious::Database::SessionIDs
|
||||
SELECT id, issued FROM session_ids
|
||||
WHERE email = $1
|
||||
ORDER BY issued DESC
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_all(request, email, as: {session: String, issued: Time})
|
||||
end
|
||||
|
||||
@@ -10,7 +10,7 @@ module Invidious::Database::Statistics
|
||||
def count_users_total : Int64
|
||||
request = <<-SQL
|
||||
SELECT count(*) FROM users
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_one(request, as: Int64)
|
||||
end
|
||||
@@ -19,7 +19,7 @@ module Invidious::Database::Statistics
|
||||
request = <<-SQL
|
||||
SELECT count(*) FROM users
|
||||
WHERE CURRENT_TIMESTAMP - updated < '6 months'
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_one(request, as: Int64)
|
||||
end
|
||||
@@ -28,7 +28,7 @@ module Invidious::Database::Statistics
|
||||
request = <<-SQL
|
||||
SELECT count(*) FROM users
|
||||
WHERE CURRENT_TIMESTAMP - updated < '1 month'
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_one(request, as: Int64)
|
||||
end
|
||||
@@ -42,7 +42,7 @@ module Invidious::Database::Statistics
|
||||
SELECT updated FROM channels
|
||||
ORDER BY updated DESC
|
||||
LIMIT 1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.query_one?(request, as: Time)
|
||||
end
|
||||
|
||||
@@ -14,13 +14,13 @@ module Invidious::Database::Users
|
||||
request = <<-SQL
|
||||
INSERT INTO users
|
||||
VALUES (#{arg_array(user_array)})
|
||||
SQL
|
||||
SQL
|
||||
|
||||
if update_on_conflict
|
||||
request += <<-SQL
|
||||
ON CONFLICT (email) DO UPDATE
|
||||
SET updated = $1, subscriptions = $3
|
||||
SQL
|
||||
SQL
|
||||
end
|
||||
|
||||
PG_DB.exec(request, args: user_array)
|
||||
@@ -30,7 +30,7 @@ module Invidious::Database::Users
|
||||
request = <<-SQL
|
||||
DELETE FROM users *
|
||||
WHERE email = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, user.email)
|
||||
end
|
||||
@@ -44,7 +44,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET watched = $1
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, user.watched, user.email)
|
||||
end
|
||||
@@ -54,7 +54,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET watched = array_append(array_remove(watched, $1), $1)
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, vid, user.email)
|
||||
end
|
||||
@@ -64,7 +64,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET watched = array_remove(watched, $1)
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, vid, user.email)
|
||||
end
|
||||
@@ -74,7 +74,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET watched = '{}'
|
||||
WHERE email = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, user.email)
|
||||
end
|
||||
@@ -88,7 +88,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET feed_needs_update = true, subscriptions = $1
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, user.subscriptions, user.email)
|
||||
end
|
||||
@@ -99,7 +99,7 @@ module Invidious::Database::Users
|
||||
SET feed_needs_update = true,
|
||||
subscriptions = array_append(subscriptions,$1)
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, ucid, user.email)
|
||||
end
|
||||
@@ -110,7 +110,7 @@ module Invidious::Database::Users
|
||||
SET feed_needs_update = true,
|
||||
subscriptions = array_remove(subscriptions, $1)
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, ucid, user.email)
|
||||
end
|
||||
@@ -125,7 +125,7 @@ module Invidious::Database::Users
|
||||
SET notifications = array_cat(notifications, $1),
|
||||
feed_needs_update = true
|
||||
WHERE $2 = ANY(subscriptions)
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, video_ids, channel_id)
|
||||
end
|
||||
@@ -135,7 +135,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET notifications = array_remove(notifications, $1)
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, vid, user.email)
|
||||
end
|
||||
@@ -145,7 +145,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET notifications = '{}', updated = now()
|
||||
WHERE email = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, user.email)
|
||||
end
|
||||
@@ -159,7 +159,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET feed_needs_update = true
|
||||
WHERE $1 = ANY(subscriptions)
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, channel_id)
|
||||
end
|
||||
@@ -169,7 +169,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET preferences = $1
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, user.preferences.to_json, user.email)
|
||||
end
|
||||
@@ -179,7 +179,7 @@ module Invidious::Database::Users
|
||||
UPDATE users
|
||||
SET password = $1
|
||||
WHERE email = $2
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, pass, user.email)
|
||||
end
|
||||
@@ -192,7 +192,7 @@ module Invidious::Database::Users
|
||||
request = <<-SQL
|
||||
SELECT * FROM users
|
||||
WHERE email = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, email, as: User)
|
||||
end
|
||||
@@ -202,7 +202,7 @@ module Invidious::Database::Users
|
||||
request = <<-SQL
|
||||
SELECT * FROM users
|
||||
WHERE email = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one(request, email, as: User)
|
||||
end
|
||||
@@ -211,7 +211,7 @@ module Invidious::Database::Users
|
||||
request = <<-SQL
|
||||
SELECT * FROM users
|
||||
WHERE token = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, token, as: User)
|
||||
end
|
||||
@@ -221,7 +221,7 @@ module Invidious::Database::Users
|
||||
SELECT notifications
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one(request, user.email, as: Array(String))
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ module Invidious::Database::Videos
|
||||
INSERT INTO videos
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, video.id, video.info.to_json, video.updated)
|
||||
end
|
||||
@@ -17,7 +17,7 @@ module Invidious::Database::Videos
|
||||
request = <<-SQL
|
||||
DELETE FROM videos *
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, id)
|
||||
end
|
||||
@@ -26,7 +26,7 @@ module Invidious::Database::Videos
|
||||
request = <<-SQL
|
||||
DELETE FROM videos *
|
||||
WHERE updated < (now() - interval '6 hours')
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request)
|
||||
end
|
||||
@@ -36,7 +36,7 @@ module Invidious::Database::Videos
|
||||
UPDATE videos
|
||||
SET (id, info, updated) = ($1, $2, $3)
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
PG_DB.exec(request, video.id, video.info.to_json, video.updated)
|
||||
end
|
||||
@@ -45,7 +45,7 @@ module Invidious::Database::Videos
|
||||
request = <<-SQL
|
||||
SELECT * FROM videos
|
||||
WHERE id = $1
|
||||
SQL
|
||||
SQL
|
||||
|
||||
return PG_DB.query_one?(request, id, as: Video)
|
||||
end
|
||||
|
||||
@@ -38,3 +38,15 @@ end
|
||||
# some important informations, and that the query should be sent again.
|
||||
class RetryOnceException < Exception
|
||||
end
|
||||
|
||||
# Exception for invalid video IDs.
|
||||
class InvalidVideoID < InfoException
|
||||
getter id : String?
|
||||
|
||||
def initialize(@id)
|
||||
end
|
||||
|
||||
def message
|
||||
return "Invalid video ID '#{id}'"
|
||||
end
|
||||
end
|
||||
@@ -16,33 +16,33 @@ module Invidious::Frontend::Comments
|
||||
|
||||
if child.depth > 0
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-24">
|
||||
</div>
|
||||
<div class="pure-u-23-24">
|
||||
END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-24">
|
||||
</div>
|
||||
<div class="pure-u-23-24">
|
||||
END_HTML
|
||||
else
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
|
||||
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
|
||||
#{I18n.translate_count(locale, "comments_points_count", child.score, I18n::NumberFormatting::Separator)}
|
||||
<span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{I18n.translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
|
||||
<a href="https://www.reddit.com#{child.permalink}" title="#{I18n.translate(locale, "permalink")}">#{I18n.translate(locale, "permalink")}</a>
|
||||
</p>
|
||||
<div>
|
||||
#{body_html}
|
||||
#{replies_html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-onclick="toggle_parent">[ − ]</a>
|
||||
<b><a href="https://www.reddit.com/user/#{child.author}">#{child.author}</a></b>
|
||||
#{I18n.translate_count(locale, "comments_points_count", child.score, I18n::NumberFormatting::Separator)}
|
||||
<span title="#{child.created_utc.to_s("%a %B %-d %T %Y UTC")}">#{I18n.translate(locale, "`x` ago", recode_date(child.created_utc, locale))}</span>
|
||||
<a href="https://www.reddit.com#{child.permalink}" title="#{I18n.translate(locale, "permalink")}">#{I18n.translate(locale, "permalink")}</a>
|
||||
</p>
|
||||
<div>
|
||||
#{body_html}
|
||||
#{replies_html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -13,16 +13,16 @@ module Invidious::Frontend::Comments
|
||||
)
|
||||
|
||||
replies_html = <<-END_HTML
|
||||
<div id="replies" class="pure-g">
|
||||
<div class="pure-u-1-24"></div>
|
||||
<div class="pure-u-23-24">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-replies>#{replies_count_text}</a>
|
||||
</p>
|
||||
<div id="replies" class="pure-g">
|
||||
<div class="pure-u-1-24"></div>
|
||||
<div class="pure-u-23-24">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{child["replies"]["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-replies>#{replies_count_text}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
END_HTML
|
||||
elsif comments["authorId"]? && !comments["singlePost"]?
|
||||
# for posts we should display a link to the post
|
||||
replies_count_text = I18n.translate_count(locale,
|
||||
@@ -32,15 +32,15 @@ module Invidious::Frontend::Comments
|
||||
)
|
||||
|
||||
replies_html = <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-24"></div>
|
||||
<div class="pure-u-23-24">
|
||||
<p>
|
||||
<a href="/post/#{child["commentId"]}?ucid=#{comments["authorId"]}">#{replies_count_text}</a>
|
||||
</p>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1-24"></div>
|
||||
<div class="pure-u-23-24">
|
||||
<p>
|
||||
<a href="/post/#{child["commentId"]}?ucid=#{comments["authorId"]}">#{replies_count_text}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
END_HTML
|
||||
end
|
||||
|
||||
if !thin_mode
|
||||
@@ -70,18 +70,18 @@ module Invidious::Frontend::Comments
|
||||
end
|
||||
end
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g" style="width:100%">
|
||||
<div class="channel-profile pure-u-4-24 pure-u-md-2-24">
|
||||
<img loading="lazy" style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}" alt="" />
|
||||
</div>
|
||||
<div class="pure-u-20-24 pure-u-md-22-24">
|
||||
<p>
|
||||
<b>
|
||||
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{author_name}</a>
|
||||
</b>
|
||||
#{sponsor_icon}
|
||||
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
|
||||
END_HTML
|
||||
<div class="pure-g" style="width:100%">
|
||||
<div class="channel-profile pure-u-4-24 pure-u-md-2-24">
|
||||
<img loading="lazy" style="margin-right:1em;margin-top:1em;width:90%" src="#{author_thumbnail}" alt="" />
|
||||
</div>
|
||||
<div class="pure-u-20-24 pure-u-md-22-24">
|
||||
<p>
|
||||
<b>
|
||||
<a class="#{child["authorIsChannelOwner"] == true ? "channel-owner" : ""}" href="#{child["authorUrl"]}">#{author_name}</a>
|
||||
</b>
|
||||
#{sponsor_icon}
|
||||
<p style="white-space:pre-wrap">#{child["contentHtml"]}</p>
|
||||
END_HTML
|
||||
|
||||
if child["attachment"]?
|
||||
attachment = child["attachment"]
|
||||
@@ -91,25 +91,25 @@ module Invidious::Frontend::Comments
|
||||
attachment = attachment["imageThumbnails"][1]
|
||||
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-2">
|
||||
<img loading="lazy" style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}" alt="" />
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1 pure-u-md-1-2">
|
||||
<img loading="lazy" style="width:100%" src="/ggpht#{URI.parse(attachment["url"].as_s).request_target}" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
END_HTML
|
||||
when "video"
|
||||
if attachment["error"]?
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g video-iframe-wrapper">
|
||||
<p>#{attachment["error"]}</p>
|
||||
</div>
|
||||
END_HTML
|
||||
<div class="pure-g video-iframe-wrapper">
|
||||
<p>#{attachment["error"]}</p>
|
||||
</div>
|
||||
END_HTML
|
||||
else
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g video-iframe-wrapper">
|
||||
<iframe class="video-iframe" src='/embed/#{attachment["videoId"]?}?autoplay=0'></iframe>
|
||||
</div>
|
||||
END_HTML
|
||||
<div class="pure-g video-iframe-wrapper">
|
||||
<iframe class="video-iframe" src='/embed/#{attachment["videoId"]?}?autoplay=0'></iframe>
|
||||
</div>
|
||||
END_HTML
|
||||
end
|
||||
when "multiImage"
|
||||
html << <<-END_HTML
|
||||
@@ -137,35 +137,34 @@ module Invidious::Frontend::Comments
|
||||
END_HTML
|
||||
end
|
||||
html << <<-END_HTML
|
||||
</div>
|
||||
<div id="skip-#{child["commentId"]}"></div>
|
||||
</section>
|
||||
END_HTML
|
||||
else nil # Ignore
|
||||
</div>
|
||||
<div id="skip-#{child["commentId"]}"></div>
|
||||
</section>
|
||||
END_HTML
|
||||
end
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
<p>
|
||||
<span title="#{Time.unix(child["published"].as_i64).to_s(I18n.translate(locale, "%A %B %-d, %Y"))}">#{I18n.translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? I18n.translate(locale, "(edited)") : ""}</span>
|
||||
|
|
||||
END_HTML
|
||||
<p>
|
||||
<span title="#{Time.unix(child["published"].as_i64).to_s(I18n.translate(locale, "%A %B %-d, %Y"))}">#{I18n.translate(locale, "`x` ago", recode_date(Time.unix(child["published"].as_i64), locale))} #{child["isEdited"] == true ? I18n.translate(locale, "(edited)") : ""}</span>
|
||||
|
|
||||
END_HTML
|
||||
|
||||
if comments["videoId"]?
|
||||
html << <<-END_HTML
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/watch?v=#{comments["videoId"]}&lc=#{child["commentId"]}" title="#{I18n.translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
END_HTML
|
||||
elsif comments["authorId"]?
|
||||
html << <<-END_HTML
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com/channel/#{comments["authorId"]}/community?lb=#{child["commentId"]}" title="#{I18n.translate(locale, "YouTube comment permalink")}">[YT]</a>
|
||||
|
|
||||
END_HTML
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
<i class="icon ion-ios-thumbs-up"></i> #{number_with_separator(child["likeCount"])}
|
||||
END_HTML
|
||||
END_HTML
|
||||
|
||||
if child["creatorHeart"]?
|
||||
if !thin_mode
|
||||
@@ -184,28 +183,28 @@ module Invidious::Frontend::Comments
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
END_HTML
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html << <<-END_HTML
|
||||
</p>
|
||||
#{replies_html}
|
||||
</p>
|
||||
#{replies_html}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
END_HTML
|
||||
end
|
||||
|
||||
if comments["continuation"]?
|
||||
html << <<-END_HTML
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{I18n.translate(locale, "Load more")}</a>
|
||||
</p>
|
||||
<div class="pure-g">
|
||||
<div class="pure-u-1">
|
||||
<p>
|
||||
<a href="javascript:void(0)" data-continuation="#{comments["continuation"]}"
|
||||
data-onclick="get_youtube_replies" data-load-more #{"data-load-replies" if is_replies}>#{I18n.translate(locale, "Load more")}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
END_HTML
|
||||
END_HTML
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,11 +41,11 @@ module Invidious::Frontend::SearchFilters
|
||||
str << "\t\t\t\t<div class=\"filter-column\"><fieldset>\n"
|
||||
|
||||
str << "\t\t\t\t\t<legend><div class=\"filter-name underlined\">"
|
||||
str << I18n.translate(locale, "search_filters_{{name}}_label")
|
||||
str << I18n.translate(locale, "search_filters_{{ name }}_label")
|
||||
str << "</div></legend>\n"
|
||||
|
||||
str << "\t\t\t\t\t<div class=\"filter-options\">\n"
|
||||
make_{{name}}_filter_options(str, filters.{{name}}, locale)
|
||||
make_{{ name }}_filter_options(str, filters.{{ name }}, locale)
|
||||
str << "\t\t\t\t\t</div>"
|
||||
|
||||
str << "\t\t\t\t</fieldset></div>\n"
|
||||
@@ -57,12 +57,12 @@ module Invidious::Frontend::SearchFilters
|
||||
{% date = value.underscore %}
|
||||
|
||||
str << "\t\t\t\t\t\t<div>"
|
||||
str << "<input type='radio' name='date' id='filter-date-{{date}}' value='{{date}}'"
|
||||
str << " checked" if value.{{date}}?
|
||||
str << "<input type='radio' name='date' id='filter-date-{{ date }}' value='{{ date }}'"
|
||||
str << " checked" if value.{{ date }}?
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-date-{{date}}'>"
|
||||
str << I18n.translate(locale, "search_filters_date_option_{{date}}")
|
||||
str << "<label for='filter-date-{{ date }}'>"
|
||||
str << I18n.translate(locale, "search_filters_date_option_{{ date }}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
end
|
||||
@@ -73,12 +73,12 @@ module Invidious::Frontend::SearchFilters
|
||||
{% type = value.underscore %}
|
||||
|
||||
str << "\t\t\t\t\t\t<div>"
|
||||
str << "<input type='radio' name='type' id='filter-type-{{type}}' value='{{type}}'"
|
||||
str << " checked" if value.{{type}}?
|
||||
str << "<input type='radio' name='type' id='filter-type-{{ type }}' value='{{ type }}'"
|
||||
str << " checked" if value.{{ type }}?
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-type-{{type}}'>"
|
||||
str << I18n.translate(locale, "search_filters_type_option_{{type}}")
|
||||
str << "<label for='filter-type-{{ type }}'>"
|
||||
str << I18n.translate(locale, "search_filters_type_option_{{ type }}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
end
|
||||
@@ -89,12 +89,12 @@ module Invidious::Frontend::SearchFilters
|
||||
{% duration = value.underscore %}
|
||||
|
||||
str << "\t\t\t\t\t\t<div>"
|
||||
str << "<input type='radio' name='duration' id='filter-duration-{{duration}}' value='{{duration}}'"
|
||||
str << " checked" if value.{{duration}}?
|
||||
str << "<input type='radio' name='duration' id='filter-duration-{{ duration }}' value='{{ duration }}'"
|
||||
str << " checked" if value.{{ duration }}?
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-duration-{{duration}}'>"
|
||||
str << I18n.translate(locale, "search_filters_duration_option_{{duration}}")
|
||||
str << "<label for='filter-duration-{{ duration }}'>"
|
||||
str << I18n.translate(locale, "search_filters_duration_option_{{ duration }}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
end
|
||||
@@ -106,12 +106,12 @@ module Invidious::Frontend::SearchFilters
|
||||
{% feature = value.underscore %}
|
||||
|
||||
str << "\t\t\t\t\t\t<div>"
|
||||
str << "<input type='checkbox' name='features' id='filter-feature-{{feature}}' value='{{feature}}'"
|
||||
str << " checked" if value.{{feature}}?
|
||||
str << "<input type='checkbox' name='features' id='filter-feature-{{ feature }}' value='{{ feature }}'"
|
||||
str << " checked" if value.{{ feature }}?
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-feature-{{feature}}'>"
|
||||
str << I18n.translate(locale, "search_filters_features_option_{{feature}}")
|
||||
str << "<label for='filter-feature-{{ feature }}'>"
|
||||
str << I18n.translate(locale, "search_filters_features_option_{{ feature }}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
{% end %}
|
||||
@@ -123,12 +123,12 @@ module Invidious::Frontend::SearchFilters
|
||||
{% sort = value.underscore %}
|
||||
|
||||
str << "\t\t\t\t\t\t<div>"
|
||||
str << "<input type='radio' name='sort' id='filter-sort-{{sort}}' value='{{sort}}'"
|
||||
str << " checked" if value.{{sort}}?
|
||||
str << "<input type='radio' name='sort' id='filter-sort-{{ sort }}' value='{{ sort }}'"
|
||||
str << " checked" if value.{{ sort }}?
|
||||
str << '>'
|
||||
|
||||
str << "<label for='filter-sort-{{sort}}'>"
|
||||
str << I18n.translate(locale, "search_filters_sort_option_{{sort}}")
|
||||
str << "<label for='filter-sort-{{ sort }}'>"
|
||||
str << I18n.translate(locale, "search_filters_sort_option_{{ sort }}")
|
||||
str << "</label></div>\n"
|
||||
{% end %}
|
||||
end
|
||||
|
||||
@@ -68,7 +68,7 @@ class HTTP::Server::Response
|
||||
class Output
|
||||
private def unbuffered_flush
|
||||
@io.flush
|
||||
rescue ex : IO::Error
|
||||
rescue IO::Error
|
||||
unbuffered_close
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# -------------------
|
||||
|
||||
macro error_template(*args)
|
||||
error_template_helper(env, {{args.splat}})
|
||||
error_template_helper(env, {{ args.splat }})
|
||||
end
|
||||
|
||||
def github_details(summary : String, content : String)
|
||||
@@ -22,12 +22,12 @@ def get_issue_template(env : HTTP::Server::Context, exception : Exception) : Tup
|
||||
issue_title = "#{exception.message} (#{exception.class})"
|
||||
|
||||
issue_template = <<-TEXT
|
||||
Title: `#{HTML.escape(issue_title)}`
|
||||
Date: `#{Time::Format::ISO_8601_DATE_TIME.format(Time.utc)}`
|
||||
Route: `#{HTML.escape(env.request.resource)}`
|
||||
Version: `#{SOFTWARE["version"]} @ #{SOFTWARE["branch"]}`
|
||||
Title: `#{HTML.escape(issue_title)}`
|
||||
Date: `#{Time::Format::ISO_8601_DATE_TIME.format(Time.utc)}`
|
||||
Route: `#{HTML.escape(env.request.resource)}`
|
||||
Version: `#{SOFTWARE["version"]} @ #{SOFTWARE["branch"]}`
|
||||
|
||||
TEXT
|
||||
TEXT
|
||||
|
||||
issue_template += github_details("Backtrace", exception.inspect_with_backtrace)
|
||||
|
||||
@@ -80,7 +80,7 @@ def error_template_helper(env : HTTP::Server::Context, status_code : Int32, exce
|
||||
<!-- TODO: Add a "copy to clipboard" button -->
|
||||
<pre class="error-issue-template">#{issue_template}</pre>
|
||||
</div>
|
||||
END_HTML
|
||||
END_HTML
|
||||
|
||||
# Don't show the usual "next steps" widget. The same options are
|
||||
# proposed above the error message, just worded differently.
|
||||
@@ -106,7 +106,7 @@ end
|
||||
# -------------------
|
||||
|
||||
macro error_atom(*args)
|
||||
error_atom_helper(env, {{args.splat}})
|
||||
error_atom_helper(env, {{ args.splat }})
|
||||
end
|
||||
|
||||
def error_atom_helper(env : HTTP::Server::Context, status_code : Int32, exception : Exception)
|
||||
@@ -132,14 +132,14 @@ end
|
||||
# -------------------
|
||||
|
||||
macro error_json(*args)
|
||||
error_json_helper(env, {{args.splat}})
|
||||
error_json_helper(env, {{ args.splat }})
|
||||
end
|
||||
|
||||
def error_json_helper(
|
||||
env : HTTP::Server::Context,
|
||||
status_code : Int32,
|
||||
exception : Exception,
|
||||
additional_fields : Hash(String, Object) | Nil = nil,
|
||||
additional_fields : Hash(String, Object)? = nil,
|
||||
)
|
||||
if exception.is_a?(InfoException)
|
||||
return error_json_helper(env, status_code, exception.message || "", additional_fields)
|
||||
@@ -161,7 +161,7 @@ def error_json_helper(
|
||||
env : HTTP::Server::Context,
|
||||
status_code : Int32,
|
||||
message : String,
|
||||
additional_fields : Hash(String, Object) | Nil = nil,
|
||||
additional_fields : Hash(String, Object)? = nil,
|
||||
)
|
||||
env.response.content_type = "application/json"
|
||||
env.response.status_code = status_code
|
||||
@@ -204,7 +204,7 @@ def error_redirect_helper(env : HTTP::Server::Context)
|
||||
<a rel="noreferrer noopener" href="https://www.youtube.com#{env.request.resource}">#{go_to_youtube}</a>
|
||||
</li>
|
||||
</ul>
|
||||
END_HTML
|
||||
END_HTML
|
||||
else
|
||||
return ""
|
||||
end
|
||||
|
||||
@@ -2,10 +2,10 @@ module HTTP::Handler
|
||||
@@exclude_routes_tree = Radix::Tree(String).new
|
||||
|
||||
macro exclude(paths, method = "GET")
|
||||
class_name = {{@type.name}}
|
||||
method_downcase = {{method.downcase}}
|
||||
class_name = {{ @type.name }}
|
||||
method_downcase = {{ method.downcase }}
|
||||
class_name_method = "#{class_name}/#{method_downcase}"
|
||||
({{paths}}).each do |path|
|
||||
({{ paths }}).each do |path|
|
||||
@@exclude_routes_tree.add class_name_method + path, '/' + method_downcase + path
|
||||
end
|
||||
end
|
||||
@@ -20,8 +20,8 @@ module HTTP::Handler
|
||||
end
|
||||
|
||||
class Kemal::RouteHandler
|
||||
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %}
|
||||
exclude ["/api/v1/*"], {{method}}
|
||||
{% for method in %w[GET POST PUT HEAD DELETE PATCH OPTIONS] %}
|
||||
exclude ["/api/v1/*"], {{ method }}
|
||||
{% end %}
|
||||
|
||||
# Processes the route if it's a match. Otherwise renders 404.
|
||||
@@ -44,8 +44,8 @@ class Kemal::RouteHandler
|
||||
end
|
||||
|
||||
class Kemal::ExceptionHandler
|
||||
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %}
|
||||
exclude ["/api/v1/*"], {{method}}
|
||||
{% for method in %w[GET POST PUT HEAD DELETE PATCH OPTIONS] %}
|
||||
exclude ["/api/v1/*"], {{ method }}
|
||||
{% end %}
|
||||
|
||||
private def call_exception_with_status_code(context : HTTP::Server::Context, exception : Exception, status_code : Int32)
|
||||
@@ -72,8 +72,8 @@ class FilteredCompressHandler < HTTP::CompressHandler
|
||||
end
|
||||
|
||||
class AuthHandler < Kemal::Handler
|
||||
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %}
|
||||
only ["/api/v1/auth/*"], {{method}}
|
||||
{% for method in %w[GET POST PUT HEAD DELETE PATCH OPTIONS] %}
|
||||
only ["/api/v1/auth/*"], {{ method }}
|
||||
{% end %}
|
||||
|
||||
def call(env)
|
||||
@@ -121,8 +121,8 @@ class AuthHandler < Kemal::Handler
|
||||
end
|
||||
|
||||
class APIHandler < Kemal::Handler
|
||||
{% for method in %w(GET POST PUT HEAD DELETE PATCH OPTIONS) %}
|
||||
only ["/api/v1/*"], {{method}}
|
||||
{% for method in %w[GET POST PUT HEAD DELETE PATCH OPTIONS] %}
|
||||
only ["/api/v1/*"], {{ method }}
|
||||
{% end %}
|
||||
exclude ["/api/v1/auth/notifications"], "GET"
|
||||
exclude ["/api/v1/auth/notifications"], "POST"
|
||||
@@ -134,9 +134,9 @@ class APIHandler < Kemal::Handler
|
||||
end
|
||||
|
||||
class DisableAbusableAPIHandler < Kemal::Handler
|
||||
{% for method in %w(GET HEAD) %}
|
||||
{% for method in %w[GET HEAD] %}
|
||||
# This endpoints make a video request to Invidious companion.
|
||||
{% for endpoint in %w(videos clips transcripts) %}
|
||||
{% for endpoint in %w[videos clips transcripts] %}
|
||||
only ["/api/v1/{{ endpoint.id }}/:id"], {{ method }}
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
@@ -93,7 +93,7 @@ module Helpers
|
||||
sleep 1.minute
|
||||
Fiber.yield
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -149,7 +149,7 @@ module Helpers
|
||||
|
||||
id += 1
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
ensure
|
||||
connection_channel.send({false, connection})
|
||||
end
|
||||
@@ -163,7 +163,7 @@ module Helpers
|
||||
env.response.flush
|
||||
sleep (20 + rand(11)).seconds
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
ensure
|
||||
connection_channel.send({false, connection})
|
||||
end
|
||||
|
||||
@@ -103,7 +103,7 @@ module I18n
|
||||
return locales
|
||||
end
|
||||
|
||||
def translate(locale : String?, key : String, text : String | Hash(String, String) | Nil = nil) : String
|
||||
def translate(locale : String?, key : String, text : String? | Hash(String, String)? = nil) : String
|
||||
# Log a warning if "key" doesn't exist in en-US locale and return
|
||||
# that key as the text, so this is more or less transparent to the user.
|
||||
if !LOCALES["en-US"].has_key?(key)
|
||||
|
||||
@@ -55,10 +55,10 @@ class Invidious::LogHandler < Kemal::BaseLogHandler
|
||||
end
|
||||
end
|
||||
|
||||
{% for level in %w(trace debug info warn error fatal) %}
|
||||
def {{level.id}}(message : String)
|
||||
if LogLevel::{{level.id.capitalize}} >= @level
|
||||
puts("#{Time.utc} [{{level.id}}] #{message}".colorize(color(LogLevel::{{level.id.capitalize}})))
|
||||
{% for level in %w[trace debug info warn error fatal] %}
|
||||
def {{ level.id }}(message : String)
|
||||
if LogLevel::{{ level.id.capitalize }} >= @level
|
||||
puts("#{Time.utc} [{{ level.id }}] #{message}".colorize(color(LogLevel::{{ level.id.capitalize }})))
|
||||
end
|
||||
end
|
||||
{% end %}
|
||||
|
||||
@@ -49,24 +49,24 @@ module JSON::Serializable
|
||||
end
|
||||
|
||||
macro templated(_filename, template = "template", navbar_search = true)
|
||||
navbar_search = {{navbar_search}}
|
||||
navbar_search = {{ navbar_search }}
|
||||
|
||||
{{ filename = "src/invidious/views/" + _filename + ".ecr" }}
|
||||
{{ layout = "src/invidious/views/" + template + ".ecr" }}
|
||||
|
||||
__content_filename__ = {{filename}}
|
||||
render {{filename}}, {{layout}}
|
||||
__content_filename__ = {{ filename }}
|
||||
render {{ filename }}, {{ layout }}
|
||||
end
|
||||
|
||||
macro rendered(filename)
|
||||
render("src/invidious/views/#{{{filename}}}.ecr")
|
||||
render("src/invidious/views/#{{{ filename }}}.ecr")
|
||||
end
|
||||
|
||||
# Similar to Kemals halt method but works in a
|
||||
# method.
|
||||
macro haltf(env, status_code = 200, response = "")
|
||||
{{env}}.response.status_code = {{status_code}}
|
||||
{{env}}.response.print {{response}}
|
||||
{{env}}.response.close
|
||||
{{ env }}.response.status_code = {{ status_code }}
|
||||
{{ env }}.response.print {{ response }}
|
||||
{{ env }}.response.close
|
||||
return
|
||||
end
|
||||
@@ -116,7 +116,7 @@ def scope_includes_scope(scope, subset)
|
||||
subset_endpoint = subset_endpoint.downcase
|
||||
|
||||
if methods.empty?
|
||||
methods = %w(GET POST PUT HEAD DELETE PATCH OPTIONS)
|
||||
methods = %w[GET POST PUT HEAD DELETE PATCH OPTIONS]
|
||||
end
|
||||
|
||||
if methods & subset_methods != subset_methods
|
||||
|
||||
@@ -102,7 +102,7 @@ def decode_date(string : String)
|
||||
# Try to parse as format Jul 10, 2000
|
||||
begin
|
||||
return Time.parse(string, "%b %-d, %Y", Time::Location.local)
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
|
||||
case string
|
||||
@@ -110,7 +110,6 @@ def decode_date(string : String)
|
||||
return Time.utc
|
||||
when "yesterday"
|
||||
return Time.utc - 1.day
|
||||
else nil # Continue
|
||||
end
|
||||
|
||||
# String matches format "20 hours ago", "4 months ago", "20s ago", "15min ago"...
|
||||
@@ -177,7 +176,7 @@ def short_text_to_number(short_text : String) : Int64
|
||||
end
|
||||
|
||||
return number.to_i64
|
||||
rescue ex
|
||||
rescue
|
||||
return 0_i64
|
||||
end
|
||||
|
||||
@@ -404,3 +403,10 @@ def invidious_companion_encrypt(data)
|
||||
encrypted_data = encrypt_ecb_without_salt("#{timestamp}|#{data}", CONFIG.invidious_companion_key)
|
||||
return Base64.urlsafe_encode(encrypted_data)
|
||||
end
|
||||
|
||||
def validate_video_id(id : String) : Bool
|
||||
# This is the video ID regex. May be need to be changed
|
||||
# if Youtube ever decides to add more characters to their
|
||||
# video IDs.
|
||||
/\A[a-zA-Z0-9_-]{11}\z/.matches?(id)
|
||||
end
|
||||
@@ -106,7 +106,7 @@ module Invidious::HttpServer
|
||||
# Can be removed once https://github.com/crystal-lang/crystal/issues/15817 is fixed.
|
||||
private def serve_file_range(context : HTTP::Server::Context, file : IO, range_header : String, file_info)
|
||||
# Paste in the body of inherited serve_file_range
|
||||
{{@type.superclass.methods.select(&.name.==("serve_file_range"))[0].body}}
|
||||
{{ @type.superclass.methods.select(&.name.==("serve_file_range"))[0].body }}
|
||||
end
|
||||
|
||||
# Clear cached files.
|
||||
|
||||
@@ -69,7 +69,7 @@ class Invidious::Jobs::InstanceListRefreshJob < Invidious::Jobs::BaseJob
|
||||
|
||||
raw_instance_list = JSON.parse(instance_api_client.get("/instances.json").body).as_a
|
||||
instance_api_client.close
|
||||
rescue ex : Socket::ConnectError | IO::TimeoutError | JSON::ParseException
|
||||
rescue Socket::ConnectError | IO::TimeoutError | JSON::ParseException
|
||||
raw_instance_list = [] of JSON::Any
|
||||
end
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ module Invidious::JSONify::APIv1
|
||||
|
||||
if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"])
|
||||
json.field "container", fmt_info["ext"]
|
||||
json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"]
|
||||
json.field("encoding", fmt_info["vcodec"]? || fmt_info["acodec"])
|
||||
end
|
||||
|
||||
# Livestream chunk infos
|
||||
@@ -198,8 +198,8 @@ module Invidious::JSONify::APIv1
|
||||
end
|
||||
|
||||
if fmt_info = Invidious::Videos::Formats.itag_to_metadata?(fmt["itag"])
|
||||
json.field "container", fmt_info["ext"]
|
||||
json.field "encoding", fmt_info["vcodec"]? || fmt_info["acodec"]
|
||||
json.field("container", fmt_info["ext"])
|
||||
json.field("encoding", fmt_info["vcodec"]? || fmt_info["acodec"])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+13
-13
@@ -83,14 +83,14 @@ end
|
||||
|
||||
def template_mix(mix, listen)
|
||||
html = <<-END_HTML
|
||||
<h3>
|
||||
<a href="/mix?list=#{mix["mixId"]}">
|
||||
#{mix["title"]}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="pure-menu pure-menu-scrollable playlist-restricted">
|
||||
<ol class="pure-menu-list">
|
||||
END_HTML
|
||||
<h3>
|
||||
<a href="/mix?list=#{mix["mixId"]}">
|
||||
#{mix["title"]}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="pure-menu pure-menu-scrollable playlist-restricted">
|
||||
<ol class="pure-menu-list">
|
||||
END_HTML
|
||||
|
||||
mix["videos"].as_a.each do |video|
|
||||
html += <<-END_HTML
|
||||
@@ -106,14 +106,14 @@ def template_mix(mix, listen)
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
END_HTML
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html += <<-END_HTML
|
||||
</ol>
|
||||
</div>
|
||||
<hr>
|
||||
END_HTML
|
||||
</ol>
|
||||
</div>
|
||||
<hr>
|
||||
END_HTML
|
||||
|
||||
html
|
||||
end
|
||||
+15
-15
@@ -500,10 +500,10 @@ def extract_playlist_videos(playlist_id : String, initial_data : Hash(String, JS
|
||||
|
||||
# Find the metadataParts with commandRuns inside, which contains author
|
||||
# information.
|
||||
metadata_parts = metadata_rows.try &.find { |row|
|
||||
metadata_parts = metadata_rows.try &.find do |row|
|
||||
parts = row["metadataParts"]?.try &.as_a
|
||||
parts && parts.any? { |item2| item2.dig?("text", "commandRuns").try &.as_a }
|
||||
}.try &.["metadataParts"].as_a
|
||||
end.try &.["metadataParts"].as_a
|
||||
|
||||
if author_info = metadata_parts.try &.find(&.dig?("text", "commandRuns"))
|
||||
.try &.["text"]
|
||||
@@ -543,14 +543,14 @@ end
|
||||
|
||||
def template_playlist(playlist, listen)
|
||||
html = <<-END_HTML
|
||||
<h3>
|
||||
<a href="/playlist?list=#{playlist["playlistId"]}">
|
||||
#{playlist["title"]}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="pure-menu pure-menu-scrollable playlist-restricted">
|
||||
<ol class="pure-menu-list">
|
||||
END_HTML
|
||||
<h3>
|
||||
<a href="/playlist?list=#{playlist["playlistId"]}">
|
||||
#{playlist["title"]}
|
||||
</a>
|
||||
</h3>
|
||||
<div class="pure-menu pure-menu-scrollable playlist-restricted">
|
||||
<ol class="pure-menu-list">
|
||||
END_HTML
|
||||
|
||||
playlist["videos"].as_a.each do |video|
|
||||
html += <<-END_HTML
|
||||
@@ -566,14 +566,14 @@ def template_playlist(playlist, listen)
|
||||
</p>
|
||||
</a>
|
||||
</li>
|
||||
END_HTML
|
||||
END_HTML
|
||||
end
|
||||
|
||||
html += <<-END_HTML
|
||||
</ol>
|
||||
</div>
|
||||
<hr>
|
||||
END_HTML
|
||||
</ol>
|
||||
</div>
|
||||
<hr>
|
||||
END_HTML
|
||||
|
||||
html
|
||||
end
|
||||
@@ -19,9 +19,9 @@ module Invidious::Routes::API::Manifest
|
||||
|
||||
begin
|
||||
video = get_video(id, region: region)
|
||||
rescue ex : NotFoundException
|
||||
rescue NotFoundException
|
||||
haltf env, status_code: 404
|
||||
rescue ex
|
||||
rescue
|
||||
haltf env, status_code: 403
|
||||
end
|
||||
|
||||
@@ -81,7 +81,6 @@ module Invidious::Routes::API::Manifest
|
||||
xml.element("AdaptationSet", id: i, mimeType: mime_type, startWithSAP: 1, subsegmentAlignment: true, label: "#{displayname} [#{bitrate}k]", lang: lang) do
|
||||
codecs = fmt["mimeType"].as_s.split("codecs=")[1].strip('"')
|
||||
bandwidth = fmt["bitrate"].as_i
|
||||
itag = fmt["itag"].as_i
|
||||
url = fmt["url"].as_s
|
||||
|
||||
xml.element("Role", schemeIdUri: "urn:mpeg:dash:role:2011", value: is_default ? "main" : "alternate")
|
||||
|
||||
@@ -42,16 +42,18 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
user = env.get("user").as(User)
|
||||
|
||||
begin
|
||||
if body = env.request.body
|
||||
body = env.request.body.not_nil!.gets_to_end
|
||||
else
|
||||
body = "{}"
|
||||
end
|
||||
body = if raw_body = env.request.body
|
||||
raw_body.gets_to_end
|
||||
else
|
||||
"{}"
|
||||
end
|
||||
Invidious::User::Import.from_invidious(user, body)
|
||||
rescue
|
||||
env.response.status_code = 204
|
||||
rescue ex : InfoException
|
||||
return error_json(400, ex)
|
||||
rescue ex
|
||||
return error_json(500, ex)
|
||||
end
|
||||
|
||||
env.response.status_code = 204
|
||||
end
|
||||
|
||||
def self.get_history(env)
|
||||
@@ -81,9 +83,10 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
return error_json(409, "Watch history is disabled in preferences.")
|
||||
end
|
||||
|
||||
# Sanity checks
|
||||
id = env.params.url["id"]
|
||||
if !id.match(/^[a-zA-Z0-9_-]{11}$/)
|
||||
return error_json(400, "Invalid video id.")
|
||||
unless validate_video_id(id)
|
||||
return error_json(400, InvalidVideoID.new(id))
|
||||
end
|
||||
|
||||
Invidious::Database::Users.mark_watched(user, id)
|
||||
@@ -97,12 +100,12 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
return error_json(409, "Watch history is disabled in preferences.")
|
||||
end
|
||||
|
||||
id = env.params.url["id"]
|
||||
if !id.match(/^[a-zA-Z0-9_-]{11}$/)
|
||||
return error_json(400, "Invalid video id.")
|
||||
video_id = env.params.url["id"]
|
||||
unless video_id && validate_video_id(video_id)
|
||||
return error_json(400, InvalidVideoID.new(video_id))
|
||||
end
|
||||
|
||||
Invidious::Database::Users.mark_unwatched(user, id)
|
||||
Invidious::Database::Users.mark_unwatched(user, video_id)
|
||||
env.response.status_code = 204
|
||||
end
|
||||
|
||||
@@ -309,8 +312,9 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
end
|
||||
|
||||
video_id = env.params.json["videoId"].try &.as(String)
|
||||
if !video_id
|
||||
return error_json(403, "Invalid videoId")
|
||||
# Sanity checks
|
||||
unless video_id && validate_video_id(video_id)
|
||||
return error_json(400, InvalidVideoID.new(video_id))
|
||||
end
|
||||
|
||||
begin
|
||||
@@ -378,7 +382,6 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
def self.get_tokens(env)
|
||||
env.response.content_type = "application/json"
|
||||
user = env.get("user").as(User)
|
||||
scopes = env.get("scopes").as(Array(String))
|
||||
|
||||
tokens = Invidious::Database::SessionIDs.select_all(user.email)
|
||||
|
||||
@@ -396,6 +399,8 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
|
||||
def self.register_token(env)
|
||||
user = env.get("user").as(User)
|
||||
# Used by template bellow.
|
||||
# ameba:disable Lint/UselessAssign
|
||||
locale = env.get("preferences").as(Preferences).locale
|
||||
|
||||
case env.request.headers["Content-Type"]?
|
||||
@@ -422,6 +427,8 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
if sid = env.get?("sid").try &.as(String)
|
||||
env.response.content_type = "text/html"
|
||||
|
||||
# Used by template bellow.
|
||||
# ameba:disable Lint/UselessAssign
|
||||
csrf_token = generate_response(sid, {":authorize_token"}, HMAC_KEY, use_nonce: true)
|
||||
return templated "user/authorize_token"
|
||||
else
|
||||
@@ -460,7 +467,6 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
def self.unregister_token(env)
|
||||
env.response.content_type = "application/json"
|
||||
|
||||
user = env.get("user").as(User)
|
||||
scopes = env.get("scopes").as(Array(String))
|
||||
|
||||
session = env.params.json["session"]?.try &.as(String)
|
||||
@@ -482,7 +488,7 @@ module Invidious::Routes::API::V1::Authenticated
|
||||
env.response.content_type = "text/event-stream"
|
||||
|
||||
raw_topics = env.params.body["topics"]? || env.params.query["topics"]?
|
||||
topics = raw_topics.try &.split(",").uniq.first(1000)
|
||||
topics = raw_topics.try &.split(",").uniq!.first(1000)
|
||||
topics ||= [] of String
|
||||
|
||||
Helpers.create_notification_stream(env, topics, CONNECTION_CHANNEL)
|
||||
|
||||
@@ -31,11 +31,10 @@ module Invidious::Routes::API::V1::Channels
|
||||
begin
|
||||
playlist = get_playlist(channel.ucid.sub("UC", "UULF"))
|
||||
videos = get_playlist_videos(playlist, offset: 0)
|
||||
rescue ex : InfoException
|
||||
rescue InfoException
|
||||
# playlist doesnt exist.
|
||||
videos = [] of PlaylistVideo
|
||||
end
|
||||
next_continuation = nil
|
||||
else
|
||||
begin
|
||||
videos, _ = Channel::Tabs.get_videos(channel, sort_by: sort_by)
|
||||
@@ -119,7 +118,7 @@ module Invidious::Routes::API::V1::Channels
|
||||
# Fetch related channels
|
||||
begin
|
||||
related_channels, _ = fetch_related_channels(channel)
|
||||
rescue ex
|
||||
rescue
|
||||
related_channels = [] of SearchChannel
|
||||
end
|
||||
|
||||
@@ -158,7 +157,7 @@ module Invidious::Routes::API::V1::Channels
|
||||
begin
|
||||
playlist = get_playlist(channel.ucid.sub("UC", "UULF"))
|
||||
videos = get_playlist_videos(playlist, offset: 0)
|
||||
rescue ex : InfoException
|
||||
rescue InfoException
|
||||
# playlist doesnt exist.
|
||||
videos = [] of PlaylistVideo
|
||||
end
|
||||
@@ -204,7 +203,7 @@ module Invidious::Routes::API::V1::Channels
|
||||
begin
|
||||
playlist = get_playlist(channel.ucid.sub("UC", "UUSH"))
|
||||
videos = get_playlist_videos(playlist, offset: 0)
|
||||
rescue ex : InfoException
|
||||
rescue InfoException
|
||||
# playlist doesnt exist.
|
||||
videos = [] of PlaylistVideo
|
||||
end
|
||||
@@ -250,7 +249,7 @@ module Invidious::Routes::API::V1::Channels
|
||||
begin
|
||||
playlist = get_playlist(channel.ucid.sub("UC", "UULV"))
|
||||
videos = get_playlist_videos(playlist, offset: 0)
|
||||
rescue ex : InfoException
|
||||
rescue InfoException
|
||||
# playlist doesnt exist.
|
||||
videos = [] of PlaylistVideo
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ module Invidious::Routes::API::V1::Feeds
|
||||
trending_type = env.params.query["type"]?
|
||||
|
||||
begin
|
||||
trending, plid = fetch_trending(trending_type, region, locale)
|
||||
trending, _ = fetch_trending(trending_type, region, locale)
|
||||
rescue ex
|
||||
return error_json(500, ex)
|
||||
end
|
||||
|
||||
@@ -53,7 +53,7 @@ module Invidious::Routes::API::V1::Misc
|
||||
playlist = get_playlist(plid)
|
||||
rescue ex : InfoException
|
||||
return error_json(404, ex)
|
||||
rescue ex
|
||||
rescue
|
||||
return error_json(404, "Playlist does not exist.")
|
||||
end
|
||||
|
||||
|
||||
@@ -32,8 +32,9 @@ module Invidious::Routes::API::V1::Videos
|
||||
id = env.params.url["id"]
|
||||
region = env.params.query["region"]? || env.params.body["region"]?
|
||||
|
||||
if id.nil? || id.size != 11 || !id.matches?(/^[\w-]+$/)
|
||||
return error_json(400, "Invalid video ID")
|
||||
# Sanity checks
|
||||
unless validate_video_id(id)
|
||||
return error_json(400, InvalidVideoID.new(id))
|
||||
end
|
||||
|
||||
# See https://github.com/ytdl-org/youtube-dl/blob/6ab30ff50bf6bd0585927cb73c7421bef184f87a/youtube_dl/extractor/youtube.py#L1354
|
||||
@@ -46,9 +47,9 @@ module Invidious::Routes::API::V1::Videos
|
||||
|
||||
begin
|
||||
video = get_video(id, region: region)
|
||||
rescue ex : NotFoundException
|
||||
rescue NotFoundException
|
||||
haltf env, 404
|
||||
rescue ex
|
||||
rescue
|
||||
haltf env, 500
|
||||
end
|
||||
|
||||
@@ -186,9 +187,9 @@ module Invidious::Routes::API::V1::Videos
|
||||
|
||||
begin
|
||||
video = get_video(id, region: region)
|
||||
rescue ex : NotFoundException
|
||||
rescue NotFoundException
|
||||
haltf env, 404
|
||||
rescue ex
|
||||
rescue
|
||||
haltf env, 500
|
||||
end
|
||||
|
||||
@@ -256,33 +257,31 @@ module Invidious::Routes::API::V1::Videos
|
||||
def self.annotations(env)
|
||||
env.response.content_type = "text/xml"
|
||||
|
||||
id = env.params.url["id"]
|
||||
video_id = env.params.url["id"]
|
||||
source = env.params.query["source"]?
|
||||
source ||= "archive"
|
||||
|
||||
if !id.match(/[a-zA-Z0-9_-]{11}/)
|
||||
unless video_id && validate_video_id(video_id)
|
||||
haltf env, 400
|
||||
end
|
||||
|
||||
annotations = ""
|
||||
|
||||
case source
|
||||
when "archive"
|
||||
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(id))
|
||||
if CONFIG.cache_annotations && (cached_annotation = Invidious::Database::Annotations.select(video_id))
|
||||
annotations = cached_annotation.annotations
|
||||
else
|
||||
index = CHARS_SAFE.index!(id[0]).to_s.rjust(2, '0')
|
||||
index = CHARS_SAFE.index!(video_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')
|
||||
video_id = video_id.sub(/^-/, 'A')
|
||||
end
|
||||
|
||||
file = URI.encode_www_form("#{id[0, 3]}/#{id}.xml")
|
||||
file = URI.encode_www_form("#{video_id[0, 3]}/#{video_id}.xml")
|
||||
|
||||
location = make_client(INTERNET_ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{id[0, 2]}.tar/#{file}"))
|
||||
location = make_client(INTERNET_ARCHIVE_URL, &.get("/download/youtubeannotations_#{index}/#{video_id[0, 2]}.tar/#{file}"))
|
||||
|
||||
if !location.headers["Location"]?
|
||||
env.response.status_code = location.status_code
|
||||
@@ -300,10 +299,10 @@ module Invidious::Routes::API::V1::Videos
|
||||
|
||||
annotations = response.body
|
||||
|
||||
Helpers.cache_annotation(id, annotations)
|
||||
Helpers.cache_annotation(video_id, annotations)
|
||||
end
|
||||
else # "youtube"
|
||||
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{id}")
|
||||
response = YT_POOL.client &.get("/annotations_invideo?video_id=#{video_id}")
|
||||
|
||||
if response.status_code != 200
|
||||
haltf env, response.status_code
|
||||
@@ -339,6 +338,9 @@ module Invidious::Routes::API::V1::Videos
|
||||
format ||= "json"
|
||||
|
||||
action = env.params.query["action"]?
|
||||
# 2026-09-14 TODO: Check if action is or can be used somewhere
|
||||
# as is unused currently, but for some reason is it here.
|
||||
# ameba:disable Lint/UselessAssign
|
||||
action ||= "action_get_comments"
|
||||
|
||||
continuation = env.params.query["continuation"]?
|
||||
@@ -361,7 +363,7 @@ module Invidious::Routes::API::V1::Videos
|
||||
|
||||
begin
|
||||
comments, reddit_thread = Comments.fetch_reddit(id, sort_by: sort_by)
|
||||
rescue ex
|
||||
rescue
|
||||
comments = nil
|
||||
reddit_thread = nil
|
||||
end
|
||||
|
||||
@@ -44,7 +44,7 @@ module Invidious::Routes::Channels
|
||||
begin
|
||||
playlist = get_playlist(channel.ucid.sub("UC", "UULF"))
|
||||
items = get_playlist_videos(playlist, offset: 0)
|
||||
rescue ex : InfoException
|
||||
rescue InfoException
|
||||
# playlist doesnt exist.
|
||||
items = [] of PlaylistVideo
|
||||
end
|
||||
@@ -79,7 +79,7 @@ module Invidious::Routes::Channels
|
||||
begin
|
||||
playlist = get_playlist(channel.ucid.sub("UC", "UUSH"))
|
||||
items = get_playlist_videos(playlist, offset: 0)
|
||||
rescue ex : InfoException
|
||||
rescue InfoException
|
||||
# playlist doesnt exist.
|
||||
items = [] of PlaylistVideo
|
||||
end
|
||||
@@ -114,7 +114,7 @@ module Invidious::Routes::Channels
|
||||
begin
|
||||
playlist = get_playlist(channel.ucid.sub("UC", "UULV"))
|
||||
items = get_playlist_videos(playlist, offset: 0)
|
||||
rescue ex : InfoException
|
||||
rescue InfoException
|
||||
# playlist doesnt exist.
|
||||
items = [] of PlaylistVideo
|
||||
end
|
||||
@@ -353,7 +353,7 @@ module Invidious::Routes::Channels
|
||||
begin
|
||||
resolved_url = YoutubeAPI.resolve_url("https://www.youtube.com#{env.request.path}#{yt_url_params.size > 0 ? "?#{yt_url_params}" : ""}")
|
||||
ucid = resolved_url["endpoint"]["browseEndpoint"]["browseId"]
|
||||
rescue ex : InfoException | KeyError
|
||||
rescue InfoException | KeyError
|
||||
return error_template(404, I18n.translate(locale, "This channel does not exist."))
|
||||
end
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ module Invidious::Routes::Companion
|
||||
return self.proxy_companion(env, resp)
|
||||
end
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
@@ -29,7 +29,7 @@ module Invidious::Routes::Companion
|
||||
return self.proxy_companion(env, resp)
|
||||
end
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,7 +45,7 @@ module Invidious::Routes::Companion
|
||||
return self.proxy_companion(env, resp)
|
||||
end
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -119,7 +119,6 @@ module Invidious::Routes::Embed
|
||||
end
|
||||
|
||||
return env.redirect url
|
||||
else nil # Continue
|
||||
end
|
||||
|
||||
params = Invidious::Videos.process_video_params(env.params.query, preferences)
|
||||
@@ -182,14 +181,14 @@ module Invidious::Routes::Embed
|
||||
|
||||
captions = video.captions
|
||||
|
||||
preferred_captions = captions.select { |caption|
|
||||
preferred_captions = captions.select do |caption|
|
||||
params.preferred_captions.includes?(caption.name) ||
|
||||
params.preferred_captions.includes?(caption.language_code.split("-")[0])
|
||||
}
|
||||
preferred_captions.sort_by! { |caption|
|
||||
end
|
||||
preferred_captions.sort_by! do |caption|
|
||||
(params.preferred_captions.index(caption.name) ||
|
||||
params.preferred_captions.index(caption.language_code.split("-")[0])).not_nil!
|
||||
}
|
||||
end
|
||||
captions = captions - preferred_captions
|
||||
|
||||
aspect_ratio = nil
|
||||
|
||||
@@ -327,7 +327,6 @@ module Invidious::Routes::Feeds
|
||||
request_target = URI.parse(node[attribute.name]).request_target
|
||||
query_string_opt = request_target.starts_with?("/watch?v=") ? ("&#{params}" if !params.empty?) : ""
|
||||
node[attribute.name] = "#{HOST_URL}#{request_target}#{query_string_opt}"
|
||||
else nil # Skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,7 @@ module Invidious::Routes::Images
|
||||
GGPHT_POOL.client &.get(url, headers) do |resp|
|
||||
return self.proxy_image(env, resp)
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
@@ -46,7 +46,7 @@ module Invidious::Routes::Images
|
||||
env.response.headers["Connection"] = "close"
|
||||
return self.proxy_image(env, resp)
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
@@ -68,7 +68,7 @@ module Invidious::Routes::Images
|
||||
get_ytimg_pool(authority).client &.get(url, headers) do |resp|
|
||||
return self.proxy_image(env, resp)
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
@@ -108,7 +108,7 @@ module Invidious::Routes::Images
|
||||
|
||||
Helpers.proxy_file(response, env)
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
@@ -140,7 +140,7 @@ module Invidious::Routes::Images
|
||||
get_ytimg_pool("i").client &.get(url, headers) do |resp|
|
||||
return self.proxy_image(env, resp)
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ module Invidious::Routes::Login
|
||||
|
||||
begin
|
||||
validate_request(tokens[0], answer, env.request, HMAC_KEY, locale)
|
||||
rescue ex : InfoException
|
||||
rescue InfoException
|
||||
return error_template(400, InfoException.new("Erroneous CAPTCHA"))
|
||||
rescue ex
|
||||
return error_template(400, ex)
|
||||
|
||||
@@ -164,7 +164,7 @@ module Invidious::Routes::Playlists
|
||||
|
||||
begin
|
||||
items = get_playlist_videos(playlist, offset: (page - 1) * 100)
|
||||
rescue ex
|
||||
rescue
|
||||
items = [] of PlaylistVideo
|
||||
end
|
||||
|
||||
@@ -255,7 +255,7 @@ module Invidious::Routes::Playlists
|
||||
begin
|
||||
query = Invidious::Search::Query.new(env.params.query, :playlist, region)
|
||||
items = query.process.select(SearchVideo).map(&.as(SearchVideo))
|
||||
rescue ex
|
||||
rescue
|
||||
items = [] of SearchVideo
|
||||
end
|
||||
|
||||
|
||||
@@ -362,7 +362,6 @@ module Invidious::Routes::PreferencesRoute
|
||||
response: error_template(415, "Uploaded file is too large")
|
||||
)
|
||||
end
|
||||
else nil # Ignore
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,7 +115,7 @@ module Invidious::Routes::VideoPlayback
|
||||
|
||||
IO.copy(resp.body_io, env.response)
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
end
|
||||
else
|
||||
if query_params["title"]? && CONFIG.disabled?("downloads") ||
|
||||
@@ -268,8 +268,8 @@ module Invidious::Routes::VideoPlayback
|
||||
itag = env.params.query["itag"]?.try &.to_i?
|
||||
|
||||
# Sanity checks
|
||||
if id.nil? || id.size != 11 || !id.matches?(/^[\w-]+$/)
|
||||
return error_template(400, "Invalid video ID")
|
||||
unless id && validate_video_id(id)
|
||||
return error_template(400, InvalidVideoID.new(id))
|
||||
end
|
||||
|
||||
if !itag.nil? && (itag <= 0 || itag >= 1000)
|
||||
|
||||
@@ -18,14 +18,8 @@ module Invidious::Routes::Watch
|
||||
return error_template(400, "Invalid parameters.")
|
||||
end
|
||||
|
||||
if id.size > 11
|
||||
url = "/watch?v=#{id[0, 11]}"
|
||||
env.params.query.delete_all("v")
|
||||
if env.params.query.size > 0
|
||||
url += "&#{env.params.query}"
|
||||
end
|
||||
|
||||
return env.redirect url
|
||||
unless validate_video_id(id)
|
||||
return error_template(400, InvalidVideoID.new(id))
|
||||
end
|
||||
else
|
||||
return env.redirect "/"
|
||||
@@ -88,7 +82,7 @@ module Invidious::Routes::Watch
|
||||
if source == "youtube"
|
||||
begin
|
||||
comment_html = JSON.parse(Comments.fetch_youtube(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"]
|
||||
rescue ex
|
||||
rescue
|
||||
if preferences.comments[1] == "reddit"
|
||||
comments, reddit_thread = Comments.fetch_reddit(id)
|
||||
comment_html = Frontend::Comments.template_reddit(comments, locale)
|
||||
@@ -104,7 +98,7 @@ module Invidious::Routes::Watch
|
||||
|
||||
comment_html = Comments.fill_links(comment_html, "https", "www.reddit.com")
|
||||
comment_html = Comments.replace_links(comment_html)
|
||||
rescue ex
|
||||
rescue
|
||||
if preferences.comments[1] == "youtube"
|
||||
comment_html = JSON.parse(Comments.fetch_youtube(id, nil, "html", locale, preferences.thin_mode, region))["contentHtml"]
|
||||
end
|
||||
@@ -149,14 +143,14 @@ module Invidious::Routes::Watch
|
||||
|
||||
captions = video.captions
|
||||
|
||||
preferred_captions = captions.select { |caption|
|
||||
preferred_captions = captions.select do |caption|
|
||||
params.preferred_captions.includes?(caption.name) ||
|
||||
params.preferred_captions.includes?(caption.language_code.split("-")[0])
|
||||
}
|
||||
preferred_captions.sort_by! { |caption|
|
||||
end
|
||||
preferred_captions.sort_by! do |caption|
|
||||
(params.preferred_captions.index(caption.name) ||
|
||||
params.preferred_captions.index(caption.language_code.split("-")[0])).not_nil!
|
||||
}
|
||||
end
|
||||
captions = captions - preferred_captions
|
||||
|
||||
aspect_ratio = "16:9"
|
||||
@@ -235,7 +229,7 @@ module Invidious::Routes::Watch
|
||||
token = env.params.body["csrf_token"]?
|
||||
|
||||
id = env.params.query["id"]?
|
||||
if !id
|
||||
unless id && validate_video_id(id)
|
||||
env.response.status_code = 400
|
||||
return
|
||||
end
|
||||
|
||||
+61
-61
@@ -3,12 +3,12 @@ module Invidious::Routing
|
||||
|
||||
{% for http_method in {"get", "post", "delete", "options", "patch", "put"} %}
|
||||
|
||||
macro {{http_method.id}}(path, controller, method = :handle)
|
||||
macro {{ http_method.id }}(path, controller, method = :handle)
|
||||
unless Kemal::Utils.path_starts_with_slash?(\{{path}})
|
||||
raise Kemal::Exceptions::InvalidPathStartException.new({{http_method}}, \{{path}})
|
||||
raise Kemal::Exceptions::InvalidPathStartException.new({{ http_method }}, \{{path}})
|
||||
end
|
||||
|
||||
Kemal::RouteHandler::INSTANCE.add_route({{http_method.upcase}}, \{{path}}) do |env|
|
||||
Kemal::RouteHandler::INSTANCE.add_route({{ http_method.upcase }}, \{{path}}) do |env|
|
||||
\{{ controller }}.\{{ method.id }}(env)
|
||||
end
|
||||
end
|
||||
@@ -241,91 +241,91 @@ module Invidious::Routing
|
||||
|
||||
def register_api_v1_routes
|
||||
{% begin %}
|
||||
{{namespace = Routes::API::V1}}
|
||||
{{ namespace = Routes::API::V1 }}
|
||||
|
||||
# Videos
|
||||
get "/api/v1/videos/:id", {{namespace}}::Videos, :videos
|
||||
get "/api/v1/storyboards/:id", {{namespace}}::Videos, :storyboards
|
||||
get "/api/v1/captions/:id", {{namespace}}::Videos, :captions
|
||||
get "/api/v1/annotations/:id", {{namespace}}::Videos, :annotations
|
||||
get "/api/v1/comments/:id", {{namespace}}::Videos, :comments
|
||||
get "/api/v1/clips/:id", {{namespace}}::Videos, :clips
|
||||
get "/api/v1/transcripts/:id", {{namespace}}::Videos, :transcripts
|
||||
get "/api/v1/videos/:id", {{ namespace }}::Videos, :videos
|
||||
get "/api/v1/storyboards/:id", {{ namespace }}::Videos, :storyboards
|
||||
get "/api/v1/captions/:id", {{ namespace }}::Videos, :captions
|
||||
get "/api/v1/annotations/:id", {{ namespace }}::Videos, :annotations
|
||||
get "/api/v1/comments/:id", {{ namespace }}::Videos, :comments
|
||||
get "/api/v1/clips/:id", {{ namespace }}::Videos, :clips
|
||||
get "/api/v1/transcripts/:id", {{ namespace }}::Videos, :transcripts
|
||||
|
||||
# Feeds
|
||||
get "/api/v1/trending", {{namespace}}::Feeds, :trending
|
||||
get "/api/v1/popular", {{namespace}}::Feeds, :popular
|
||||
get "/api/v1/trending", {{ namespace }}::Feeds, :trending
|
||||
get "/api/v1/popular", {{ namespace }}::Feeds, :popular
|
||||
|
||||
# Channels
|
||||
get "/api/v1/channels/:ucid", {{namespace}}::Channels, :home
|
||||
get "/api/v1/channels/:ucid/latest", {{namespace}}::Channels, :latest
|
||||
get "/api/v1/channels/:ucid/videos", {{namespace}}::Channels, :videos
|
||||
get "/api/v1/channels/:ucid/shorts", {{namespace}}::Channels, :shorts
|
||||
get "/api/v1/channels/:ucid/streams", {{namespace}}::Channels, :streams
|
||||
get "/api/v1/channels/:ucid/podcasts", {{namespace}}::Channels, :podcasts
|
||||
get "/api/v1/channels/:ucid/releases", {{namespace}}::Channels, :releases
|
||||
get "/api/v1/channels/:ucid/courses", {{namespace}}::Channels, :courses
|
||||
get "/api/v1/channels/:ucid/playlists", {{namespace}}::Channels, :playlists
|
||||
get "/api/v1/channels/:ucid/community", {{namespace}}::Channels, :community
|
||||
get "/api/v1/channels/:ucid/posts", {{namespace}}::Channels, :community
|
||||
get "/api/v1/channels/:ucid/channels", {{namespace}}::Channels, :channels
|
||||
get "/api/v1/channels/:ucid/search", {{namespace}}::Channels, :search
|
||||
get "/api/v1/channels/:ucid", {{ namespace }}::Channels, :home
|
||||
get "/api/v1/channels/:ucid/latest", {{ namespace }}::Channels, :latest
|
||||
get "/api/v1/channels/:ucid/videos", {{ namespace }}::Channels, :videos
|
||||
get "/api/v1/channels/:ucid/shorts", {{ namespace }}::Channels, :shorts
|
||||
get "/api/v1/channels/:ucid/streams", {{ namespace }}::Channels, :streams
|
||||
get "/api/v1/channels/:ucid/podcasts", {{ namespace }}::Channels, :podcasts
|
||||
get "/api/v1/channels/:ucid/releases", {{ namespace }}::Channels, :releases
|
||||
get "/api/v1/channels/:ucid/courses", {{ namespace }}::Channels, :courses
|
||||
get "/api/v1/channels/:ucid/playlists", {{ namespace }}::Channels, :playlists
|
||||
get "/api/v1/channels/:ucid/community", {{ namespace }}::Channels, :community
|
||||
get "/api/v1/channels/:ucid/posts", {{ namespace }}::Channels, :community
|
||||
get "/api/v1/channels/:ucid/channels", {{ namespace }}::Channels, :channels
|
||||
get "/api/v1/channels/:ucid/search", {{ namespace }}::Channels, :search
|
||||
|
||||
# Posts
|
||||
get "/api/v1/post/:id", {{namespace}}::Channels, :post
|
||||
get "/api/v1/post/:id/comments", {{namespace}}::Channels, :post_comments
|
||||
get "/api/v1/post/:id", {{ namespace }}::Channels, :post
|
||||
get "/api/v1/post/:id/comments", {{ namespace }}::Channels, :post_comments
|
||||
|
||||
# 301 redirects to new /api/v1/channels/community/:ucid and /:ucid/community
|
||||
get "/api/v1/channels/comments/:ucid", {{namespace}}::Channels, :channel_comments_redirect
|
||||
get "/api/v1/channels/:ucid/comments", {{namespace}}::Channels, :channel_comments_redirect
|
||||
get "/api/v1/channels/comments/:ucid", {{ namespace }}::Channels, :channel_comments_redirect
|
||||
get "/api/v1/channels/:ucid/comments", {{ namespace }}::Channels, :channel_comments_redirect
|
||||
|
||||
# Search
|
||||
get "/api/v1/search", {{namespace}}::Search, :search
|
||||
get "/api/v1/search/suggestions", {{namespace}}::Search, :search_suggestions
|
||||
get "/api/v1/hashtag/:hashtag", {{namespace}}::Search, :hashtag
|
||||
get "/api/v1/search", {{ namespace }}::Search, :search
|
||||
get "/api/v1/search/suggestions", {{ namespace }}::Search, :search_suggestions
|
||||
get "/api/v1/hashtag/:hashtag", {{ namespace }}::Search, :hashtag
|
||||
|
||||
|
||||
# Authenticated
|
||||
|
||||
get "/api/v1/auth/preferences", {{namespace}}::Authenticated, :get_preferences
|
||||
post "/api/v1/auth/preferences", {{namespace}}::Authenticated, :set_preferences
|
||||
get "/api/v1/auth/preferences", {{ namespace }}::Authenticated, :get_preferences
|
||||
post "/api/v1/auth/preferences", {{ namespace }}::Authenticated, :set_preferences
|
||||
|
||||
get "/api/v1/auth/export/invidious", {{namespace}}::Authenticated, :export_invidious
|
||||
post "/api/v1/auth/import/invidious", {{namespace}}::Authenticated, :import_invidious
|
||||
get "/api/v1/auth/export/invidious", {{ namespace }}::Authenticated, :export_invidious
|
||||
post "/api/v1/auth/import/invidious", {{ namespace }}::Authenticated, :import_invidious
|
||||
|
||||
get "/api/v1/auth/history", {{namespace}}::Authenticated, :get_history
|
||||
post "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_watched
|
||||
delete "/api/v1/auth/history/:id", {{namespace}}::Authenticated, :mark_unwatched
|
||||
delete "/api/v1/auth/history", {{namespace}}::Authenticated, :clear_history
|
||||
get "/api/v1/auth/history", {{ namespace }}::Authenticated, :get_history
|
||||
post "/api/v1/auth/history/:id", {{ namespace }}::Authenticated, :mark_watched
|
||||
delete "/api/v1/auth/history/:id", {{ namespace }}::Authenticated, :mark_unwatched
|
||||
delete "/api/v1/auth/history", {{ namespace }}::Authenticated, :clear_history
|
||||
|
||||
get "/api/v1/auth/feed", {{namespace}}::Authenticated, :feed
|
||||
get "/api/v1/auth/feed", {{ namespace }}::Authenticated, :feed
|
||||
|
||||
get "/api/v1/auth/subscriptions", {{namespace}}::Authenticated, :get_subscriptions
|
||||
post "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :subscribe_channel
|
||||
delete "/api/v1/auth/subscriptions/:ucid", {{namespace}}::Authenticated, :unsubscribe_channel
|
||||
get "/api/v1/auth/subscriptions", {{ namespace }}::Authenticated, :get_subscriptions
|
||||
post "/api/v1/auth/subscriptions/:ucid", {{ namespace }}::Authenticated, :subscribe_channel
|
||||
delete "/api/v1/auth/subscriptions/:ucid", {{ namespace }}::Authenticated, :unsubscribe_channel
|
||||
|
||||
get "/api/v1/auth/playlists", {{namespace}}::Authenticated, :list_playlists
|
||||
post "/api/v1/auth/playlists", {{namespace}}::Authenticated, :create_playlist
|
||||
patch "/api/v1/auth/playlists/:plid",{{namespace}}:: Authenticated, :update_playlist_attribute
|
||||
delete "/api/v1/auth/playlists/:plid", {{namespace}}::Authenticated, :delete_playlist
|
||||
post "/api/v1/auth/playlists/:plid/videos", {{namespace}}::Authenticated, :insert_video_into_playlist
|
||||
delete "/api/v1/auth/playlists/:plid/videos/:index", {{namespace}}::Authenticated, :delete_video_in_playlist
|
||||
get "/api/v1/auth/playlists", {{ namespace }}::Authenticated, :list_playlists
|
||||
post "/api/v1/auth/playlists", {{ namespace }}::Authenticated, :create_playlist
|
||||
patch "/api/v1/auth/playlists/:plid",{{ namespace }}:: Authenticated, :update_playlist_attribute
|
||||
delete "/api/v1/auth/playlists/:plid", {{ namespace }}::Authenticated, :delete_playlist
|
||||
post "/api/v1/auth/playlists/:plid/videos", {{ namespace }}::Authenticated, :insert_video_into_playlist
|
||||
delete "/api/v1/auth/playlists/:plid/videos/:index", {{ namespace }}::Authenticated, :delete_video_in_playlist
|
||||
|
||||
get "/api/v1/auth/tokens", {{namespace}}::Authenticated, :get_tokens
|
||||
post "/api/v1/auth/tokens/register", {{namespace}}::Authenticated, :register_token
|
||||
post "/api/v1/auth/tokens/unregister", {{namespace}}::Authenticated, :unregister_token
|
||||
get "/api/v1/auth/tokens", {{ namespace }}::Authenticated, :get_tokens
|
||||
post "/api/v1/auth/tokens/register", {{ namespace }}::Authenticated, :register_token
|
||||
post "/api/v1/auth/tokens/unregister", {{ namespace }}::Authenticated, :unregister_token
|
||||
|
||||
if CONFIG.enable_user_notifications
|
||||
get "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
|
||||
post "/api/v1/auth/notifications", {{namespace}}::Authenticated, :notifications
|
||||
get "/api/v1/auth/notifications", {{ namespace }}::Authenticated, :notifications
|
||||
post "/api/v1/auth/notifications", {{ namespace }}::Authenticated, :notifications
|
||||
end
|
||||
|
||||
# Misc
|
||||
get "/api/v1/stats", {{namespace}}::Misc, :stats
|
||||
get "/api/v1/playlists/:plid", {{namespace}}::Misc, :get_playlist
|
||||
get "/api/v1/auth/playlists/:plid", {{namespace}}::Misc, :get_playlist
|
||||
get "/api/v1/mixes/:rdid", {{namespace}}::Misc, :mixes
|
||||
get "/api/v1/resolveurl", {{namespace}}::Misc, :resolve_url
|
||||
get "/api/v1/stats", {{ namespace }}::Misc, :stats
|
||||
get "/api/v1/playlists/:plid", {{ namespace }}::Misc, :get_playlist
|
||||
get "/api/v1/auth/playlists/:plid", {{ namespace }}::Misc, :get_playlist
|
||||
get "/api/v1/mixes/:rdid", {{ namespace }}::Misc, :mixes
|
||||
get "/api/v1/resolveurl", {{ namespace }}::Misc, :resolve_url
|
||||
{% end %}
|
||||
end
|
||||
end
|
||||
@@ -39,7 +39,7 @@ module Invidious::Search
|
||||
def subscriptions(query : Query, user : Invidious::User) : Array(ChannelVideo)
|
||||
view_name = "subscriptions_#{sha256(user.email)}"
|
||||
|
||||
return PG_DB.query_all("
|
||||
return PG_DB.query_all(<<-SQL,
|
||||
SELECT id,title,published,updated,ucid,author,length_seconds
|
||||
FROM (
|
||||
SELECT *,
|
||||
@@ -47,7 +47,8 @@ module Invidious::Search
|
||||
to_tsvector(#{view_name}.author)
|
||||
as document
|
||||
FROM #{view_name}
|
||||
) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;",
|
||||
) v_search WHERE v_search.document @@ plainto_tsquery($1) LIMIT 20 OFFSET $2;
|
||||
SQL
|
||||
query.text, (query.page - 1) * 20,
|
||||
as: ChannelVideo
|
||||
)
|
||||
|
||||
@@ -2,10 +2,10 @@ def fetch_trending(trending_type, region, locale)
|
||||
region ||= "US"
|
||||
region = region.upcase
|
||||
|
||||
# 2026-10-12: plid is only used in line 47
|
||||
# so we can remove this later.
|
||||
plid = nil
|
||||
|
||||
browse_id = ""
|
||||
|
||||
case trending_type.try &.downcase
|
||||
when "gaming"
|
||||
browse_id = "UCOpNcN46UbXVtpKMrmU4Abg"
|
||||
|
||||
@@ -20,28 +20,28 @@ struct Invidious::User
|
||||
end
|
||||
|
||||
clock_svg = <<-END_SVG
|
||||
<svg viewBox="0 0 100 100" width="200px" height="200px">
|
||||
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
|
||||
<svg viewBox="0 0 100 100" width="200px" height="200px">
|
||||
<circle cx="50" cy="50" r="45" fill="#eee" stroke="black" stroke-width="2"></circle>
|
||||
|
||||
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
|
||||
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
|
||||
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
|
||||
<text x="82.909" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 4</text>
|
||||
<text x="69" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 5</text>
|
||||
<text x="50" y="91" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 6</text>
|
||||
<text x="31" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 7</text>
|
||||
<text x="17.091" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 8</text>
|
||||
<text x="12" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 9</text>
|
||||
<text x="17.091" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">10</text>
|
||||
<text x="31" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">11</text>
|
||||
<text x="50" y="15" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">12</text>
|
||||
<text x="69" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 1</text>
|
||||
<text x="82.909" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 2</text>
|
||||
<text x="88" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 3</text>
|
||||
<text x="82.909" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 4</text>
|
||||
<text x="69" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 5</text>
|
||||
<text x="50" y="91" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 6</text>
|
||||
<text x="31" y="85.909" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 7</text>
|
||||
<text x="17.091" y="72" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 8</text>
|
||||
<text x="12" y="53" text-anchor="middle" fill="black" font-family="Arial" font-size="10px"> 9</text>
|
||||
<text x="17.091" y="34" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">10</text>
|
||||
<text x="31" y="20.091" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">11</text>
|
||||
<text x="50" y="15" text-anchor="middle" fill="black" font-family="Arial" font-size="10px">12</text>
|
||||
|
||||
<circle cx="50" cy="50" r="3" fill="black"></circle>
|
||||
<line id="second" transform="rotate(#{second_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="12" fill="black" stroke="black" stroke-width="1"></line>
|
||||
<line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line>
|
||||
<line id="hour" transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line>
|
||||
</svg>
|
||||
END_SVG
|
||||
<circle cx="50" cy="50" r="3" fill="black"></circle>
|
||||
<line id="second" transform="rotate(#{second_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="12" fill="black" stroke="black" stroke-width="1"></line>
|
||||
<line id="minute" transform="rotate(#{minute_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="16" fill="black" stroke="black" stroke-width="2"></line>
|
||||
<line id="hour" transform="rotate(#{hour_angle}, 50, 50)" x1="50" y1="50" x2="50" y2="24" fill="black" stroke="black" stroke-width="2"></line>
|
||||
</svg>
|
||||
END_SVG
|
||||
|
||||
image = "data:image/png;base64,"
|
||||
image += Process.run(%(rsvg-convert -w 400 -h 400 -b none -f png), shell: true,
|
||||
|
||||
@@ -56,7 +56,7 @@ struct Invidious::User
|
||||
|
||||
begin
|
||||
video = get_video(video_id)
|
||||
rescue ex
|
||||
rescue
|
||||
next
|
||||
end
|
||||
|
||||
@@ -97,7 +97,12 @@ struct Invidious::User
|
||||
end
|
||||
|
||||
if data["watch_history"]?
|
||||
user.watched += data["watch_history"].as_a.map(&.as_s)
|
||||
user.watched += data["watch_history"].as_a.map do |video_id|
|
||||
video_id = video_id.as_s
|
||||
# Sanity check for the imported video IDs
|
||||
raise InvalidVideoID.new(video_id) unless validate_video_id(video_id)
|
||||
video_id
|
||||
end
|
||||
user.watched.reverse!.uniq!.reverse!
|
||||
Invidious::Database::Users.update_watch_history(user)
|
||||
end
|
||||
@@ -130,7 +135,7 @@ struct Invidious::User
|
||||
|
||||
begin
|
||||
video = get_video(video_id, false)
|
||||
rescue ex
|
||||
rescue
|
||||
next
|
||||
end
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ struct Preferences
|
||||
else
|
||||
result
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
if value.read_bool
|
||||
"dark"
|
||||
else
|
||||
@@ -199,7 +199,7 @@ struct Preferences
|
||||
value.read_array do
|
||||
result << HTML.escape(value.read_string[0, 100])
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
result = [HTML.escape(value.read_string[0, 100]), ""]
|
||||
end
|
||||
|
||||
@@ -228,7 +228,7 @@ struct Preferences
|
||||
|
||||
result << HTML.escape(item.value[0, 100])
|
||||
end
|
||||
rescue ex
|
||||
rescue
|
||||
if node.is_a?(YAML::Nodes::Scalar)
|
||||
result = [HTML.escape(node.value[0, 100]), ""]
|
||||
else
|
||||
|
||||
@@ -19,7 +19,7 @@ struct Invidious::User
|
||||
def self.from_rs(rs)
|
||||
begin
|
||||
Preferences.from_json(rs.read(String))
|
||||
rescue ex
|
||||
rescue
|
||||
Preferences.from_json("{}")
|
||||
end
|
||||
end
|
||||
|
||||
+2
-14
@@ -45,7 +45,6 @@ def get_subscription_feed(user, max_results = 40, page = 1)
|
||||
notifications.sort_by!(&.author)
|
||||
when "channel name - reverse"
|
||||
notifications.sort_by!(&.author).reverse!
|
||||
else nil # Ignore
|
||||
end
|
||||
else
|
||||
if user.preferences.latest_only
|
||||
@@ -53,12 +52,7 @@ 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)
|
||||
videos = PG_DB.query_all("SELECT DISTINCT ON (ucid) * FROM #{view_name} WHERE NOT id = ANY ($1::text[]) ORDER BY ucid, published DESC", user.watched, as: ChannelVideo)
|
||||
else
|
||||
# Show latest video from each channel
|
||||
|
||||
@@ -70,12 +64,7 @@ def get_subscription_feed(user, max_results = 40, page = 1)
|
||||
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 * FROM #{view_name} WHERE NOT id = ANY ($1::text[]) ORDER BY published DESC LIMIT $2 OFFSET $3", user.watched, limit, offset, as: ChannelVideo)
|
||||
else
|
||||
# Sort subscriptions as normal
|
||||
|
||||
@@ -94,7 +83,6 @@ def get_subscription_feed(user, max_results = 40, page = 1)
|
||||
videos.sort_by!(&.author)
|
||||
when "channel name - reverse"
|
||||
videos.sort_by!(&.author).reverse!
|
||||
else nil # Ignore
|
||||
end
|
||||
|
||||
notifications = Invidious::Database::Users.select_notifications(user)
|
||||
|
||||
+35
-35
@@ -48,7 +48,7 @@ struct Video
|
||||
end
|
||||
end
|
||||
|
||||
def to_json(json : JSON::Builder | Nil = nil)
|
||||
def to_json(json : JSON::Builder? = nil)
|
||||
to_json(nil, json)
|
||||
end
|
||||
|
||||
@@ -150,7 +150,7 @@ struct Video
|
||||
|
||||
def dash_manifest_url : String?
|
||||
raw_dash_url = info.dig?("streamingData", "dashManifestUrl").try &.as_s
|
||||
return nil if raw_dash_url.nil?
|
||||
return if raw_dash_url.nil?
|
||||
|
||||
# Use manifest v5 parameter to reduce file size
|
||||
# See https://github.com/iv-org/invidious/issues/4186
|
||||
@@ -183,14 +183,14 @@ struct Video
|
||||
end
|
||||
|
||||
def music : Array(VideoMusic)
|
||||
info["music"].as_a.map { |music_json|
|
||||
info["music"].as_a.map do |music_json|
|
||||
VideoMusic.new(
|
||||
music_json["song"].as_s,
|
||||
music_json["album"].as_s,
|
||||
music_json["artist"].as_s,
|
||||
music_json["license"].as_s
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if comments are enabled on the video
|
||||
@@ -201,37 +201,37 @@ struct Video
|
||||
# Macros defining getters/setters for various types of data
|
||||
|
||||
private macro getset_string(name)
|
||||
# Return {{name.stringify}} from `info`
|
||||
def {{name.id.underscore}} : String
|
||||
return info[{{name.stringify}}]?.try &.as_s || ""
|
||||
# Return {{ name.stringify }} from `info`
|
||||
def {{ name.id.underscore }} : String
|
||||
return info[{{ name.stringify }}]?.try &.as_s || ""
|
||||
end
|
||||
|
||||
# Update {{name.stringify}} into `info`
|
||||
def {{name.id.underscore}}=(value : String)
|
||||
info[{{name.stringify}}] = JSON::Any.new(value)
|
||||
# Update {{ name.stringify }} into `info`
|
||||
def {{ name.id.underscore }}=(value : String)
|
||||
info[{{ name.stringify }}] = JSON::Any.new(value)
|
||||
end
|
||||
|
||||
{% if flag?(:debug_macros) %} {{debug}} {% end %}
|
||||
{% if flag?(:debug_macros) %} {{ debug }} {% end %}
|
||||
end
|
||||
|
||||
private macro getset_string_array(name)
|
||||
# Return {{name.stringify}} from `info`
|
||||
def {{name.id.underscore}} : Array(String)
|
||||
return info[{{name.stringify}}]?.try &.as_a.map &.as_s || [] of String
|
||||
# Return {{ name.stringify }} from `info`
|
||||
def {{ name.id.underscore }} : Array(String)
|
||||
return info[{{ name.stringify }}]?.try &.as_a.map &.as_s || [] of String
|
||||
end
|
||||
|
||||
# Update {{name.stringify}} into `info`
|
||||
def {{name.id.underscore}}=(value : Array(String))
|
||||
info[{{name.stringify}}] = JSON::Any.new(value)
|
||||
# Update {{ name.stringify }} into `info`
|
||||
def {{ name.id.underscore }}=(value : Array(String))
|
||||
info[{{ name.stringify }}] = JSON::Any.new(value)
|
||||
end
|
||||
|
||||
{% if flag?(:debug_macros) %} {{debug}} {% end %}
|
||||
{% if flag?(:debug_macros) %} {{ debug }} {% end %}
|
||||
end
|
||||
|
||||
{% for op, type in {i32: Int32, i64: Int64} %}
|
||||
private macro getset_{{op}}(name)
|
||||
def \{{name.id.underscore}} : {{type}}
|
||||
return info[\{{name.stringify}}]?.try &.as_i64.to_{{op}} || 0_{{op}}
|
||||
private macro getset_{{ op }}(name)
|
||||
def \{{name.id.underscore}} : {{ type }}
|
||||
return info[\{{name.stringify}}]?.try &.as_i64.to_{{ op }} || 0_{{ op }}
|
||||
end
|
||||
|
||||
def \{{name.id.underscore}}=(value : Int)
|
||||
@@ -243,32 +243,32 @@ struct Video
|
||||
{% end %}
|
||||
|
||||
private macro getset_bool(name)
|
||||
# Return {{name.stringify}} from `info`
|
||||
def {{name.id.underscore}} : Bool
|
||||
return info[{{name.stringify}}]?.try &.as_bool || false
|
||||
# Return {{ name.stringify }} from `info`
|
||||
def {{ name.id.underscore }} : Bool
|
||||
return info[{{ name.stringify }}]?.try &.as_bool || false
|
||||
end
|
||||
|
||||
# Update {{name.stringify}} into `info`
|
||||
def {{name.id.underscore}}=(value : Bool)
|
||||
info[{{name.stringify}}] = JSON::Any.new(value)
|
||||
# Update {{ name.stringify }} into `info`
|
||||
def {{ name.id.underscore }}=(value : Bool)
|
||||
info[{{ name.stringify }}] = JSON::Any.new(value)
|
||||
end
|
||||
|
||||
{% if flag?(:debug_macros) %} {{debug}} {% end %}
|
||||
{% if flag?(:debug_macros) %} {{ debug }} {% end %}
|
||||
end
|
||||
|
||||
# Macro to generate ? and = accessor methods for attributes in `info`
|
||||
private macro predicate_bool(method_name, name)
|
||||
# Return {{name.stringify}} from `info`
|
||||
def {{method_name.id.underscore}}? : Bool
|
||||
return info[{{name.stringify}}]?.try &.as_bool || false
|
||||
# Return {{ name.stringify }} from `info`
|
||||
def {{ method_name.id.underscore }}? : Bool
|
||||
return info[{{ name.stringify }}]?.try &.as_bool || false
|
||||
end
|
||||
|
||||
# Update {{name.stringify}} into `info`
|
||||
def {{method_name.id.underscore}}=(value : Bool)
|
||||
info[{{name.stringify}}] = JSON::Any.new(value)
|
||||
# Update {{ name.stringify }} into `info`
|
||||
def {{ method_name.id.underscore }}=(value : Bool)
|
||||
info[{{ name.stringify }}] = JSON::Any.new(value)
|
||||
end
|
||||
|
||||
{% if flag?(:debug_macros) %} {{debug}} {% end %}
|
||||
{% if flag?(:debug_macros) %} {{ debug }} {% end %}
|
||||
end
|
||||
|
||||
# Method definitions, using the macros above
|
||||
|
||||
@@ -10,7 +10,7 @@ module Invidious::Videos::Parser
|
||||
# TODO: "compactRadioRenderer" (Mix) and
|
||||
# TODO: Use a proper struct/class instead of a hacky JSON object
|
||||
def parse_related_video(related : JSON::Any) : Hash(String, JSON::Any)?
|
||||
return nil if !related["videoId"]?
|
||||
return if !related["videoId"]?
|
||||
|
||||
# The compact renderer has video length in seconds, where the end
|
||||
# screen rendered has a full text version ("42:40")
|
||||
@@ -60,7 +60,7 @@ module Invidious::Videos::Parser
|
||||
player_response = YoutubeAPI.player(video_id: video_id)
|
||||
|
||||
if player_response.nil?
|
||||
return nil
|
||||
return
|
||||
end
|
||||
|
||||
playability_status = player_response.dig?("playabilityStatus", "status").try &.as_s
|
||||
@@ -150,7 +150,7 @@ module Invidious::Videos::Parser
|
||||
elsif playability_status == "OK"
|
||||
return response
|
||||
else
|
||||
return nil
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@
|
||||
<% end %>
|
||||
|
||||
<% if captcha %>
|
||||
<% captcha = captcha.not_nil! %>
|
||||
<img style="width:50%" src='<%= captcha[:question] %>'/>
|
||||
<img src='<%= captcha[:question] %>' style="width:50%"/>
|
||||
<% captcha[:tokens].each_with_index do |token, i| %>
|
||||
<input type="hidden" name="token[<%= i %>]" value="<%= HTML.escape(token) %>">
|
||||
<% end %>
|
||||
|
||||
@@ -20,7 +20,7 @@ struct YoutubeConnectionPool
|
||||
|
||||
begin
|
||||
response = yield conn
|
||||
rescue ex
|
||||
rescue
|
||||
conn.close
|
||||
conn = make_client(url, force_resolve: true)
|
||||
|
||||
@@ -88,7 +88,7 @@ struct CompanionConnectionPool
|
||||
|
||||
begin
|
||||
response = yield wrapper
|
||||
rescue ex
|
||||
rescue
|
||||
wrapper.close
|
||||
|
||||
companion = CONFIG.invidious_companion.sample
|
||||
@@ -104,7 +104,7 @@ struct CompanionConnectionPool
|
||||
end
|
||||
end
|
||||
|
||||
def add_yt_headers(request)
|
||||
def add_yt_headers(request, no_cookies)
|
||||
request.headers.delete("User-Agent") if request.headers["User-Agent"] == "Crystal"
|
||||
request.headers["User-Agent"] ||= "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
|
||||
|
||||
@@ -112,10 +112,12 @@ def add_yt_headers(request)
|
||||
request.headers["Accept"] ||= "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||
request.headers["Accept-Language"] ||= "en-us,en;q=0.5"
|
||||
|
||||
# Preserve original cookies and add new YT consent cookie for EU servers
|
||||
request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}"
|
||||
if !CONFIG.cookies.empty?
|
||||
request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
|
||||
unless no_cookies
|
||||
# Preserve original cookies and add new YT consent cookie for EU servers
|
||||
request.headers["Cookie"] = "#{request.headers["cookie"]?}; CONSENT=PENDING+#{Random.rand(100..999)}"
|
||||
if !CONFIG.cookies.empty?
|
||||
request.headers["Cookie"] = "#{(CONFIG.cookies.map { |c| "#{c.name}=#{c.value}" }).join("; ")}; #{request.headers["cookie"]?}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -129,7 +131,16 @@ def make_client(url : URI, region = nil, force_resolve : Bool = false, force_you
|
||||
client.family = Socket::Family::INET if client.family == Socket::Family::UNSPEC
|
||||
end
|
||||
|
||||
client.before_request { |r| add_yt_headers(r) } if url.host.try &.ends_with?("youtube.com") || force_youtube_headers
|
||||
# ytimg.com does not contain any cookies, so we skip the youtube cookies
|
||||
# for that domain and subdomains (i9.ytimg.com, i.ytimg.com, etc)
|
||||
yt = url.host.try { |host| host == "youtube.com" || host.ends_with?(".youtube.com") }
|
||||
ytimg = url.host.try { |host| host == "ytimg.com" || host.ends_with?(".ytimg.com") }
|
||||
youtube_domain = yt || ytimg
|
||||
|
||||
client.before_request do |r|
|
||||
add_yt_headers(r, ytimg) if youtube_domain || force_youtube_headers
|
||||
end
|
||||
|
||||
client.read_timeout = 10.seconds
|
||||
client.connect_timeout = 10.seconds
|
||||
|
||||
|
||||
@@ -40,8 +40,8 @@ private module Parsers
|
||||
begin
|
||||
return parse_internal(*args)
|
||||
rescue ex
|
||||
LOGGER.debug("#{{{@type.name}}}: Failed to render item.")
|
||||
LOGGER.debug("#{{{@type.name}}}: Got exception: #{ex.message}")
|
||||
LOGGER.debug("#{{{ @type.name }}}: Failed to render item.")
|
||||
LOGGER.debug("#{{{ @type.name }}}: Got exception: #{ex.message}")
|
||||
ProblematicTimelineItem.new(
|
||||
parse_exception: ex
|
||||
)
|
||||
@@ -152,7 +152,6 @@ private module Parsers
|
||||
when "Premium"
|
||||
# TODO: Potentially available as item_contents["topStandaloneBadge"]["metadataBadgeRenderer"]
|
||||
badges |= VideoBadges::Premium
|
||||
else nil # Ignore
|
||||
end
|
||||
end
|
||||
|
||||
@@ -173,7 +172,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -240,7 +239,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -289,7 +288,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -334,7 +333,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -395,7 +394,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -467,7 +466,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -496,7 +495,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -527,7 +526,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -626,7 +625,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -655,6 +654,10 @@ private module Parsers
|
||||
thumbnail_view_model = item_contents.dig(
|
||||
"contentImage", "thumbnailViewModel"
|
||||
)
|
||||
# TODO: This is an useless assignment, is not used anywhere
|
||||
# so check if it can be used somewhere later.
|
||||
#
|
||||
# ameba:disable Lint/UselessAssign
|
||||
thumbnail = thumbnail_view_model.dig("image", "sources", 0, "url").as_s
|
||||
|
||||
video_id = item_contents["contentId"].as_s
|
||||
@@ -739,9 +742,9 @@ private module Parsers
|
||||
video_count = thumbnail_view_model.dig("overlays").as_a
|
||||
.compact_map(&.dig?("thumbnailOverlayBadgeViewModel", "thumbnailBadges").try &.as_a)
|
||||
.flatten
|
||||
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try { |node|
|
||||
.find(nil, &.dig?("thumbnailBadgeViewModel", "text").try do |node|
|
||||
{"episodes", "videos", "lessons"}.any? { |str| node.as_s.ends_with?(str) }
|
||||
})
|
||||
end)
|
||||
.try &.dig("thumbnailBadgeViewModel", "text").as_s.to_i(strict: false)
|
||||
|
||||
metadata = item_contents.dig("metadata", "lockupMetadataViewModel")
|
||||
@@ -790,10 +793,10 @@ private module Parsers
|
||||
# ]
|
||||
#
|
||||
metadata_rows = metadata.dig?("metadata", "contentMetadataViewModel", "metadataRows").try &.as_a
|
||||
metadata_parts = metadata_rows.try &.find { |row|
|
||||
metadata_parts = metadata_rows.try &.find do |row|
|
||||
parts = row["metadataParts"]?.try &.as_a
|
||||
parts && !parts.any? { |item| item.dig?("text", "content").try &.as_s == "View full playlist" }
|
||||
}.try &.["metadataParts"].as_a
|
||||
end.try &.["metadataParts"].as_a
|
||||
|
||||
if author_info = metadata_parts.try &.find(&.dig?("text", "commandRuns"))
|
||||
.try &.["text"]
|
||||
@@ -831,7 +834,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -889,7 +892,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -918,7 +921,7 @@ private module Parsers
|
||||
end
|
||||
|
||||
def self.parser_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1005,7 +1008,7 @@ private module Extractors
|
||||
end
|
||||
|
||||
def self.extractor_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1046,7 +1049,7 @@ private module Extractors
|
||||
end
|
||||
|
||||
def self.extractor_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1080,7 +1083,7 @@ private module Extractors
|
||||
end
|
||||
|
||||
def self.extractor_name
|
||||
return {{@type.name}}
|
||||
return {{ @type.name }}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -17,15 +17,13 @@
|
||||
# another nil will be returned.
|
||||
def extract_text(item : JSON::Any?) : String?
|
||||
if item.nil?
|
||||
return nil
|
||||
return
|
||||
end
|
||||
|
||||
if text_container = item["simpleText"]?
|
||||
return text_container.as_s
|
||||
elsif text_container = item["runs"]?
|
||||
return text_container.as_a.map(&.["text"].as_s).join("")
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ module UrlSanitizer
|
||||
when "results", "search"
|
||||
return :search
|
||||
else # hashtag, post, trending, brand URLs, etc..
|
||||
return nil
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ module YoutubeAPI
|
||||
|
||||
# Region to provide to youtube, e.g to alter search results
|
||||
# (this is passed as the `gl` parameter).
|
||||
property region : String | Nil
|
||||
property region : String?
|
||||
|
||||
# Initialization function
|
||||
def initialize(
|
||||
@@ -286,7 +286,7 @@ module YoutubeAPI
|
||||
# Return, as a Hash, the "context" data required to request the
|
||||
# youtube API endpoints.
|
||||
#
|
||||
private def make_context(client_config : ClientConfig | Nil, video_id = "dQw4w9WgXcQ") : Hash
|
||||
private def make_context(client_config : ClientConfig?, video_id = "dQw4w9WgXcQ") : Hash
|
||||
# Use the default client config if nil is passed
|
||||
client_config ||= DEFAULT_CLIENT_CONFIG
|
||||
|
||||
@@ -356,7 +356,7 @@ module YoutubeAPI
|
||||
#
|
||||
# - A playlist ID (parameters MUST be an empty string)
|
||||
#
|
||||
def browse(continuation : String, client_config : ClientConfig | Nil = nil)
|
||||
def browse(continuation : String, client_config : ClientConfig? = nil)
|
||||
# JSON Request data, required by the API
|
||||
data = {
|
||||
"context" => self.make_context(client_config),
|
||||
@@ -371,7 +371,7 @@ module YoutubeAPI
|
||||
browse_id : String,
|
||||
*, # Force the following parameters to be passed by name
|
||||
params : String,
|
||||
client_config : ClientConfig | Nil = nil,
|
||||
client_config : ClientConfig? = nil,
|
||||
)
|
||||
# JSON Request data, required by the API
|
||||
data = {
|
||||
@@ -424,7 +424,7 @@ module YoutubeAPI
|
||||
# })
|
||||
# ```
|
||||
#
|
||||
def next(continuation : String, *, client_config : ClientConfig | Nil = nil)
|
||||
def next(continuation : String, *, client_config : ClientConfig? = nil)
|
||||
# JSON Request data, required by the API
|
||||
data = {
|
||||
"context" => self.make_context(client_config),
|
||||
@@ -435,7 +435,7 @@ module YoutubeAPI
|
||||
end
|
||||
|
||||
# :ditto:
|
||||
def next(data : Hash, *, client_config : ClientConfig | Nil = nil)
|
||||
def next(data : Hash, *, client_config : ClientConfig? = nil)
|
||||
# JSON Request data, required by the API
|
||||
data2 = data.merge({
|
||||
"context" => self.make_context(client_config),
|
||||
@@ -445,7 +445,7 @@ module YoutubeAPI
|
||||
end
|
||||
|
||||
# Allow a NamedTuple to be passed, too.
|
||||
def next(data : NamedTuple, *, client_config : ClientConfig | Nil = nil)
|
||||
def next(data : NamedTuple, *, client_config : ClientConfig? = nil)
|
||||
return self.next(data.to_h, client_config: client_config)
|
||||
end
|
||||
|
||||
@@ -466,7 +466,7 @@ module YoutubeAPI
|
||||
if CONFIG.invidious_companion.present?
|
||||
return self._post_invidious_companion("/youtubei/v1/player", data)
|
||||
else
|
||||
return nil
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
@@ -498,7 +498,7 @@ module YoutubeAPI
|
||||
# channel_b = YoutubeAPI.resolve_url("https://www.youtube.com/c/invalid")
|
||||
# ```
|
||||
#
|
||||
def resolve_url(url : String, client_config : ClientConfig | Nil = nil)
|
||||
def resolve_url(url : String, client_config : ClientConfig? = nil)
|
||||
data = {
|
||||
"context" => self.make_context(nil),
|
||||
"url" => url,
|
||||
@@ -524,7 +524,7 @@ module YoutubeAPI
|
||||
def search(
|
||||
search_query : String,
|
||||
params : String,
|
||||
client_config : ClientConfig | Nil = nil,
|
||||
client_config : ClientConfig? = nil,
|
||||
)
|
||||
# JSON Request data, required by the API
|
||||
data = {
|
||||
@@ -550,7 +550,7 @@ module YoutubeAPI
|
||||
|
||||
def get_transcript(
|
||||
params : String,
|
||||
client_config : ClientConfig | Nil = nil,
|
||||
client_config : ClientConfig? = nil,
|
||||
) : Hash(String, JSON::Any)
|
||||
data = {
|
||||
"context" => self.make_context(client_config),
|
||||
@@ -572,7 +572,7 @@ module YoutubeAPI
|
||||
def _post_json(
|
||||
endpoint : String,
|
||||
data : Hash,
|
||||
client_config : ClientConfig | Nil,
|
||||
client_config : ClientConfig?,
|
||||
) : Hash(String, JSON::Any)
|
||||
# Use the default client config if nil is passed
|
||||
client_config ||= DEFAULT_CLIENT_CONFIG
|
||||
@@ -580,12 +580,20 @@ module YoutubeAPI
|
||||
# Query parameters
|
||||
url = "#{endpoint}?prettyPrint=false"
|
||||
|
||||
# true if logged in into a Google account on Youtube, therefore
|
||||
# we set it to true if the instance has cookies enabled.
|
||||
logged_in = if !CONFIG.cookies.empty?
|
||||
"true"
|
||||
else
|
||||
"false"
|
||||
end
|
||||
|
||||
headers = HTTP::Headers{
|
||||
"Content-Type" => "application/json; charset=UTF-8",
|
||||
"Accept-Encoding" => "gzip, deflate",
|
||||
"x-goog-api-format-version" => "2",
|
||||
"x-youtube-client-name" => client_config.name_proto,
|
||||
"x-youtube-client-version" => client_config.version,
|
||||
"Content-Type" => "application/json",
|
||||
"Accept-Encoding" => "gzip, deflate",
|
||||
"x-youtube-bootstrap-logged-in" => logged_in,
|
||||
"x-youtube-client-name" => client_config.name_proto,
|
||||
"x-youtube-client-version" => client_config.version,
|
||||
}
|
||||
|
||||
if user_agent = client_config.user_agent
|
||||
@@ -688,7 +696,7 @@ module YoutubeAPI
|
||||
# Multiple encodings can be combined, and are listed in the order
|
||||
# in which they were applied. E.g: "deflate, gzip" means that the
|
||||
# content must be first "gunzipped", then "defated".
|
||||
encodings.split(',').reverse.each do |enc|
|
||||
encodings.split(',').reverse!.each do |enc|
|
||||
case enc.strip(' ')
|
||||
when "gzip"
|
||||
body_io = Compress::Gzip::Reader.new(body_io, sync_close: true)
|
||||
|
||||
Reference in new issue
Block a user