Compare commits

...
27 Commits
Author SHA1 Message Date
Emilien efcfb1ef03 fix: retry 3 times create new release script 2026-10-19 09:28:14 +02:00
Fijxu 57cc097022 chore: Add Video ID validation to imported watched videos and improve SQL query 2026-10-13 08:06:23 -03:00
Fijxu 41d044692c chore: update to Crystal 1.22.0 (#6083) 2026-10-13 06:26:10 -03:00
Fijxu 907fe54aab chore: update to ameba 1.7.0 and adapt most of the code to it (#6082)
* chore: update to ameba 1.7.0 and adapt most of the code to it

* forgot to add this to build ameba into the ./bin directory for use, as ameba does not longer offer a postinstall script

* forgot to update ameba in the shards files

* remove leftover : in rescue

* Use ameba linter action instead of installing it with shards
2026-10-13 06:14:16 -03:00
Fijxu 7286c6dea4 chore: update openssl to 3.6.5 in OCI (#6080) 2026-10-11 13:51:17 -03:00
Fijxu b91e517c36 chore(deps): update ameba to 1.6.4 and spectator to 1.12.4 (#6047) 2026-10-11 13:17:39 -03:00
dependabot[bot] dc1011d74e chore(deps): bump nixpkgs from 6774f7b to 151fa4e in /nix (#6078)
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `6774f7b` to `151fa4e`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/6774f7bc253789b113a4f39285dc0fa100abeacc...151fa4e8ddfdd8dd25d945ad94ed54a13de9f6e4)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 151fa4e8ddfdd8dd25d945ad94ed54a13de9f6e4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 12:51:17 -03:00
dependabot[bot] 20bcaa0e1d chore(deps): bump crystal-lang/install-crystal from 1.9.2 to 1.9.3 (#6077)
Bumps [crystal-lang/install-crystal](https://github.com/crystal-lang/install-crystal) from 1.9.2 to 1.9.3.
- [Release notes](https://github.com/crystal-lang/install-crystal/releases)
- [Commits](https://github.com/crystal-lang/install-crystal/compare/v1.9.2...v1.9.3)

---
updated-dependencies:
- dependency-name: crystal-lang/install-crystal
  dependency-version: 1.9.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-11 12:51:03 -03:00
dependabot[bot] 381c6e8c92 chore(deps): bump nixpkgs from dc5d91f to 6774f7b in /nix (#6064)
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `dc5d91f` to `6774f7b`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/dc5d91f840324650bac8c379428c7037a416959a...6774f7bc253789b113a4f39285dc0fa100abeacc)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 6774f7bc253789b113a4f39285dc0fa100abeacc
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-30 20:14:26 -03:00
Fijxu 1454eada06 chore: Use full URL for mocks git submodule (#6066)
When working on Invidious forks that are not iv-org/invidious, the git
submodule will try to be fetched from the user fork instead of
`iv-org/mocks`, so it's better to use a full URL instead.
2026-09-30 17:20:46 -03:00
Fijxu c88230067b fix: Also add User-Agent for ytimg.com requests (#6038)
* fix: Also add User-Agent for ytimg.com requests, excluding youtube cookies

* strictly match only youtube.com and ytimg.com subdomains
2026-09-16 00:28:31 -03:00
dependabot[bot] 3012b9c0e3 chore(deps): bump nixpkgs from 34ab990 to dc5d91f in /nix (#6028)
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `34ab990` to `dc5d91f`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/34ab99075ac4f7e40cf037eef32cb1c360bb85e9...dc5d91f840324650bac8c379428c7037a416959a)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: dc5d91f840324650bac8c379428c7037a416959a
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-16 00:24:40 -03:00
Fijxu 248c315b4e chore: update openssl to 3.6.4 in OCI 2026-09-15 18:04:56 -03:00
Fijxu e4ad826f1f chore: lint and remove unused variables 2026-09-14 23:38:43 -03:00
Fijxu 9f8bc9e2c4 chore(ameba): Do not exclude /api routes from Lint/UselessAssign (#5900) 2026-09-14 23:07:37 -03:00
Fijxu b052ced0b4 feat: show message for unsupported Crystal versions at compile time (#5909)
Show a nice message for unsupported versions of the Crystal compiler, encouraging the user compiling Invidious, to update it's Crystal compiler version.
2026-09-14 23:07:10 -03:00
Fijxu 47756e0d1e chore: Update headers sent to innertube in JSON requests (#6040)
* chore: Update headers sent to innertube in JSON requests

* it should be a String, not a Bool
2026-09-14 23:05:07 -03:00
Fijxu f3e6d6d84e feat: sanity check video IDs with regex (#6037)
* feat: sanity check video IDs with regex

* Return 400 instead

* fix wrong function

* add coderabbit suggestion about PCRE2 regex and length of the string with a new line

* Use validate_video_id on more places

* Add video id validation to /watch route.
2026-09-14 23:04:25 -03:00
NielsNL12 7e613cf228 Fix stale bot incorrectly claiming it will close PRs in 30 days (#6044)
* Change stale closure date in description to correctly state 60 days rather than 30

* Fix typo
2026-09-14 23:03:45 -03:00
dependabot[bot] 049d591d29 chore(deps): bump nixpkgs from 56c02bc to 34ab990 in /nix (#6006)
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `56c02bc` to `34ab990`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/56c02bc00adcf003215cc4bd996d6efaf4cff188...34ab99075ac4f7e40cf037eef32cb1c360bb85e9)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 34ab99075ac4f7e40cf037eef32cb1c360bb85e9
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-04 16:46:52 -04:00
dependabot[bot] b3a3f3a6df chore(deps): bump actions/checkout from 6 to 7 (#5996)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-28 17:50:32 -04:00
dependabot[bot] a625015187 chore(deps): bump nixpkgs from ffb3c9b to 56c02bc in /nix (#5997)
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `ffb3c9b` to `56c02bc`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/ffb3c9b700e759be2ef13237c9d8f953b32a1e46...56c02bc00adcf003215cc4bd996d6efaf4cff188)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: 56c02bc00adcf003215cc4bd996d6efaf4cff188
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-28 17:50:22 -04:00
Fijxu 34173a3d84 ci: use less specific version for int128/docker-manifest-create-action (use @v2 instead of @v2.y.x) 2026-08-23 16:33:31 -04:00
Fijxu 4daa142844 chore: update nix development files in paths-ignore for github workflows 2026-08-23 14:34:49 -04:00
dependabot[bot] eff609d0cd chore(deps): bump nixpkgs from 0e251e2 to ec2d622 in /nix (#5968)
Bumps [nixpkgs](https://github.com/NixOS/nixpkgs) from `0e251e2` to `ec2d622`.
- [Commits](https://github.com/NixOS/nixpkgs/compare/0e251e24a4f24e036a084b6b4b2d2491af4167f4...ec2d622de0773551768cf98f3fc50cbcc003b9c5)

---
updated-dependencies:
- dependency-name: nixpkgs
  dependency-version: ec2d622de0773551768cf98f3fc50cbcc003b9c5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 22:12:37 -04:00
dependabot[bot] 9c8c37ea86 chore(deps): bump actions/checkout from 6 to 7 (#5782)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-21 22:12:28 -04:00
Fijxu c756c35268 chore: Add myself to assignees for Dependabot PRs (#5972)
So I get notified about deps updates.

https://docs.github.com/en/code-security/tutorials/secure-your-dependencies/customizing-dependabot-prs#automatically-adding-assignees
2026-08-21 22:10:09 -04:00
91 changed files with 914 additions and 806 deletions

No files matched your search

+22 -9
View File
@@ -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
#
+6
View File
@@ -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
+2 -2
View File
@@ -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
View File
@@ -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
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -1,3 +1,3 @@
[submodule "mocks"]
path = mocks
url = ../mocks
url = https://github.com/iv-org/mocks.git
+9 -4
View File
@@ -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
View File
@@ -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.
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+6 -4
View File
@@ -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
+15 -15
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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)
+2 -2
View File
@@ -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
+9 -9
View File
@@ -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 %}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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|
+11 -11
View File
@@ -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
+1 -1
View File
@@ -44,6 +44,6 @@ class Invidious::Database::Migrator
id bigserial PRIMARY KEY,
version bigint NOT NULL
)
SQL
SQL
end
end
+4 -4
View File
@@ -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
+21 -21
View File
@@ -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
+6 -6
View File
@@ -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
+4 -4
View File
@@ -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
+20 -20
View File
@@ -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
+5 -5
View File
@@ -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
+12
View File
@@ -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
+22 -22
View File
@@ -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
+66 -67
View File
@@ -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
+22 -22
View File
@@ -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
+12 -12
View File
@@ -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
+13 -13
View File
@@ -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 %}
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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 %}
+7 -7
View File
@@ -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
+1 -1
View File
@@ -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
+9 -3
View File
@@ -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.
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -3
View File
@@ -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")
+25 -19
View File
@@ -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)
+5 -6
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+20 -18
View File
@@ -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
+4 -4
View File
@@ -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
+3 -3
View File
@@ -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
+4 -5
View File
@@ -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
-1
View File
@@ -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
+5 -5
View File
@@ -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
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
-1
View File
@@ -362,7 +362,6 @@ module Invidious::Routes::PreferencesRoute
response: error_template(415, "Uploaded file is too large")
)
end
else nil # Ignore
end
end
end
+3 -3
View File
@@ -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)
+9 -15
View File
@@ -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
View File
@@ -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
+3 -2
View File
@@ -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 -2
View File
@@ -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 -20
View File
@@ -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,
+8 -3
View File
@@ -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
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+3 -3
View File
@@ -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
+1 -2
View File
@@ -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 %>
+19 -8
View File
@@ -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
+25 -22
View File
@@ -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
+1 -3
View File
@@ -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
+1 -1
View File
@@ -46,7 +46,7 @@ module UrlSanitizer
when "results", "search"
return :search
else # hashtag, post, trending, brand URLs, etc..
return nil
return
end
end
+26 -18
View File
@@ -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)