-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathnuxt.config.ts
397 lines (380 loc) · 16.3 KB
/
nuxt.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
// https://nuxt.com/docs/api/configuration/nuxt-config
import { defineNuxtConfig } from "nuxt/config";
import GraphemeSplitter from "grapheme-splitter";
import { appDataPath } from "./server/utils";
const splitter = new GraphemeSplitter();
const rateLimitIntervalMs = 10 * 60 * 1000; // 10 minutes (in ms)
// Default limit equivalent to 1 frame / second average load. There are 4
// requests per frame.
const frameLimitPerInterval =
parseInt(process.env.FR_REQUEST_LIMIT || "0") ||
Math.round(rateLimitIntervalMs / 1000) + 1;
const requestLimitPerInterval = frameLimitPerInterval * 4;
/* eslint sort-keys: "error" */
export default defineNuxtConfig({
app: {
head: {
// If given, all the characters in FR_FAVICON_EMOJI will be drawn on top
// of each other one by one to create a favicon. Otherwise, we have a
// default png favicon.
link: process.env.FR_FAVICON_EMOJI // A good example value is 🎞️.
? [
{
// Emojis aren't split properly by String.prototype.split.
href: `data:image/svg+xml,${encodeURI(
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">${splitter
.splitGraphemes(process.env.FR_FAVICON_EMOJI)
.map((e) => `<text y="829" font-size="1000">${e}</text>`)
.join("")}</svg>`,
)}`,
rel: "icon",
},
]
: [
{
href: "/apple-touch-icon.png",
rel: "apple-touch-icon",
sizes: "180x180",
},
{
href: "/favicon-32x32.png",
rel: "icon",
sizes: "32x32",
type: "image/png",
},
{
href: "/favicon-16x16.png",
rel: "icon",
sizes: "16x16",
type: "image/png",
},
{
href: "/site.webmanifest",
rel: "manifest",
},
],
meta: [
{
content: "width=device-width, initial-scale=1",
name: "viewport",
},
{
charset: "utf-8",
},
],
noscript: [
"Unfortunately, this site requires JavaScript. However, in principle, a no-JavaScript version could be made…",
],
},
},
compatibilityDate: "2024-04-03",
devtools: {
enabled: false, // Toggle this to enable devtools.
},
i18n: {
defaultLocale: "en",
detectBrowserLanguage: {
cookieKey: "i18n_redirected",
fallbackLocale: "en",
redirectOn: "root",
useCookie: true,
},
langDir: "lang",
lazy: true,
locales: [
{ code: "de", file: "de.json", iso: "de", name: "Deutsch" },
{ code: "en", file: "en.json", iso: "en", name: "English" },
{ code: "fr", file: "fr.json", iso: "fr", name: "Français" },
{ code: "pl", file: "pl.json", iso: "pl", name: "Polski" },
{ code: "ru", file: "ru.json", iso: "ru", name: "русский" },
{ code: "zh", file: "zh.json", iso: "zh", name: "中文" },
],
strategy: "prefix_except_default",
},
modules: [
"@pinia/nuxt",
"@pinia-plugin-persistedstate/nuxt",
"@nuxtjs/i18n",
"nuxt-security",
],
nitro: {
esbuild: {
options: {
target: "node18",
},
},
storage: {
answer: {
base: appDataPath("answer"),
driver: "fs",
},
archivedRun: {
base: appDataPath("archived-run"),
driver: "fs",
},
ffprobeCache: {
base: appDataPath("ffprobe-cache"),
driver: "fs",
},
resourceState: {
base: appDataPath("resource-state"),
driver: "fs",
},
runState: {
base: appDataPath("run-state"),
driver: "fs",
},
},
},
routeRules: {
"/api/resource/gen": {
headers: {
"cache-control": "no-cache, no-store",
},
security: {
rateLimiter: {
interval: rateLimitIntervalMs,
tokensPerInterval: frameLimitPerInterval,
},
},
},
"/api/resource/get/**": {
headers: {
// private: browser cache only, no CDN or Cloudflare cache.
// immutable: image path is a UUID that will not change.
// max-age: one week default, a reasonable guess for how long a user
// might want the image to stick around
"cache-control": `private, immutable, max-age=${
process.env.FR_FRAME_CACHE_AGE || 60 * 60 * 24 * 7
}`,
// Change MIME type to image. Otherwise the browser might not know what
// file type to save it as, Cloudflare analytics will be messed up, etc.
// We would like to read from runtimeConfig.public.imageOutputExtension
// directly, but there doesn't seem to be a good way to do this in Nuxt.
"content-type": process.env.FR_IMAGE_CONTENT_TYPE || "image/webp",
},
},
"/api/resource/getAudio/**": {
headers: {
// private: browser cache only, no CDN or Cloudflare cache.
// immutable: image path is a UUID that will not change.
// max-age: one week default, a reasonable guess for how long a user
// might want the image to stick around
"cache-control": `private, immutable, max-age=${
process.env.FR_FRAME_CACHE_AGE || 60 * 60 * 24 * 7
}`,
// Change MIME type to image. Otherwise the browser might not know what
// file type to save it as, Cloudflare analytics will be messed up, etc.
// We would like to read from runtimeConfig.public.imageOutputExtension
// directly, but there doesn't seem to be a good way to do this in Nuxt.
"content-type": process.env.FR_AUDIO_CONTENT_TYPE || "audio/mpeg",
},
},
"/api/show": {
headers: {
"cache-control": `public, max-age=${
process.env.FR_SHOW_CACHE_AGE || 60 * 60 * 8
}`,
},
},
},
// These can be set per the instructions in
// https://nuxt.com/docs/guide/directory-structure/env. All options that are
// undefined here are required to be set in env params.
//
// This is unstable software; required options, option availability, and
// option interpretation can change between versions. Check the changelog or
// commit history when upgrading
runtimeConfig: {
// Whether to error out if episodes are missing, or simply print a warning.
allowMissingEpisodes: true,
// How long to keep an answer around for after a frame is served.
answerExpiryMs: 4 * 60 * 60 * 1000, // 4 hours.
// How often to check resourceOutputDir, frame state storage, and answer
// storage for expired or orphaned images.
cleanupIntervalMs: 5 * 60 * 1000, // 5 minutes.
// If given, this will be injected into the ffmpeg command used to generate
// the audio clips. Useful for specifying image encoding/quality options.
// Consult ffmpeg documentation (https://ffmpeg.org/ffmpeg-codecs.html).
ffmpegAudioCommandInject: undefined,
// If given, this will be injected into the ffmpeg command used to generate
// the images. Useful for specifying image encoding/quality options. Consult
// ffmpeg documentation (https://ffmpeg.org/ffmpeg-codecs.html).
ffmpegImageCommandInject: undefined,
// Path to ffmpeg binary, or "ffmpeg" to use the one from the system PATH.
ffmpegPath: "ffmpeg",
// On server initialization, ffprobe is used to find how long each episode
// is. Limit ffprobe invocations to this number at a time. 0 or Infinity for
// no limit.
ffprobeInitialLoadLimit: Infinity,
// Path to ffmpeg binary, or "ffmpeg" to use the one from the system PATH.
ffprobePath: "ffprobe",
// How many times to attempt frame generation before it's considered
// unrecoverable. Additionally, if frameRequiredStandardDeviation256 is set, a
// minimum standard deviation is required. If image generation fails this
// many times, give up and use the last generated image, waiving the
// standard deviation requirement. While it will prevent frame generation
// from hanging on frame generation indefinitely, hitting the limit will
// still increase frame generation times significantly.
frameGenMaxAttempts: 5,
// Require a standard deviation (from ImageMagick's identify command) of
// more than this amount. This is related to the standard deviation of RGB
// values on a scale from 0 to 256. If unsure, consider testing with
// identify on some borderline frames. Set to 0 to disable.
frameRequiredStandardDeviation256: 9.765,
// Path to ImageMagick identify command.
imageMagickIdentifyPath: "identify",
// Private key, used for signing verified runs.
privateKey: "",
// Per Nuxt documentation, these values will be sent to client-side code.
public: {
// Enable color changing animation effect for April Fools Day only.
aprilFoolsColorChanging: false,
// Whether to include the disclaimer required by TMDB for use of its API
// (see
// https://developer.themoviedb.org/docs/faq#what-are-the-attribution-requirements).
attributeTmdb: false,
// What extension to output audio files as. Naturally, these have
// different tradeoffs in terms of output filesize, generation/encoding
// time, etc.
audioOutputExtension: "mp3",
// Provide a custom format string for linking the source episode when
// showing the answer. "{season}" and "{episode}" will be substituted.
episodeUrlFormat: undefined,
// How long to keep a frame image around for after it's released to a user
// but not deleted.
frameExpiryMs: 5 * 60 * 1000, // 10 minutes.
// Number of characters required for a match. If the user inputs a search
// query shorter than this, nothing will happen. This is only the default;
// the user can change this in the settings.
// https://fusejs.io/api/options.html#minmatchcharlength
fuzzySearchMinMatchLength: 3,
// The threshold used by Fuse for what is considered a match. Increasing
// this will relax the match strictness. This is only the default; the
// user can change this in the settings.
// https://fusejs.io/api/options.html#threshold.
fuzzySearchThreshold: 0.2,
// Weight assigned to the episode name, relative to other fields. This is
// only the default; the user can change this in the settings.
// https://fusejs.io/examples.html#weighted-search
fuzzySearchWeightName: 1.0,
// Weight assigned to the episode name in the original language, relative
// to other fields. This is only the default; the user can change this in
// the settings.
// https://fusejs.io/examples.html#weighted-search
fuzzySearchWeightOriginalLanguage: 0.95,
// Weight assigned to the episode overview/synopsis, relative to other
// fields. This is only the default; the user can change this in the
// settings.
// https://fusejs.io/examples.html#weighted-search
fuzzySearchWeightSynopsis: 0.25,
// What extension to output images as. Naturally, these have different
// tradeoffs in terms of output filesize, generation/encoding time, etc.
imageOutputExtension: "webp",
// JSON instance info that will be shown in the About section.
instanceInfoData: undefined,
// Instance info that will be shown in the About section. This allows HTML
// tags; use this if you want to include HTML. You might want to include a
// way for users to contact you if there are problems, attribution or
// acknowledgement to your data source (e.g. TMDB) or contributors, etc.
instanceInfoHtml: undefined,
// Instance info, but allowing plain text only; use this for safety if you
// don't need to include HTML.
instanceInfoText: undefined,
// Required. Instance name shown to users.
instanceName: undefined,
// Description added to meta tags.
metaDescription: "Frame randomizer instance",
// Public key, which can be used to verify server signatures.
publicKey: "",
// Software version displayed in UI.
softwareVersion: "0.3.5",
// Link to your version of the source code. If you build and run a
// modified version of this software to users over a network, the AGPL
// requires you to provide users with a link to view/download your
// modified version. If you don't provide a different link here, you
// attest that your instance's code is unmodified.
sourceCodeUrl: "https://github.com/steadygaze/frame-randomizer",
// If attributing TMDB and this is given, also link the TMDB page.
tmdbTvShowId: undefined,
},
// If a particular combination hasn't been queued, how many to queue at
// once.
queueExhaustionQueueCount: 3,
// Limit number of simultaneously generated resources to this amount.
// Assuming resource generation is a CPU-bound task, common advice is to set
// this to 1 more than the number of CPU cores available.
resourceGenMaxParallelism: 3,
// Where generated audio and images will be outputted to and served from.
// Apparently orphaned files will be cleaned out of this directory, so
// don't point it to somewhere that has existing data!
resourceOutputDir: "./frame-randomizer/resources",
// For all different pregenerated kinds of resources (e.g. frames
// with/without subtitles, audio clips length 5, 10, 15 seconds, etc.), how
// many to keep pregenerated minimum, regardless of traffic patterns and
// pregen caps.
resourcePerKindMinimum: 2,
// Number of audio clips and images to pregenerate. These will be ready for
// serving right away, and will be replaced as soon as they're served.
resourcePregenCount: 3,
// How long until unimportant runs are deleted.
runExpiryMs: 1 * 60 * 60 * 1000, // 1 hour.
// How many entries a run must have to be considered important.
runRetentionThreshold: 100,
// Whether to search subdirectories of subtitleSourceDir recursively.
// Directory path is not considered when matching files with the right
// episode, only filename.
searchSubtitleDirRecursively: true,
// Whether to search subdirectories of videoSourceDir. Directory path is not
// considered when matching files with the right episode, only filename.
searchVideoDirRecursively: true,
// Required. Where the show data is. See README.md and server/load.ts for
// more info.
showDataPath: undefined,
// Font to use when generating subtitles. The underlying ffmpeg library will
// fall back to some other font if this is unavailable.
subtitleFontName: "Impact",
// Font size when generating subtitles.
subtitleFontSize: 32,
// Where the subtitles can be found. Files should include the season and
// episode numbers in SxxExx or xx,xx format or similar.
subtitleSourceDir: undefined,
// Whether the ffprobe FS cache will be used. If video files at the same
// paths aren't expected to change (as in most use cases), this can remain
// true for faster server restarts. Note that if this is false, the cache
// isn't touched (not read from, cleared, or repopulated). If you need to
// clear and repopulate the cache, simply "rm -r" the ffprobe cache
// directory (see ffprobeCache.base).
useFfprobeCache: true,
// Used to give generated images random names. Recommend setting this to a
// different one for your own instance from:
// https://www.uuidtools.com/generate/v4
uuidNamespace: "b219dcdb-c910-417c-8403-01c6b40c5fb4",
// Video file extensions to look for videos for. The default list will
// contain common video files; if you have a less common video format in
// mind, it's better to set this explicitly.
videoFileExtension: "avi,mkv,mov,mp4,webm",
// Required. Where source videos are found. Files should include the season
// and episode numbers in SxxExx or xx,xx format or similar.
videoSourceDir: undefined,
},
security: {
corsHandler: {
origin: ["static.cloudflareinsights.com"],
},
headers: {
// Allow devtools to work.
crossOriginEmbedderPolicy:
process.env.NODE_ENV === "development" ? "unsafe-none" : "require-corp",
},
rateLimiter: {
interval: rateLimitIntervalMs,
throwError: true,
tokensPerInterval: requestLimitPerInterval,
},
},
typescript: {
strict: true,
typeCheck: true,
},
});