Skip to content

Commit 0b2ec10

Browse files
committed
Complete API-only mode implementation
This commit completes the API-only mode feature that allows Invidious to be built and run without GUI/frontend components, significantly reducing the binary size and dependencies. Changes include: - Add conditional compilation flags throughout the codebase to exclude frontend-specific code - Create stub implementations for database operations in API-only mode - Update Docker configurations to support API-only builds - Refactor require statements for better modularity - Add DummyDB and stub types for API-only mode - Ensure all routes work correctly without frontend dependencies The API-only mode can be enabled by: - Using -Dapi_only flag during compilation - Setting API_ONLY=1 when using make - Using --build-arg api_only=1 with Docker builds This is particularly useful for: - Microservice architectures where only the API is needed - Reducing resource usage in containerized environments - Creating lightweight API servers for mobile/desktop applications
1 parent df8839d commit 0b2ec10

File tree

11 files changed

+471
-110
lines changed

11 files changed

+471
-110
lines changed

docker/Dockerfile

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ FROM crystallang/crystal:1.16.3-alpine AS builder
33
RUN apk add --no-cache sqlite-static yaml-static
44

55
ARG release
6+
ARG api_only
67

78
WORKDIR /invidious
89
COPY ./shard.yml ./shard.yml
@@ -21,16 +22,13 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
2122

2223
RUN crystal spec --warnings all \
2324
--link-flags "-lxml2 -llzma"
24-
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
25-
crystal build ./src/invidious.cr \
26-
--release \
27-
--static --warnings all \
28-
--link-flags "-lxml2 -llzma"; \
29-
else \
30-
crystal build ./src/invidious.cr \
31-
--static --warnings all \
32-
--link-flags "-lxml2 -llzma"; \
33-
fi
25+
26+
RUN --mount=type=cache,target=/root/.cache/crystal \
27+
crystal build ./src/invidious.cr \
28+
${release:+--release} \
29+
--static --warnings all \
30+
--link-flags "-lxml2 -llzma" \
31+
${api_only:+-Dapi_only -Dskip_videojs_download}
3432

3533
FROM alpine:3.21
3634
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata

docker/Dockerfile.arm64

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ RUN apk add --no-cache 'crystal=1.14.0-r0' shards sqlite-static yaml-static yaml
33
zlib-static openssl-libs-static openssl-dev musl-dev xz-static
44

55
ARG release
6+
ARG api_only
67

78
WORKDIR /invidious
89
COPY ./shard.yml ./shard.yml
@@ -22,16 +23,12 @@ COPY ./videojs-dependencies.yml ./videojs-dependencies.yml
2223
RUN crystal spec --warnings all \
2324
--link-flags "-lxml2 -llzma"
2425

25-
RUN --mount=type=cache,target=/root/.cache/crystal if [[ "${release}" == 1 ]] ; then \
26-
crystal build ./src/invidious.cr \
27-
--release \
28-
--static --warnings all \
29-
--link-flags "-lxml2 -llzma"; \
30-
else \
31-
crystal build ./src/invidious.cr \
32-
--static --warnings all \
33-
--link-flags "-lxml2 -llzma"; \
34-
fi
26+
RUN --mount=type=cache,target=/root/.cache/crystal \
27+
crystal build ./src/invidious.cr \
28+
${release:+--release} \
29+
--static --warnings all \
30+
--link-flags "-lxml2 -llzma" \
31+
${api_only:+-Dapi_only -Dskip_videojs_download}
3532

3633
FROM alpine:3.21
3734
RUN apk add --no-cache rsvg-convert ttf-opensans tini tzdata

src/invidious.cr

Lines changed: 67 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -31,24 +31,33 @@ require "yaml"
3131
require "compress/zip"
3232
require "protodec/utils"
3333

34-
require "./invidious/database/*"
35-
require "./invidious/database/migrations/*"
34+
# Database requires
35+
{% unless flag?(:api_only) %}
36+
require "./invidious/database/*"
37+
require "./invidious/database/migrations/*"
38+
{% else %}
39+
require "./invidious/database/api_only_stubs"
40+
{% end %}
41+
42+
# Core requires
3643
require "./invidious/http_server/*"
3744
require "./invidious/helpers/*"
3845
require "./invidious/yt_backend/*"
3946
require "./invidious/frontend/*"
4047
require "./invidious/videos/*"
41-
4248
require "./invidious/jsonify/**"
43-
44-
require "./invidious/*"
49+
require "./invidious/requires"
4550
require "./invidious/comments/*"
4651
require "./invidious/channels/*"
4752
require "./invidious/user/*"
4853
require "./invidious/search/*"
4954
require "./invidious/routes/**"
50-
require "./invidious/jobs/base_job"
51-
require "./invidious/jobs/*"
55+
56+
# Jobs (not needed in API-only mode)
57+
{% unless flag?(:api_only) %}
58+
require "./invidious/jobs/base_job"
59+
require "./invidious/jobs/*"
60+
{% end %}
5261

