Skip to content

Commit 01f3946

Browse files
committed
fix(api): swagger docs
chore: generate api-client fix(web): improved new generative api-client support fix(web): force update metadata on scan action click fix(api): proper TMDB_API lookup
1 parent fabe7d4 commit 01f3946

File tree

27 files changed

+1107
-702
lines changed

27 files changed

+1107
-702
lines changed

apps/api/src/config/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const envSchema = z.object({
3636
CORS_ORIGIN: z.string().default("http://localhost:5173"),
3737
WEB_URL: z.string().optional(),
3838

39-
// API Keys
39+
// External API Keys (Legacy - prefer configuring via Settings UI)
4040
TMDB_API_KEY: z.string().optional(),
4141

4242
// Rate Limiting

apps/api/src/config/swagger.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,23 @@ const options: swaggerJsdoc.Options = {
644644
nullable: true,
645645
example: "https://example.com/backdrop.jpg",
646646
},
647+
isLibrary: {
648+
type: "boolean",
649+
example: false,
650+
},
651+
libraryPath: {
652+
type: "string",
653+
nullable: true,
654+
example: "/media/movies",
655+
},
656+
libraryType: {
657+
$ref: "#/components/schemas/MediaType",
658+
nullable: true,
659+
},
660+
mediaCount: {
661+
type: "integer",
662+
example: 42,
663+
},
647664
},
648665
},
649666
MediaCollection: {
@@ -703,6 +720,51 @@ const options: swaggerJsdoc.Options = {
703720
},
704721
},
705722
},
723+
Settings: {
724+
type: "object",
725+
properties: {
726+
id: {
727+
type: "string",
728+
example: "default",
729+
},
730+
isSetupComplete: {
731+
type: "boolean",
732+
example: false,
733+
},
734+
tmdbApiKey: {
735+
type: "string",
736+
nullable: true,
737+
example: "your_tmdb_api_key",
738+
},
739+
requireAuth: {
740+
type: "boolean",
741+
example: true,
742+
},
743+
allowRegistration: {
744+
type: "boolean",
745+
example: false,
746+
},
747+
sessionDuration: {
748+
type: "integer",
749+
example: 604800,
750+
description: "Session duration in seconds",
751+
},
752+
createdAt: {
753+
type: "string",
754+
format: "date-time",
755+
},
756+
updatedAt: {
757+
type: "string",
758+
format: "date-time",
759+
},
760+
libraries: {
761+
type: "array",
762+
items: {
763+
$ref: "#/components/schemas/Collection",
764+
},
765+
},
766+
},
767+
},
706768
},
707769
},
708770
},

apps/api/src/lib/metadata/providers/tmdb.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
} from "../../../generated/prisma/index.js";
55
import { BaseMetadataProvider } from "../provider.js";
66
import logger from "../../../config/logger.js";
7+
import { prisma } from "../../../lib/prisma.js";
78
import type {
89
MediaMetadata,
910
MediaSearchResult,
@@ -14,9 +15,8 @@ import type {
1415

1516
/**
1617
* TMDB API configuration
17-
* Set TMDB_API_KEY environment variable to enable
18+
* Configure via application settings (Settings > Libraries > TMDB API Key)
1819
*/
19-
const TMDB_API_KEY = process.env.TMDB_API_KEY;
2020
const TMDB_BASE_URL = "https://api.themoviedb.org/3";
2121
const TMDB_IMAGE_BASE_URL = "https://image.tmdb.org/t/p";
2222

@@ -133,11 +133,28 @@ export class TMDBProvider extends BaseMetadataProvider {
133133
readonly source = ExternalIdSource.TMDB;
134134
readonly supportedMediaTypes = [MediaType.MOVIE, MediaType.TV_SHOW];
135135

136+
/**
137+
* Get TMDB API key from database settings
138+
*/
139+
private async getApiKey(): Promise<string | null> {
140+
try {
141+
const settings = await prisma.settings.findUnique({
142+
where: { id: "default" },
143+
select: { tmdbApiKey: true },
144+
});
145+
return settings?.tmdbApiKey || null;
146+
} catch (error) {
147+
logger.error("Failed to fetch TMDB API key from database:", { error });
148+
return null;
149+
}
150+
}
151+
136152
/**
137153
* Check if TMDB API key is configured
138154
*/
139155
override async isAvailable(): Promise<boolean> {
140-
return !!TMDB_API_KEY;
156+
const apiKey = await this.getApiKey();
157+
return !!apiKey;
141158
}
142159

143160
/**
@@ -147,14 +164,16 @@ export class TMDBProvider extends BaseMetadataProvider {
147164
endpoint: string,
148165
params: Record<string, string | number | boolean> = {}
149166
): Promise<T | null> {
150-
if (!TMDB_API_KEY) {
167+
const apiKey = await this.getApiKey();
168+
169+
if (!apiKey) {
151170
logger.warn("TMDB API key not configured");
152171
return null;
153172
}
154173

155174
try {
156175
const url = new URL(`${TMDB_BASE_URL}${endpoint}`);
157-
url.searchParams.append("api_key", TMDB_API_KEY);
176+
url.searchParams.append("api_key", apiKey);
158177

159178
for (const [key, value] of Object.entries(params)) {
160179
url.searchParams.append(key, String(value));

apps/api/src/lib/rateLimiter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,12 @@ function createRateLimiter(
107107
userId: req.user?.id,
108108
});
109109

110+
// CORS headers are already set by cors middleware, just send response
110111
res.status(429).json(rateLimitResponse);
111112
},
113+
114+
// Skip rate limiting in development
115+
skip: () => env.NODE_ENV === "development",
112116
};
113117

114118
// Use Redis store if cache is enabled (distributed rate limiting)

apps/api/src/routes/collections/collections.module.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,22 @@ router.get(
5151
* responses:
5252
* 200:
5353
* description: Successfully retrieved libraries
54+
* content:
55+
* application/json:
56+
* schema:
57+
* allOf:
58+
* - $ref: '#/components/schemas/SuccessResponse'
59+
* - type: object
60+
* properties:
61+
* data:
62+
* type: object
63+
* properties:
64+
* message:
65+
* type: string
66+
* collections:
67+
* type: array
68+
* items:
69+
* $ref: '#/components/schemas/Collection'
5470
*/
5571
router.get(
5672
"/libraries",

apps/api/src/routes/comics/comics.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const router: RouterType = Router();
55

66
/**
77
* @openapi
8-
* /api/comics:
8+
* /api/v1/comics:
99
* get:
1010
* summary: Get all comics
1111
* description: Retrieve all comics with optional filtering, sorting, and pagination

apps/api/src/routes/media/media.module.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const router: RouterType = Router();
55

66
/**
77
* @openapi
8-
* /api/media:
8+
* /api/v1/media:
99
* get:
1010
* summary: Get all media
1111
* description: Retrieve all media items with optional filtering, sorting, and pagination
@@ -126,7 +126,7 @@ router.get("/", (req, res, next) => {
126126

127127
/**
128128
* @openapi
129-
* /api/media/statistics:
129+
* /api/v1/media/statistics:
130130
* get:
131131
* summary: Get media statistics
132132
* description: Retrieve statistics about media in the library
@@ -170,7 +170,7 @@ router.get("/statistics", (req, res, next) => {
170170

171171
/**
172172
* @openapi
173-
* /api/media/{id}:
173+
* /api/v1/media/{id}:
174174
* get:
175175
* summary: Get media by ID
176176
* description: Retrieve a specific media item by its ID
@@ -213,7 +213,7 @@ router.get("/:id", (req, res, next) => {
213213

214214
/**
215215
* @openapi
216-
* /api/media/stream/movie/{id}:
216+
* /api/v1/media/stream/movie/{id}:
217217
* get:
218218
* summary: Stream a movie
219219
* description: Stream a movie file with byte-range support for seeking
@@ -255,7 +255,7 @@ router.get("/stream/movie/:id", (req, res, next) => {
255255

256256
/**
257257
* @openapi
258-
* /api/media/stream/episode/{id}/{seasonNumber}/{episodeNumber}:
258+
* /api/v1/media/stream/episode/{id}/{seasonNumber}/{episodeNumber}:
259259
* get:
260260
* summary: Stream a TV episode
261261
* description: Stream a TV episode file with byte-range support for seeking

apps/api/src/routes/movies/movies.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const router: RouterType = Router();
55

66
/**
77
* @openapi
8-
* /api/movies:
8+
* /api/v1/movies:
99
* get:
1010
* summary: Get all movies
1111
* description: Retrieve all movies with optional filtering, sorting, and pagination
@@ -121,7 +121,7 @@ router.get("/", (req, res, next) => {
121121

122122
/**
123123
* @openapi
124-
* /api/movies/{id}:
124+
* /api/v1/movies/{id}:
125125
* get:
126126
* summary: Get movie by ID
127127
* description: Retrieve a specific movie by its ID

apps/api/src/routes/music/music.module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const router: RouterType = Router();
55

66
/**
77
* @openapi
8-
* /api/music:
8+
* /api/v1/music:
99
* get:
1010
* summary: Get all music
1111
* description: Retrieve all music items with optional filtering, sorting, and pagination

apps/api/src/routes/scan/scan.module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const router: RouterType = Router();
55

66
/**
77
* @openapi
8-
* /api/scan:
8+
* /api/v1/scan:
99
* post:
1010
* summary: Scan a directory for media files
1111
* description: Recursively scans a directory and returns all media files matching the specified media type
@@ -73,7 +73,7 @@ router.post("/", (req, res, next) => {
7373

7474
/**
7575
* @openapi
76-
* /api/scan/sync:
76+
* /api/v1/scan/sync:
7777
* post:
7878
* summary: Sync a collection
7979
* description: Checks for file modifications and removals in a collection
@@ -126,7 +126,7 @@ router.post("/sync", (req, res, next) => {
126126

127127
/**
128128
* @openapi
129-
* /api/scan/sync-all:
129+
* /api/v1/scan/sync-all:
130130
* post:
131131
* summary: Sync all collections
132132
* description: Checks for file modifications and removals across all collections (for cron jobs)

0 commit comments

Comments
 (0)