Files
iptvnator/libs/shared/database/src/lib/schema.ts
T
4gray 2043b7d8a1 feat(xtream): add category management to hide/show categories
Add the ability to hide irrelevant or empty categories in Xtream playlists.
Users can now manage category visibility through a dialog accessible from
the sidebar header in both VOD/Series and Live TV views.

Features:
- New category management dialog with search filter and select/deselect all
- Visibility settings persisted to database per playlist
- Hidden categories are filtered from sidebar navigation
- New categories added during playlist refresh default to visible
- Database migration for existing installations

Files changed:
- Database: schema.ts, connection.ts (hidden column + migration)
- Backend: category.events.ts, main.preload.ts (IPC handlers)
- Frontend: new dialog component, store method, UI integration
- Translations: en.json
- Documentation: docs/architecture/category-management.md
2026-01-06 21:34:44 +01:00

193 lines
6.6 KiB
TypeScript

/**
* Drizzle ORM schema for IPTVnator database
* This schema defines the structure for Xtream Codes API data storage
*
* Single source of truth for database schema - used by:
* - electron-backend (full read-write access)
* - agent-backend (read-only access for AI queries)
*/
import { sql } from 'drizzle-orm';
import {
index,
integer,
sqliteTable,
text,
uniqueIndex,
} from 'drizzle-orm/sqlite-core';
// Playlists table
export const playlists = sqliteTable('playlists', {
id: text('id').primaryKey(),
name: text('name').notNull(),
serverUrl: text('serverUrl'),
username: text('username'),
password: text('password'),
dateCreated: text('date_created').default(sql`CURRENT_TIMESTAMP`),
lastUpdated: text('last_updated'),
type: text('type', {
enum: ['xtream', 'stalker', 'm3u-file', 'm3u-text', 'm3u-url'],
}).notNull(),
userAgent: text('userAgent'),
origin: text('origin'),
referrer: text('referrer'),
filePath: text('filePath'),
autoRefresh: integer('autoRefresh', { mode: 'boolean' }).default(false),
macAddress: text('macAddress'),
url: text('url'),
lastUsage: text('last_usage'),
});
// Categories table
export const categories = sqliteTable(
'categories',
{
id: integer('id').primaryKey({ autoIncrement: true }),
playlistId: text('playlist_id')
.notNull()
.references(() => playlists.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
type: text('type', { enum: ['live', 'movies', 'series'] }).notNull(),
xtreamId: integer('xtream_id').notNull(),
hidden: integer('hidden', { mode: 'boolean' }).default(false),
},
(table) => ({
playlistIdx: index('idx_categories_playlist').on(table.playlistId),
typeIdx: index('idx_categories_type').on(table.type),
})
);
// Content table (streams/VODs/series)
export const content = sqliteTable(
'content',
{
id: integer('id').primaryKey({ autoIncrement: true }),
categoryId: integer('category_id')
.notNull()
.references(() => categories.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
rating: text('rating'),
added: text('added'),
posterUrl: text('poster_url'),
xtreamId: integer('xtream_id').notNull(),
type: text('type', { enum: ['live', 'movie', 'series'] }).notNull(),
},
(table) => ({
typeIdx: index('idx_content_type').on(table.type),
categoryIdx: index('idx_content_category').on(table.categoryId),
titleIdx: index('idx_content_title').on(table.title),
xtreamIdx: index('idx_content_xtream').on(table.xtreamId),
})
);
// Recently viewed table
export const recentlyViewed = sqliteTable(
'recently_viewed',
{
id: integer('id').primaryKey({ autoIncrement: true }),
contentId: integer('content_id')
.notNull()
.references(() => content.id, { onDelete: 'cascade' }),
playlistId: text('playlist_id')
.notNull()
.references(() => playlists.id, { onDelete: 'cascade' }),
viewedAt: text('viewed_at').default(sql`CURRENT_TIMESTAMP`),
},
(table) => ({
contentPlaylistUnique: uniqueIndex(
'recently_viewed_content_playlist_unique'
).on(table.contentId, table.playlistId),
playlistIdx: index('recently_viewed_playlist_idx').on(table.playlistId),
viewedAtIdx: index('recently_viewed_viewed_at_idx').on(table.viewedAt),
})
);
// Favorites table
export const favorites = sqliteTable(
'favorites',
{
id: integer('id').primaryKey({ autoIncrement: true }),
contentId: integer('content_id')
.notNull()
.references(() => content.id, { onDelete: 'cascade' }),
playlistId: text('playlist_id')
.notNull()
.references(() => playlists.id, { onDelete: 'cascade' }),
addedAt: text('added_at').default(sql`CURRENT_TIMESTAMP`),
},
(table) => ({
contentPlaylistUnique: uniqueIndex(
'favorites_content_playlist_unique'
).on(table.contentId, table.playlistId),
playlistIdx: index('favorites_playlist_idx').on(table.playlistId),
contentIdx: index('favorites_content_idx').on(table.contentId),
})
);
// EPG Channels table
export const epgChannels = sqliteTable(
'epg_channels',
{
id: text('id').primaryKey(), // Channel ID from EPG source
displayName: text('display_name').notNull(),
iconUrl: text('icon_url'),
url: text('url'),
sourceUrl: text('source_url').notNull(), // Which EPG URL this came from
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`),
},
(table) => ({
sourceIdx: index('idx_epg_channels_source').on(table.sourceUrl),
nameIdx: index('idx_epg_channels_name').on(table.displayName),
})
);
// EPG Programs table
export const epgPrograms = sqliteTable(
'epg_programs',
{
id: integer('id').primaryKey({ autoIncrement: true }),
channelId: text('channel_id')
.notNull()
.references(() => epgChannels.id, { onDelete: 'cascade' }),
start: text('start').notNull(), // ISO datetime
stop: text('stop').notNull(), // ISO datetime
title: text('title').notNull(),
description: text('description'),
category: text('category'),
iconUrl: text('icon_url'),
rating: text('rating'),
episodeNum: text('episode_num'),
},
(table) => ({
channelIdx: index('idx_epg_programs_channel').on(table.channelId),
startIdx: index('idx_epg_programs_start').on(table.start),
timeRangeIdx: index('idx_epg_programs_time_range').on(
table.channelId,
table.start,
table.stop
),
})
);
// Type exports for TypeScript
export type Playlist = typeof playlists.$inferSelect;
export type NewPlaylist = typeof playlists.$inferInsert;
export type Category = typeof categories.$inferSelect;
export type NewCategory = typeof categories.$inferInsert;
export type Content = typeof content.$inferSelect;
export type NewContent = typeof content.$inferInsert;
export type RecentlyViewed = typeof recentlyViewed.$inferSelect;
export type NewRecentlyViewed = typeof recentlyViewed.$inferInsert;
export type Favorite = typeof favorites.$inferSelect;
export type NewFavorite = typeof favorites.$inferInsert;
export type EpgChannel = typeof epgChannels.$inferSelect;
export type NewEpgChannel = typeof epgChannels.$inferInsert;
export type EpgProgramDb = typeof epgPrograms.$inferSelect;
export type NewEpgProgramDb = typeof epgPrograms.$inferInsert;