5362
# Declare the base namespace for invidious
5463
module Invidious
@@ -60,7 +69,13 @@ alias IV = Invidious
6069
CONFIG = Config.load
6170
HMAC_KEY = CONFIG.hmac_key
6271

63-
PG_DB = DB.open CONFIG.database_url
72+
# Database connection
73+
{% unless flag?(:api_only) %}
74+
PG_DB = DB.open CONFIG.database_url
75+
{% else %}
76+
require "./invidious/api_only_types"
77+
PG_DB = DummyDB.new
78+
{% end %}
6479
ARCHIVE_URL = URI.parse("https://archive.org")
6580
PUBSUB_URL = URI.parse("https://pubsubhubbub.appspot.com")
6681
REDDIT_URL = URI.parse("https://www.reddit.com")
@@ -133,7 +148,11 @@ Kemal.config.extra_options do |parser|
133148
exit
134149
end
135150
parser.on("--migrate", "Run any migrations (beta, use at your own risk!!") do
136-
Invidious::Database::Migrator.new(PG_DB).migrate
151+
{% unless flag?(:api_only) %}
152+
Invidious::Database::Migrator.new(PG_DB).migrate
153+
{% else %}
154+
puts "Database migrations are not available in API-only mode"
155+
{% end %}
137156
exit
138157
end
139158
end
@@ -147,9 +166,11 @@ OUTPUT = CONFIG.output.upcase == "STDOUT" ? STDOUT : File.open(CONFIG.output, mo
147166
LOGGER = Invidious::LogHandler.new(OUTPUT, CONFIG.log_level, CONFIG.colorize_logs)
148167

149168
# Check table integrity
150-
Invidious::Database.check_integrity(CONFIG)
169+
{% unless flag?(:api_only) %}
170+
Invidious::Database.check_integrity(CONFIG)
171+
{% end %}
151172

152-
{% if !flag?(:skip_videojs_download) %}
173+
{% if !flag?(:skip_videojs_download) && !flag?(:api_only) %}
153174
# Resolve player dependencies. This is done at compile time.
154175
#
155176
# Running the script by itself would show some colorful feedback while this doesn't.
@@ -175,38 +196,48 @@ DECRYPT_FUNCTION =
175196

176197
# Start jobs
177198

178-
if CONFIG.channel_threads > 0
179-
Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB)
180-
end
181-
182-
if CONFIG.feed_threads > 0
183-
Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB)
184-
end
185-
186-
if CONFIG.statistics_enabled
187-
Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE)
188-
end
189-
190-
if (CONFIG.use_pubsub_feeds.is_a?(Bool) && CONFIG.use_pubsub_feeds.as(Bool)) || (CONFIG.use_pubsub_feeds.is_a?(Int32) && CONFIG.use_pubsub_feeds.as(Int32) > 0)
191-
Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, HMAC_KEY)
192-
end
199+
{% unless flag?(:api_only) %}
200+
if CONFIG.channel_threads > 0
201+
Invidious::Jobs.register Invidious::Jobs::RefreshChannelsJob.new(PG_DB)
202+
end
193203

194-
if CONFIG.popular_enabled
195-
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
196-
end
204+
if CONFIG.feed_threads > 0
205+
Invidious::Jobs.register Invidious::Jobs::RefreshFeedsJob.new(PG_DB)
206+
end
197207

198-
NOTIFICATION_CHANNEL = ::Channel(VideoNotification).new(32)
199-
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
200-
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(NOTIFICATION_CHANNEL, CONNECTION_CHANNEL, CONFIG.database_url)
208+
if CONFIG.statistics_enabled
209+
Invidious::Jobs.register Invidious::Jobs::StatisticsRefreshJob.new(PG_DB, SOFTWARE)
210+
end
201211

202-
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
212+
if (CONFIG.use_pubsub_feeds.is_a?(Bool) && CONFIG.use_pubsub_feeds.as(Bool)) || (CONFIG.use_pubsub_feeds.is_a?(Int32) && CONFIG.use_pubsub_feeds.as(Int32) > 0)
213+
Invidious::Jobs.register Invidious::Jobs::SubscribeToFeedsJob.new(PG_DB, HMAC_KEY)
214+
end
203215

