Skip to content

Commit d43c397

Browse files
authored
Merge pull request #234 from mchestr/feature/jellyfin-invite-system
feat: add Jellyfin server support with invite system
2 parents faea47f + 3645786 commit d43c397

File tree

23 files changed

+3214
-71
lines changed

23 files changed

+3214
-71
lines changed

__tests__/utils/test-builders.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,35 @@ export const makeOverseerrConfig = (overrides: any = {}) => ({
255255
...overrides,
256256
})
257257

258+
/**
259+
* Creates a mock Jellyfin server configuration
260+
* @param overrides - Partial properties to override defaults
261+
* @returns Jellyfin server config object
262+
*/
263+
export const makeJellyfinServerConfig = (overrides: any = {}) => ({
264+
url: 'https://jellyfin.example.com:8096',
265+
apiKey: 'jellyfin-api-key',
266+
...overrides,
267+
})
268+
269+
/**
270+
* Creates a mock Prisma Jellyfin server record
271+
* @param overrides - Partial properties to override defaults
272+
* @returns Prisma JellyfinServer object
273+
*/
274+
export const makePrismaJellyfinServer = (overrides: any = {}) => ({
275+
id: 'jellyfin-server-1',
276+
url: 'https://jellyfin.example.com:8096',
277+
apiKey: 'jellyfin-api-key',
278+
name: 'Test Jellyfin Server',
279+
isActive: true,
280+
publicUrl: null,
281+
adminUserId: null,
282+
createdAt: new Date('2024-01-01T00:00:00Z'),
283+
updatedAt: new Date('2024-01-01T00:00:00Z'),
284+
...overrides,
285+
})
286+
258287
/**
259288
* Creates a mock Prisma Overseerr server record
260289
* @param overrides - Partial properties to override defaults

actions/admin/admin-servers.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,48 @@
33
import { requireAdmin } from "@/lib/admin"
44
import { prisma } from "@/lib/prisma"
55

6+
/**
7+
* Get Jellyfin libraries for invite creation
8+
*/
9+
export async function getJellyfinLibraries() {
10+
await requireAdmin()
11+
12+
try {
13+
const jellyfinServer = await prisma.jellyfinServer.findFirst({
14+
where: { isActive: true },
15+
})
16+
17+
if (!jellyfinServer) {
18+
return { success: false, error: "No active Jellyfin server configured" }
19+
}
20+
21+
const { getJellyfinLibraries: fetchLibraries } = await import("@/lib/connections/jellyfin")
22+
23+
const result = await fetchLibraries({
24+
url: jellyfinServer.url,
25+
apiKey: jellyfinServer.apiKey,
26+
})
27+
28+
if (!result.success) {
29+
return { success: false, error: result.error }
30+
}
31+
32+
// Transform to simpler format for UI
33+
const libraries = (result.data || []).map((lib) => ({
34+
id: lib.ItemId,
35+
name: lib.Name,
36+
type: lib.CollectionType || "unknown",
37+
}))
38+
39+
return { success: true, data: libraries }
40+
} catch (error) {
41+
if (error instanceof Error) {
42+
return { success: false, error: error.message }
43+
}
44+
return { success: false, error: "Failed to fetch Jellyfin libraries" }
45+
}
46+
}
47+
648
/**
749
* Update Plex server configuration (admin only)
850
*/
@@ -425,6 +467,108 @@ export async function updatePrometheus(data: { name: string; url: string; query:
425467
}
426468
}
427469

