feat: Added media_servers table, MediaServer type, and CRUD repository…

- "src/db/schema/media-servers.ts"
- "src/db/repositories/media-server-repository.ts"
- "src/types/index.ts"
- "drizzle/0016_right_galactus.sql"

GSD-Task: S04/T01
This commit is contained in:
jlightner 2026-04-04 05:50:33 +00:00
parent 9e7d98c7c7
commit 6aa7e21b90
7 changed files with 1310 additions and 0 deletions

View file

@ -0,0 +1,11 @@
CREATE TABLE `media_servers` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`type` text NOT NULL,
`url` text NOT NULL,
`token` text NOT NULL,
`library_section` text,
`enabled` integer DEFAULT true NOT NULL,
`created_at` text DEFAULT (datetime('now')) NOT NULL,
`updated_at` text DEFAULT (datetime('now')) NOT NULL
);

File diff suppressed because it is too large Load diff

View file

@ -113,6 +113,13 @@
"when": 1775280800944,
"tag": "0015_perfect_lethal_legion",
"breakpoints": true
},
{
"idx": 16,
"version": "6",
"when": 1775281783887,
"tag": "0016_right_galactus",
"breakpoints": true
}
]
}

View file

@ -0,0 +1,137 @@
import { eq } from 'drizzle-orm';
import { type LibSQLDatabase } from 'drizzle-orm/libsql';
import type * as schema from '../schema/index';
import { mediaServers } from '../schema/index';
import type { MediaServer, MediaServerType } from '../../types/index';
// ── Types ──
/** Fields needed to create a new media server. */
export interface CreateMediaServerData {
name: string;
type: MediaServerType;
url: string;
token: string;
librarySection?: string | null;
enabled?: boolean;
}
/** Fields that can be updated on an existing media server. */
export interface UpdateMediaServerData {
name?: string;
type?: MediaServerType;
url?: string;
token?: string;
librarySection?: string | null;
enabled?: boolean;
}
type Db = LibSQLDatabase<typeof schema>;
// ── Repository Functions ──
/** Insert a new media server. Returns the created row. */
export async function createMediaServer(
db: Db,
data: CreateMediaServerData
): Promise<MediaServer> {
const result = await db
.insert(mediaServers)
.values({
name: data.name,
type: data.type,
url: data.url,
token: data.token,
librarySection: data.librarySection ?? null,
enabled: data.enabled ?? true,
})
.returning();
return mapRow(result[0]);
}
/** Get all media servers, ordered by name. */
export async function getAllMediaServers(db: Db): Promise<MediaServer[]> {
const rows = await db
.select()
.from(mediaServers)
.orderBy(mediaServers.name);
return rows.map(mapRow);
}
/** Get a media server by ID. Returns null if not found. */
export async function getMediaServerById(
db: Db,
id: number
): Promise<MediaServer | null> {
const rows = await db
.select()
.from(mediaServers)
.where(eq(mediaServers.id, id))
.limit(1);
return rows.length > 0 ? mapRow(rows[0]) : null;
}
/** Get all enabled media servers. */
export async function getEnabledMediaServers(
db: Db
): Promise<MediaServer[]> {
const rows = await db
.select()
.from(mediaServers)
.where(eq(mediaServers.enabled, true));
return rows.map(mapRow);
}
/**
* Update a media server. Sets updatedAt to current time.
* Returns updated server or null if not found.
*/
export async function updateMediaServer(
db: Db,
id: number,
data: UpdateMediaServerData
): Promise<MediaServer | null> {
const result = await db
.update(mediaServers)
.set({
...data,
updatedAt: new Date().toISOString(),
})
.where(eq(mediaServers.id, id))
.returning();
return result.length > 0 ? mapRow(result[0]) : null;
}
/** Delete a media server by ID. Returns true if a row was deleted. */
export async function deleteMediaServer(
db: Db,
id: number
): Promise<boolean> {
const result = await db
.delete(mediaServers)
.where(eq(mediaServers.id, id))
.returning({ id: mediaServers.id });
return result.length > 0;
}
// ── Row Mapping ──
function mapRow(row: typeof mediaServers.$inferSelect): MediaServer {
return {
id: row.id,
name: row.name,
type: row.type as MediaServerType,
url: row.url,
token: row.token,
librarySection: row.librarySection,
enabled: row.enabled,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
};
}

View file

@ -6,3 +6,4 @@ export { downloadHistory } from './history';
export { notificationSettings } from './notifications';
export { platformSettings } from './platform-settings';
export { playlists, contentPlaylist } from './playlists';
export { mediaServers } from './media-servers';

View file

@ -0,0 +1,19 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
/** Media server connections for triggering library scans (Plex, Jellyfin). */
export const mediaServers = sqliteTable('media_servers', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
type: text('type').notNull(), // 'plex' | 'jellyfin'
url: text('url').notNull(),
token: text('token').notNull(),
librarySection: text('library_section'), // nullable — Plex section ID or Jellyfin library ID
enabled: integer('enabled', { mode: 'boolean' }).notNull().default(true),
createdAt: text('created_at')
.notNull()
.default(sql`(datetime('now'))`),
updatedAt: text('updated_at')
.notNull()
.default(sql`(datetime('now'))`),
});

View file

@ -196,6 +196,24 @@ export interface SystemConfig {
updatedAt: string;
}
export const MediaServerType = {
Plex: 'plex',
Jellyfin: 'jellyfin',
} as const;
export type MediaServerType = (typeof MediaServerType)[keyof typeof MediaServerType];
export interface MediaServer {
id: number;
name: string;
type: MediaServerType;
url: string;
token: string;
librarySection: string | null;
enabled: boolean;
createdAt: string;
updatedAt: string;
}
export interface Playlist {
id: number;
channelId: number;