Merge pull request #23 from FifthWit/master

updates
This commit is contained in:
FifthWit 2025-10-25 14:37:51 -05:00 committed by GitHub
commit 441895b26f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 171 additions and 1068 deletions

882
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,14 +1,6 @@
import { useAuth } from '~/utils/auth'; import { useAuth } from '~/utils/auth';
import { z } from 'zod'; import { z } from 'zod';
import { bookmarks } from '@prisma/client';
interface BookmarkWithFavorites {
tmdb_id: string;
user_id: string;
meta: any;
group: string[];
favorite_episodes: string[];
updated_at: Date;
}
const bookmarkMetaSchema = z.object({ const bookmarkMetaSchema = z.object({
title: z.string(), title: z.string(),
@ -42,7 +34,7 @@ export default defineEventHandler(async event => {
where: { user_id: userId }, where: { user_id: userId },
}); });
return bookmarks.map((bookmark: BookmarkWithFavorites) => ({ return bookmarks.map((bookmark: bookmarks) => ({
tmdbId: bookmark.tmdb_id, tmdbId: bookmark.tmdb_id,
meta: bookmark.meta, meta: bookmark.meta,
group: bookmark.group, group: bookmark.group,
@ -88,7 +80,7 @@ export default defineEventHandler(async event => {
favorite_episodes: normalizedFavoriteEpisodes, favorite_episodes: normalizedFavoriteEpisodes,
updated_at: now, updated_at: now,
} as any, } as any,
}) as BookmarkWithFavorites; }) as bookmarks;
results.push({ results.push({
tmdbId: bookmark.tmdb_id, tmdbId: bookmark.tmdb_id,

View file

@ -2,15 +2,6 @@ import { useAuth } from '~/utils/auth';
import { z } from 'zod'; import { z } from 'zod';
import { scopedLogger } from '~/utils/logger'; import { scopedLogger } from '~/utils/logger';
interface BookmarkWithFavorites {
tmdb_id: string;
user_id: string;
meta: any;
group: string[];
favorite_episodes: string[];
updated_at: Date;
}
const log = scopedLogger('user-bookmarks'); const log = scopedLogger('user-bookmarks');
const bookmarkMetaSchema = z.object({ const bookmarkMetaSchema = z.object({
@ -20,7 +11,6 @@ const bookmarkMetaSchema = z.object({
type: z.enum(['movie', 'show']), type: z.enum(['movie', 'show']),
}); });
// Support both formats: direct fields or nested under meta
const bookmarkRequestSchema = z.object({ const bookmarkRequestSchema = z.object({
meta: bookmarkMetaSchema.optional(), meta: bookmarkMetaSchema.optional(),
tmdbId: z.string().optional(), tmdbId: z.string().optional(),
@ -31,14 +21,10 @@ const bookmarkRequestSchema = z.object({
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const userId = getRouterParam(event, 'id'); const userId = getRouterParam(event, 'id');
const tmdbId = getRouterParam(event, 'tmdbid'); const tmdbId = getRouterParam(event, 'tmdbid');
const session = await useAuth().getCurrentSession(); const session = await useAuth().getCurrentSession();
if (session.user !== userId) { if (session.user !== userId) {
throw createError({ throw createError({ statusCode: 403, message: 'Cannot access bookmarks for other users' });
statusCode: 403,
message: 'Cannot access bookmarks for other users',
});
} }
if (event.method === 'POST') { if (event.method === 'POST') {
@ -46,51 +32,18 @@ export default defineEventHandler(async event => {
const body = await readBody(event); const body = await readBody(event);
log.info('Creating bookmark', { userId, tmdbId, body }); log.info('Creating bookmark', { userId, tmdbId, body });
// Parse and validate the request body const validated = bookmarkRequestSchema.parse(body);
const validatedRequest = bookmarkRequestSchema.parse(body); const meta = bookmarkMetaSchema.parse(validated.meta || body);
const group = validated.group ? (Array.isArray(validated.group) ? validated.group : [validated.group]) : [];
// Extract the meta data - either directly from meta field or from the root const favoriteEpisodes = validated.favoriteEpisodes || [];
const metaData = validatedRequest.meta || body;
// Validate the meta data separately
const validatedMeta = bookmarkMetaSchema.parse(metaData);
// Extract group from the validated request
const groupFromBody = validatedRequest.group;
// Normalize group to always be an array if present
const normalizedGroup = groupFromBody
? (Array.isArray(groupFromBody) ? groupFromBody : [groupFromBody])
: [];
// Normalize favoriteEpisodes to always be an array
const normalizedFavoriteEpisodes = validatedRequest.favoriteEpisodes || [];
const bookmark = await prisma.bookmarks.upsert({ const bookmark = await prisma.bookmarks.upsert({
where: { where: { tmdb_id_user_id: { tmdb_id: tmdbId, user_id: session.user } },
tmdb_id_user_id: { update: { meta, group, favorite_episodes: favoriteEpisodes, updated_at: new Date() },
tmdb_id: tmdbId, create: { user_id: session.user, tmdb_id: tmdbId, meta, group, favorite_episodes: favoriteEpisodes, updated_at: new Date() },
user_id: session.user, });
},
},
update: {
meta: validatedMeta,
group: normalizedGroup,
favorite_episodes: normalizedFavoriteEpisodes,
updated_at: new Date(),
} as any,
create: {
user_id: session.user,
tmdb_id: tmdbId,
meta: validatedMeta,
group: normalizedGroup,
favorite_episodes: normalizedFavoriteEpisodes,
updated_at: new Date(),
} as any,
}) as BookmarkWithFavorites;
log.info('Bookmark created successfully', { userId, tmdbId }); log.info('Bookmark created successfully', { userId, tmdbId });
return { return {
tmdbId: bookmark.tmdb_id, tmdbId: bookmark.tmdb_id,
meta: bookmark.meta, meta: bookmark.meta,
@ -99,51 +52,20 @@ export default defineEventHandler(async event => {
updatedAt: bookmark.updated_at, updatedAt: bookmark.updated_at,
}; };
} catch (error) { } catch (error) {
log.error('Failed to create bookmark', { log.error('Failed to create bookmark', { userId, tmdbId, error: error instanceof Error ? error.message : String(error) });
userId, if (error instanceof z.ZodError) throw createError({ statusCode: 400, message: JSON.stringify(error.errors, null, 2) });
tmdbId,
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof z.ZodError) {
throw createError({
statusCode: 400,
message: JSON.stringify(error.errors, null, 2),
});
}
throw error; throw error;
} }
} else if (event.method === 'DELETE') { } else if (event.method === 'DELETE') {
log.info('Deleting bookmark', { userId, tmdbId }); log.info('Deleting bookmark', { userId, tmdbId });
try { try {
await prisma.bookmarks.delete({ await prisma.bookmarks.delete({ where: { tmdb_id_user_id: { tmdb_id: tmdbId, user_id: session.user } } });
where: {
tmdb_id_user_id: {
tmdb_id: tmdbId,
user_id: session.user,
},
},
});
log.info('Bookmark deleted successfully', { userId, tmdbId }); log.info('Bookmark deleted successfully', { userId, tmdbId });
return { success: true, tmdbId };
} catch (error) { } catch (error) {
log.error('Failed to delete bookmark', { log.error('Failed to delete bookmark', { userId, tmdbId, error: error instanceof Error ? error.message : String(error) });
userId,
tmdbId,
error: error instanceof Error ? error.message : String(error),
});
// If bookmark doesn't exist, still return success
return { success: true, tmdbId };
} }
return { success: true, tmdbId };
} }
throw createError({ throw createError({ statusCode: 405, message: 'Method not allowed' });
statusCode: 405,
message: 'Method not allowed',
});
}); });

View file

@ -12,8 +12,8 @@ const progressMetaSchema = z.object({
const progressItemSchema = z.object({ const progressItemSchema = z.object({
meta: progressMetaSchema, meta: progressMetaSchema,
tmdbId: z.string(), tmdbId: z.string(),
duration: z.number().transform(n => Math.round(n)), duration: z.number().transform(Math.round),
watched: z.number().transform(n => Math.round(n)), watched: z.number().transform(Math.round),
seasonId: z.string().optional(), seasonId: z.string().optional(),
episodeId: z.string().optional(), episodeId: z.string().optional(),
seasonNumber: z.number().optional(), seasonNumber: z.number().optional(),
@ -24,139 +24,97 @@ const progressItemSchema = z.object({
// 13th July 2021 - movie-web epoch // 13th July 2021 - movie-web epoch
const minEpoch = 1626134400000; const minEpoch = 1626134400000;
function defaultAndCoerceDateTime(dateTime: string | undefined) { const coerceDateTime = (dateTime?: string) => {
const epoch = dateTime ? new Date(dateTime).getTime() : Date.now(); const epoch = dateTime ? new Date(dateTime).getTime() : Date.now();
const clampedEpoch = Math.max(minEpoch, Math.min(epoch, Date.now())); return new Date(Math.max(minEpoch, Math.min(epoch, Date.now())));
return new Date(clampedEpoch); };
}
export default defineEventHandler(async event => { const normalizeIds = (metaType: string, seasonId?: string, episodeId?: string) => ({
const userId = event.context.params?.id; seasonId: metaType === 'movie' ? '\n' : seasonId || null,
const tmdbId = event.context.params?.tmdb_id; episodeId: metaType === 'movie' ? '\n' : episodeId || null,
});
const formatProgressItem = (item: any) => ({
id: item.id,
tmdbId: item.tmdb_id,
userId: item.user_id,
seasonId: item.season_id === '\n' ? null : item.season_id,
episodeId: item.episode_id === '\n' ? null : item.episode_id,
seasonNumber: item.season_number,
episodeNumber: item.episode_number,
meta: item.meta,
duration: Number(item.duration),
watched: Number(item.watched),
updatedAt: item.updated_at,
});
export default defineEventHandler(async (event) => {
const { id: userId, tmdb_id: tmdbId } = event.context.params!;
const method = event.method; const method = event.method;
const session = await useAuth().getCurrentSession(); const session = await useAuth().getCurrentSession();
if (session.user !== userId) { if (session.user !== userId) {
throw createError({ throw createError({ statusCode: 403, message: 'Unauthorized' });
statusCode: 403,
message: 'Different userId than authenticated session',
});
} }
if (method === 'PUT') { if (method === 'PUT') {
const body = await readBody(event); const body = await readBody(event);
const validatedBody = progressItemSchema.parse(body); let parsedBody;
try {
parsedBody = progressItemSchema.parse(body);
} catch (error) {
throw createError({ statusCode: 400, message: error.message });
}
const { meta, tmdbId: duration, watched, seasonId, episodeId, seasonNumber, episodeNumber, updatedAt } = parsedBody;
const now = defaultAndCoerceDateTime(validatedBody.updatedAt); const now = coerceDateTime(updatedAt);
const { seasonId: normSeasonId, episodeId: normEpisodeId } = normalizeIds(meta.type, seasonId, episodeId);
const isMovie = validatedBody.meta.type === 'movie'; const existing = await prisma.progress_items.findUnique({
const seasonId = isMovie ? '\n' : validatedBody.seasonId || null; where: { tmdb_id_user_id_season_id_episode_id: { tmdb_id: tmdbId, user_id: userId, season_id: normSeasonId, episode_id: normEpisodeId } },
const episodeId = isMovie ? '\n' : validatedBody.episodeId || null;
const existingItem = await prisma.progress_items.findUnique({
where: {
tmdb_id_user_id_season_id_episode_id: {
tmdb_id: tmdbId,
user_id: userId,
season_id: seasonId,
episode_id: episodeId,
},
},
}); });
let progressItem; const data = {
duration: BigInt(duration),
if (existingItem) { watched: BigInt(watched),
progressItem = await prisma.progress_items.update({ meta,
where: { updated_at: now,
id: existingItem.id,
},
data: {
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now,
},
});
} else {
progressItem = await prisma.progress_items.create({
data: {
id: randomUUID(),
tmdb_id: tmdbId,
user_id: userId,
season_id: seasonId,
episode_id: episodeId,
season_number: validatedBody.seasonNumber || null,
episode_number: validatedBody.episodeNumber || null,
duration: BigInt(validatedBody.duration),
watched: BigInt(validatedBody.watched),
meta: validatedBody.meta,
updated_at: now,
},
});
}
return {
id: progressItem.id,
tmdbId: progressItem.tmdb_id,
userId: progressItem.user_id,
seasonId: progressItem.season_id === '\n' ? null : progressItem.season_id,
episodeId: progressItem.episode_id === '\n' ? null : progressItem.episode_id,
seasonNumber: progressItem.season_number,
episodeNumber: progressItem.episode_number,
meta: progressItem.meta,
duration: Number(progressItem.duration),
watched: Number(progressItem.watched),
updatedAt: progressItem.updated_at,
};
} else if (method === 'DELETE') {
const body = await readBody(event).catch(() => ({}));
const whereClause: any = {
user_id: userId,
tmdb_id: tmdbId,
}; };
if (body.seasonId) { const progressItem = existing
whereClause.season_id = body.seasonId; ? await prisma.progress_items.update({ where: { id: existing.id }, data })
} else if (body.meta?.type === 'movie') { : await prisma.progress_items.create({
whereClause.season_id = '\n'; data: {
} id: randomUUID(),
tmdb_id: tmdbId,
user_id: userId,
season_id: normSeasonId,
episode_id: normEpisodeId,
season_number: seasonNumber || null,
episode_number: episodeNumber || null,
...data,
},
});
if (body.episodeId) { return formatProgressItem(progressItem);
whereClause.episode_id = body.episodeId;
} else if (body.meta?.type === 'movie') {
whereClause.episode_id = '\n';
}
const itemsToDelete = await prisma.progress_items.findMany({
where: whereClause,
});
if (itemsToDelete.length === 0) {
return {
count: 0,
tmdbId,
episodeId: body.episodeId,
seasonId: body.seasonId,
};
}
await prisma.progress_items.deleteMany({
where: whereClause,
});
return {
count: itemsToDelete.length,
tmdbId,
episodeId: body.episodeId,
seasonId: body.seasonId,
};
} }
throw createError({ if (method === 'DELETE') {
statusCode: 405, const body = await readBody(event).catch(() => ({}));
message: 'Method not allowed', const where: any = { user_id: userId, tmdb_id: tmdbId };
});
if (body.seasonId) where.season_id = body.seasonId;
else if (body.meta?.type === 'movie') where.season_id = '\n';
if (body.episodeId) where.episode_id = body.episodeId;
else if (body.meta?.type === 'movie') where.episode_id = '\n';
const items = await prisma.progress_items.findMany({ where });
if (items.length === 0) return { count: 0, tmdbId, episodeId: body.episodeId, seasonId: body.seasonId };
await prisma.progress_items.deleteMany({ where });
return { count: items.length, tmdbId, episodeId: body.episodeId, seasonId: body.seasonId };
}
throw createError({ statusCode: 405, message: 'Method not allowed' });
}); });

View file

@ -1,3 +0,0 @@
export default defineEventHandler(() => {
return;
});

View file

@ -1,42 +1,10 @@
import { useAuth } from '~/utils/auth'; import { useAuth } from '~/utils/auth';
import { z } from 'zod'; import { z } from 'zod';
import { scopedLogger } from '~/utils/logger'; import { scopedLogger } from '~/utils/logger';
import { user_settings } from '@prisma/client';
const log = scopedLogger('user-settings'); const log = scopedLogger('user-settings');
interface UserSettings {
id: string;
application_theme: string | null;
application_language: string;
default_subtitle_language: string | null;
proxy_urls: string[];
trakt_key: string | null;
febbox_key: string | null;
real_debrid_key: string | null;
enable_thumbnails: boolean;
enable_autoplay: boolean;
enable_skip_credits: boolean;
enable_discover: boolean;
enable_featured: boolean;
enable_details_modal: boolean;
enable_image_logos: boolean;
enable_carousel_view: boolean;
force_compact_episode_view: boolean;
source_order: string[];
enable_source_order: boolean;
disabled_sources: string[];
embed_order: string[];
enable_embed_order: boolean;
disabled_embeds: string[];
proxy_tmdb: boolean;
enable_low_performance_mode: boolean;
enable_native_subtitles: boolean;
enable_hold_to_boost: boolean;
home_section_order: string[];
manual_source_selection: boolean;
enable_double_click_to_seek: boolean;
}
const userSettingsSchema = z.object({ const userSettingsSchema = z.object({
applicationTheme: z.string().nullable().optional(), applicationTheme: z.string().nullable().optional(),
applicationLanguage: z.string().optional().default('en'), applicationLanguage: z.string().optional().default('en'),
@ -97,7 +65,7 @@ export default defineEventHandler(async event => {
try { try {
const settings = await prisma.user_settings.findUnique({ const settings = await prisma.user_settings.findUnique({
where: { id: userId }, where: { id: userId },
}) as unknown as UserSettings | null; }) as unknown as user_settings | null;
return { return {
id: userId, id: userId,
@ -222,7 +190,7 @@ export default defineEventHandler(async event => {
id: userId, id: userId,
...createData, ...createData,
}, },
}) as unknown as UserSettings; }) as unknown as user_settings;
log.info('Settings updated successfully', { userId }); log.info('Settings updated successfully', { userId });