A self hosted solution for privately rating and reviewing different sorts of media
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add translations from tmdb (#38)

* add

* add

* season name overlap

* change type to extendeduserentry

authored by

Willem Dinkelspiel and committed by
GitHub
7aada330 5947004f

+1174 -402
+2 -1
.gitignore
··· 46 46 47 47 cli/dist 48 48 migration/__pycache__ 49 - migration/connections.json 49 + migration/connections.json 50 + migration/apiKey
+3 -3
app/(app)/dashboard/page.tsx
··· 6 6 import { Dialog, DialogContent } from '@/components/ui/dialog'; 7 7 import { UserEntryCardObject } from '@/components/userEntryCard'; 8 8 import { api } from '@/trpc/react'; 9 - import { Entry, User, UserEntry, UserList } from '@prisma/client'; 9 + import { Entry, UserList } from '@prisma/client'; 10 10 import { useQuery } from '@tanstack/react-query'; 11 11 import { Loader2 } from 'lucide-react'; 12 12 import { useEffect, useRef, useState } from 'react'; 13 13 import { toast } from 'sonner'; 14 14 import { SidebarButtons } from '../_components/sidebar'; 15 15 import { FilterView } from './_components/FilterView'; 16 - import { useDashboardStore } from './state'; 16 + import { ExtendedUserEntry, useDashboardStore } from './state'; 17 17 18 18 const Page = () => { 19 19 const { data, isPending: dataIsPending } = api.dashboard.get.useQuery(); ··· 38 38 const Dashboard = ({ 39 39 userEntries: originalUserEntries, 40 40 }: { 41 - userEntries: (UserEntry & { entry: Entry } & { user: User })[]; 41 + userEntries: ExtendedUserEntry[]; 42 42 topCompletedNotCompleted: Entry[]; 43 43 topRatedNotCompleted: Entry[]; 44 44 }) => {
+5 -1
app/(app)/dashboard/state.ts
··· 1 1 import { 2 2 Category, 3 3 Entry, 4 + EntryAlternativeTitle, 5 + EntryTranslation, 4 6 User, 5 7 UserEntry, 6 8 UserEntryStatus, ··· 8 10 import { create } from 'zustand'; 9 11 10 12 export type FilterStyle = 'rating' | 'az' | 'completed' | 'updated'; 11 - export type ExtendedUserEntry = UserEntry & { entry: Entry } & { user: User }; 13 + export type ExtendedUserEntry = UserEntry & { 14 + entry: Entry & { translations: EntryTranslation[] }; 15 + } & { user: User }; 12 16 13 17 type DashboardStore = { 14 18 filterStatus: UserEntryStatus | 'all';
+18 -2
app/(app)/users/[username]/_components/diary.ts
··· 1 + import { getUserTitleFromEntry } from '@/server/api/routers/dashboard'; 2 + import { validateSessionToken } from '@/server/auth/validateSession'; 1 3 import prisma from '@/server/db'; 2 4 3 5 export type Diary = Record< ··· 9 11 >; 10 12 11 13 export const getUserDiary = async (userId: number): Promise<Diary> => { 14 + const authUser = await validateSessionToken(); 12 15 const userEntries = await prisma.userEntry.findMany({ 13 16 where: { 14 17 userId, ··· 19 22 }, 20 23 take: 10, 21 24 include: { 22 - entry: true, 25 + entry: { 26 + include: { 27 + translations: { 28 + where: { 29 + language: { 30 + id: authUser 31 + ? (authUser.showMediaMetaInId ?? undefined) 32 + : undefined, 33 + iso_639_1: !authUser ? 'en' : undefined, 34 + }, 35 + }, 36 + }, 37 + }, 38 + }, 23 39 }, 24 40 }); 25 41 ··· 37 53 diary[monthMinusYear] = []; 38 54 } 39 55 diary[monthMinusYear]!.push({ 40 - title: userEntry.entry.originalTitle, 56 + title: getUserTitleFromEntry(userEntry.entry), 41 57 day: userEntry.watchedAt!.getDate(), 42 58 }); 43 59 });
+10
app/(app)/users/[username]/_components/serverUserEntryTitle.tsx
··· 1 + import { getUserTitleFromEntryId } from '@/server/api/routers/dashboard'; 2 + 3 + export const ServerEntryTitleForUser = async ({ 4 + entryId, 5 + }: { 6 + entryId: number; 7 + }) => { 8 + const title = await getUserTitleFromEntryId(entryId); 9 + return title; 10 + };
+12 -14
app/(app)/users/[username]/_components/sidebar.tsx
··· 1 - 'use client'; 2 - 3 - import { useMediaQuery } from 'usehooks-ts'; 4 1 import SmallRating from '@/components/smallRating'; 5 2 import { Entry, User, UserEntry, UserFollow } from '@prisma/client'; 6 3 import { cn } from '@/lib/utils'; ··· 8 5 import { Calendar } from 'lucide-react'; 9 6 import { Progress } from '@/components/ui/progress'; 10 7 import { Fragment } from 'react'; 8 + import { ServerEntryTitleForUser } from './serverUserEntryTitle'; 11 9 12 10 export const ProfileSidebar = ({ 13 11 profileUser, ··· 47 45 return ( 48 46 <div className={cn('flex flex-col gap-6', className)}> 49 47 <div className="flex flex-col gap-4"> 50 - <div className="border-b-base-200 flex justify-between border-b pb-2 text-lg font-medium"> 48 + <div className="flex justify-between border-b border-b-base-200 pb-2 text-lg font-medium"> 51 49 Watchlist 52 - <span className="text-base-500 ms-auto"> 50 + <span className="ms-auto text-base-500"> 53 51 { 54 52 profileUser.userEntries.filter(e => e.status === 'planning') 55 53 .length ··· 76 74 {profileUser.userEntries.filter(e => e.status === 'watching').length > 77 75 0 && ( 78 76 <div className="flex flex-col gap-4"> 79 - <div className="border-b-base-200 flex w-full justify-between border-b pb-2 text-lg font-medium"> 77 + <div className="flex w-full justify-between border-b border-b-base-200 pb-2 text-lg font-medium"> 80 78 In Progress 81 - <span className="text-base-500 ms-auto"> 79 + <span className="ms-auto text-base-500"> 82 80 { 83 81 profileUser.userEntries.filter(e => e.status === 'watching') 84 82 .length ··· 89 87 .filter(e => e.status === 'watching') 90 88 .map(entry => ( 91 89 <div className="flex flex-col gap-1.5" key={entry.id}> 92 - {entry.entry.originalTitle} 90 + <ServerEntryTitleForUser entryId={entry.entry.id} /> 93 91 <Progress 94 92 value={(entry.progress / entry.entry.length) * 100} 95 93 /> ··· 99 97 )} 100 98 </div> 101 99 <div className="flex flex-col items-center gap-4 lg:items-start"> 102 - <div className="border-b-base-200 flex w-full justify-between border-b pb-2 text-lg font-medium"> 100 + <div className="flex w-full justify-between border-b border-b-base-200 pb-2 text-lg font-medium"> 103 101 Ratings 104 - <span className="text-base-500 ms-auto">{totalRatings}</span> 102 + <span className="ms-auto text-base-500">{totalRatings}</span> 105 103 </div> 106 104 <div className="flex w-full flex-row items-end justify-between gap-2 2xl:w-[250px]"> 107 105 <SmallRating rating={20} className="pb-1" /> 108 - <div className="border-b-base-200 flex w-[135px] flex-row items-end gap-0.5 border-b pb-1"> 106 + <div className="flex w-[135px] flex-row items-end gap-0.5 border-b border-b-base-200 pb-1"> 109 107 {ratings.map((ratingPercentage, idx) => ( 110 108 <div 111 109 key={idx} 112 - className="bg-base-300 w-full" 110 + className="w-full bg-base-300" 113 111 style={{ height: 110 * ratingPercentage }} 114 112 ></div> 115 113 ))} ··· 126 124 Daily streak hasn't been updated 127 125 </div> 128 126 )} 129 - <div className="border-b-base-200 flex w-full justify-between border-b pb-2 text-lg font-medium"> 127 + <div className="flex w-full justify-between border-b border-b-base-200 pb-2 text-lg font-medium"> 130 128 Diary 131 - <span className="text-base-500 ms-auto"> 129 + <span className="ms-auto text-base-500"> 132 130 {profileUser.dailyStreakLength} Day Streak 133 131 </span> 134 132 </div>
+24 -4
app/(app)/users/[username]/page.tsx
··· 12 12 import { ProfileHeader } from './_components/header'; 13 13 import { ProfileSidebar } from './_components/sidebar'; 14 14 import { Stats } from './_components/stats'; 15 + import { getUserTitleFromEntry } from '@/server/api/routers/dashboard'; 16 + import { ServerEntryTitleForUser } from './_components/serverUserEntryTitle'; 15 17 16 18 const Profile404 = async () => { 17 19 const user = await validateSessionToken(); ··· 133 135 watchedAt: true, 134 136 entry: { 135 137 select: { 138 + id: true, 136 139 originalTitle: true, 137 140 posterPath: true, 138 141 length: true, ··· 175 178 select: { 176 179 notes: false, 177 180 rating: true, 178 - entry: true, 181 + entry: { 182 + select: { 183 + originalTitle: true, 184 + posterPath: true, 185 + releaseDate: true, 186 + category: true, 187 + translations: { 188 + where: { 189 + language: { 190 + id: user ? (user.showMediaMetaInId ?? undefined) : undefined, 191 + iso_639_1: !user ? 'en' : undefined, 192 + }, 193 + }, 194 + }, 195 + }, 196 + }, 179 197 }, 180 198 }); 181 199 ··· 242 260 <UserEntryCard 243 261 key={`userEntry-${idx}`} 244 262 {...{ 245 - title: userEntry.entry.originalTitle, 263 + title: getUserTitleFromEntry(userEntry.entry as any), 246 264 backgroundImage: userEntry.entry.posterPath, 247 265 releaseDate: userEntry.entry.releaseDate, 248 266 rating: userEntry.rating, ··· 256 274 <UserEntryCard 257 275 key={`userEntry-${idx}`} 258 276 {...{ 259 - title: userEntry.entry.originalTitle, 277 + title: getUserTitleFromEntry(userEntry.entry as any), 260 278 backgroundImage: userEntry.entry.posterPath, 261 279 releaseDate: userEntry.entry.releaseDate, 262 280 rating: userEntry.rating, ··· 306 324 <div className="flex h-full flex-col justify-center gap-3 pb-3 2xl:border-b-0 2xl:pb-0"> 307 325 <div className="space-x-3"> 308 326 <span className="text-lg font-semibold 2xl:text-2xl"> 309 - {activity.entry.originalTitle} 327 + <ServerEntryTitleForUser 328 + entryId={activity.entry.id} 329 + /> 310 330 </span> 311 331 <span className="text-sm font-medium text-base-500"> 312 332 {activity.entry.releaseDate.getFullYear()}
+41 -4
app/(guest)/page.tsx
··· 14 14 } from '@/components/ui/card'; 15 15 import UserEntryCard, { UserEntryCardObject } from '@/components/userEntryCard'; 16 16 import prisma from '@/server/db'; 17 - import { Entry } from '@prisma/client'; 17 + import { Entry, EntryAlternativeTitle } from '@prisma/client'; 18 18 import { ExtendedUserEntry } from '../(app)/dashboard/state'; 19 19 20 20 const Page = async () => { 21 21 const user = await validateSessionToken(); 22 22 23 + const getEnglishTitles = async (entryId: number) => { 24 + return await prisma.entryTranslation.findMany({ 25 + where: { 26 + entryId: parseInt(entryId.toString()), 27 + language: { 28 + iso_639_1: 'en', 29 + }, 30 + }, 31 + }); 32 + }; 33 + 23 34 const randomMovie = ( 24 35 (await prisma.$queryRawUnsafe( 25 36 `SELECT * FROM Entry WHERE category = "Movie" ORDER BY RAND() LIMIT 1;` 26 37 )) as Entry[] 27 38 )[0] as Entry; 28 39 40 + const movieTitles = await getEnglishTitles(randomMovie.id); 41 + 29 42 const randomSeries = ( 30 43 (await prisma.$queryRawUnsafe( 31 44 `SELECT * FROM Entry WHERE category = "Series" ORDER BY RAND() LIMIT 1;` 32 45 )) as Entry[] 33 46 )[0] as Entry; 34 47 48 + const seriesTitles = await getEnglishTitles(randomSeries.id); 49 + 35 50 const randomBook = ( 36 51 (await prisma.$queryRawUnsafe( 37 52 `SELECT * FROM Entry WHERE category = "Book" ORDER BY RAND() LIMIT 1;` 38 53 )) as Entry[] 39 54 )[0] as Entry; 55 + const bookTitles = await getEnglishTitles(randomBook.id); 40 56 41 57 return ( 42 58 <div className="flex w-full flex-col items-center bg-white"> ··· 82 98 <CardContent className="grid grid-cols-3 gap-3 lg:gap-6"> 83 99 {randomMovie && ( 84 100 <UserEntryCardObject 85 - userEntry={{ entry: randomMovie } as ExtendedUserEntry} 101 + userEntry={ 102 + { 103 + entry: { 104 + ...randomMovie, 105 + translations: movieTitles, 106 + }, 107 + } as ExtendedUserEntry 108 + } 86 109 /> 87 110 )} 88 111 {randomBook && ( 89 112 <UserEntryCardObject 90 - userEntry={{ entry: randomBook } as ExtendedUserEntry} 113 + userEntry={ 114 + { 115 + entry: { 116 + ...randomBook, 117 + translations: bookTitles, 118 + }, 119 + } as ExtendedUserEntry 120 + } 91 121 /> 92 122 )} 93 123 {randomSeries && ( 94 124 <UserEntryCardObject 95 - userEntry={{ entry: randomSeries } as ExtendedUserEntry} 125 + userEntry={ 126 + { 127 + entry: { 128 + ...randomSeries, 129 + translations: seriesTitles, 130 + }, 131 + } as ExtendedUserEntry 132 + } 96 133 /> 97 134 )} 98 135 </CardContent>
+32
app/_components/SettingsContext.tsx
··· 1 + 'use client'; 2 + 3 + import { Language, Theme } from '@prisma/client'; 4 + import { createContext, useContext } from 'react'; 5 + 6 + export type Settings = { 7 + theme: Theme; 8 + showMediaMetaIn: Language; 9 + }; 10 + const SettingsContext = createContext<Settings | null>(null); 11 + 12 + export function SettingsProvider({ 13 + settings, 14 + children, 15 + }: { 16 + settings: Settings; 17 + children: React.ReactNode; 18 + }) { 19 + return ( 20 + <SettingsContext.Provider value={settings}> 21 + {children} 22 + </SettingsContext.Provider> 23 + ); 24 + } 25 + 26 + export function useSettings() { 27 + const context = useContext(SettingsContext); 28 + if (!context) { 29 + throw new Error('useTheme must be used within a ThemeProvider'); 30 + } 31 + return context; 32 + }
-26
app/_components/ThemeContext.tsx
··· 1 - 'use client'; 2 - 3 - import { Theme } from '@prisma/client'; 4 - import { createContext, useContext } from 'react'; 5 - 6 - const ThemeContext = createContext<Theme | null>(null); 7 - 8 - export function ThemeProvider({ 9 - theme, 10 - children, 11 - }: { 12 - theme: Theme; 13 - children: React.ReactNode; 14 - }) { 15 - return ( 16 - <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider> 17 - ); 18 - } 19 - 20 - export function useTheme() { 21 - const context = useContext(ThemeContext); 22 - if (!context) { 23 - throw new Error('useTheme must be used within a ThemeProvider'); 24 - } 25 - return context; 26 - }
+37
app/_components/settings.ts
··· 1 + import prisma from '@/server/db'; 2 + import { Settings } from './SettingsContext'; 3 + 4 + import { validateSessionToken } from '@/server/auth/validateSession'; 5 + import { Language } from '@prisma/client'; 6 + 7 + export const getTheme = async () => { 8 + const user = await validateSessionToken(); 9 + return user ? user.theme : 'neutral'; 10 + }; 11 + 12 + export const getUserShowMediaIn = async () => { 13 + const user = await validateSessionToken(); 14 + let language: Language; 15 + if (user && user.showMediaMetaInId) { 16 + language = (await prisma.language.findFirst({ 17 + where: { 18 + id: user.showMediaMetaInId, 19 + }, 20 + }))!; 21 + } else { 22 + // Default to english 23 + language = (await prisma.language.findFirst({ 24 + where: { 25 + iso_639_1: 'en', 26 + }, 27 + }))!; 28 + } 29 + return language; 30 + }; 31 + 32 + export const getSettings = async () => { 33 + return { 34 + theme: await getTheme(), 35 + showMediaMetaIn: await getUserShowMediaIn(), 36 + } satisfies Settings; 37 + };
-6
app/_components/theme.ts
··· 1 - import { validateSessionToken } from '@/server/auth/validateSession'; 2 - 3 - export const getTheme = async () => { 4 - const user = await validateSessionToken(); 5 - return user ? user.theme : 'neutral'; 6 - };
+32
app/api/import/movie/route.ts
··· 33 33 `https://api.themoviedb.org/3/movie/${id}/alternative_titles?language=en-US`, 34 34 options 35 35 ); 36 + const translations = await axios.get( 37 + `https://api.themoviedb.org/3/movie/${id}/translations?language=en-US`, 38 + options 39 + ); 36 40 const credits = await axios.get( 37 41 `https://api.themoviedb.org/3/movie/${id}/credits?language=en-US`, 38 42 options ··· 52 56 } 53 57 54 58 data['alternative_titles'] = altTitles.data['titles']; 59 + data['translations'] = translations.data['translations']; 55 60 data['cast'] = creditsData['cast']; 56 61 data['crew'] = creditsData['crew']; 57 62 data['watch_providers'] = watchProviders.data['results']; ··· 78 83 const existingCollection = await prisma.collection.findFirst({ 79 84 where: { 80 85 foreignId: data.belongs_to_collection.id.toString(), 86 + category: 'Movie', 81 87 }, 82 88 }); 83 89 ··· 389 395 }) 390 396 )?.id!, 391 397 title: alternativeTitle.title, 398 + }, 399 + }); 400 + } 401 + 402 + for (const translation of data.translations) { 403 + await prisma.entryTranslation.create({ 404 + data: { 405 + entryId: entry.id!, 406 + countryId: ( 407 + await prisma.country.findFirst({ 408 + where: { 409 + iso_3166_1: translation.iso_3166_1, 410 + }, 411 + }) 412 + )?.id!, 413 + languageId: ( 414 + await prisma.language.findFirst({ 415 + where: { 416 + iso_639_1: translation.iso_639_1, 417 + }, 418 + }) 419 + )?.id!, 420 + name: translation.name, 421 + overview: translation.overview, 422 + homepage: translation.homepage, 423 + tagline: translation.tagline, 392 424 }, 393 425 }); 394 426 }
+42 -4
app/api/import/series/route.ts
··· 34 34 `https://api.themoviedb.org/3/tv/${id}/alternative_titles?language=en-US`, 35 35 options 36 36 ); 37 + const translations = await axios.get( 38 + `https://api.themoviedb.org/3/tv/${id}/translations?language=en-US`, 39 + options 40 + ); 37 41 const watchProviders = await axios.get( 38 42 `https://api.themoviedb.org/3/tv/${id}/watch/providers?language=en-US`, 39 43 options ··· 47 51 } 48 52 49 53 data['alternative_titles'] = altTitles.data['results']; 54 + data['translations'] = translations.data['translations']; 50 55 data['watch_providers'] = watchProviders.data['results']; 51 56 52 57 for (var i = 0; i < data.seasons.length; i++) { ··· 65 70 66 71 let entry = await prisma.entry.findFirst({ 67 72 where: { 68 - foreignId: data.id.toString(), 69 73 category: 'Series', 74 + collection: { 75 + foreignId: data.id.toString(), 76 + }, 70 77 }, 71 78 }); 72 79 ··· 84 91 const existingCollection = await prisma.collection.findFirst({ 85 92 where: { 86 93 foreignId: data.id.toString(), 94 + category: 'Series', 87 95 }, 88 96 }); 89 97 ··· 108 116 for (const season of data.seasons) { 109 117 const existingEntry = await prisma.entry.findFirst({ 110 118 where: { 111 - foreignId: season.id.toString(), 119 + foreignId: season.season_number.toString(), 120 + collection: { 121 + foreignId: data.id.toString(), 122 + }, 123 + category: 'Series', 112 124 }, 113 125 }); 114 126 ··· 119 131 entry = await prisma.entry.create({ 120 132 data: { 121 133 originalTitle: `${data.original_name}: ${season.name}`, 122 - foreignId: season.id.toString(), 134 + foreignId: season.season_number.toString(), 123 135 collectionId, 124 136 posterPath: 'https://image.tmdb.org/t/p/original/' + season.poster_path, 125 137 tagline: data.tagline, ··· 138 150 }, 139 151 }); 140 152 141 - if (firstSeason === undefined) { 153 + if (season.season_number == 1) { 142 154 firstSeason = entry; 143 155 } 144 156 ··· 411 423 }, 412 424 }); 413 425 } 426 + } 427 + 428 + for (const translation of data.translations) { 429 + await prisma.entryTranslation.create({ 430 + data: { 431 + entryId: entry!.id!, 432 + countryId: ( 433 + await prisma.country.findFirst({ 434 + where: { 435 + iso_3166_1: translation.iso_3166_1, 436 + }, 437 + }) 438 + )?.id!, 439 + languageId: ( 440 + await prisma.language.findFirst({ 441 + where: { 442 + iso_639_1: translation.iso_639_1, 443 + }, 444 + }) 445 + )?.id!, 446 + name: translation.name, 447 + overview: translation.overview, 448 + homepage: translation.homepage, 449 + tagline: translation.tagline, 450 + }, 451 + }); 414 452 } 415 453 416 454 return Response.json({
+83
app/api/migration/fix_fid/route.ts
··· 1 + import { validateSessionToken } from '@/server/auth/validateSession'; 2 + import prisma from '@/server/db'; 3 + import axios from 'axios'; 4 + import { NextRequest } from 'next/server'; 5 + 6 + export const GET = async (req: NextRequest) => { 7 + const user = await validateSessionToken(); 8 + if (!user) { 9 + return; 10 + } 11 + 12 + if (process.env.NODE_ENV !== 'development') { 13 + return new Response('Not dev', { 14 + status: 400, 15 + }); 16 + } 17 + 18 + const options = { 19 + headers: { 20 + accept: 'application/json', 21 + Authorization: `Bearer ${process.env.TMDB_ACCESS_TOKEN}`, 22 + }, 23 + }; 24 + 25 + let ignore: number[] = []; 26 + const entries = await prisma.entry.findMany({ 27 + include: { 28 + collection: true, 29 + }, 30 + }); 31 + for (const entry of entries) { 32 + console.log('\n----------'); 33 + if (entry.category !== 'Series') { 34 + console.log('Skipped non series'); 35 + continue; 36 + } 37 + 38 + console.log('Processing series'); 39 + 40 + const seasons = ( 41 + await axios.get( 42 + `https://api.themoviedb.org/3/tv/${entry.collection?.foreignId}?language=en-US`, 43 + options 44 + ) 45 + ).data['seasons']; 46 + 47 + for (const season of seasons) { 48 + const seasonEntry = await prisma.entry.findFirst({ 49 + where: { 50 + foreignId: season['id'].toString(), 51 + }, 52 + }); 53 + 54 + if (!seasonEntry) { 55 + continue; 56 + return new Response('issue ' + JSON.stringify(season), { 57 + status: 200, 58 + }); 59 + } 60 + 61 + if (ignore.includes(seasonEntry['id'])) { 62 + continue; 63 + } 64 + 65 + console.log(season); 66 + 67 + await prisma.entry.updateMany({ 68 + where: { 69 + foreignId: season['id'].toString(), 70 + }, 71 + data: { 72 + foreignId: season['season_number'].toString(), 73 + }, 74 + }); 75 + 76 + ignore.push(seasonEntry['id']); 77 + } 78 + } 79 + 80 + return new Response('Finish', { 81 + status: 200, 82 + }); 83 + };
+206
app/api/migration/translations/route.ts
··· 1 + import { validateSessionToken } from '@/server/auth/validateSession'; 2 + import prisma from '@/server/db'; 3 + import axios from 'axios'; 4 + import { Pi } from 'lucide-react'; 5 + import { NextRequest } from 'next/server'; 6 + 7 + export const GET = async (req: NextRequest) => { 8 + const user = await validateSessionToken(); 9 + if (!user) { 10 + return; 11 + } 12 + 13 + if (process.env.NODE_ENV !== 'development') { 14 + return new Response('Not dev', { 15 + status: 400, 16 + }); 17 + } 18 + 19 + const options = { 20 + headers: { 21 + accept: 'application/json', 22 + Authorization: `Bearer ${process.env.TMDB_ACCESS_TOKEN}`, 23 + }, 24 + }; 25 + 26 + const langHr = await prisma.language.findFirst({ 27 + where: { 28 + iso_639_1: 'hr', 29 + }, 30 + }); 31 + if (!langHr) { 32 + await prisma.language.create({ 33 + data: { 34 + name: 'Croatian', 35 + iso_639_1: 'hr', 36 + iso_639_2: 'hrv', 37 + }, 38 + }); 39 + } 40 + 41 + const langZh = await prisma.language.findFirst({ 42 + where: { 43 + iso_639_1: 'zh', 44 + }, 45 + }); 46 + if (!langZh) { 47 + await prisma.language.create({ 48 + data: { 49 + name: 'Chinese', 50 + iso_639_1: 'zh', 51 + iso_639_2: 'chi', 52 + }, 53 + }); 54 + } 55 + 56 + const langSr = await prisma.language.findFirst({ 57 + where: { 58 + iso_639_1: 'sr', 59 + }, 60 + }); 61 + if (!langSr) { 62 + await prisma.language.create({ 63 + data: { 64 + name: 'Serbian', 65 + iso_639_1: 'sr', 66 + iso_639_2: 'srp', 67 + }, 68 + }); 69 + } 70 + 71 + const langBs = await prisma.language.findFirst({ 72 + where: { 73 + iso_639_1: 'bs', 74 + }, 75 + }); 76 + if (!langBs) { 77 + await prisma.language.create({ 78 + data: { 79 + name: 'Bosnian', 80 + iso_639_1: 'bs', 81 + iso_639_2: 'bos', 82 + }, 83 + }); 84 + } 85 + 86 + await prisma.entryTranslation.deleteMany(); 87 + console.log(`asd ${(await prisma.entryTranslation.findMany()).length}`); 88 + 89 + const entries = await prisma.entry.findMany({ 90 + include: { 91 + collection: true, 92 + }, 93 + }); 94 + 95 + for (const entry of entries) { 96 + if (entry.category === 'Book') { 97 + continue; 98 + } 99 + console.log('\n------'); 100 + console.log(entry.id); 101 + 102 + let url = ''; 103 + if (entry.category === 'Series') { 104 + url = `https://api.themoviedb.org/3/tv/${entry.collection?.foreignId}/season/${entry.foreignId}/translations?language=en-US`; 105 + } else { 106 + url = `https://api.themoviedb.org/3/movie/${entry.foreignId}/translations?language=en-US`; 107 + } 108 + 109 + console.log(url); 110 + let translations; 111 + try { 112 + translations = (await axios.get(url, options)).data['translations']; 113 + } catch { 114 + console.log('request failed'); 115 + continue; 116 + } 117 + 118 + for (const translation of translations) { 119 + console.log(translation['iso_639_1']); 120 + if (translation['iso_639_1'] === 'sh') { 121 + continue; 122 + } 123 + const language = await prisma.language.findFirst({ 124 + where: { 125 + iso_639_1: translation['iso_639_1'], 126 + }, 127 + }); 128 + 129 + const country = await prisma.country.findFirst({ 130 + where: { 131 + iso_3166_1: translation['iso_3166_1'], 132 + }, 133 + }); 134 + 135 + let collectionTranslation = undefined; 136 + if (entry.category === 'Series') { 137 + collectionTranslation = ( 138 + ( 139 + await axios.get( 140 + `https://api.themoviedb.org/3/tv/${entry.collection?.foreignId}/translations?language=en-US`, 141 + options 142 + ) 143 + ).data['translations'] as any[] 144 + ).find(e => e.iso_639_1 === translation.iso_639_1); 145 + } 146 + 147 + const translationExists = await prisma.entryTranslation.findFirst({ 148 + where: { 149 + entryId: entry.id, 150 + name: translation.name, 151 + languageId: language!.id, 152 + }, 153 + }); 154 + 155 + if (translationExists) { 156 + continue; 157 + } 158 + 159 + let title = ''; 160 + if (entry.category === 'Series') { 161 + if (!collectionTranslation.data.name) { 162 + console.log(`No name for collection ${entry.id} ${translation.name}`); 163 + continue; 164 + } 165 + 166 + let season = translation.data.name 167 + ? translation.data.name 168 + : parseInt(entry.foreignId) > 0 169 + ? 'Season ' + entry.foreignId 170 + : 'Specials'; 171 + 172 + title = 173 + collectionTranslation.data.name + 174 + (season === collectionTranslation.data.name ? '' : `: ${season}`); 175 + } else if (translation.data.title) { 176 + title = translation.data.title; 177 + } else { 178 + console.log(`No name for ${entry.id} ${translation.name}`); 179 + continue; 180 + } 181 + 182 + await prisma.entryTranslation.create({ 183 + data: { 184 + entryId: entry.id, 185 + languageId: language!.id, 186 + countryId: country ? country.id : undefined, 187 + name: title, 188 + overview: 189 + entry.category === 'Series' 190 + ? (collectionTranslation.overview ?? '') 191 + : (translation.overview ?? ''), 192 + tagline: 193 + entry.category === 'Series' 194 + ? (collectionTranslation.tagline ?? '') 195 + : (translation.tagline ?? ''), 196 + homepage: 197 + entry.category === 'Series' 198 + ? (collectionTranslation.overview ?? '') 199 + : (translation.overview ?? ''), 200 + }, 201 + }); 202 + 203 + console.log(`Finished ${entry.id} ${title} in ${language?.iso_639_1}`); 204 + } 205 + } 206 + };
+1 -1
app/auth/layout.tsx
··· 2 2 import { redirect } from 'next/navigation'; 3 3 import { ReactNode } from 'react'; 4 4 import { Toaster } from 'sonner'; 5 - import { getTheme } from '../_components/theme'; 5 + import { getTheme } from '../_components/settings'; 6 6 7 7 const Layout = async ({ children }: { children: ReactNode }) => { 8 8 const theme = await getTheme();
+5 -5
app/layout.tsx
··· 2 2 import '@/styles/themes.css'; 3 3 import { TRPCReactProvider } from '@/trpc/react'; 4 4 import { Metadata, Viewport } from 'next'; 5 - import { ThemeProvider } from './_components/ThemeContext'; 6 - import { getTheme } from './_components/theme'; 5 + import { SettingsProvider } from './_components/SettingsContext'; 7 6 import Providers from './providers'; 7 + import { getSettings } from './_components/settings'; 8 8 9 9 export default async function RootLayout({ 10 10 children, 11 11 }: { 12 12 children: React.ReactNode; 13 13 }) { 14 - const theme = await getTheme(); 14 + const settings = await getSettings(); 15 15 16 16 return ( 17 17 <TRPCReactProvider> 18 - <ThemeProvider theme={theme}> 18 + <SettingsProvider settings={settings}> 19 19 <Providers>{children}</Providers> 20 - </ThemeProvider> 20 + </SettingsProvider> 21 21 </TRPCReactProvider> 22 22 ); 23 23 }
+91 -34
components/islands/settings.tsx
··· 1 1 import { useAuthUser } from '@/app/(app)/_components/AuthUserContext'; 2 - import { useTheme } from '@/app/_components/ThemeContext'; 2 + import { useSettings } from '@/app/_components/SettingsContext'; 3 3 import { capitalizeFirst } from '@/lib/capitalizeFirst'; 4 4 import { colors } from '@/lib/colors'; 5 5 import { api } from '@/trpc/react'; 6 - import { Theme } from '@prisma/client'; 6 + import { Language, Theme } from '@prisma/client'; 7 7 import { Sun } from 'lucide-react'; 8 8 import { useRouter } from 'next/navigation'; 9 9 import { useEffect, useState } from 'react'; ··· 12 12 import { Card, CardContent } from '../ui/card'; 13 13 import { Label } from '../ui/label'; 14 14 import { Select, SelectContent, SelectItem, SelectTrigger } from '../ui/select'; 15 + import { toast } from 'sonner'; 15 16 16 17 const Settings = () => { 17 - const selectedTheme_ = useTheme(); 18 + const originalSettings = useSettings(); 18 19 const [selectedTheme, setSelectedTheme] = useState<Theme>(); 20 + const [selectedShowMediaMetaIn, setSelectedShowMediaMetaIn] = 21 + useState<Language>(); 19 22 20 23 const router = useRouter(); 21 24 const setTheme = api.settings.setTheme.useMutation({ 22 25 onSuccess: () => { 26 + toast.success(`Set theme to ${selectedTheme}`); 23 27 router.refresh(); 24 28 }, 25 29 }); 30 + const setShowMediaMetaIn = api.settings.setShowMediaMetaIn.useMutation({ 31 + onSuccess: () => { 32 + toast.success( 33 + `Set show media meta to ${selectedShowMediaMetaIn ? selectedShowMediaMetaIn.name : ''}` 34 + ); 35 + router.refresh(); 36 + }, 37 + }); 38 + 39 + const languages = api.settings.getLanguages.useQuery(); 26 40 27 41 useEffect(() => { 28 - setSelectedTheme(selectedTheme_); 42 + setSelectedTheme(originalSettings.theme); 43 + setSelectedShowMediaMetaIn(originalSettings.showMediaMetaIn); 29 44 }, []); 30 45 31 46 const user = useAuthUser(); ··· 90 105 Appearance 91 106 </div> 92 107 <Card> 93 - <CardContent className="flex items-center justify-between p-2 ps-4"> 94 - <div className="whitespace-nowrap text-sm font-medium text-base-600"> 95 - Theme 108 + <CardContent className="divide-y divide-base-100 p-0 pe-2 ps-4"> 109 + <div className="flex items-center justify-between py-2"> 110 + <div className="whitespace-nowrap text-sm font-medium text-base-600"> 111 + Theme 112 + </div> 113 + <div className="w-fit"> 114 + <Select 115 + value={selectedTheme} 116 + onValueChange={theme => { 117 + setSelectedTheme(theme as Theme); 118 + setTheme.mutate({ theme }); 119 + }} 120 + > 121 + <SelectTrigger> 122 + {selectedTheme ? ( 123 + <ThemeComponent theme={selectedTheme} /> 124 + ) : ( 125 + 'No theme selected' 126 + )} 127 + </SelectTrigger> 128 + <SelectContent> 129 + {Object.keys(Theme).map(theme => ( 130 + <SelectItem 131 + key={theme} 132 + value={theme} 133 + className="cursor-pointer hover:bg-base-400" 134 + > 135 + <ThemeComponent theme={theme} /> 136 + </SelectItem> 137 + ))} 138 + </SelectContent> 139 + </Select> 140 + </div> 96 141 </div> 97 - <div className="w-fit"> 98 - <Select 99 - value={selectedTheme} 100 - onValueChange={theme => { 101 - setSelectedTheme(theme as Theme); 102 - setTheme.mutate({ theme }); 103 - }} 104 - > 105 - <SelectTrigger> 106 - {selectedTheme ? ( 107 - <ThemeComponent theme={selectedTheme} /> 108 - ) : ( 109 - 'No theme selected' 110 - )} 111 - </SelectTrigger> 112 - <SelectContent> 113 - {Object.keys(Theme).map(theme => ( 114 - <SelectItem 115 - key={theme} 116 - value={theme} 117 - className="cursor-pointer hover:bg-base-400" 118 - > 119 - <ThemeComponent theme={theme} /> 120 - </SelectItem> 121 - ))} 122 - </SelectContent> 123 - </Select> 142 + <div className="flex items-center justify-between py-2"> 143 + <div className="whitespace-nowrap text-sm font-medium text-base-600"> 144 + Show media meda in 145 + </div> 146 + <div className="w-fit"> 147 + <Select 148 + value={ 149 + selectedShowMediaMetaIn 150 + ? selectedShowMediaMetaIn.iso_639_2! 151 + : '' 152 + } 153 + onValueChange={iso_639_2 => { 154 + setSelectedShowMediaMetaIn( 155 + languages.data!.find(e => e.iso_639_2 === iso_639_2) 156 + ); 157 + setShowMediaMetaIn.mutate({ iso_639_2: iso_639_2 }); 158 + }} 159 + > 160 + <SelectTrigger> 161 + {selectedShowMediaMetaIn 162 + ? selectedShowMediaMetaIn.name 163 + : 'No language selected'} 164 + </SelectTrigger> 165 + <SelectContent> 166 + {languages.data && 167 + languages.data 168 + .sort((a, b) => a.name.localeCompare(b.name)) 169 + .map(language => ( 170 + <SelectItem 171 + key={language.id} 172 + value={language.iso_639_2!} 173 + className="cursor-pointer hover:bg-base-400" 174 + > 175 + {language.name} 176 + </SelectItem> 177 + ))} 178 + </SelectContent> 179 + </Select> 180 + </div> 124 181 </div> 125 182 </CardContent> 126 183 </Card>
+1 -1
components/layouts/base.tsx
··· 1 - import { getTheme } from '@/app/_components/theme'; 1 + import { getTheme } from '@/app/_components/settings'; 2 2 import { cn } from '@/lib/utils'; 3 3 import React, { ReactNode } from 'react'; 4 4
+1 -1
components/layouts/sidebar.tsx
··· 1 - import { getTheme } from '@/app/_components/theme'; 1 + import { getTheme } from '@/app/_components/settings'; 2 2 import { cn } from '@/lib/utils'; 3 3 import React, { ReactNode } from 'react'; 4 4 import BaseLayout from './base';
+9 -1
components/modifyUserEntry.tsx
··· 52 52 } from './ui/dialog'; 53 53 import { Label } from './ui/label'; 54 54 import { DateTimePicker } from './ui/date-time-picker'; 55 + import { api } from '@/trpc/react'; 55 56 56 57 const ModifyUserEntry = ({ 57 58 userEntry, ··· 62 63 setUserEntry, 63 64 removeUserEntry: removeUserEntryClient, 64 65 }: { 65 - userEntry: UserEntry & { user: User } & { entry: Entry }; 66 + userEntry: ExtendedUserEntry; 66 67 userLists: UserList[]; 67 68 userListsWithEntry: UserList[]; 68 69 refetchUserLists: () => Promise<void>; ··· 78 79 saveUserEntry, 79 80 {} 80 81 ); 82 + 83 + const utils = api.useUtils(); 84 + useEffect(() => { 85 + if (saveUserEntryState.message) { 86 + utils.dashboard.invalidate(); 87 + } 88 + }, [saveUserEntryState]); 81 89 const [removeUserEntryState, removeUserEntryAction] = useFormState( 82 90 removeUserEntry, 83 91 {}
+4 -1
components/userEntryCard.tsx
··· 87 87 } & HTMLProps<HTMLDivElement>) => ( 88 88 <UserEntryCard 89 89 {...{ 90 - title: userEntry.entry.originalTitle, 90 + title: 91 + userEntry.entry.translations.length !== 0 92 + ? userEntry.entry.translations[0]!.name 93 + : userEntry.entry.originalTitle, 91 94 backgroundImage: userEntry.entry.posterPath, 92 95 releaseDate: userEntry.entry.releaseDate, 93 96 category: userEntry.entry.category,
+1 -1
components/userEntryExternal.tsx
··· 13 13 openOverride, 14 14 setUserEntry, 15 15 }: { 16 - userEntry: UserEntry & { user: User } & { entry: Entry }; 16 + userEntry: ExtendedUserEntry; 17 17 openOverride?: 18 18 | [open: boolean, setOpen: Dispatch<SetStateAction<boolean>>] 19 19 | undefined;
+73
migration/fix_series_foreign_id.py
··· 1 + import mysql.connector 2 + import requests 3 + import json 4 + import sys 5 + import time 6 + 7 + apiKey = input("apikey:") 8 + 9 + db = mysql.connector.connect( 10 + host="mysql", 11 + user="root", 12 + password="prisma", 13 + database="database" 14 + ) 15 + 16 + cursor = db.cursor(buffered=True, dictionary=True) 17 + cursor2 = db.cursor(buffered=True, dictionary=True) 18 + cursor3 = db.cursor(buffered=True, dictionary=True) 19 + 20 + SERIES_CATEGORY = 1 21 + MOVIE_CATEGORY = 2 22 + BOOK_CATEGORY = 3 23 + 24 + # cursor.execute(f"SELECT * FROM Entry WHERE foreignId = 293342") 25 + # print(cursor.fetchone()) 26 + 27 + ignore = [] 28 + cursor.execute(f"SELECT * FROM Entry") 29 + entries = cursor.fetchall() 30 + # cursor.close() 31 + for entry in entries: 32 + print("\n------------------------") 33 + if(entry["category"] != "Series"): 34 + print("Skipped non series") 35 + continue 36 + print("Processing series") 37 + 38 + cursor.execute(f"SELECT * FROM Collection WHERE id = {entry['collectionId']}") 39 + collection = cursor.fetchone() 40 + # cursor.close() 41 + print(collection) 42 + 43 + seasons = json.loads(requests.get(f"https://api.themoviedb.org/3/tv/{collection['foreignId']}?language=en-US", headers={ 44 + "Authorization": f"Bearer {apiKey}" 45 + }).content)["seasons"] 46 + 47 + for season in seasons: 48 + cursor2.execute("SELECT * FROM Entry WHERE foreignId = %s", (str(season['id']),)) 49 + season_entry = cursor2.fetchone() 50 + print(season_entry) 51 + print(cursor2.statement) 52 + 53 + if season_entry is None: 54 + print(season) 55 + print("issue") 56 + time.sleep(2) 57 + continue 58 + 59 + if season_entry["id"] in ignore: 60 + continue 61 + 62 + print(season) 63 + print(season_entry) 64 + 65 + cursor3.execute( 66 + "UPDATE Entry SET foreignId = %s WHERE foreignId = %s", 67 + (str(season['season_number']), str(season['id'])) 68 + ) 69 + 70 + ignore.append(season_entry["id"]) 71 + 72 + 73 + db.commit()
+54
migration/populate_translations.py
··· 1 + import mysql.connector 2 + import requests 3 + import json 4 + import sys 5 + 6 + apiKey = input("apikey:") 7 + 8 + db = mysql.connector.connect( 9 + host="mysql", 10 + user="root", 11 + password="prisma", 12 + database="database" 13 + ) 14 + 15 + cursor = db.cursor(buffered=True, dictionary=True) 16 + cursor2 = db.cursor(buffered=True, dictionary=True) 17 + 18 + SERIES_CATEGORY = 1 19 + MOVIE_CATEGORY = 2 20 + BOOK_CATEGORY = 3 21 + 22 + cursor.execute(f"SELECT * FROM Entry") 23 + entries = cursor.fetchall() 24 + for entry in entries: 25 + print("\n------------------------") 26 + if(entry["category"] == "Book"): 27 + print("Skipped book") 28 + continue 29 + 30 + cursor.execute(f"SELECT * FROM Collection WHERE id = '{entry['collectionId']}'") 31 + collection = cursor.fetchone() 32 + print(collection) 33 + 34 + if(entry['category'] == 'Movie'): 35 + url = f"https://api.themoviedb.org/3/movie/{entry['foreignId']}/translations?language=en-US" 36 + else: 37 + url = f"https://api.themoviedb.org/3/tv/{entry['foreignId']}/translations?language=en-US" 38 + print(url) 39 + print(entry) 40 + translations = json.loads(requests.get(url, headers={ 41 + "Authorization": f"Bearer {apiKey}" 42 + }).content)["translations"] 43 + print(translations) 44 + 45 + for translation in translations: 46 + cursor.execute(f"SELECT * FROM Language WHERE iso_639_1 = '{translation['iso_639_1']}'") 47 + language = cursor.fetchone() 48 + print(language["name"]) 49 + cursor.execute(f"SELECT * FROM Country WHERE iso_3166_1 = '{translation['iso_3166_1']}'") 50 + country = cursor.fetchone() 51 + print(country["name"]) 52 + 53 + 54 + print(entry["id"])
+317 -292
prisma/schema.prisma
··· 1 1 generator client { 2 - provider = "prisma-client-js" 3 - binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"] 2 + provider = "prisma-client-js" 3 + binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"] 4 4 } 5 5 6 6 datasource db { 7 - provider = "mysql" 8 - url = env("DATABASE_URL") 7 + provider = "mysql" 8 + url = env("DATABASE_URL") 9 9 } 10 10 11 11 enum RatingStyle { 12 - stars 13 - range 12 + stars 13 + range 14 14 } 15 15 16 16 enum Theme { 17 - slate 18 - gray 19 - zinc 20 - neutral 21 - stone 22 - red 23 - orange 24 - amber 25 - yellow 26 - lime 27 - green 28 - emerald 29 - teal 30 - cyan 31 - sky 32 - blue 33 - indigo 34 - violet 35 - purple 36 - fuchsia 37 - pink 38 - rose 17 + slate 18 + gray 19 + zinc 20 + neutral 21 + stone 22 + red 23 + orange 24 + amber 25 + yellow 26 + lime 27 + green 28 + emerald 29 + teal 30 + cyan 31 + sky 32 + blue 33 + indigo 34 + violet 35 + purple 36 + fuchsia 37 + pink 38 + rose 39 39 } 40 40 41 41 model User { 42 - id Int @id @default(autoincrement()) @db.UnsignedInt 43 - username String @unique 44 - email String @unique 45 - password String @db.VarChar(64) 46 - sessions Session[] 47 - ratingStyle RatingStyle @default(stars) 48 - theme Theme @default(neutral) 42 + id Int @id @default(autoincrement()) @db.UnsignedInt 43 + username String @unique 44 + email String @unique 45 + password String @db.VarChar(64) 46 + sessions Session[] 47 + ratingStyle RatingStyle @default(stars) 48 + theme Theme @default(neutral) 49 + showMediaMetaInId Int? @db.UnsignedInt 50 + showMediaMetaIn Language? @relation("showMediaMetaInIn", fields: [showMediaMetaInId], references: [id]) 49 51 50 - dailyStreakStarted DateTime @default(now()) 51 - dailyStreakUpdated DateTime @default(now()) 52 - dailyStreakLength Int @default(0) @db.SmallInt 53 - dailyStreakLongest Int @default(0) @db.SmallInt 52 + dailyStreakStarted DateTime @default(now()) 53 + dailyStreakUpdated DateTime @default(now()) 54 + dailyStreakLength Int @default(0) @db.SmallInt 55 + dailyStreakLongest Int @default(0) @db.SmallInt 54 56 55 - invitedById Int? @db.UnsignedInt 56 - invitedBy User? @relation("Invite", fields: [invitedById], references: [id]) 57 - invitees User[] @relation("Invite") 57 + invitedById Int? @db.UnsignedInt 58 + invitedBy User? @relation("Invite", fields: [invitedById], references: [id]) 59 + invitees User[] @relation("Invite") 58 60 59 - createdAt DateTime @default(now()) 60 - updatedAt DateTime @default(now()) @updatedAt 61 + createdAt DateTime @default(now()) 62 + updatedAt DateTime @default(now()) @updatedAt 61 63 62 - following UserFollow[] @relation("follower") 63 - followers UserFollow[] @relation("followee") 64 - userEntries UserEntry[] 65 - activity UserActivity[] 66 - forgotPasswords UserForgotPassword[] 67 - lists UserList[] 64 + following UserFollow[] @relation("follower") 65 + followers UserFollow[] @relation("followee") 66 + userEntries UserEntry[] 67 + activity UserActivity[] 68 + forgotPasswords UserForgotPassword[] 69 + lists UserList[] 68 70 } 69 71 70 72 enum Activity { 71 - statusUpdate 72 - reviewed 73 - rewatch 74 - completeReview 73 + statusUpdate 74 + reviewed 75 + rewatch 76 + completeReview 75 77 } 76 78 77 79 model UserActivity { 78 - id Int @id @default(autoincrement()) @db.UnsignedInt 80 + id Int @id @default(autoincrement()) @db.UnsignedInt 79 81 80 - userId Int @db.UnsignedInt 81 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 82 - entryId Int @db.UnsignedInt 83 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 84 - type Activity 85 - additionalData String 82 + userId Int @db.UnsignedInt 83 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 84 + entryId Int @db.UnsignedInt 85 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 86 + type Activity 87 + additionalData String 86 88 87 - createdAt DateTime @default(now()) 88 - updatedAt DateTime @default(now()) @updatedAt 89 + createdAt DateTime @default(now()) 90 + updatedAt DateTime @default(now()) @updatedAt 89 91 } 90 92 91 93 model UserFollow { 92 - id Int @id @default(autoincrement()) @db.UnsignedInt 94 + id Int @id @default(autoincrement()) @db.UnsignedInt 93 95 94 - userId Int @db.UnsignedInt 95 - user User @relation("follower", fields: [userId], references: [id], onDelete: Cascade) 96 - followId Int @db.UnsignedInt 97 - follow User @relation("followee", fields: [followId], references: [id], onDelete: Cascade) 96 + userId Int @db.UnsignedInt 97 + user User @relation("follower", fields: [userId], references: [id], onDelete: Cascade) 98 + followId Int @db.UnsignedInt 99 + follow User @relation("followee", fields: [followId], references: [id], onDelete: Cascade) 98 100 99 - isFollowing Boolean 101 + isFollowing Boolean 100 102 101 - createdAt DateTime @default(now()) 102 - updatedAt DateTime @default(now()) @updatedAt 103 + createdAt DateTime @default(now()) 104 + updatedAt DateTime @default(now()) @updatedAt 103 105 } 104 106 105 107 enum UserEntryStatus { 106 - planning 107 - watching 108 - dnf 109 - paused 110 - completed 108 + planning 109 + watching 110 + dnf 111 + paused 112 + completed 111 113 } 112 114 113 115 model UserEntry { 114 - id Int @id @default(autoincrement()) @db.UnsignedInt 115 - userId Int @db.UnsignedInt 116 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 117 - entryId Int @db.UnsignedInt 118 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 119 - rating Int @db.SmallInt 120 - notes String @db.Text 121 - watchedAt DateTime? 122 - status UserEntryStatus @default(planning) 123 - progress Int @db.UnsignedSmallInt 124 - createdAt DateTime @default(now()) 125 - updatedAt DateTime @default(now()) @updatedAt 116 + id Int @id @default(autoincrement()) @db.UnsignedInt 117 + userId Int @db.UnsignedInt 118 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 119 + entryId Int @db.UnsignedInt 120 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 121 + rating Int @db.SmallInt 122 + notes String @db.Text 123 + watchedAt DateTime? 124 + status UserEntryStatus @default(planning) 125 + progress Int @db.UnsignedSmallInt 126 + createdAt DateTime @default(now()) 127 + updatedAt DateTime @default(now()) @updatedAt 126 128 } 127 129 128 130 enum UserListType { 129 - ordered 130 - unordered 131 + ordered 132 + unordered 131 133 } 132 134 133 135 model UserList { 134 - id Int @id @default(autoincrement()) @db.UnsignedInt 135 - userId Int @db.UnsignedInt 136 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 137 - name String 138 - description String @default("") @db.VarChar(256) 139 - type UserListType 140 - entries UserListEntry[] 141 - createdAt DateTime @default(now()) 142 - updatedAt DateTime @default(now()) @updatedAt 136 + id Int @id @default(autoincrement()) @db.UnsignedInt 137 + userId Int @db.UnsignedInt 138 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 139 + name String 140 + description String @default("") @db.VarChar(256) 141 + type UserListType 142 + entries UserListEntry[] 143 + createdAt DateTime @default(now()) 144 + updatedAt DateTime @default(now()) @updatedAt 143 145 } 144 146 145 147 model UserListEntry { 146 - id Int @id @default(autoincrement()) @db.UnsignedInt 147 - entryId Int @db.UnsignedInt 148 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 149 - listId Int @db.UnsignedInt 150 - list UserList @relation(fields: [listId], references: [id], onDelete: Cascade) 151 - order Int 152 - createdAt DateTime @default(now()) 153 - updatedAt DateTime @default(now()) @updatedAt 148 + id Int @id @default(autoincrement()) @db.UnsignedInt 149 + entryId Int @db.UnsignedInt 150 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 151 + listId Int @db.UnsignedInt 152 + list UserList @relation(fields: [listId], references: [id], onDelete: Cascade) 153 + order Int 154 + createdAt DateTime @default(now()) 155 + updatedAt DateTime @default(now()) @updatedAt 154 156 } 155 157 156 158 model Session { 157 - id Int @id @default(autoincrement()) @db.UnsignedInt 158 - token String @db.VarChar(64) 159 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 160 - userId Int @db.UnsignedInt 161 - expiry DateTime? 162 - ipAddress String 163 - userAgent String 164 - createdAt DateTime @default(now()) 165 - updatedAt DateTime @default(now()) @updatedAt 159 + id Int @id @default(autoincrement()) @db.UnsignedInt 160 + token String @db.VarChar(64) 161 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 162 + userId Int @db.UnsignedInt 163 + expiry DateTime? 164 + ipAddress String 165 + userAgent String 166 + createdAt DateTime @default(now()) 167 + updatedAt DateTime @default(now()) @updatedAt 166 168 } 167 169 168 170 model Company { 169 - id Int @id @default(autoincrement()) @db.UnsignedInt 170 - name String 171 - logo String? 172 - countryId Int @db.UnsignedInt 173 - country Country @relation(fields: [countryId], references: [id], onDelete: Cascade) 174 - foreignId String 175 - entryProductionCompanies EntryProductionCompany[] 171 + id Int @id @default(autoincrement()) @db.UnsignedInt 172 + name String 173 + logo String? 174 + countryId Int @db.UnsignedInt 175 + country Country @relation(fields: [countryId], references: [id], onDelete: Cascade) 176 + foreignId String 177 + entryProductionCompanies EntryProductionCompany[] 176 178 177 - createdAt DateTime @default(now()) 178 - updatedAt DateTime @default(now()) @updatedAt 179 + createdAt DateTime @default(now()) 180 + updatedAt DateTime @default(now()) @updatedAt 179 181 } 180 182 181 183 model Country { 182 - id Int @id @default(autoincrement()) @db.UnsignedInt 183 - name String @unique 184 - iso_3166_1 String @unique @db.VarChar(6) 185 - companies Company[] 186 - productionCountries EntryProductionCountry[] 187 - entryWatchProviders EntryWatchProvider[] 188 - entryAlternativeTitles EntryAlternativeTitle[] 184 + id Int @id @default(autoincrement()) @db.UnsignedInt 185 + name String @unique 186 + iso_3166_1 String @unique @db.VarChar(6) 187 + companies Company[] 188 + productionCountries EntryProductionCountry[] 189 + entryWatchProviders EntryWatchProvider[] 190 + entryAlternativeTitles EntryAlternativeTitle[] 191 + entryTranslation EntryTranslation[] 189 192 190 - createdAt DateTime @default(now()) 191 - updatedAt DateTime @default(now()) @updatedAt 193 + createdAt DateTime @default(now()) 194 + updatedAt DateTime @default(now()) @updatedAt 192 195 } 193 196 194 197 model Language { 195 - id Int @id @default(autoincrement()) @db.UnsignedInt 196 - name String 197 - iso_639_1 String? @db.VarChar(2) 198 - iso_639_2 String @db.VarChar(3) 199 - spokenLanguages EntrySpokenLanguage[] 200 - entryAlternativeTitles EntryAlternativeTitle[] 198 + id Int @id @default(autoincrement()) @db.UnsignedInt 199 + name String 200 + iso_639_1 String? @db.VarChar(2) 201 + iso_639_2 String @db.VarChar(3) 202 + spokenLanguages EntrySpokenLanguage[] 203 + entryAlternativeTitles EntryAlternativeTitle[] 204 + entryTranslations EntryTranslation[] 205 + userShowMediaMetaIn User[] @relation("showMediaMetaInIn") 206 + entryOriginalLanguages Entry[] 201 207 202 - createdAt DateTime @default(now()) 203 - updatedAt DateTime @default(now()) @updatedAt 204 - Entry Entry[] 208 + createdAt DateTime @default(now()) 209 + updatedAt DateTime @default(now()) @updatedAt 205 210 } 206 211 207 212 model Person { 208 - id Int @id @default(autoincrement()) @db.UnsignedInt 209 - name String 210 - foreignId String 211 - gender Int @db.TinyInt 212 - profilePath String? 213 - memberCasts EntryCast[] 214 - memberCrews EntryCrew[] 213 + id Int @id @default(autoincrement()) @db.UnsignedInt 214 + name String 215 + foreignId String 216 + gender Int @db.TinyInt 217 + profilePath String? 218 + memberCasts EntryCast[] 219 + memberCrews EntryCrew[] 215 220 216 - createdAt DateTime @default(now()) 217 - updatedAt DateTime @default(now()) @updatedAt 221 + createdAt DateTime @default(now()) 222 + updatedAt DateTime @default(now()) @updatedAt 218 223 } 219 224 220 225 model Genre { 221 - id Int @id @default(autoincrement()) @db.UnsignedInt 222 - name String 223 - foreignId String 224 - entries EntryGenre[] 226 + id Int @id @default(autoincrement()) @db.UnsignedInt 227 + name String 228 + foreignId String 229 + entries EntryGenre[] 225 230 226 - createdAt DateTime @default(now()) 227 - updatedAt DateTime @default(now()) @updatedAt 231 + createdAt DateTime @default(now()) 232 + updatedAt DateTime @default(now()) @updatedAt 228 233 } 229 234 230 235 model WatchProvider { 231 - id Int @id @default(autoincrement()) @db.UnsignedInt 232 - name String 233 - foreignId String 234 - logoPath String 235 - entries EntryWatchProvider[] 236 + id Int @id @default(autoincrement()) @db.UnsignedInt 237 + name String 238 + foreignId String 239 + logoPath String 240 + entries EntryWatchProvider[] 236 241 237 - createdAt DateTime @default(now()) 238 - updatedAt DateTime @default(now()) @updatedAt 242 + createdAt DateTime @default(now()) 243 + updatedAt DateTime @default(now()) @updatedAt 239 244 } 240 245 241 246 model Department { 242 - id Int @id @default(autoincrement()) @db.UnsignedInt 243 - name String 244 - crew EntryCrew[] 247 + id Int @id @default(autoincrement()) @db.UnsignedInt 248 + name String 249 + crew EntryCrew[] 245 250 246 - createdAt DateTime @default(now()) 247 - updatedAt DateTime @default(now()) @updatedAt 251 + createdAt DateTime @default(now()) 252 + updatedAt DateTime @default(now()) @updatedAt 248 253 } 249 254 250 255 model Job { 251 - id Int @id @default(autoincrement()) @db.UnsignedInt 252 - name String 253 - crew EntryCrew[] 256 + id Int @id @default(autoincrement()) @db.UnsignedInt 257 + name String 258 + crew EntryCrew[] 254 259 255 - createdAt DateTime @default(now()) 256 - updatedAt DateTime @default(now()) @updatedAt 260 + createdAt DateTime @default(now()) 261 + updatedAt DateTime @default(now()) @updatedAt 257 262 } 258 263 259 264 model EntryProductionCompany { 260 - id Int @id @default(autoincrement()) @db.UnsignedInt 261 - entryId Int @db.UnsignedInt 262 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 263 - companyId Int @db.UnsignedInt 264 - company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) 265 + id Int @id @default(autoincrement()) @db.UnsignedInt 266 + entryId Int @db.UnsignedInt 267 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 268 + companyId Int @db.UnsignedInt 269 + company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) 265 270 266 - createdAt DateTime @default(now()) 267 - updatedAt DateTime @default(now()) @updatedAt 271 + createdAt DateTime @default(now()) 272 + updatedAt DateTime @default(now()) @updatedAt 268 273 } 269 274 270 275 model EntryProductionCountry { 271 - id Int @id @default(autoincrement()) @db.UnsignedInt 272 - entryId Int @db.UnsignedInt 273 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 274 - countryId Int @db.UnsignedInt 275 - country Country @relation(fields: [countryId], references: [id], onDelete: Cascade) 276 + id Int @id @default(autoincrement()) @db.UnsignedInt 277 + entryId Int @db.UnsignedInt 278 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 279 + countryId Int @db.UnsignedInt 280 + country Country @relation(fields: [countryId], references: [id], onDelete: Cascade) 276 281 277 - createdAt DateTime @default(now()) 278 - updatedAt DateTime @default(now()) @updatedAt 282 + createdAt DateTime @default(now()) 283 + updatedAt DateTime @default(now()) @updatedAt 279 284 } 280 285 281 286 model EntrySpokenLanguage { 282 - id Int @id @default(autoincrement()) @db.UnsignedInt 283 - entryId Int @db.UnsignedInt 284 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 285 - languageId Int @db.UnsignedInt 286 - language Language @relation(fields: [languageId], references: [id], onDelete: Cascade) 287 + id Int @id @default(autoincrement()) @db.UnsignedInt 288 + entryId Int @db.UnsignedInt 289 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 290 + languageId Int @db.UnsignedInt 291 + language Language @relation(fields: [languageId], references: [id], onDelete: Cascade) 287 292 288 - createdAt DateTime @default(now()) 289 - updatedAt DateTime @default(now()) @updatedAt 293 + createdAt DateTime @default(now()) 294 + updatedAt DateTime @default(now()) @updatedAt 290 295 } 291 296 292 297 model EntryAlternativeTitle { 293 - id Int @id @default(autoincrement()) @db.UnsignedInt 294 - entryId Int @db.UnsignedInt 295 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 296 - countryId Int? @db.UnsignedInt 297 - country Country? @relation(fields: [countryId], references: [id], onDelete: Cascade) 298 - languageId Int? @db.UnsignedInt 299 - language Language? @relation(fields: [languageId], references: [id], onDelete: Cascade) 300 - title String 298 + id Int @id @default(autoincrement()) @db.UnsignedInt 299 + entryId Int @db.UnsignedInt 300 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 301 + countryId Int? @db.UnsignedInt 302 + country Country? @relation(fields: [countryId], references: [id], onDelete: Cascade) 303 + languageId Int? @db.UnsignedInt 304 + language Language? @relation(fields: [languageId], references: [id], onDelete: Cascade) 305 + title String 301 306 302 - createdAt DateTime @default(now()) 303 - updatedAt DateTime @default(now()) @updatedAt 307 + createdAt DateTime @default(now()) 308 + updatedAt DateTime @default(now()) @updatedAt 304 309 305 - @@index([title]) 310 + @@index([title]) 311 + } 312 + 313 + model EntryTranslation { 314 + id Int @id @default(autoincrement()) @db.UnsignedInt 315 + entryId Int @db.UnsignedInt 316 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 317 + countryId Int? @db.UnsignedInt 318 + country Country? @relation(fields: [countryId], references: [id], onDelete: Cascade) 319 + languageId Int? @db.UnsignedInt 320 + language Language? @relation(fields: [languageId], references: [id], onDelete: Cascade) 321 + name String 322 + overview String 323 + homepage String 324 + tagline String 325 + 326 + createdAt DateTime @default(now()) 327 + updatedAt DateTime @default(now()) @updatedAt 328 + 329 + @@index([name]) 306 330 } 307 331 308 332 model EntryCast { 309 - id Int @id @default(autoincrement()) @db.UnsignedInt 310 - entryId Int @db.UnsignedInt 311 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 312 - personId Int @db.UnsignedInt 313 - person Person @relation(fields: [personId], references: [id], onDelete: Cascade) 314 - character String 333 + id Int @id @default(autoincrement()) @db.UnsignedInt 334 + entryId Int @db.UnsignedInt 335 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 336 + personId Int @db.UnsignedInt 337 + person Person @relation(fields: [personId], references: [id], onDelete: Cascade) 338 + character String 315 339 316 - createdAt DateTime @default(now()) 317 - updatedAt DateTime @default(now()) @updatedAt 340 + createdAt DateTime @default(now()) 341 + updatedAt DateTime @default(now()) @updatedAt 318 342 } 319 343 320 344 model EntryCrew { 321 - id Int @id @default(autoincrement()) @db.UnsignedInt 322 - entryId Int @db.UnsignedInt 323 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 324 - personId Int @db.UnsignedInt 325 - person Person @relation(fields: [personId], references: [id], onDelete: Cascade) 326 - departmentId Int @db.UnsignedInt 327 - department Department @relation(fields: [departmentId], references: [id], onDelete: Cascade) 328 - jobId Int @db.UnsignedInt 329 - job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) 345 + id Int @id @default(autoincrement()) @db.UnsignedInt 346 + entryId Int @db.UnsignedInt 347 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 348 + personId Int @db.UnsignedInt 349 + person Person @relation(fields: [personId], references: [id], onDelete: Cascade) 350 + departmentId Int @db.UnsignedInt 351 + department Department @relation(fields: [departmentId], references: [id], onDelete: Cascade) 352 + jobId Int @db.UnsignedInt 353 + job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) 330 354 331 - createdAt DateTime @default(now()) 332 - updatedAt DateTime @default(now()) @updatedAt 355 + createdAt DateTime @default(now()) 356 + updatedAt DateTime @default(now()) @updatedAt 333 357 } 334 358 335 359 model EntryGenre { 336 - id Int @id @default(autoincrement()) @db.UnsignedInt 337 - entryId Int @db.UnsignedInt 338 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 339 - genreId Int @db.UnsignedInt 340 - genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade) 360 + id Int @id @default(autoincrement()) @db.UnsignedInt 361 + entryId Int @db.UnsignedInt 362 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 363 + genreId Int @db.UnsignedInt 364 + genre Genre @relation(fields: [genreId], references: [id], onDelete: Cascade) 341 365 342 - createdAt DateTime @default(now()) 343 - updatedAt DateTime @default(now()) @updatedAt 366 + createdAt DateTime @default(now()) 367 + updatedAt DateTime @default(now()) @updatedAt 344 368 } 345 369 346 370 enum WatchProviderType { 347 - buy 348 - rent 349 - flatrate 371 + buy 372 + rent 373 + flatrate 350 374 } 351 375 352 376 model EntryWatchProvider { 353 - id Int @id @default(autoincrement()) @db.UnsignedInt 354 - entryId Int @db.UnsignedInt 355 - entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 356 - watchProviderId Int @db.UnsignedInt 357 - watchProvider WatchProvider @relation(fields: [watchProviderId], references: [id], onDelete: Cascade) 358 - countryId Int @db.UnsignedInt 359 - country Country @relation(fields: [countryId], references: [id], onDelete: Cascade) 360 - type WatchProviderType 377 + id Int @id @default(autoincrement()) @db.UnsignedInt 378 + entryId Int @db.UnsignedInt 379 + entry Entry @relation(fields: [entryId], references: [id], onDelete: Cascade) 380 + watchProviderId Int @db.UnsignedInt 381 + watchProvider WatchProvider @relation(fields: [watchProviderId], references: [id], onDelete: Cascade) 382 + countryId Int @db.UnsignedInt 383 + country Country @relation(fields: [countryId], references: [id], onDelete: Cascade) 384 + type WatchProviderType 361 385 362 - createdAt DateTime @default(now()) 363 - updatedAt DateTime @default(now()) @updatedAt 386 + createdAt DateTime @default(now()) 387 + updatedAt DateTime @default(now()) @updatedAt 364 388 } 365 389 366 390 enum Category { 367 - Series 368 - Movie 369 - Book 391 + Series 392 + Movie 393 + Book 370 394 } 371 395 372 396 model Collection { 373 - id Int @id @default(autoincrement()) @db.UnsignedInt 374 - name String 375 - foreignId String 376 - posterPath String 377 - backdropPath String 378 - category Category 379 - entries Entry[] 397 + id Int @id @default(autoincrement()) @db.UnsignedInt 398 + name String 399 + foreignId String 400 + posterPath String 401 + backdropPath String 402 + category Category 403 + entries Entry[] 380 404 381 - createdAt DateTime @default(now()) 382 - updatedAt DateTime @default(now()) @updatedAt 405 + createdAt DateTime @default(now()) 406 + updatedAt DateTime @default(now()) @updatedAt 383 407 } 384 408 385 409 model Entry { 386 - id Int @id @default(autoincrement()) @db.UnsignedInt 387 - collectionId Int? @db.UnsignedInt 388 - collection Collection? @relation(fields: [collectionId], references: [id], onDelete: Cascade) 389 - category Category 390 - originalLanguageId Int @db.UnsignedInt 391 - originalLanguage Language @relation(fields: [originalLanguageId], references: [id], onDelete: Cascade) 392 - foreignId String 393 - posterPath String 394 - tagline String @db.Text 395 - originalTitle String 396 - overview String @db.Text 397 - backdropPath String 398 - releaseDate DateTime 399 - length Int @db.UnsignedSmallInt 410 + id Int @id @default(autoincrement()) @db.UnsignedInt 411 + collectionId Int? @db.UnsignedInt 412 + collection Collection? @relation(fields: [collectionId], references: [id], onDelete: Cascade) 413 + category Category 414 + originalLanguageId Int @db.UnsignedInt 415 + originalLanguage Language @relation(fields: [originalLanguageId], references: [id], onDelete: Cascade) 416 + foreignId String 417 + posterPath String 418 + tagline String @db.Text 419 + originalTitle String 420 + overview String @db.Text 421 + backdropPath String 422 + releaseDate DateTime 423 + length Int @db.UnsignedSmallInt 400 424 401 - productionCompanies EntryProductionCompany[] 402 - productionCountries EntryProductionCountry[] 403 - spokenLanguage EntrySpokenLanguage[] 404 - alternativeTitles EntryAlternativeTitle[] 405 - cast EntryCast[] 406 - crew EntryCrew[] 407 - genres EntryGenre[] 408 - watchProviders EntryWatchProvider[] 409 - userEntries UserEntry[] 410 - userActivity UserActivity[] 411 - userListEntries UserListEntry[] 425 + productionCompanies EntryProductionCompany[] 426 + productionCountries EntryProductionCountry[] 427 + spokenLanguage EntrySpokenLanguage[] 428 + alternativeTitles EntryAlternativeTitle[] 429 + translations EntryTranslation[] 430 + cast EntryCast[] 431 + crew EntryCrew[] 432 + genres EntryGenre[] 433 + watchProviders EntryWatchProvider[] 434 + userEntries UserEntry[] 435 + userActivity UserActivity[] 436 + userListEntries UserListEntry[] 412 437 413 - createdAt DateTime @default(now()) 414 - updatedAt DateTime @default(now()) @updatedAt 438 + createdAt DateTime @default(now()) 439 + updatedAt DateTime @default(now()) @updatedAt 415 440 416 - @@index([originalTitle]) 441 + @@index([originalTitle]) 417 442 } 418 443 419 444 model UserForgotPassword { 420 - id String @id @db.VarChar(36) 421 - userId Int @db.UnsignedInt 422 - user User @relation(fields: [userId], references: [id], onDelete: Cascade) 423 - used Boolean 424 - createdAt DateTime @default(now()) 425 - updatedAt DateTime @default(now()) @updatedAt 445 + id String @id @db.VarChar(36) 446 + userId Int @db.UnsignedInt 447 + user User @relation(fields: [userId], references: [id], onDelete: Cascade) 448 + used Boolean 449 + createdAt DateTime @default(now()) 450 + updatedAt DateTime @default(now()) @updatedAt 426 451 }
+39
server/api/routers/dashboard.ts
··· 2 2 import { protectedProcedure } from '../trpc'; 3 3 import { unstable_cache } from 'next/cache'; 4 4 import prisma from '@/server/db'; 5 + import { validateSessionToken } from '@/server/auth/validateSession'; 6 + import { Entry, EntryTranslation } from '@prisma/client'; 7 + 8 + export const getUserTitleFromEntryId = async (entryId: number) => { 9 + const user = await validateSessionToken(); 10 + const entry = await prisma.entry.findFirst({ 11 + where: { 12 + id: entryId, 13 + }, 14 + include: { 15 + translations: { 16 + where: { 17 + language: { 18 + id: user ? (user.showMediaMetaInId ?? undefined) : undefined, 19 + iso_639_1: !user ? 'en' : undefined, 20 + }, 21 + }, 22 + }, 23 + }, 24 + }); 25 + if (!entry) { 26 + return null; 27 + } 28 + 29 + return getUserTitleFromEntry(entry); 30 + }; 31 + 32 + export const getUserTitleFromEntry = ( 33 + entry: Entry & { translations: EntryTranslation[] } 34 + ) => { 35 + return entry.translations.length !== 0 36 + ? entry.translations[0]!.name 37 + : entry.originalTitle; 38 + }; 5 39 6 40 const getTop3RatedNotCompleted = unstable_cache( 7 41 async (userId: number) => { ··· 126 160 userEntries: { 127 161 where: { 128 162 userId: ctx.user.id, 163 + }, 164 + }, 165 + translations: { 166 + where: { 167 + languageId: ctx.user.showMediaMetaInId, 129 168 }, 130 169 }, 131 170 },
+31
server/api/routers/settings.ts
··· 3 3 import { Theme } from '@prisma/client'; 4 4 import z from 'zod'; 5 5 import { protectedProcedure } from '../trpc'; 6 + import { TRPCError } from '@trpc/server'; 6 7 7 8 export const settingsRouter = createTRPCRouter({ 8 9 setTheme: protectedProcedure ··· 21 22 }, 22 23 }); 23 24 }), 25 + setShowMediaMetaIn: protectedProcedure 26 + .input( 27 + z.object({ 28 + iso_639_2: z.string(), 29 + }) 30 + ) 31 + .mutation(async ({ input, ctx }) => { 32 + const language = await prisma.language.findFirst({ 33 + where: { 34 + iso_639_2: input.iso_639_2, 35 + }, 36 + }); 37 + if (!language) { 38 + throw new TRPCError({ 39 + code: 'BAD_REQUEST', 40 + message: 'Invalid iso_639_2 code', 41 + }); 42 + } 43 + await prisma.user.update({ 44 + data: { 45 + showMediaMetaInId: language.id, 46 + }, 47 + where: { 48 + id: ctx.user.id, 49 + }, 50 + }); 51 + }), 52 + getLanguages: protectedProcedure.query(async () => { 53 + return await prisma.language.findMany(); 54 + }), 24 55 });