204-
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
216+
if CONFIG.popular_enabled
217+
Invidious::Jobs.register Invidious::Jobs::PullPopularVideosJob.new(PG_DB)
218+
end
205219

206-
Invidious::Jobs.start_all
220+
NOTIFICATION_CHANNEL = ::Channel(VideoNotification).new(32)
221+
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(32)
222+
Invidious::Jobs.register Invidious::Jobs::NotificationJob.new(NOTIFICATION_CHANNEL, CONNECTION_CHANNEL, CONFIG.database_url)
223+
224+
Invidious::Jobs.register Invidious::Jobs::ClearExpiredItemsJob.new
225+
226+
Invidious::Jobs.register Invidious::Jobs::InstanceListRefreshJob.new
227+
228+
Invidious::Jobs.start_all
229+
{% else %}
230+
# Define channels for API-only mode (even though they won't be used)
231+
NOTIFICATION_CHANNEL = ::Channel(VideoNotification).new(1)
232+
CONNECTION_CHANNEL = ::Channel({Bool, ::Channel(PQ::Notification)}).new(1)
233+
{% end %}
207234

208235
def popular_videos
209-
Invidious::Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get
236+
{% if flag?(:api_only) %}
237+
[] of ChannelVideo
238+
{% else %}
239+
Invidious::Jobs::PullPopularVideosJob::POPULAR_VIDEOS.get
240+
{% end %}
210241
end
211242

212243
# Routing

src/invidious/api_only_types.cr

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# API-only mode type definitions
2+
# This file provides dummy type definitions when running in API-only mode
3+
4+
# Dummy DB class for API-only mode
5+
class DummyDB
6+
def query_all(*args, as : T.class) forall T
7+
[] of T
8+
end
9+
10+
def query_one(*args, as : T.class) forall T
11+
raise "Database not available in API-only mode"
12+
end
13+
14+
def query_one?(*args, as : T.class) forall T
15+
nil
16+
end
17+
18+
def scalar(*args)
19+
0
20+
end
21+
22+
def exec(*args)
23+
nil
24+
end
25+
end
26+
27+
# VideoNotification struct for API-only mode
28+
struct VideoNotification
29+
property video_id : String
30+
property channel_id : String
31+
property published : Time
32+
33+
def initialize(@video_id = "", @channel_id = "", @published = Time.utc)
34+
end
35+
36+
def self.from_video(video : ChannelVideo) : VideoNotification
37+
VideoNotification.new(video.id, video.ucid, video.published)
38+
end
39+
end
40+
41+
# PQ module with Notification for API-only mode
42+
module PQ
43+
struct Notification
44+
property channel : String = ""
45+
property payload : String = ""
46+
47+
def initialize(@channel = "", @payload = "")
48+
end
49+
end
50+
end

src/invidious/config.cr

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,9 @@ class Config
107107
property full_refresh : Bool = false
108108

109109
# Jobs config structure. See jobs.cr and jobs/base_job.cr
110-
property jobs = Invidious::Jobs::JobsConfig.new
110+
{% unless flag?(:api_only) %}
111+
property jobs = Invidious::Jobs::JobsConfig.new
112+
{% end %}
111113

112114
# Used to tell Invidious it is behind a proxy, so links to resources should be https://
113115
property https_only : Bool?
@@ -285,21 +287,28 @@ class Config
285287
end
286288

287289
# Build database_url from db.* if it's not set directly
288-
if config.database_url.to_s.empty?
289-
if db = config.db
290-
config.database_url = URI.new(
291-
scheme: "postgres",
292-
user: db.user,
293-
password: db.password,
294-
host: db.host,
295-
port: db.port,
296-
path: db.dbname,
297-
)
298-
else
299-
puts "Config: Either database_url or db.* is required"
300-
exit(1)
290+
{% unless flag?(:api_only) %}
291+
if config.database_url.to_s.empty?
292+
if db = config.db
293+
config.database_url = URI.new(
294+
scheme: "postgres",
295+
user: db.user,
296+
password: db.password,
297+
host: db.host,
298+
port: db.port,
299+
path: db.dbname,
300+
)
301+
else
302+
puts "Config: Either database_url or db.* is required"
303+
exit(1)
304+
end
301305
end
302-
end
306+
{% else %}
307+
# In API-only mode, database is optional
308+
if config.database_url.to_s.empty?
309+
config.database_url = URI.parse("postgres://dummy:dummy@localhost/dummy")
310+
end
311+
{% end %}
303312

304313
# Check if the socket configuration is valid
305314
if sb = config.socket_binding

0 commit comments

Comments
 (0)