470+
/**
471+
* Update Jellyfin server configuration (admin only)
472+
*/
473+
export async function updateJellyfinServer(data: { name: string; url: string; apiKey: string; publicUrl?: string }) {
474+
await requireAdmin()
475+
476+
try {
477+
const { jellyfinServerSchema } = await import("@/lib/validations/jellyfin")
478+
const { testJellyfinConnection, getJellyfinServerInfo } = await import("@/lib/connections/jellyfin")
479+
const { revalidatePath } = await import("next/cache")
480+
481+
const validated = jellyfinServerSchema.parse(data)
482+
483+
// Test connection before saving
484+
const connectionTest = await testJellyfinConnection(validated)
485+
if (!connectionTest.success) {
486+
return { success: false, error: connectionTest.error || "Failed to connect to Jellyfin server" }
487+
}
488+
489+
// Get server info to retrieve admin user ID (optional)
490+
let adminUserId: string | undefined
491+
const serverInfoResult = await getJellyfinServerInfo(validated)
492+
if (serverInfoResult.success && serverInfoResult.data) {
493+
// The API key is associated with an admin user, but we can't easily get their ID
494+
// from just the server info. This can be populated later if needed.
495+
adminUserId = undefined
496+
}
497+
498+
await prisma.$transaction(async (tx) => {
499+
// Deactivate existing active server
500+
await tx.jellyfinServer.updateMany({
501+
where: { isActive: true },
502+
data: { isActive: false },
503+
})
504+
505+
// Update or create server
506+
const existing = await tx.jellyfinServer.findFirst({
507+
where: {
508+
url: validated.url,
509+
},
510+
})
511+
512+
if (existing) {
513+
await tx.jellyfinServer.update({
514+
where: { id: existing.id },
515+
data: {
516+
name: validated.name,
517+
url: validated.url,
518+
apiKey: validated.apiKey,
519+
publicUrl: validated.publicUrl,
520+
adminUserId,
521+
isActive: true,
522+
},
523+
})
524+
} else {
525+
await tx.jellyfinServer.create({
526+
data: {
527+
name: validated.name,
528+
url: validated.url,
529+
apiKey: validated.apiKey,
530+
publicUrl: validated.publicUrl,
531+
adminUserId,
532+
isActive: true,
533+
},
534+
})
535+
}
536+
})
537+
538+
revalidatePath("/admin/settings")
539+
return { success: true }
540+
} catch (error) {
541+
if (error instanceof Error) {
542+
return { success: false, error: error.message }
543+
}
544+
return { success: false, error: "Failed to update Jellyfin server configuration" }
545+
}
546+
}
547+
548+
/**
549+
* Delete Jellyfin server configuration (admin only)
550+
*/
551+
export async function deleteJellyfinServer() {
552+
await requireAdmin()
553+
554+
try {
555+
const { revalidatePath } = await import("next/cache")
556+
557+
await prisma.jellyfinServer.updateMany({
558+
where: { isActive: true },
559+
data: { isActive: false },
560+
})
561+
562+
revalidatePath("/admin/settings")
563+
return { success: true }
564+
} catch (error) {
565+
if (error instanceof Error) {
566+
return { success: false, error: error.message }
567+
}
568+
return { success: false, error: "Failed to delete Jellyfin server configuration" }
569+
}
570+
}
571+
428572
/**
429573
* Delete Prometheus configuration (admin only)
430574
*/

actions/admin/admin-settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export async function getAdminSettings() {
1515
chatLLMProvider,
1616
wrappedLLMProvider,
1717
plexServer,
18+
jellyfinServer,
1819
tautulli,
1920
overseerr,
2021
sonarr,
@@ -27,6 +28,7 @@ export async function getAdminSettings() {
2728
prisma.lLMProvider.findFirst({ where: { isActive: true, purpose: "chat" } }),
2829
prisma.lLMProvider.findFirst({ where: { isActive: true, purpose: "wrapped" } }),
2930
prisma.plexServer.findFirst({ where: { isActive: true } }),
31+
prisma.jellyfinServer.findFirst({ where: { isActive: true } }),
3032
prisma.tautulli.findFirst({ where: { isActive: true } }),
3133
prisma.overseerr.findFirst({ where: { isActive: true } }),
3234
prisma.sonarr.findFirst({ where: { isActive: true } }),
@@ -43,6 +45,7 @@ export async function getAdminSettings() {
4345
// Keep llmProvider for backward compatibility (returns wrapped provider)
4446
llmProvider: wrappedLLMProvider,
4547
plexServer,
48+
jellyfinServer,
4649
tautulli,
4750
overseerr,
4851
sonarr,

actions/admin/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export {
2828
// Server configurations
2929
export {
3030
updatePlexServer,
31+
updateJellyfinServer,
32+
deleteJellyfinServer,
3133
updateTautulli,
3234
updateOverseerr,
3335
updateSonarr,

0 commit comments

Comments
 (0)