mirror of
https://github.com/p-stream/backend.git
synced 2026-04-21 10:22:17 +00:00
Reapply with fixes
I think I got it this time
This commit is contained in:
parent
2e27016e81
commit
96e9d832e5
10 changed files with 421 additions and 214 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -7,3 +7,4 @@ dist
|
||||||
.env
|
.env
|
||||||
.vscode
|
.vscode
|
||||||
.metrics.json
|
.metrics.json
|
||||||
|
.metrics.json
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,18 @@ const metricsProviderSchema = z.object({
|
||||||
const metricsProviderInputSchema = z.object({
|
const metricsProviderInputSchema = z.object({
|
||||||
items: z.array(metricsProviderSchema).max(10).min(1),
|
items: z.array(metricsProviderSchema).max(10).min(1),
|
||||||
tool: z.string().optional(),
|
tool: z.string().optional(),
|
||||||
|
batchId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
// Handle both POST and PUT methods
|
||||||
|
if (event.method !== 'POST' && event.method !== 'PUT') {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ensureMetricsInitialized();
|
await ensureMetricsInitialized();
|
||||||
|
|
||||||
|
|
|
||||||
2
server/routes/metrics/providers.put.ts
Normal file
2
server/routes/metrics/providers.put.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Redirect to the POST handler which now supports both methods
|
||||||
|
export { default } from './providers.post';
|
||||||
|
|
@ -5,7 +5,7 @@ const bookmarkMetaSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
year: z.number().optional(),
|
year: z.number().optional(),
|
||||||
poster: z.string().optional(),
|
poster: z.string().optional(),
|
||||||
type: z.enum(['movie', 'tv']),
|
type: z.enum(['movie', 'show']),
|
||||||
});
|
});
|
||||||
|
|
||||||
const bookmarkDataSchema = z.object({
|
const bookmarkDataSchema = z.object({
|
||||||
|
|
@ -33,7 +33,6 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
return bookmarks.map(bookmark => ({
|
return bookmarks.map(bookmark => ({
|
||||||
tmdbId: bookmark.tmdb_id,
|
tmdbId: bookmark.tmdb_id,
|
||||||
userId: bookmark.user_id,
|
|
||||||
meta: bookmark.meta,
|
meta: bookmark.meta,
|
||||||
updatedAt: bookmark.updated_at
|
updatedAt: bookmark.updated_at
|
||||||
}));
|
}));
|
||||||
|
|
@ -68,7 +67,6 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
tmdbId: bookmark.tmdb_id,
|
tmdbId: bookmark.tmdb_id,
|
||||||
userId: bookmark.user_id,
|
|
||||||
meta: bookmark.meta,
|
meta: bookmark.meta,
|
||||||
updatedAt: bookmark.updated_at
|
updatedAt: bookmark.updated_at
|
||||||
});
|
});
|
||||||
|
|
@ -111,7 +109,6 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tmdbId: bookmark.tmdb_id,
|
tmdbId: bookmark.tmdb_id,
|
||||||
userId: bookmark.user_id,
|
|
||||||
meta: bookmark.meta,
|
meta: bookmark.meta,
|
||||||
updatedAt: bookmark.updated_at
|
updatedAt: bookmark.updated_at
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,25 @@
|
||||||
|
import { useAuth } from '~/utils/auth';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { scopedLogger } from '~/utils/logger';
|
||||||
|
|
||||||
|
const log = scopedLogger('user-bookmarks');
|
||||||
|
|
||||||
|
const bookmarkMetaSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
year: z.number(),
|
||||||
|
poster: z.string().optional(),
|
||||||
|
type: z.enum(['movie', 'show'])
|
||||||
|
});
|
||||||
|
|
||||||
|
// Support both formats: direct fields or nested under meta
|
||||||
|
const bookmarkRequestSchema = z.object({
|
||||||
|
meta: bookmarkMetaSchema.optional(),
|
||||||
|
tmdbId: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
|
|
@ -12,37 +31,81 @@ export default defineEventHandler(async (event) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.method === "POST") {
|
if (event.method === "POST") {
|
||||||
const body = await readBody(event);
|
try {
|
||||||
const bookmark = await prisma.bookmarks.create({
|
const body = await readBody(event);
|
||||||
data: {
|
log.info('Creating bookmark', { userId, tmdbId, body });
|
||||||
user_id: session.user,
|
|
||||||
tmdb_id: tmdbId,
|
// Parse and validate the request body
|
||||||
meta: body.meta,
|
const validatedRequest = bookmarkRequestSchema.parse(body);
|
||||||
updated_at: new Date()
|
|
||||||
}
|
// Extract the meta data - either directly from meta field or from the root
|
||||||
});
|
const metaData = validatedRequest.meta || body;
|
||||||
|
|
||||||
return {
|
// Validate the meta data separately
|
||||||
tmdbId: bookmark.tmdb_id,
|
const validatedMeta = bookmarkMetaSchema.parse(metaData);
|
||||||
userId: bookmark.user_id,
|
|
||||||
meta: bookmark.meta,
|
const bookmark = await prisma.bookmarks.create({
|
||||||
updatedAt: bookmark.updated_at
|
data: {
|
||||||
};
|
user_id: session.user,
|
||||||
} else if (event.method === "DELETE") {
|
tmdb_id: tmdbId,
|
||||||
await prisma.bookmarks.delete({
|
meta: validatedMeta,
|
||||||
where: {
|
updated_at: new Date()
|
||||||
tmdb_id_user_id: {
|
}
|
||||||
tmdb_id: tmdbId,
|
});
|
||||||
user_id: session.user
|
|
||||||
|
log.info('Bookmark created successfully', { userId, tmdbId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
tmdbId: bookmark.tmdb_id,
|
||||||
|
meta: bookmark.meta,
|
||||||
|
updatedAt: bookmark.updated_at
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to create bookmark', {
|
||||||
|
userId,
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
});
|
} else if (event.method === "DELETE") {
|
||||||
|
log.info('Deleting bookmark', { userId, tmdbId });
|
||||||
return { success: true, tmdbId };
|
|
||||||
}
|
try {
|
||||||
|
await prisma.bookmarks.delete({
|
||||||
|
where: {
|
||||||
|
tmdb_id_user_id: {
|
||||||
|
tmdb_id: tmdbId,
|
||||||
|
user_id: session.user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info('Bookmark deleted successfully', { userId, tmdbId });
|
||||||
|
|
||||||
|
return { success: true, tmdbId };
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to delete bookmark', {
|
||||||
|
userId,
|
||||||
|
tmdbId,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
|
||||||
|
// If bookmark doesn't exist, still return success
|
||||||
|
return { success: true, tmdbId };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 405,
|
statusCode: 405,
|
||||||
message: 'Method not allowed'
|
message: 'Method not allowed'
|
||||||
})
|
});
|
||||||
})
|
});
|
||||||
78
server/routes/users/[id]/index.ts
Normal file
78
server/routes/users/[id]/index.ts
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { useAuth } from '~/utils/auth';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { scopedLogger } from '~/utils/logger';
|
||||||
|
|
||||||
|
const log = scopedLogger('user-profile');
|
||||||
|
|
||||||
|
const userProfileSchema = z.object({
|
||||||
|
profile: z.object({
|
||||||
|
icon: z.string(),
|
||||||
|
colorA: z.string(),
|
||||||
|
colorB: z.string()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const userId = event.context.params?.id;
|
||||||
|
|
||||||
|
const session = await useAuth().getCurrentSession();
|
||||||
|
|
||||||
|
if (session.user !== userId) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'Cannot modify other users'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.method === 'PATCH') {
|
||||||
|
try {
|
||||||
|
const body = await readBody(event);
|
||||||
|
log.info('Updating user profile', { userId, body });
|
||||||
|
|
||||||
|
const validatedBody = userProfileSchema.parse(body);
|
||||||
|
|
||||||
|
const user = await prisma.users.update({
|
||||||
|
where: { id: userId },
|
||||||
|
data: {
|
||||||
|
profile: validatedBody.profile
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info('User profile updated successfully', { userId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
publicKey: user.public_key,
|
||||||
|
namespace: user.namespace,
|
||||||
|
profile: user.profile,
|
||||||
|
permissions: user.permissions,
|
||||||
|
createdAt: user.created_at,
|
||||||
|
lastLoggedIn: user.last_logged_in
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to update user profile', {
|
||||||
|
userId,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Invalid profile data',
|
||||||
|
cause: error.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Failed to update user profile',
|
||||||
|
cause: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,16 +4,16 @@ import { randomUUID } from 'crypto';
|
||||||
|
|
||||||
const progressMetaSchema = z.object({
|
const progressMetaSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
|
year: z.number().optional(),
|
||||||
poster: z.string().optional(),
|
poster: z.string().optional(),
|
||||||
type: z.enum(['movie', 'tv', 'show']),
|
type: z.enum(['movie', 'show'])
|
||||||
year: z.number().optional()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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((n) => n.toString()),
|
||||||
watched: z.number().transform((n) => Math.round(n)),
|
watched: z.number().transform((n) => n.toString()),
|
||||||
seasonId: z.string().optional(),
|
seasonId: z.string().optional(),
|
||||||
episodeId: z.string().optional(),
|
episodeId: z.string().optional(),
|
||||||
seasonNumber: z.number().optional(),
|
seasonNumber: z.number().optional(),
|
||||||
|
|
@ -45,7 +45,7 @@ export default defineEventHandler(async (event) => {
|
||||||
if (session.user !== userId) {
|
if (session.user !== userId) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
message: 'Cannot modify user other than yourself'
|
message: 'Cannot access other user information'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,15 +57,18 @@ export default defineEventHandler(async (event) => {
|
||||||
return items.map(item => ({
|
return items.map(item => ({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
tmdbId: item.tmdb_id,
|
tmdbId: item.tmdb_id,
|
||||||
userId: item.user_id,
|
episode: {
|
||||||
seasonId: item.season_id,
|
id: item.episode_id || null,
|
||||||
episodeId: item.episode_id,
|
number: item.episode_number || null
|
||||||
seasonNumber: item.season_number,
|
},
|
||||||
episodeNumber: item.episode_number,
|
season: {
|
||||||
|
id: item.season_id || null,
|
||||||
|
number: item.season_number || null
|
||||||
|
},
|
||||||
meta: item.meta,
|
meta: item.meta,
|
||||||
duration: Number(item.duration),
|
duration: item.duration.toString(),
|
||||||
watched: Number(item.watched),
|
watched: item.watched.toString(),
|
||||||
updatedAt: item.updated_at
|
updatedAt: item.updated_at.toISOString()
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,27 @@
|
||||||
import { useAuth } from '~/utils/auth';
|
import { useAuth } from '~/utils/auth';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
|
import { scopedLogger } from '~/utils/logger';
|
||||||
|
|
||||||
|
const log = scopedLogger('progress-import');
|
||||||
|
|
||||||
const progressMetaSchema = z.object({
|
const progressMetaSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
poster: z.string().optional(),
|
type: z.enum(['movie', 'show']),
|
||||||
type: z.enum(['movie', 'tv', 'show']),
|
year: z.number(),
|
||||||
year: z.number().optional()
|
poster: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
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(),
|
||||||
watched: z.number().transform((n) => Math.round(n)),
|
watched: z.number(),
|
||||||
seasonId: z.string().optional(),
|
seasonId: z.string().optional(),
|
||||||
episodeId: z.string().optional(),
|
episodeId: z.string().optional(),
|
||||||
seasonNumber: z.number().optional(),
|
seasonNumber: z.number().optional(),
|
||||||
episodeNumber: z.number().optional(),
|
episodeNumber: z.number().optional(),
|
||||||
updatedAt: z.string().datetime({ offset: true }).optional(),
|
updatedAt: z.string().datetime({ offset: true }).optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
// 13th July 2021 - movie-web epoch
|
// 13th July 2021 - movie-web epoch
|
||||||
|
|
@ -49,91 +52,130 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const body = await readBody(event);
|
try {
|
||||||
const validatedBody = z.array(progressItemSchema).parse(body);
|
const body = await readBody(event);
|
||||||
|
const validatedBody = z.array(progressItemSchema).parse(body);
|
||||||
const existingItems = await prisma.progress_items.findMany({
|
|
||||||
where: { user_id: userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
const newItems = [...validatedBody];
|
|
||||||
const itemsToUpsert = [];
|
|
||||||
|
|
||||||
for (const existingItem of existingItems) {
|
|
||||||
const newItemIndex = newItems.findIndex(
|
|
||||||
(item) =>
|
|
||||||
item.tmdbId === existingItem.tmdb_id &&
|
|
||||||
item.seasonId === existingItem.season_id &&
|
|
||||||
item.episodeId === existingItem.episode_id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (newItemIndex > -1) {
|
const existingItems = await prisma.progress_items.findMany({
|
||||||
const newItem = newItems[newItemIndex];
|
where: { user_id: userId }
|
||||||
|
|
||||||
if (Number(existingItem.watched) < newItem.watched) {
|
|
||||||
itemsToUpsert.push({
|
|
||||||
id: existingItem.id,
|
|
||||||
tmdb_id: existingItem.tmdb_id,
|
|
||||||
user_id: existingItem.user_id,
|
|
||||||
season_id: existingItem.season_id,
|
|
||||||
episode_id: existingItem.episode_id,
|
|
||||||
season_number: existingItem.season_number,
|
|
||||||
episode_number: existingItem.episode_number,
|
|
||||||
duration: BigInt(newItem.duration),
|
|
||||||
watched: BigInt(newItem.watched),
|
|
||||||
meta: newItem.meta,
|
|
||||||
updated_at: defaultAndCoerceDateTime(newItem.updatedAt)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
newItems.splice(newItemIndex, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const newItem of newItems) {
|
|
||||||
itemsToUpsert.push({
|
|
||||||
id: randomUUID(),
|
|
||||||
tmdb_id: newItem.tmdbId,
|
|
||||||
user_id: userId,
|
|
||||||
season_id: newItem.seasonId || null,
|
|
||||||
episode_id: newItem.episodeId || null,
|
|
||||||
season_number: newItem.seasonNumber || null,
|
|
||||||
episode_number: newItem.episodeNumber || null,
|
|
||||||
duration: BigInt(newItem.duration),
|
|
||||||
watched: BigInt(newItem.watched),
|
|
||||||
meta: newItem.meta,
|
|
||||||
updated_at: defaultAndCoerceDateTime(newItem.updatedAt)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const newItems = [...validatedBody];
|
||||||
|
const itemsToUpsert = [];
|
||||||
|
|
||||||
|
for (const existingItem of existingItems) {
|
||||||
|
const newItemIndex = newItems.findIndex(
|
||||||
|
(item) =>
|
||||||
|
item.tmdbId === existingItem.tmdb_id &&
|
||||||
|
item.seasonId === (existingItem.season_id === '\n' ? null : existingItem.season_id) &&
|
||||||
|
item.episodeId === (existingItem.episode_id === '\n' ? null : existingItem.episode_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newItemIndex > -1) {
|
||||||
|
const newItem = newItems[newItemIndex];
|
||||||
|
|
||||||
|
if (Number(existingItem.watched) < newItem.watched) {
|
||||||
|
const isMovie = newItem.meta.type === 'movie';
|
||||||
|
itemsToUpsert.push({
|
||||||
|
id: existingItem.id,
|
||||||
|
tmdb_id: existingItem.tmdb_id,
|
||||||
|
user_id: existingItem.user_id,
|
||||||
|
season_id: isMovie ? '\n' : existingItem.season_id,
|
||||||
|
episode_id: isMovie ? '\n' : existingItem.episode_id,
|
||||||
|
season_number: existingItem.season_number,
|
||||||
|
episode_number: existingItem.episode_number,
|
||||||
|
duration: BigInt(newItem.duration),
|
||||||
|
watched: BigInt(newItem.watched),
|
||||||
|
meta: newItem.meta,
|
||||||
|
updated_at: defaultAndCoerceDateTime(newItem.updatedAt)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
newItems.splice(newItemIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new items
|
||||||
|
for (const item of newItems) {
|
||||||
|
const isMovie = item.meta.type === 'movie';
|
||||||
|
itemsToUpsert.push({
|
||||||
|
id: randomUUID(),
|
||||||
|
tmdb_id: item.tmdbId,
|
||||||
|
user_id: userId,
|
||||||
|
season_id: isMovie ? '\n' : (item.seasonId || null),
|
||||||
|
episode_id: isMovie ? '\n' : (item.episodeId || null),
|
||||||
|
season_number: isMovie ? null : item.seasonNumber,
|
||||||
|
episode_number: isMovie ? null : item.episodeNumber,
|
||||||
|
duration: BigInt(item.duration),
|
||||||
|
watched: BigInt(item.watched),
|
||||||
|
meta: item.meta,
|
||||||
|
updated_at: defaultAndCoerceDateTime(item.updatedAt)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert all items
|
||||||
|
const results = [];
|
||||||
|
for (const item of itemsToUpsert) {
|
||||||
|
try {
|
||||||
|
const result = await prisma.progress_items.upsert({
|
||||||
|
where: {
|
||||||
|
tmdb_id_user_id_season_id_episode_id: {
|
||||||
|
tmdb_id: item.tmdb_id,
|
||||||
|
user_id: item.user_id,
|
||||||
|
season_id: item.season_id,
|
||||||
|
episode_id: item.episode_id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
create: item,
|
||||||
|
update: {
|
||||||
|
duration: item.duration,
|
||||||
|
watched: item.watched,
|
||||||
|
meta: item.meta,
|
||||||
|
updated_at: item.updated_at
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
id: result.id,
|
||||||
|
tmdbId: result.tmdb_id,
|
||||||
|
episode: {
|
||||||
|
id: result.episode_id === '\n' ? null : result.episode_id,
|
||||||
|
number: result.episode_number
|
||||||
|
},
|
||||||
|
season: {
|
||||||
|
id: result.season_id === '\n' ? null : result.season_id,
|
||||||
|
number: result.season_number
|
||||||
|
},
|
||||||
|
meta: result.meta,
|
||||||
|
duration: result.duration.toString(),
|
||||||
|
watched: result.watched.toString(),
|
||||||
|
updatedAt: result.updated_at.toISOString()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to upsert progress item', {
|
||||||
|
userId,
|
||||||
|
tmdbId: item.tmdb_id,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (error) {
|
||||||
|
log.error('Failed to import progress', {
|
||||||
|
userId,
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Invalid progress data',
|
||||||
|
cause: error.errors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await prisma.$transaction(
|
|
||||||
itemsToUpsert.map(item =>
|
|
||||||
prisma.progress_items.upsert({
|
|
||||||
where: {
|
|
||||||
id: item.id
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
watched: item.watched,
|
|
||||||
duration: item.duration,
|
|
||||||
meta: item.meta,
|
|
||||||
updated_at: item.updated_at
|
|
||||||
},
|
|
||||||
create: item
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return result.map(item => ({
|
|
||||||
id: item.id,
|
|
||||||
tmdbId: item.tmdb_id,
|
|
||||||
userId: item.user_id,
|
|
||||||
seasonId: item.season_id,
|
|
||||||
episodeId: 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
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
@ -18,12 +18,10 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
return sessions.map(s => ({
|
return sessions.map(s => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
user: s.user,
|
userId: s.user,
|
||||||
createdAt: s.created_at,
|
createdAt: s.created_at.toISOString(),
|
||||||
accessedAt: s.accessed_at,
|
accessedAt: s.accessed_at.toISOString(),
|
||||||
expiresAt: s.expires_at,
|
|
||||||
device: s.device,
|
device: s.device,
|
||||||
userAgent: s.user_agent,
|
userAgent: s.user_agent
|
||||||
current: s.id === session.id
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
@ -1,13 +1,16 @@
|
||||||
import { useAuth } from '~/utils/auth';
|
import { useAuth } from '~/utils/auth';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { scopedLogger } from '~/utils/logger';
|
||||||
|
|
||||||
|
const log = scopedLogger('user-settings');
|
||||||
|
|
||||||
const userSettingsSchema = z.object({
|
const userSettingsSchema = z.object({
|
||||||
application_theme: z.string().optional(),
|
applicationTheme: z.string().nullable().optional(),
|
||||||
application_language: z.string().optional(),
|
applicationLanguage: z.string(),
|
||||||
default_subtitle_language: z.string().optional(),
|
defaultSubtitleLanguage: z.string().nullable().optional(),
|
||||||
proxy_urls: z.array(z.string()).optional(),
|
proxyUrls: z.array(z.string()).nullable().optional(),
|
||||||
trakt_key: z.string().optional(),
|
traktKey: z.string().nullable().optional(),
|
||||||
febbox_key: z.string().optional()
|
febboxKey: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
|
@ -22,82 +25,93 @@ export default defineEventHandler(async (event) => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.method === 'PUT') {
|
if (event.method === 'GET') {
|
||||||
try {
|
|
||||||
const body = await readBody(event);
|
|
||||||
const validatedSettings = userSettingsSchema.parse(body);
|
|
||||||
|
|
||||||
const existingSettings = await prisma.user_settings.findUnique({
|
|
||||||
where: { id: userId }
|
|
||||||
});
|
|
||||||
|
|
||||||
let settings;
|
|
||||||
|
|
||||||
if (existingSettings) {
|
|
||||||
settings = await prisma.user_settings.update({
|
|
||||||
where: { id: userId },
|
|
||||||
data: validatedSettings
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
settings = await prisma.user_settings.create({
|
|
||||||
data: {
|
|
||||||
id: userId,
|
|
||||||
...validatedSettings
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
settings: {
|
|
||||||
applicationTheme: settings.application_theme,
|
|
||||||
applicationLanguage: settings.application_language,
|
|
||||||
defaultSubtitleLanguage: settings.default_subtitle_language,
|
|
||||||
proxyUrls: settings.proxy_urls,
|
|
||||||
traktKey: settings.trakt_key,
|
|
||||||
febboxKey: settings.febbox_key
|
|
||||||
}
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof z.ZodError) {
|
|
||||||
throw createError({
|
|
||||||
statusCode: 400,
|
|
||||||
message: 'Invalid settings data'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw createError({
|
|
||||||
statusCode: 500,
|
|
||||||
message: 'Failed to update settings'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (event.method === 'GET') {
|
|
||||||
const settings = await prisma.user_settings.findUnique({
|
const settings = await prisma.user_settings.findUnique({
|
||||||
where: { id: userId }
|
where: { id: userId }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!settings) {
|
|
||||||
return {
|
|
||||||
settings: {
|
|
||||||
applicationTheme: null,
|
|
||||||
applicationLanguage: null,
|
|
||||||
defaultSubtitleLanguage: null,
|
|
||||||
proxyUrls: [],
|
|
||||||
traktKey: null,
|
|
||||||
febboxKey: null
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
settings: {
|
id: userId,
|
||||||
|
applicationTheme: settings?.application_theme || null,
|
||||||
|
applicationLanguage: settings?.application_language || 'en',
|
||||||
|
defaultSubtitleLanguage: settings?.default_subtitle_language || null,
|
||||||
|
proxyUrls: settings?.proxy_urls || null,
|
||||||
|
traktKey: settings?.trakt_key || null,
|
||||||
|
febboxKey: settings?.febbox_key || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.method === 'PUT') {
|
||||||
|
try {
|
||||||
|
const body = await readBody(event);
|
||||||
|
log.info('Updating user settings', { userId, body });
|
||||||
|
|
||||||
|
const validatedBody = userSettingsSchema.parse(body);
|
||||||
|
|
||||||
|
// Handle proxyUrls properly - ensure it's an array or empty array when null
|
||||||
|
const proxyUrls = validatedBody.proxyUrls === null ? [] : (validatedBody.proxyUrls || []);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
application_theme: validatedBody.applicationTheme ?? null,
|
||||||
|
application_language: validatedBody.applicationLanguage,
|
||||||
|
default_subtitle_language: validatedBody.defaultSubtitleLanguage ?? null,
|
||||||
|
proxy_urls: proxyUrls,
|
||||||
|
trakt_key: validatedBody.traktKey ?? null,
|
||||||
|
febbox_key: validatedBody.febboxKey ?? null
|
||||||
|
};
|
||||||
|
|
||||||
|
log.info('Preparing to upsert settings', {
|
||||||
|
userId,
|
||||||
|
data: { ...data, proxy_urls: Array.isArray(data.proxy_urls) ? data.proxy_urls.length : 'not an array' }
|
||||||
|
});
|
||||||
|
|
||||||
|
const settings = await prisma.user_settings.upsert({
|
||||||
|
where: { id: userId },
|
||||||
|
update: data,
|
||||||
|
create: {
|
||||||
|
id: userId,
|
||||||
|
...data
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
log.info('Settings updated successfully', { userId });
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: userId,
|
||||||
applicationTheme: settings.application_theme,
|
applicationTheme: settings.application_theme,
|
||||||
applicationLanguage: settings.application_language,
|
applicationLanguage: settings.application_language,
|
||||||
defaultSubtitleLanguage: settings.default_subtitle_language,
|
defaultSubtitleLanguage: settings.default_subtitle_language,
|
||||||
proxyUrls: settings.proxy_urls,
|
proxyUrls: settings.proxy_urls,
|
||||||
traktKey: settings.trakt_key,
|
traktKey: settings.trakt_key,
|
||||||
febboxKey: settings.febbox_key
|
febboxKey: settings.febbox_key
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof z.ZodError) {
|
||||||
|
log.error('Validation error in settings update', {
|
||||||
|
userId,
|
||||||
|
errors: error.errors
|
||||||
|
});
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Invalid settings data',
|
||||||
|
cause: error.errors
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
// Log the specific error for debugging
|
||||||
|
log.error('Failed to update settings', {
|
||||||
|
userId,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
stack: error instanceof Error ? error.stack : undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Failed to update settings',
|
||||||
|
cause: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw createError({
|
throw createError({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue