A personal media tracker built on the AT Protocol opnshelf.xyz
0
fork

Configure Feed

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

fix: deleting show & season

+612 -91
+126 -58
apps/web/src/components/CastSection.tsx
··· 3 3 4 4 interface CastSectionProps { 5 5 cast: TmdbCastDto[] | undefined; 6 + guestStars?: TmdbCastDto[] | undefined; 6 7 colors: { 7 8 primary?: string; 8 9 muted?: string; 9 10 }; 10 11 } 11 12 12 - export function CastSection({ cast, colors }: CastSectionProps) { 13 - if (!cast || cast.length === 0) return null; 13 + export function CastSection({ cast, guestStars, colors }: CastSectionProps) { 14 + const hasCast = cast && cast.length > 0; 15 + const hasGuestStars = guestStars && guestStars.length > 0; 16 + 17 + if (!hasCast && !hasGuestStars) return null; 14 18 15 19 return ( 16 - <section className="pt-4 min-w-0"> 17 - <h2 18 - className="text-xl font-semibold mb-4" 19 - style={{ color: colors.primary }} 20 - > 21 - Cast 22 - </h2> 23 - <div className="relative w-full overflow-hidden"> 24 - <div className="flex gap-4 overflow-x-auto pb-4 scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-transparent w-full pr-8"> 25 - {cast.map((person) => { 26 - const profileUrl = getTmdbProfileUrl(person.profile_path); 27 - return ( 28 - <div 29 - key={person.id} 30 - className="shrink-0 w-32 group cursor-pointer" 31 - > 32 - <div className="relative overflow-hidden rounded-lg bg-gray-900/50 aspect-2/3 mb-2 transition-transform duration-300 group-hover:scale-[1.02]"> 33 - {profileUrl ? ( 34 - <img 35 - src={profileUrl} 36 - alt={person.name} 37 - className="w-full h-full object-cover transition-opacity duration-300 group-hover:opacity-90" 38 - loading="lazy" 39 - /> 40 - ) : ( 41 - <div className="w-full h-full bg-gray-800 flex items-center justify-center"> 42 - <span className="text-gray-600 text-xs text-center px-2"> 43 - No photo 44 - </span> 20 + <> 21 + {hasCast && ( 22 + <section className="pt-4 min-w-0"> 23 + <h2 24 + className="text-xl font-semibold mb-4" 25 + style={{ color: colors.primary }} 26 + > 27 + Cast 28 + </h2> 29 + <div className="relative w-full overflow-hidden"> 30 + <div className="flex gap-4 overflow-x-auto pb-4 scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-transparent w-full pr-8"> 31 + {cast.map((person) => { 32 + const profileUrl = getTmdbProfileUrl(person.profile_path); 33 + return ( 34 + <div 35 + key={`cast-${person.id}`} 36 + className="shrink-0 w-32 group cursor-pointer" 37 + > 38 + <div className="relative overflow-hidden rounded-lg bg-gray-900/50 aspect-2/3 mb-2 transition-transform duration-300 group-hover:scale-[1.02]"> 39 + {profileUrl ? ( 40 + <img 41 + src={profileUrl} 42 + alt={person.name} 43 + className="w-full h-full object-cover transition-opacity duration-300 group-hover:opacity-90" 44 + loading="lazy" 45 + /> 46 + ) : ( 47 + <div className="w-full h-full bg-gray-800 flex items-center justify-center"> 48 + <span className="text-gray-600 text-xs text-center px-2"> 49 + No photo 50 + </span> 51 + </div> 52 + )} 45 53 </div> 46 - )} 47 - </div> 48 - <div className="space-y-0.5"> 49 - <p className="text-sm font-medium text-gray-200 line-clamp-2 transition-colors duration-200 group-hover:text-white"> 50 - {person.name} 51 - </p> 52 - {person.character && ( 53 - <p 54 - className="text-xs line-clamp-2" 55 - style={{ color: colors.muted }} 56 - > 57 - as {person.character} 58 - </p> 59 - )} 60 - </div> 61 - </div> 62 - ); 63 - })} 64 - </div> 65 - <div 66 - className="absolute right-0 top-0 bottom-4 w-16 pointer-events-none" 67 - style={{ 68 - background: `linear-gradient(to left, rgb(3, 7, 18), transparent)`, 69 - }} 70 - /> 71 - </div> 72 - </section> 54 + <div className="space-y-0.5"> 55 + <p className="text-sm font-medium text-gray-200 line-clamp-2 transition-colors duration-200 group-hover:text-white"> 56 + {person.name} 57 + </p> 58 + {person.character && ( 59 + <p 60 + className="text-xs line-clamp-2" 61 + style={{ color: colors.muted }} 62 + > 63 + as {person.character} 64 + </p> 65 + )} 66 + </div> 67 + </div> 68 + ); 69 + })} 70 + </div> 71 + <div 72 + className="absolute right-0 top-0 bottom-4 w-16 pointer-events-none" 73 + style={{ 74 + background: `linear-gradient(to left, rgb(3, 7, 18), transparent)`, 75 + }} 76 + /> 77 + </div> 78 + </section> 79 + )} 80 + 81 + {hasGuestStars && ( 82 + <section className="pt-4 min-w-0"> 83 + <h2 84 + className="text-xl font-semibold mb-4" 85 + style={{ color: colors.primary }} 86 + > 87 + Guest Stars 88 + </h2> 89 + <div className="relative w-full overflow-hidden"> 90 + <div className="flex gap-4 overflow-x-auto pb-4 scrollbar-thin scrollbar-thumb-gray-700 scrollbar-track-transparent w-full pr-8"> 91 + {guestStars.map((person) => { 92 + const profileUrl = getTmdbProfileUrl(person.profile_path); 93 + return ( 94 + <div 95 + key={`guest-${person.id}`} 96 + className="shrink-0 w-32 group cursor-pointer" 97 + > 98 + <div className="relative overflow-hidden rounded-lg bg-gray-900/50 aspect-2/3 mb-2 transition-transform duration-300 group-hover:scale-[1.02]"> 99 + {profileUrl ? ( 100 + <img 101 + src={profileUrl} 102 + alt={person.name} 103 + className="w-full h-full object-cover transition-opacity duration-300 group-hover:opacity-90" 104 + loading="lazy" 105 + /> 106 + ) : ( 107 + <div className="w-full h-full bg-gray-800 flex items-center justify-center"> 108 + <span className="text-gray-600 text-xs text-center px-2"> 109 + No photo 110 + </span> 111 + </div> 112 + )} 113 + </div> 114 + <div className="space-y-0.5"> 115 + <p className="text-sm font-medium text-gray-200 line-clamp-2 transition-colors duration-200 group-hover:text-white"> 116 + {person.name} 117 + </p> 118 + {person.character && ( 119 + <p 120 + className="text-xs line-clamp-2" 121 + style={{ color: colors.muted }} 122 + > 123 + as {person.character} 124 + </p> 125 + )} 126 + </div> 127 + </div> 128 + ); 129 + })} 130 + </div> 131 + <div 132 + className="absolute right-0 top-0 bottom-4 w-16 pointer-events-none" 133 + style={{ 134 + background: `linear-gradient(to left, rgb(3, 7, 18), transparent)`, 135 + }} 136 + /> 137 + </div> 138 + </section> 139 + )} 140 + </> 73 141 ); 74 142 }
+120 -14
apps/web/src/components/detail/EpisodeCard.tsx
··· 1 + import { 2 + showsControllerGetShowWatchHistoryQueryKey, 3 + showsControllerGetUserShowsQueryKey, 4 + showsControllerMarkWatchedMutation, 5 + showsControllerUnmarkWatchedMutation, 6 + } from "@opnshelf/api"; 7 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 1 8 import { Link } from "@tanstack/react-router"; 2 - import { Calendar, Star } from "lucide-react"; 9 + import { Calendar, Loader2, Plus, Star, Trash2 } from "lucide-react"; 10 + import { toast } from "sonner"; 3 11 import { formatDateOnly } from "@/lib/utils"; 4 12 import type { ColorTheme, EpisodeSummary } from "./types"; 5 13 ··· 10 18 episode: EpisodeSummary; 11 19 watchedCount?: number; 12 20 colors: ColorTheme; 21 + userDid?: string; 13 22 }; 14 23 15 24 export function EpisodeCard({ ··· 19 28 episode, 20 29 watchedCount = 0, 21 30 colors, 31 + userDid, 22 32 }: EpisodeCardProps) { 33 + const queryClient = useQueryClient(); 34 + 35 + const markMutation = useMutation({ 36 + ...showsControllerMarkWatchedMutation(), 37 + onSuccess: () => { 38 + if (userDid) { 39 + queryClient.invalidateQueries({ 40 + queryKey: showsControllerGetUserShowsQueryKey({ 41 + path: { userDid }, 42 + }), 43 + }); 44 + queryClient.invalidateQueries({ 45 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 46 + path: { userDid, showId }, 47 + }), 48 + }); 49 + } 50 + toast.success("Episode marked watched"); 51 + }, 52 + onError: () => { 53 + toast.error("Failed to mark episode watched"); 54 + }, 55 + }); 56 + 57 + const unmarkMutation = useMutation({ 58 + ...showsControllerUnmarkWatchedMutation(), 59 + onSuccess: () => { 60 + if (userDid) { 61 + queryClient.invalidateQueries({ 62 + queryKey: showsControllerGetUserShowsQueryKey({ 63 + path: { userDid }, 64 + }), 65 + }); 66 + queryClient.invalidateQueries({ 67 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 68 + path: { userDid, showId }, 69 + }), 70 + }); 71 + } 72 + toast.success("Removed from your shelf"); 73 + }, 74 + onError: () => { 75 + toast.error("Failed to remove from shelf"); 76 + }, 77 + }); 78 + 79 + const isPending = markMutation.isPending || unmarkMutation.isPending; 80 + 81 + const handleToggleWatched = (e: React.MouseEvent) => { 82 + e.preventDefault(); 83 + e.stopPropagation(); 84 + 85 + if (watchedCount > 0) { 86 + unmarkMutation.mutate({ 87 + path: { showId }, 88 + query: { 89 + mode: "all", 90 + seasonNumber, 91 + episodeNumber: String(episode.episode_number), 92 + }, 93 + }); 94 + } else { 95 + markMutation.mutate({ 96 + body: { 97 + showId, 98 + seasonNumber: Number(seasonNumber), 99 + episodeNumber: episode.episode_number, 100 + }, 101 + }); 102 + } 103 + }; 104 + 23 105 return ( 24 106 <Link 25 107 to="/shows/$showId/$title/seasons/$seasonNumber/episodes/$episodeNumber" ··· 38 120 }} 39 121 > 40 122 <div className="grid grid-cols-[120px_1fr] gap-4"> 41 - <div className="h-full bg-gray-900 min-h-[67px]"> 123 + <div className="h-full bg-gray-900 min-h-[67px] relative"> 42 124 {episode.still_path ? ( 43 125 <img 44 126 src={`https://image.tmdb.org/t/p/w300${episode.still_path}`} ··· 66 148 <p className="text-xs text-gray-400 line-clamp-2 mb-2"> 67 149 {episode.overview || "No overview available."} 68 150 </p> 69 - <div className="flex items-center gap-3 text-xs text-gray-400"> 70 - <span className="flex items-center gap-1"> 71 - <Calendar className="w-3 h-3" /> 72 - {episode.air_date ? formatDateOnly(episode.air_date) : "TBA"} 73 - </span> 74 - {watchedCount > 0 && ( 75 - <span 76 - className="flex items-center gap-1" 77 - style={{ color: colors.primary }} 78 - > 79 - {watchedCount} watched 151 + <div className="flex items-center justify-between gap-3"> 152 + <div className="flex items-center gap-3 text-xs text-gray-400"> 153 + <span className="flex items-center gap-1"> 154 + <Calendar className="w-3 h-3" /> 155 + {episode.air_date ? formatDateOnly(episode.air_date) : "TBA"} 80 156 </span> 81 - )} 157 + </div> 82 158 </div> 159 + 160 + {userDid && ( 161 + <button 162 + type="button" 163 + onClick={handleToggleWatched} 164 + disabled={isPending} 165 + className={`flex items-center gap-1 px-4 py-2 rounded-md text-xs font-medium transition-all mt-2 ${watchedCount > 0 ? "bg-red-500/20 text-red-500" : "bg-green-500/20 text-green-500"}`} 166 + title="Add to Shelf" 167 + > 168 + {isPending ? ( 169 + <> 170 + <Loader2 className="w-3 h-3 animate-spin" /> 171 + <span>Loading</span> 172 + </> 173 + ) : ( 174 + <> 175 + {watchedCount > 0 ? ( 176 + <Trash2 className="w-3 h-3" /> 177 + ) : ( 178 + <Plus className="w-3 h-3" /> 179 + )} 180 + {watchedCount > 0 ? ( 181 + <span>Remove from Shelf</span> 182 + ) : ( 183 + <span>Add to Shelf</span> 184 + )} 185 + </> 186 + )} 187 + </button> 188 + )} 83 189 </div> 84 190 </div> 85 191 </Link>
+22 -3
apps/web/src/components/detail/EpisodeNav.tsx
··· 3 3 import { formatDateOnly } from "@/lib/utils"; 4 4 import type { ColorTheme, EpisodeSummary } from "./types"; 5 5 6 + type EpisodeContext = { 7 + seasonNumber: number; 8 + episodeNumber: number; 9 + }; 10 + 6 11 type EpisodeNavProps = { 7 12 showId: string; 8 13 title: string; ··· 12 17 nextEpisode: EpisodeSummary | null; 13 18 colors: ColorTheme; 14 19 variant?: "sidebar" | "full"; 20 + previousContext?: EpisodeContext | null; 21 + nextContext?: EpisodeContext | null; 15 22 }; 16 23 17 24 export function EpisodeNav({ ··· 23 30 nextEpisode, 24 31 colors, 25 32 variant = "full", 33 + previousContext, 34 + nextContext, 26 35 }: EpisodeNavProps) { 27 36 if (variant === "sidebar") { 28 37 const hasPrev = previousEpisode !== null; ··· 40 49 params={{ 41 50 showId, 42 51 title, 43 - seasonNumber, 52 + seasonNumber: String( 53 + previousContext?.seasonNumber ?? seasonNumber, 54 + ), 44 55 episodeNumber: String(previousEpisode.episode_number), 45 56 }} 46 57 className="flex-1 flex items-center justify-center gap-2 py-2 px-4 rounded-lg border border-(--md-sys-color-outline) hover:bg-gray-900/40 transition-colors text-sm" ··· 58 69 params={{ 59 70 showId, 60 71 title, 61 - seasonNumber, 72 + seasonNumber: String(nextContext?.seasonNumber ?? seasonNumber), 62 73 episodeNumber: String(nextEpisode.episode_number), 63 74 }} 64 75 className="flex-1 flex items-center justify-center gap-2 py-2 px-4 rounded-lg border border-(--md-sys-color-outline) hover:bg-gray-900/40 transition-colors text-sm" ··· 78 89 label: "Previous Episode", 79 90 icon: <ArrowLeft className="w-4 h-4" />, 80 91 episode: previousEpisode, 92 + context: previousContext, 81 93 highlighted: false, 82 94 }, 83 95 { ··· 85 97 label: "Current Episode", 86 98 icon: <CircleDot className="w-4 h-4" />, 87 99 episode: currentEpisode, 100 + context: { 101 + seasonNumber: Number(seasonNumber), 102 + episodeNumber: currentEpisode.episode_number, 103 + }, 88 104 highlighted: true, 89 105 }, 90 106 { ··· 92 108 label: "Next Episode", 93 109 icon: <ArrowRight className="w-4 h-4" />, 94 110 episode: nextEpisode, 111 + context: nextContext, 95 112 highlighted: false, 96 113 }, 97 114 ]; ··· 131 148 params={{ 132 149 showId, 133 150 title, 134 - seasonNumber, 151 + seasonNumber: String( 152 + slot.context?.seasonNumber ?? seasonNumber, 153 + ), 135 154 episodeNumber: String(slot.episode.episode_number), 136 155 }} 137 156 className={`rounded-lg border p-3 transition-colors ${
+115 -5
apps/web/src/components/detail/SeasonCard.tsx
··· 1 - import type { TmdbShowDetailDto } from "@opnshelf/api"; 1 + import { 2 + showsControllerGetShowWatchHistoryQueryKey, 3 + showsControllerGetUserShowsQueryKey, 4 + showsControllerMarkSeasonWatchedMutation, 5 + showsControllerUnmarkWatchedMutation, 6 + } from "@opnshelf/api"; 7 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 8 import { Link } from "@tanstack/react-router"; 3 - import { Calendar, Film } from "lucide-react"; 9 + import { Calendar, Film, Loader2, Plus, Trash2 } from "lucide-react"; 10 + import { toast } from "sonner"; 4 11 import type { ColorTheme } from "./types"; 5 12 6 13 type SeasonCardProps = { ··· 13 20 watchedCount: number; 14 21 overview?: string; 15 22 colors: ColorTheme; 16 - showData?: TmdbShowDetailDto; 23 + showData?: { number_of_episodes?: number }; 24 + userDid?: string; 17 25 }; 18 26 19 27 export function SeasonCard({ ··· 26 34 watchedCount, 27 35 overview, 28 36 colors, 37 + userDid, 29 38 }: SeasonCardProps) { 39 + const queryClient = useQueryClient(); 40 + 30 41 const progress = 31 42 episodeCount > 0 ? Math.round((watchedCount / episodeCount) * 100) : 0; 43 + const hasWatchedEpisodes = watchedCount > 0; 44 + 45 + const markMutation = useMutation({ 46 + ...showsControllerMarkSeasonWatchedMutation(), 47 + onSuccess: (data) => { 48 + if (userDid) { 49 + queryClient.invalidateQueries({ 50 + queryKey: showsControllerGetUserShowsQueryKey({ 51 + path: { userDid }, 52 + }), 53 + }); 54 + queryClient.invalidateQueries({ 55 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 56 + path: { userDid, showId }, 57 + }), 58 + }); 59 + } 60 + toast.success(`Marked ${data.count} episodes as watched`); 61 + }, 62 + onError: () => { 63 + toast.error("Failed to mark season as watched"); 64 + }, 65 + }); 66 + 67 + const unmarkMutation = useMutation({ 68 + ...showsControllerUnmarkWatchedMutation(), 69 + onSuccess: () => { 70 + if (userDid) { 71 + queryClient.invalidateQueries({ 72 + queryKey: showsControllerGetUserShowsQueryKey({ 73 + path: { userDid }, 74 + }), 75 + }); 76 + queryClient.invalidateQueries({ 77 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 78 + path: { userDid, showId }, 79 + }), 80 + }); 81 + } 82 + toast.success("Removed season from your shelf"); 83 + }, 84 + onError: () => { 85 + toast.error("Failed to remove from shelf"); 86 + }, 87 + }); 88 + 89 + const isPending = markMutation.isPending || unmarkMutation.isPending; 90 + 91 + const handleToggleWatched = (e: React.MouseEvent) => { 92 + e.preventDefault(); 93 + e.stopPropagation(); 94 + 95 + if (hasWatchedEpisodes) { 96 + unmarkMutation.mutate({ 97 + path: { showId }, 98 + query: { 99 + mode: "all", 100 + seasonNumber: String(seasonNumber), 101 + }, 102 + }); 103 + } else { 104 + markMutation.mutate({ 105 + body: { 106 + showId, 107 + seasonNumber, 108 + }, 109 + }); 110 + } 111 + }; 32 112 33 113 return ( 34 114 <Link ··· 42 122 style={{ borderColor: "var(--md-sys-color-outline)" }} 43 123 > 44 124 <div className="grid grid-cols-[100px_1fr] gap-4"> 45 - <div className="aspect-2/3 bg-gray-900"> 125 + <div className="aspect-2/3 bg-gray-900 relative"> 46 126 {posterUrl ? ( 47 127 <img 48 128 src={posterUrl} ··· 88 168 )} 89 169 90 170 {episodeCount > 0 && ( 91 - <div className="w-full h-1.5 bg-gray-800 rounded-full overflow-hidden"> 171 + <div className="w-full h-1.5 bg-gray-800 rounded-full overflow-hidden mb-3"> 92 172 <div 93 173 className="h-full rounded-full transition-all" 94 174 style={{ ··· 97 177 }} 98 178 /> 99 179 </div> 180 + )} 181 + 182 + {userDid && ( 183 + <button 184 + type="button" 185 + onClick={handleToggleWatched} 186 + disabled={isPending} 187 + className={`flex items-center gap-1 px-4 py-2 rounded-md text-xs font-medium transition-all mt-2 ${hasWatchedEpisodes ? "bg-red-500/20 text-red-500" : "bg-green-500/20 text-green-500"}`} 188 + title="Add to Shelf" 189 + > 190 + {isPending ? ( 191 + <> 192 + <Loader2 className="w-3 h-3 animate-spin" /> 193 + <span>Loading</span> 194 + </> 195 + ) : ( 196 + <> 197 + {hasWatchedEpisodes ? ( 198 + <Trash2 className="w-3 h-3" /> 199 + ) : ( 200 + <Plus className="w-3 h-3" /> 201 + )} 202 + {hasWatchedEpisodes ? ( 203 + <span>Remove from Shelf</span> 204 + ) : ( 205 + <span>Add to Shelf</span> 206 + )} 207 + </> 208 + )} 209 + </button> 100 210 )} 101 211 </div> 102 212 </div>
+1 -1
apps/web/src/components/detail/TrackedStatusCard.tsx
··· 81 81 </> 82 82 )} 83 83 84 - {totalWatches <= 1 && onRemove && ( 84 + {totalWatches >= 1 && onRemove && ( 85 85 <button 86 86 type="button" 87 87 onClick={onRemove}
+40 -6
apps/web/src/routes/shows.$showId.$title.seasons.$seasonNumber.episodes.$episodeNumber.tsx
··· 269 269 270 270 const seasonEpisodeContext = useMemo(() => { 271 271 if (!season?.episodes?.length) 272 - return { previous: null, current: null, next: null }; 272 + return { 273 + previous: null, 274 + current: null, 275 + next: null, 276 + previousContext: null, 277 + nextContext: null, 278 + }; 273 279 const sortedEpisodes = [...season.episodes].sort( 274 280 (a, b) => a.episode_number - b.episode_number, 275 281 ); 276 282 const currentIndex = sortedEpisodes.findIndex( 277 283 (e) => e.episode_number === Number(episodeNumber), 278 284 ); 279 - if (currentIndex < 0) return { previous: null, current: null, next: null }; 285 + if (currentIndex < 0) 286 + return { 287 + previous: null, 288 + current: null, 289 + next: null, 290 + previousContext: null, 291 + nextContext: null, 292 + }; 293 + 294 + const previousEp = sortedEpisodes[currentIndex - 1] ?? null; 295 + const nextEp = sortedEpisodes[currentIndex + 1] ?? null; 296 + 297 + const apiContext = ( 298 + episode as { 299 + _context?: { 300 + previous: { seasonNumber: number; episodeNumber: number } | null; 301 + next: { seasonNumber: number; episodeNumber: number } | null; 302 + }; 303 + } 304 + )?._context; 305 + 280 306 return { 281 - previous: sortedEpisodes[currentIndex - 1] ?? null, 307 + previous: previousEp, 282 308 current: sortedEpisodes[currentIndex] ?? null, 283 - next: sortedEpisodes[currentIndex + 1] ?? null, 309 + next: nextEp, 310 + previousContext: apiContext?.previous ?? null, 311 + nextContext: apiContext?.next ?? null, 284 312 }; 285 - }, [season?.episodes, episodeNumber]); 313 + }, [season?.episodes, episodeNumber, episode]); 286 314 287 315 const formattedWatchedDate = useMemo(() => { 288 316 if (!latestEpisodeWatch) return null; ··· 378 406 previousEpisode={seasonEpisodeContext.previous} 379 407 currentEpisode={seasonEpisodeContext.current} 380 408 nextEpisode={seasonEpisodeContext.next} 409 + previousContext={seasonEpisodeContext.previousContext} 410 + nextContext={seasonEpisodeContext.nextContext} 381 411 colors={colors} 382 412 variant="sidebar" 383 413 /> ··· 399 429 </p> 400 430 </section> 401 431 402 - <CastSection cast={show?.credits?.cast} colors={colors} /> 432 + <CastSection 433 + cast={show?.credits?.cast} 434 + guestStars={episode?.guest_stars} 435 + colors={colors} 436 + /> 403 437 <CrewSection crew={show?.credits?.crew} colors={colors} /> 404 438 </div> 405 439 </div>
+38 -1
apps/web/src/routes/shows.$showId.$title.seasons.$seasonNumber.tsx
··· 4 4 showsControllerGetSeasonDetailsOptions, 5 5 showsControllerGetShowDetailsOptions, 6 6 showsControllerGetShowWatchHistoryOptions, 7 + showsControllerGetShowWatchHistoryQueryKey, 7 8 showsControllerGetUserShowsQueryKey, 8 9 showsControllerMarkSeasonWatchedMutation, 10 + showsControllerUnmarkWatchedMutation, 9 11 type TmdbSeasonDetailDto, 10 12 type TmdbShowDetailDto, 11 13 } from "@opnshelf/api"; ··· 140 142 }), 141 143 }); 142 144 queryClient.invalidateQueries({ 143 - queryKey: ["showsControllerGetShowWatchHistory"], 145 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 146 + path: { userDid: user?.did || "", showId }, 147 + }), 144 148 }); 145 149 toast.success(`Marked ${data.count} episodes as watched`); 146 150 }, ··· 149 153 }, 150 154 }); 151 155 156 + const unmarkSeasonWatchedMutation = useMutation({ 157 + ...showsControllerUnmarkWatchedMutation(), 158 + onSuccess: () => { 159 + queryClient.invalidateQueries({ 160 + queryKey: showsControllerGetUserShowsQueryKey({ 161 + path: { userDid: user?.did || "" }, 162 + }), 163 + }); 164 + queryClient.invalidateQueries({ 165 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 166 + path: { userDid: user?.did || "", showId }, 167 + }), 168 + }); 169 + toast.success("Removed season from your shelf"); 170 + }, 171 + onError: () => { 172 + toast.error("Failed to remove from shelf. Please try again."); 173 + }, 174 + }); 175 + 152 176 const handleMarkWatched = () => { 153 177 markSeasonWatchedMutation.mutate({ 154 178 body: { ··· 158 182 }); 159 183 }; 160 184 185 + const handleUnmarkWatched = () => { 186 + unmarkSeasonWatchedMutation.mutate({ 187 + path: { showId }, 188 + query: { 189 + mode: "all", 190 + seasonNumber: seasonNumber, 191 + }, 192 + }); 193 + }; 194 + 161 195 const watchedEpisodeCount = useMemo(() => { 162 196 if (!history) return 0; 163 197 return history.filter((h) => h.seasonNumber === Number(seasonNumber)) ··· 222 256 watchedDate={null} 223 257 totalWatches={watchedEpisodeCount} 224 258 onMarkWatched={handleMarkWatched} 259 + onUnmarkWatched={handleUnmarkWatched} 225 260 onShowDatePicker={() => {}} 226 261 isMarkingPending={markSeasonWatchedMutation.isPending} 262 + isUnmarkingPending={unmarkSeasonWatchedMutation.isPending} 227 263 listsCount={listsCount} 228 264 onShowListModal={() => setShowListModal(true)} 229 265 isLoggedIn={!!user} ··· 278 314 0 279 315 } 280 316 colors={colors} 317 + userDid={user?.did} 281 318 /> 282 319 ))} 283 320 </div>
+35 -1
apps/web/src/routes/shows.$showId.$title.tsx
··· 3 3 listsControllerGetListsForItemOptions, 4 4 showsControllerGetShowDetailsOptions, 5 5 showsControllerGetShowWatchHistoryOptions, 6 + showsControllerGetShowWatchHistoryQueryKey, 6 7 showsControllerGetUserShowsQueryKey, 7 8 showsControllerMarkShowWatchedMutation, 9 + showsControllerUnmarkWatchedMutation, 8 10 type TmdbShowDetailDto, 9 11 } from "@opnshelf/api"; 10 12 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; ··· 121 123 }), 122 124 }); 123 125 queryClient.invalidateQueries({ 124 - queryKey: ["showsControllerGetShowWatchHistory"], 126 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 127 + path: { userDid: user?.did || "", showId }, 128 + }), 125 129 }); 126 130 toast.success(`Marked ${data.count} episodes as watched`); 127 131 }, ··· 130 134 }, 131 135 }); 132 136 137 + const unmarkShowWatchedMutation = useMutation({ 138 + ...showsControllerUnmarkWatchedMutation(), 139 + onSuccess: () => { 140 + queryClient.invalidateQueries({ 141 + queryKey: showsControllerGetUserShowsQueryKey({ 142 + path: { userDid: user?.did || "" }, 143 + }), 144 + }); 145 + queryClient.invalidateQueries({ 146 + queryKey: showsControllerGetShowWatchHistoryQueryKey({ 147 + path: { userDid: user?.did || "", showId }, 148 + }), 149 + }); 150 + toast.success("Removed all episodes from your shelf"); 151 + }, 152 + onError: () => { 153 + toast.error("Failed to remove from shelf. Please try again."); 154 + }, 155 + }); 156 + 133 157 const handleMarkWatched = () => { 134 158 markShowWatchedMutation.mutate({ 135 159 body: { showId }, 136 160 }); 137 161 }; 138 162 163 + const handleUnmarkWatched = () => { 164 + unmarkShowWatchedMutation.mutate({ 165 + path: { showId }, 166 + query: { mode: "all" }, 167 + }); 168 + }; 169 + 139 170 const metadataItems = useMemo(() => { 140 171 const items = []; 141 172 if (show?.first_air_date) { ··· 193 224 watchedDate={null} 194 225 totalWatches={watchedEpisodeCount} 195 226 onMarkWatched={handleMarkWatched} 227 + onUnmarkWatched={handleUnmarkWatched} 196 228 onShowDatePicker={() => {}} 197 229 isMarkingPending={markShowWatchedMutation.isPending} 230 + isUnmarkingPending={unmarkShowWatchedMutation.isPending} 198 231 listsCount={listsCount} 199 232 onShowListModal={() => setShowListModal(true)} 200 233 isLoggedIn={!!user} ··· 246 279 season.poster_path, 247 280 "w500", 248 281 )} 282 + userDid={user?.did} 249 283 /> 250 284 ); 251 285 })}
+19
backend/src/shows/dto/show.dto.ts
··· 165 165 overview?: string; 166 166 } 167 167 168 + export class EpisodeReferenceDto { 169 + @ApiProperty() 170 + seasonNumber: number; 171 + 172 + @ApiProperty() 173 + episodeNumber: number; 174 + } 175 + 176 + export class EpisodeContextDto { 177 + @ApiPropertyOptional({ type: EpisodeReferenceDto }) 178 + previous: EpisodeReferenceDto | null; 179 + 180 + @ApiPropertyOptional({ type: EpisodeReferenceDto }) 181 + next: EpisodeReferenceDto | null; 182 + } 183 + 168 184 export class TMDBEpisodeDto { 169 185 @ApiProperty() 170 186 id: number; ··· 210 226 211 227 @ApiPropertyOptional({ type: [TMDBCastDto] }) 212 228 guest_stars?: TMDBCastDto[]; 229 + 230 + @ApiPropertyOptional({ type: EpisodeContextDto }) 231 + _context?: EpisodeContextDto; 213 232 } 214 233 215 234 export class TMDBSeasonDetailDto {
+11 -1
backend/src/shows/shows.controller.ts
··· 100 100 @Param("seasonNumber") seasonNumber: string, 101 101 @Param("episodeNumber") episodeNumber: string, 102 102 ) { 103 - return this.showsService.getEpisodeDetails( 103 + const episode = await this.showsService.getEpisodeDetails( 104 104 showId, 105 105 Number(seasonNumber), 106 106 Number(episodeNumber), 107 107 ); 108 + const context = await this.showsService.getEpisodeContext( 109 + showId, 110 + Number(seasonNumber), 111 + Number(episodeNumber), 112 + ); 113 + 114 + return { 115 + ...episode, 116 + _context: context, 117 + }; 108 118 } 109 119 110 120 @Get("user/:userDid")
+73
backend/src/shows/shows.service.ts
··· 193 193 return response.json() as Promise<TMDBEpisode>; 194 194 } 195 195 196 + async getEpisodeContext( 197 + showId: string, 198 + seasonNumber: number, 199 + episodeNumber: number, 200 + ): Promise<{ 201 + previous: { seasonNumber: number; episodeNumber: number } | null; 202 + next: { seasonNumber: number; episodeNumber: number } | null; 203 + }> { 204 + const show = await this.getShowDetails(showId); 205 + const numberOfSeasons = show.number_of_seasons || 1; 206 + 207 + let previous: { seasonNumber: number; episodeNumber: number } | null = null; 208 + let next: { seasonNumber: number; episodeNumber: number } | null = null; 209 + 210 + // Try to find previous episode 211 + // First check current season for previous episode 212 + const currentSeason = await this.getSeasonDetails(showId, seasonNumber); 213 + const currentEpisodes = currentSeason.episodes || []; 214 + 215 + const prevInCurrentSeason = currentEpisodes.find( 216 + (e) => e.episode_number === episodeNumber - 1, 217 + ); 218 + if (prevInCurrentSeason) { 219 + previous = { seasonNumber, episodeNumber: episodeNumber - 1 }; 220 + } else if (seasonNumber > 1) { 221 + // Look in previous seasons 222 + for (let s = seasonNumber - 1; s >= 1; s--) { 223 + const prevSeason = await this.getSeasonDetails(showId, s); 224 + const prevEpisodes = prevSeason.episodes || []; 225 + if (prevEpisodes.length > 0) { 226 + const lastEpisode = prevEpisodes.reduce((max, ep) => 227 + ep.episode_number > max.episode_number ? ep : max, 228 + ); 229 + if (lastEpisode) { 230 + previous = { 231 + seasonNumber: s, 232 + episodeNumber: lastEpisode.episode_number, 233 + }; 234 + break; 235 + } 236 + } 237 + } 238 + } 239 + 240 + // Try to find next episode 241 + const nextInCurrentSeason = currentEpisodes.find( 242 + (e) => e.episode_number === episodeNumber + 1, 243 + ); 244 + if (nextInCurrentSeason) { 245 + next = { seasonNumber, episodeNumber: episodeNumber + 1 }; 246 + } else { 247 + // Look in next seasons 248 + for (let s = seasonNumber + 1; s <= numberOfSeasons; s++) { 249 + const nextSeason = await this.getSeasonDetails(showId, s); 250 + const nextEpisodes = nextSeason.episodes || []; 251 + if (nextEpisodes.length > 0) { 252 + const firstEpisode = nextEpisodes.reduce((min, ep) => 253 + ep.episode_number < min.episode_number ? ep : min, 254 + ); 255 + if (firstEpisode) { 256 + next = { 257 + seasonNumber: s, 258 + episodeNumber: firstEpisode.episode_number, 259 + }; 260 + break; 261 + } 262 + } 263 + } 264 + } 265 + 266 + return { previous, next }; 267 + } 268 + 196 269 async getShowByTMDBId(showId: string) { 197 270 return this.prisma.show.findUnique({ 198 271 where: { showId },
+1 -1
packages/api/src/generated/index.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 3 3 export { authControllerCallback, authControllerGetClientMetadata, authControllerLogin, authControllerLogout, authControllerMe, authControllerSuggestions, listsControllerAddItemToList, listsControllerAddToList, listsControllerCreateList, listsControllerDeleteList, listsControllerGetList, listsControllerGetListsForItem, listsControllerGetListsForMovie, listsControllerGetUserLists, listsControllerInitDefaultLists, listsControllerRemoveFromList, listsControllerRemoveItemFromList, listsControllerUpdateList, moviesControllerDeleteWatchHistoryEntry, moviesControllerDiscoverMovies, moviesControllerGetMovie, moviesControllerGetMovieDetails, moviesControllerGetMovieWatchHistory, moviesControllerGetUserMovies, moviesControllerGetUserMoviesPaginated, moviesControllerMarkWatched, moviesControllerSearchMovies, moviesControllerUnmarkWatched, type Options, shelfControllerGetUserShelf, showsControllerDeleteEpisodeWatchHistoryEntry, showsControllerDiscoverShows, showsControllerGetEpisodeDetails, showsControllerGetSeasonDetails, showsControllerGetShow, showsControllerGetShowDetails, showsControllerGetShowWatchHistory, showsControllerGetUserEpisodesPaginated, showsControllerGetUserShows, showsControllerMarkSeasonWatched, showsControllerMarkShowWatched, showsControllerMarkWatched, showsControllerSearchShows, showsControllerUnmarkWatched, usersControllerDeleteMyAccount, usersControllerGetMySettings, usersControllerUpdateMySettings } from './sdk.gen'; 4 - export type { AddToListDto, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponse, AuthControllerMeResponses, AuthControllerSuggestionsData, AuthControllerSuggestionsResponses, ClientOptions, CreateListDto, DeleteUserAccountDto, EpisodeHistoryItemDto, ListsControllerAddItemToListData, ListsControllerAddItemToListErrors, ListsControllerAddItemToListResponses, ListsControllerAddToListData, ListsControllerAddToListResponses, ListsControllerCreateListData, ListsControllerCreateListErrors, ListsControllerCreateListResponse, ListsControllerCreateListResponses, ListsControllerDeleteListData, ListsControllerDeleteListErrors, ListsControllerDeleteListResponses, ListsControllerGetListData, ListsControllerGetListErrors, ListsControllerGetListResponse, ListsControllerGetListResponses, ListsControllerGetListsForItemData, ListsControllerGetListsForItemErrors, ListsControllerGetListsForItemResponse, ListsControllerGetListsForItemResponses, ListsControllerGetListsForMovieData, ListsControllerGetListsForMovieResponses, ListsControllerGetUserListsData, ListsControllerGetUserListsErrors, ListsControllerGetUserListsResponse, ListsControllerGetUserListsResponses, ListsControllerInitDefaultListsData, ListsControllerInitDefaultListsErrors, ListsControllerInitDefaultListsResponse, ListsControllerInitDefaultListsResponses, ListsControllerRemoveFromListData, ListsControllerRemoveFromListResponses, ListsControllerRemoveItemFromListData, ListsControllerRemoveItemFromListErrors, ListsControllerRemoveItemFromListResponses, ListsControllerUpdateListData, ListsControllerUpdateListErrors, ListsControllerUpdateListResponse, ListsControllerUpdateListResponses, MarkedEpisodesResponseDto, MarkEpisodeWatchedDto, MarkSeasonWatchedDto, MarkShowWatchedDto, MediaInListDto, MovieColorsDto, MovieDto, MovieListDto, MovieListsForItemDto, MovieListSummaryDto, MovieListWithMoviesDto, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryErrors, MoviesControllerDeleteWatchHistoryEntryResponse, MoviesControllerDeleteWatchHistoryEntryResponses, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponse, MoviesControllerDiscoverMoviesResponses, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponse, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponse, MoviesControllerGetMovieResponses, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryErrors, MoviesControllerGetMovieWatchHistoryResponse, MoviesControllerGetMovieWatchHistoryResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponse, MoviesControllerGetUserMoviesPaginatedResponses, MoviesControllerGetUserMoviesResponse, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponse, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponse, MoviesControllerUnmarkWatchedResponses, PaginatedEpisodesResponseDto, PaginatedMoviesResponseDto, SearchResultsDto, SearchShowsResultsDto, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponse, ShelfControllerGetUserShelfResponses, ShelfResponseDto, ShowDto, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryErrors, ShowsControllerDeleteEpisodeWatchHistoryEntryResponse, ShowsControllerDeleteEpisodeWatchHistoryEntryResponses, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponse, ShowsControllerDiscoverShowsResponses, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponse, ShowsControllerGetEpisodeDetailsResponses, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponse, ShowsControllerGetSeasonDetailsResponses, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponse, ShowsControllerGetShowDetailsResponses, ShowsControllerGetShowResponse, ShowsControllerGetShowResponses, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryErrors, ShowsControllerGetShowWatchHistoryResponse, ShowsControllerGetShowWatchHistoryResponses, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponse, ShowsControllerGetUserEpisodesPaginatedResponses, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponse, ShowsControllerGetUserShowsResponses, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedErrors, ShowsControllerMarkSeasonWatchedResponse, ShowsControllerMarkSeasonWatchedResponses, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedErrors, ShowsControllerMarkShowWatchedResponse, ShowsControllerMarkShowWatchedResponses, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedErrors, ShowsControllerMarkWatchedResponse, ShowsControllerMarkWatchedResponses, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponse, ShowsControllerSearchShowsResponses, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponse, ShowsControllerUnmarkWatchedResponses, TmdbCastDto, TmdbCreditsDto, TmdbCrewDto, TmdbEpisodeDto, TmdbGenreDto, TmdbMovieDetailDto, TmdbMovieResultDto, TmdbNetworkDto, TmdbSeasonDetailDto, TmdbSeasonSummaryDto, TmdbShowDetailDto, TmdbShowResultDto, TrackedEpisodeDto, TrackedMovieDto, TrackedShowSummaryDto, UpdateListDto, UpdateUserSettingsDto, UserDto, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponse, UsersControllerDeleteMyAccountResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponse, UsersControllerGetMySettingsResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponse, UsersControllerUpdateMySettingsResponses, UserSettingsDto, WatchHistoryItemDto } from './types.gen'; 4 + export type { AddToListDto, AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponse, AuthControllerMeResponses, AuthControllerSuggestionsData, AuthControllerSuggestionsResponses, ClientOptions, CreateListDto, DeleteUserAccountDto, EpisodeContextDto, EpisodeHistoryItemDto, EpisodeReferenceDto, ListsControllerAddItemToListData, ListsControllerAddItemToListErrors, ListsControllerAddItemToListResponses, ListsControllerAddToListData, ListsControllerAddToListResponses, ListsControllerCreateListData, ListsControllerCreateListErrors, ListsControllerCreateListResponse, ListsControllerCreateListResponses, ListsControllerDeleteListData, ListsControllerDeleteListErrors, ListsControllerDeleteListResponses, ListsControllerGetListData, ListsControllerGetListErrors, ListsControllerGetListResponse, ListsControllerGetListResponses, ListsControllerGetListsForItemData, ListsControllerGetListsForItemErrors, ListsControllerGetListsForItemResponse, ListsControllerGetListsForItemResponses, ListsControllerGetListsForMovieData, ListsControllerGetListsForMovieResponses, ListsControllerGetUserListsData, ListsControllerGetUserListsErrors, ListsControllerGetUserListsResponse, ListsControllerGetUserListsResponses, ListsControllerInitDefaultListsData, ListsControllerInitDefaultListsErrors, ListsControllerInitDefaultListsResponse, ListsControllerInitDefaultListsResponses, ListsControllerRemoveFromListData, ListsControllerRemoveFromListResponses, ListsControllerRemoveItemFromListData, ListsControllerRemoveItemFromListErrors, ListsControllerRemoveItemFromListResponses, ListsControllerUpdateListData, ListsControllerUpdateListErrors, ListsControllerUpdateListResponse, ListsControllerUpdateListResponses, MarkedEpisodesResponseDto, MarkEpisodeWatchedDto, MarkSeasonWatchedDto, MarkShowWatchedDto, MediaInListDto, MovieColorsDto, MovieDto, MovieListDto, MovieListsForItemDto, MovieListSummaryDto, MovieListWithMoviesDto, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryErrors, MoviesControllerDeleteWatchHistoryEntryResponse, MoviesControllerDeleteWatchHistoryEntryResponses, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponse, MoviesControllerDiscoverMoviesResponses, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponse, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponse, MoviesControllerGetMovieResponses, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryErrors, MoviesControllerGetMovieWatchHistoryResponse, MoviesControllerGetMovieWatchHistoryResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponse, MoviesControllerGetUserMoviesPaginatedResponses, MoviesControllerGetUserMoviesResponse, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponse, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponse, MoviesControllerUnmarkWatchedResponses, PaginatedEpisodesResponseDto, PaginatedMoviesResponseDto, SearchResultsDto, SearchShowsResultsDto, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponse, ShelfControllerGetUserShelfResponses, ShelfResponseDto, ShowDto, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryErrors, ShowsControllerDeleteEpisodeWatchHistoryEntryResponse, ShowsControllerDeleteEpisodeWatchHistoryEntryResponses, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponse, ShowsControllerDiscoverShowsResponses, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponse, ShowsControllerGetEpisodeDetailsResponses, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponse, ShowsControllerGetSeasonDetailsResponses, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponse, ShowsControllerGetShowDetailsResponses, ShowsControllerGetShowResponse, ShowsControllerGetShowResponses, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryErrors, ShowsControllerGetShowWatchHistoryResponse, ShowsControllerGetShowWatchHistoryResponses, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponse, ShowsControllerGetUserEpisodesPaginatedResponses, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponse, ShowsControllerGetUserShowsResponses, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedErrors, ShowsControllerMarkSeasonWatchedResponse, ShowsControllerMarkSeasonWatchedResponses, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedErrors, ShowsControllerMarkShowWatchedResponse, ShowsControllerMarkShowWatchedResponses, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedErrors, ShowsControllerMarkWatchedResponse, ShowsControllerMarkWatchedResponses, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponse, ShowsControllerSearchShowsResponses, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponse, ShowsControllerUnmarkWatchedResponses, TmdbCastDto, TmdbCreditsDto, TmdbCrewDto, TmdbEpisodeDto, TmdbGenreDto, TmdbMovieDetailDto, TmdbMovieResultDto, TmdbNetworkDto, TmdbSeasonDetailDto, TmdbSeasonSummaryDto, TmdbShowDetailDto, TmdbShowResultDto, TrackedEpisodeDto, TrackedMovieDto, TrackedShowSummaryDto, UpdateListDto, UpdateUserSettingsDto, UserDto, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponse, UsersControllerDeleteMyAccountResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponse, UsersControllerGetMySettingsResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponse, UsersControllerUpdateMySettingsResponses, UserSettingsDto, WatchHistoryItemDto } from './types.gen';
+11
packages/api/src/generated/types.gen.ts
··· 182 182 origin_country?: string; 183 183 }; 184 184 185 + export type EpisodeReferenceDto = { 186 + seasonNumber: number; 187 + episodeNumber: number; 188 + }; 189 + 190 + export type EpisodeContextDto = { 191 + previous?: EpisodeReferenceDto; 192 + next?: EpisodeReferenceDto; 193 + }; 194 + 185 195 export type TmdbEpisodeDto = { 186 196 id: number; 187 197 name: string; ··· 198 208 show_id?: number; 199 209 crew?: Array<TmdbCrewDto>; 200 210 guest_stars?: Array<TmdbCastDto>; 211 + _context?: EpisodeContextDto; 201 212 }; 202 213 203 214 export type TmdbSeasonDetailDto = {