mirror of
https://github.com/p-stream/backend.git
synced 2026-03-11 17:55:35 +00:00
Latest changes
This commit is contained in:
parent
a0ffb32fd5
commit
a5baed2ee4
23 changed files with 2525 additions and 1096 deletions
|
|
@ -22,6 +22,9 @@ TMDB_API_KEY=''
|
|||
TRAKT_CLIENT_ID=''
|
||||
TRAKT_SECRET_ID=''
|
||||
|
||||
# Optional: PostgreSQL connection pool size (default: 100000)
|
||||
# DB_POOL_MAX=100000
|
||||
|
||||
# Optional: Captcha
|
||||
CAPTCHA=false
|
||||
CAPTCHA_CLIENT_KEY=''
|
||||
|
|
|
|||
3135
package-lock.json
generated
3135
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -15,18 +15,23 @@
|
|||
"eslint-plugin-prettier": "^5.4.0",
|
||||
"nitropack": "latest",
|
||||
"prettier": "^3.5.3",
|
||||
"prisma": "^7.0.1"
|
||||
"prisma": "^7.0.1",
|
||||
"rollup": "^4.59.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/adapter-pg": "^7.0.1",
|
||||
"@prisma/client": "^7.0.1",
|
||||
"@types/pg": "^8.18.0",
|
||||
"cheerio": "^1.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"p-limit": "^7.3.0",
|
||||
"pg": "^8.19.0",
|
||||
"prom-client": "^15.1.3",
|
||||
"tmdb-ts": "^2.0.1",
|
||||
"trakt.tv": "^8.2.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"uuidv7": "^1.1.0",
|
||||
"whatwg-url": "^14.2.0",
|
||||
"zod": "^3.24.2"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../generated"
|
||||
moduleFormat = "esm"
|
||||
provider = "prisma-client"
|
||||
output = "../generated"
|
||||
moduleFormat = "esm"
|
||||
previewFeatures = ["relationJoins"]
|
||||
}
|
||||
|
||||
datasource db {
|
||||
|
|
@ -18,6 +19,7 @@ model bookmarks {
|
|||
|
||||
@@id([tmdb_id, user_id])
|
||||
@@unique([tmdb_id, user_id], map: "bookmarks_tmdb_id_user_id_unique")
|
||||
@@index([user_id], type: Hash)
|
||||
}
|
||||
|
||||
model challenge_codes {
|
||||
|
|
@ -72,6 +74,7 @@ model progress_items {
|
|||
episode_number Int?
|
||||
|
||||
@@unique([tmdb_id, user_id, season_id, episode_id], map: "progress_items_tmdb_id_user_id_season_id_episode_id_unique")
|
||||
@@index([user_id], type: Hash)
|
||||
}
|
||||
|
||||
model sessions {
|
||||
|
|
@ -82,6 +85,8 @@ model sessions {
|
|||
expires_at DateTime @db.Timestamptz(0)
|
||||
device String
|
||||
user_agent String
|
||||
|
||||
@@index([user], type: Hash)
|
||||
}
|
||||
|
||||
model user_group_order {
|
||||
|
|
@ -158,4 +163,5 @@ model watch_history {
|
|||
updated_at DateTime @default(now()) @db.Timestamptz(0)
|
||||
|
||||
@@unique([tmdb_id, user_id, season_id, episode_id], map: "watch_history_tmdb_id_user_id_season_id_episode_id_unique")
|
||||
@@index([user_id, watched_at(sort: Desc)])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export default defineEventHandler(async event => {
|
|||
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { public_key: body.publicKey },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
import { useChallenge } from '~/utils/challenge';
|
||||
import { useAuth } from '~/utils/auth';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
import { generateRandomNickname } from '~/utils/nickname';
|
||||
|
||||
const completeSchema = z.object({
|
||||
|
|
@ -50,7 +50,7 @@ export default defineEventHandler(async event => {
|
|||
});
|
||||
}
|
||||
|
||||
const userId = randomUUID();
|
||||
const userId = uuidv7();
|
||||
const now = new Date();
|
||||
const nickname = generateRandomNickname();
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,14 @@ export default defineEventHandler(async event => {
|
|||
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { id: session.user },
|
||||
select: {
|
||||
id: true,
|
||||
public_key: true,
|
||||
namespace: true,
|
||||
nickname: true,
|
||||
profile: true,
|
||||
permissions: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useAuth } from '~/utils/auth';
|
||||
import { z } from 'zod';
|
||||
import { bookmarks } from '@prisma/client';
|
||||
|
||||
const bookmarkMetaSchema = z.object({
|
||||
title: z.string(),
|
||||
|
|
@ -32,9 +31,16 @@ export default defineEventHandler(async event => {
|
|||
if (method === 'GET') {
|
||||
const bookmarks = await prisma.bookmarks.findMany({
|
||||
where: { user_id: userId },
|
||||
select: {
|
||||
tmdb_id: true,
|
||||
meta: true,
|
||||
group: true,
|
||||
favorite_episodes: true,
|
||||
updated_at: true,
|
||||
},
|
||||
});
|
||||
|
||||
return bookmarks.map((bookmark: bookmarks) => ({
|
||||
return bookmarks.map((bookmark: any) => ({
|
||||
tmdbId: bookmark.tmdb_id,
|
||||
meta: bookmark.meta,
|
||||
group: bookmark.group,
|
||||
|
|
@ -48,18 +54,16 @@ export default defineEventHandler(async event => {
|
|||
const validatedBody = z.array(bookmarkDataSchema).parse(body);
|
||||
|
||||
const now = new Date();
|
||||
const results = [];
|
||||
|
||||
for (const item of validatedBody) {
|
||||
const upserts = validatedBody.map((item: any) => {
|
||||
// Normalize group to always be an array
|
||||
const normalizedGroup = item.group
|
||||
const normalizedGroup = item.group
|
||||
? (Array.isArray(item.group) ? item.group : [item.group])
|
||||
: [];
|
||||
|
||||
// Normalize favoriteEpisodes to always be an array
|
||||
const normalizedFavoriteEpisodes = item.favoriteEpisodes || [];
|
||||
|
||||
const bookmark = await prisma.bookmarks.upsert({
|
||||
return prisma.bookmarks.upsert({
|
||||
where: {
|
||||
tmdb_id_user_id: {
|
||||
tmdb_id: item.tmdbId,
|
||||
|
|
@ -80,18 +84,18 @@ export default defineEventHandler(async event => {
|
|||
favorite_episodes: normalizedFavoriteEpisodes,
|
||||
updated_at: now,
|
||||
} as any,
|
||||
}) as bookmarks;
|
||||
|
||||
results.push({
|
||||
tmdbId: bookmark.tmdb_id,
|
||||
meta: bookmark.meta,
|
||||
group: bookmark.group,
|
||||
favoriteEpisodes: bookmark.favorite_episodes,
|
||||
updatedAt: bookmark.updated_at,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
const bookmarks = await prisma.$transaction(upserts);
|
||||
|
||||
return bookmarks.map((bookmark: any) => ({
|
||||
tmdbId: bookmark.tmdb_id,
|
||||
meta: bookmark.meta,
|
||||
group: bookmark.group,
|
||||
favoriteEpisodes: bookmark.favorite_episodes,
|
||||
updatedAt: bookmark.updated_at,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
import { useAuth } from '~/utils/auth';
|
||||
import { z } from 'zod';
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export default defineEventHandler(async event => {
|
|||
updated_at: new Date(),
|
||||
},
|
||||
create: {
|
||||
id: randomUUID(),
|
||||
id: uuidv7(),
|
||||
user_id: userId,
|
||||
group_order: validatedGroupOrder,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -94,11 +94,35 @@ export default defineEventHandler(async event => {
|
|||
where: { user_id: userId },
|
||||
});
|
||||
|
||||
await tx.watch_history.deleteMany({
|
||||
where: { user_id: userId },
|
||||
});
|
||||
|
||||
const userLists = await tx.lists.findMany({
|
||||
where: { user_id: userId },
|
||||
select: { id: true }
|
||||
});
|
||||
const listIds = userLists.map((l: any) => l.id);
|
||||
|
||||
if (listIds.length > 0) {
|
||||
await tx.list_items.deleteMany({
|
||||
where: { list_id: { in: listIds } },
|
||||
});
|
||||
}
|
||||
|
||||
await tx.lists.deleteMany({
|
||||
where: { user_id: userId },
|
||||
});
|
||||
|
||||
await tx.user_group_order.deleteMany({
|
||||
where: { user_id: userId },
|
||||
});
|
||||
|
||||
await tx.user_settings
|
||||
.delete({
|
||||
where: { id: userId },
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
|
||||
await tx.sessions.deleteMany({
|
||||
where: { user: userId },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useAuth } from '#imports';
|
||||
import { z } from 'zod';
|
||||
import { prisma } from '#imports';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
const listItemSchema = z.object({
|
||||
tmdb_id: z.string(),
|
||||
|
|
@ -62,6 +63,7 @@ export default defineEventHandler(async event => {
|
|||
description:
|
||||
validatedBody.description !== undefined ? validatedBody.description : list.description,
|
||||
public: validatedBody.public ?? list.public,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -76,6 +78,7 @@ export default defineEventHandler(async event => {
|
|||
if (itemsToAdd.length > 0) {
|
||||
await tx.list_items.createMany({
|
||||
data: itemsToAdd.map(item => ({
|
||||
id: uuidv7(),
|
||||
list_id: list.id,
|
||||
tmdb_id: item.tmdb_id,
|
||||
type: item.type,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useAuth } from '#imports';
|
||||
import { prisma } from '~/utils/prisma';
|
||||
import { z } from 'zod';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
const listItemSchema = z.object({
|
||||
tmdb_id: z.string(),
|
||||
|
|
@ -40,18 +41,30 @@ export default defineEventHandler(async event => {
|
|||
const validatedBody = createListSchema.parse(parsedBody);
|
||||
|
||||
const result = await prisma.$transaction(async tx => {
|
||||
const existing = await tx.lists.findFirst({
|
||||
where: { user_id: userId, name: validatedBody.name }
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw createError({ statusCode: 409, message: 'A list with this name already exists' });
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const newList = await tx.lists.create({
|
||||
data: {
|
||||
id: uuidv7(),
|
||||
user_id: userId,
|
||||
name: validatedBody.name,
|
||||
description: validatedBody.description || null,
|
||||
public: validatedBody.public || false,
|
||||
updated_at: now,
|
||||
},
|
||||
});
|
||||
|
||||
if (validatedBody.items && validatedBody.items.length > 0) {
|
||||
await tx.list_items.createMany({
|
||||
data: validatedBody.items.map(item => ({
|
||||
id: uuidv7(),
|
||||
list_id: newList.id,
|
||||
tmdb_id: item.tmdb_id,
|
||||
type: item.type, // Type is mapped here
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useAuth } from '~/utils/auth';
|
||||
import { z } from 'zod';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
function progressIsNotStarted(duration: number, watched: number): boolean {
|
||||
// too short watch time
|
||||
|
|
@ -56,7 +56,7 @@ async function shouldSaveProgress(
|
|||
const epDuration = Number(episode.duration);
|
||||
const epWatched = Number(episode.watched);
|
||||
return !progressIsNotStarted(epDuration, epWatched) &&
|
||||
!progressIsCompleted(epDuration, epWatched);
|
||||
!progressIsCompleted(epDuration, epWatched);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -110,6 +110,18 @@ export default defineEventHandler(async event => {
|
|||
if (method === 'GET') {
|
||||
const items = await prisma.progress_items.findMany({
|
||||
where: { user_id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
tmdb_id: true,
|
||||
episode_id: true,
|
||||
episode_number: true,
|
||||
season_id: true,
|
||||
season_number: true,
|
||||
meta: true,
|
||||
duration: true,
|
||||
watched: true,
|
||||
updated_at: true,
|
||||
},
|
||||
});
|
||||
|
||||
return items.map(item => ({
|
||||
|
|
@ -179,7 +191,7 @@ export default defineEventHandler(async event => {
|
|||
const duration = Number(item.duration);
|
||||
const watched = Number(item.watched);
|
||||
return !progressIsNotStarted(duration, watched) &&
|
||||
!progressIsCompleted(duration, watched);
|
||||
!progressIsCompleted(duration, watched);
|
||||
});
|
||||
|
||||
if (hasAcceptableEpisodes) {
|
||||
|
|
@ -245,7 +257,7 @@ export default defineEventHandler(async event => {
|
|||
|
||||
const now = defaultAndCoerceDateTime(validatedBody.updatedAt);
|
||||
|
||||
const existingItem = await prisma.progress_items.findUnique({
|
||||
const progressItem = await prisma.progress_items.upsert({
|
||||
where: {
|
||||
tmdb_id_user_id_season_id_episode_id: {
|
||||
tmdb_id: tmdbId,
|
||||
|
|
@ -254,40 +266,27 @@ export default defineEventHandler(async event => {
|
|||
episode_id: validatedBody.episodeId || null,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
duration: BigInt(validatedBody.duration),
|
||||
watched: BigInt(validatedBody.watched),
|
||||
meta: validatedBody.meta,
|
||||
updated_at: now,
|
||||
},
|
||||
create: {
|
||||
id: uuidv7(),
|
||||
tmdb_id: tmdbId,
|
||||
user_id: userId,
|
||||
season_id: validatedBody.seasonId || null,
|
||||
episode_id: validatedBody.episodeId || null,
|
||||
season_number: validatedBody.seasonNumber || null,
|
||||
episode_number: validatedBody.episodeNumber || null,
|
||||
duration: BigInt(validatedBody.duration),
|
||||
watched: BigInt(validatedBody.watched),
|
||||
meta: validatedBody.meta,
|
||||
updated_at: now,
|
||||
},
|
||||
});
|
||||
|
||||
let progressItem;
|
||||
|
||||
if (existingItem) {
|
||||
progressItem = await prisma.progress_items.update({
|
||||
where: {
|
||||
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: validatedBody.seasonId || null,
|
||||
episode_id: validatedBody.episodeId || null,
|
||||
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,
|
||||
|
|
@ -314,25 +313,12 @@ export default defineEventHandler(async event => {
|
|||
if (body.seasonId) whereClause.season_id = body.seasonId;
|
||||
if (body.episodeId) whereClause.episode_id = body.episodeId;
|
||||
|
||||
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({
|
||||
const { count } = await prisma.progress_items.deleteMany({
|
||||
where: whereClause,
|
||||
});
|
||||
|
||||
return {
|
||||
count: itemsToDelete.length,
|
||||
count,
|
||||
tmdbId,
|
||||
episodeId: body.episodeId,
|
||||
seasonId: body.seasonId,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useAuth } from '~/utils/auth';
|
||||
import { z } from 'zod';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
const progressMetaSchema = z.object({
|
||||
title: z.string(),
|
||||
|
|
@ -70,10 +70,6 @@ export default defineEventHandler(async (event) => {
|
|||
const now = coerceDateTime(updatedAt);
|
||||
const { seasonId: normSeasonId, episodeId: normEpisodeId } = normalizeIds(meta.type, seasonId, episodeId);
|
||||
|
||||
const existing = await prisma.progress_items.findUnique({
|
||||
where: { tmdb_id_user_id_season_id_episode_id: { tmdb_id: tmdbId, user_id: userId, season_id: normSeasonId, episode_id: normEpisodeId } },
|
||||
});
|
||||
|
||||
const data = {
|
||||
duration: BigInt(duration),
|
||||
watched: BigInt(watched),
|
||||
|
|
@ -81,20 +77,20 @@ export default defineEventHandler(async (event) => {
|
|||
updated_at: now,
|
||||
};
|
||||
|
||||
const progressItem = existing
|
||||
? await prisma.progress_items.update({ where: { id: existing.id }, data })
|
||||
: await prisma.progress_items.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
tmdb_id: tmdbId,
|
||||
user_id: userId,
|
||||
season_id: normSeasonId,
|
||||
episode_id: normEpisodeId,
|
||||
season_number: seasonNumber || null,
|
||||
episode_number: episodeNumber || null,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
const progressItem = await prisma.progress_items.upsert({
|
||||
where: { tmdb_id_user_id_season_id_episode_id: { tmdb_id: tmdbId, user_id: userId, season_id: normSeasonId, episode_id: normEpisodeId } },
|
||||
update: data,
|
||||
create: {
|
||||
id: uuidv7(),
|
||||
tmdb_id: tmdbId,
|
||||
user_id: userId,
|
||||
season_id: normSeasonId,
|
||||
episode_id: normEpisodeId,
|
||||
season_number: seasonNumber || null,
|
||||
episode_number: episodeNumber || null,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
|
||||
return formatProgressItem(progressItem);
|
||||
}
|
||||
|
|
@ -109,11 +105,8 @@ export default defineEventHandler(async (event) => {
|
|||
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 };
|
||||
const { count } = await prisma.progress_items.deleteMany({ where });
|
||||
return { count, tmdbId, episodeId: body.episodeId, seasonId: body.seasonId };
|
||||
}
|
||||
|
||||
throw createError({ statusCode: 405, message: 'Method not allowed' });
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useAuth } from '~/utils/auth';
|
||||
import { z } from 'zod';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
import { scopedLogger } from '~/utils/logger';
|
||||
|
||||
const log = scopedLogger('progress-import');
|
||||
|
|
@ -14,7 +14,7 @@ const progressMetaSchema = z.object({
|
|||
|
||||
const progressItemSchema = z.object({
|
||||
meta: progressMetaSchema,
|
||||
tmdbId: z.string().transform(val => val || randomUUID()),
|
||||
tmdbId: z.string().transform(val => val || uuidv7()),
|
||||
duration: z
|
||||
.number()
|
||||
.min(0)
|
||||
|
|
@ -54,6 +54,7 @@ export default defineEventHandler(async event => {
|
|||
// First check if user exists
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true }
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
|
@ -117,7 +118,7 @@ export default defineEventHandler(async event => {
|
|||
for (const item of newItems) {
|
||||
const isMovie = item.meta.type === 'movie';
|
||||
itemsToUpsert.push({
|
||||
id: randomUUID(),
|
||||
id: uuidv7(),
|
||||
tmdb_id: item.tmdbId,
|
||||
user_id: userId,
|
||||
season_id: isMovie ? '\n' : item.seasonId || null,
|
||||
|
|
@ -132,54 +133,54 @@ export default defineEventHandler(async event => {
|
|||
}
|
||||
|
||||
// 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,
|
||||
},
|
||||
const upsertPromises = itemsToUpsert.map(item =>
|
||||
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,
|
||||
},
|
||||
});
|
||||
},
|
||||
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;
|
||||
}
|
||||
try {
|
||||
const transactionResults = await prisma.$transaction(upsertPromises);
|
||||
|
||||
const results = transactionResults.map(result => ({
|
||||
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(),
|
||||
}));
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
log.error('Failed to batch upsert progress items', {
|
||||
userId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
log.error('Failed to import progress', {
|
||||
userId,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,14 @@ export default defineEventHandler(async event => {
|
|||
|
||||
const sessions = await prisma.sessions.findMany({
|
||||
where: { user: userId },
|
||||
select: {
|
||||
id: true,
|
||||
user: true,
|
||||
created_at: true,
|
||||
accessed_at: true,
|
||||
device: true,
|
||||
user_agent: true,
|
||||
}
|
||||
});
|
||||
|
||||
return sessions.map(s => ({
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export default defineEventHandler(async event => {
|
|||
// First check if user exists
|
||||
const user = await prisma.users.findUnique({
|
||||
where: { id: userId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useAuth } from '~/utils/auth';
|
||||
import { z } from 'zod';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
const watchHistoryMetaSchema = z.object({
|
||||
title: z.string(),
|
||||
|
|
@ -54,6 +54,18 @@ export default defineEventHandler(async event => {
|
|||
const items = await prisma.watch_history.findMany({
|
||||
where: { user_id: userId },
|
||||
orderBy: { watched_at: 'desc' },
|
||||
select: {
|
||||
tmdb_id: true,
|
||||
episode_id: true,
|
||||
episode_number: true,
|
||||
season_id: true,
|
||||
season_number: true,
|
||||
meta: true,
|
||||
duration: true,
|
||||
watched: true,
|
||||
watched_at: true,
|
||||
completed: true,
|
||||
}
|
||||
});
|
||||
|
||||
return items.map(item => ({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useAuth } from '~/utils/auth';
|
||||
import { z } from 'zod';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
const watchHistoryMetaSchema = z.object({
|
||||
title: z.string(),
|
||||
|
|
@ -63,9 +63,7 @@ export default defineEventHandler(async event => {
|
|||
const parsed = bodySchema.parse(body);
|
||||
const items = Array.isArray(parsed) ? parsed : [parsed];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const validatedBody of items) {
|
||||
const upsertPromises = items.map(validatedBody => {
|
||||
const itemTmdbId = items.length === 1 ? tmdbId : (validatedBody.tmdbId ?? tmdbId);
|
||||
const watchedAt = defaultAndCoerceDateTime(validatedBody.watchedAt);
|
||||
const now = new Date();
|
||||
|
|
@ -74,17 +72,6 @@ export default defineEventHandler(async event => {
|
|||
const normSeasonId = validatedBody.meta.type === 'movie' ? '\n' : validatedBody.seasonId ?? null;
|
||||
const normEpisodeId = validatedBody.meta.type === 'movie' ? '\n' : validatedBody.episodeId ?? null;
|
||||
|
||||
const existingItem = await prisma.watch_history.findUnique({
|
||||
where: {
|
||||
tmdb_id_user_id_season_id_episode_id: {
|
||||
tmdb_id: itemTmdbId,
|
||||
user_id: userId,
|
||||
season_id: normSeasonId,
|
||||
episode_id: normEpisodeId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const data = {
|
||||
duration: parseFloat(validatedBody.duration),
|
||||
watched: parseFloat(validatedBody.watched),
|
||||
|
|
@ -94,45 +81,47 @@ export default defineEventHandler(async event => {
|
|||
updated_at: now,
|
||||
};
|
||||
|
||||
let watchHistoryItem;
|
||||
|
||||
if (existingItem) {
|
||||
watchHistoryItem = await prisma.watch_history.update({
|
||||
where: { id: existingItem.id },
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
watchHistoryItem = await prisma.watch_history.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
return prisma.watch_history.upsert({
|
||||
where: {
|
||||
tmdb_id_user_id_season_id_episode_id: {
|
||||
tmdb_id: itemTmdbId,
|
||||
user_id: userId,
|
||||
season_id: normSeasonId,
|
||||
episode_id: normEpisodeId,
|
||||
season_number: validatedBody.seasonNumber ?? null,
|
||||
episode_number: validatedBody.episodeNumber ?? null,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
id: watchHistoryItem.id,
|
||||
tmdbId: watchHistoryItem.tmdb_id,
|
||||
userId: watchHistoryItem.user_id,
|
||||
seasonId: watchHistoryItem.season_id,
|
||||
episodeId: watchHistoryItem.episode_id,
|
||||
seasonNumber: watchHistoryItem.season_number,
|
||||
episodeNumber: watchHistoryItem.episode_number,
|
||||
meta: watchHistoryItem.meta,
|
||||
duration: watchHistoryItem.duration,
|
||||
watched: watchHistoryItem.watched,
|
||||
watchedAt: watchHistoryItem.watched_at.toISOString(),
|
||||
completed: watchHistoryItem.completed,
|
||||
updatedAt: watchHistoryItem.updated_at.toISOString(),
|
||||
},
|
||||
update: data,
|
||||
create: {
|
||||
id: uuidv7(),
|
||||
tmdb_id: itemTmdbId,
|
||||
user_id: userId,
|
||||
season_id: normSeasonId,
|
||||
episode_id: normEpisodeId,
|
||||
season_number: validatedBody.seasonNumber ?? null,
|
||||
episode_number: validatedBody.episodeNumber ?? null,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const transactionResults = await prisma.$transaction(upsertPromises);
|
||||
|
||||
const results = transactionResults.map(watchHistoryItem => ({
|
||||
success: true,
|
||||
id: watchHistoryItem.id,
|
||||
tmdbId: watchHistoryItem.tmdb_id,
|
||||
userId: watchHistoryItem.user_id,
|
||||
seasonId: watchHistoryItem.season_id,
|
||||
episodeId: watchHistoryItem.episode_id,
|
||||
seasonNumber: watchHistoryItem.season_number,
|
||||
episodeNumber: watchHistoryItem.episode_number,
|
||||
meta: watchHistoryItem.meta,
|
||||
duration: watchHistoryItem.duration,
|
||||
watched: watchHistoryItem.watched,
|
||||
watchedAt: watchHistoryItem.watched_at.toISOString(),
|
||||
completed: watchHistoryItem.completed,
|
||||
updatedAt: watchHistoryItem.updated_at.toISOString(),
|
||||
}));
|
||||
|
||||
return results.length === 1 ? results[0] : { success: true, count: results.length, items: results };
|
||||
} catch (dbError) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { prisma } from './prisma';
|
||||
import jwt from 'jsonwebtoken';
|
||||
const { sign, verify } = jwt;
|
||||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
|
||||
// 21 days in ms
|
||||
const SESSION_EXPIRY_MS = 21 * 24 * 60 * 60 * 1000;
|
||||
|
|
@ -40,9 +40,14 @@ export function useAuth() {
|
|||
const now = new Date();
|
||||
const expiryDate = new Date(now.getTime() + SESSION_EXPIRY_MS);
|
||||
|
||||
// Cleanup existing session for this device
|
||||
await prisma.sessions.deleteMany({
|
||||
where: { user, device },
|
||||
});
|
||||
|
||||
return await prisma.sessions.create({
|
||||
data: {
|
||||
id: randomUUID(),
|
||||
id: uuidv7(),
|
||||
user,
|
||||
device,
|
||||
user_agent: userAgent,
|
||||
|
|
@ -56,7 +61,7 @@ export function useAuth() {
|
|||
const makeSessionToken = (session: { id: string }) => {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const cryptoSecret = runtimeConfig.cryptoSecret || process.env.CRYPTO_SECRET;
|
||||
|
||||
|
||||
if (!cryptoSecret) {
|
||||
console.error('CRYPTO_SECRET is missing from both runtime config and environment');
|
||||
console.error('Available runtime config keys:', Object.keys(runtimeConfig));
|
||||
|
|
@ -66,7 +71,7 @@ export function useAuth() {
|
|||
});
|
||||
throw new Error('CRYPTO_SECRET environment variable is not set');
|
||||
}
|
||||
|
||||
|
||||
return sign({ sid: session.id }, cryptoSecret, {
|
||||
algorithm: 'HS256',
|
||||
});
|
||||
|
|
@ -76,12 +81,12 @@ export function useAuth() {
|
|||
try {
|
||||
const runtimeConfig = useRuntimeConfig();
|
||||
const cryptoSecret = runtimeConfig.cryptoSecret || process.env.CRYPTO_SECRET;
|
||||
|
||||
|
||||
if (!cryptoSecret) {
|
||||
console.error('CRYPTO_SECRET is missing for token verification');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
const payload = verify(token, cryptoSecret, {
|
||||
algorithms: ['HS256'],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
import { uuidv7 } from 'uuidv7';
|
||||
import { prisma } from './prisma';
|
||||
import nacl from 'tweetnacl';
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ export function useChallenge() {
|
|||
|
||||
return await prisma.challenge_codes.create({
|
||||
data: {
|
||||
code: randomUUID(),
|
||||
code: uuidv7(),
|
||||
flow,
|
||||
auth_type: authType,
|
||||
created_at: now,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export interface PlayerStatus {
|
|||
|
||||
// In-memory store for player status data
|
||||
// Key: userId+roomCode, Value: Status data array
|
||||
export const playerStatusStore = new Map<string, Array<PlayerStatus>>();
|
||||
export const playerStatusStore = new Map<string, PlayerStatus[]>();
|
||||
|
||||
// Cleanup interval (30 minutes in milliseconds)
|
||||
export const CLEANUP_INTERVAL = 30 * 60 * 1000;
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
import { Pool } from 'pg';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { PrismaClient } from '../../generated/client';
|
||||
|
||||
const adapter = new PrismaPg({
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
max: parseInt(process.env.DB_POOL_MAX || '100000', 10),
|
||||
connectionTimeoutMillis: 10000,
|
||||
idleTimeoutMillis: 300000,
|
||||
});
|
||||
|
||||
const adapter = new PrismaPg(pool);
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma = new PrismaClient({ adapter });
|
||||
export const prisma = globalForPrisma.prisma || new PrismaClient({ adapter });
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
|
||||
|
|
|
|||
Loading…
Reference in a new issue