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.

feat: search overhaul

+1152 -325
+14
AGENTS.md
··· 190 190 191 191 ## TanStack Query 192 192 193 + **Always use the generated query options from `@opnshelf/api`** - never use raw `fetch()`. The API client is pre-configured with the base URL and handles authentication automatically. 194 + 195 + ```typescript 196 + // ✅ Correct - use generated options 197 + const { data } = useQuery({ 198 + ...searchControllerSearchAllOptions({ 199 + query: { query: searchTerm }, 200 + }), 201 + }); 202 + 203 + // ❌ Wrong - don't use fetch directly 204 + const response = await fetch(`${API_URL}/search/all?query=...`); 205 + ``` 206 + 193 207 **Always use `mutationKey`** on all `useMutation` calls. This enables better debugging, caching, and mutation state tracking. 194 208 195 209 ```typescript
+262 -155
apps/mobile/app/(tabs)/search.tsx
··· 1 1 import { 2 - moviesControllerDiscoverMoviesOptions, 3 2 moviesControllerGetUserMoviesOptions, 4 3 moviesControllerGetUserMoviesQueryKey, 5 4 moviesControllerMarkWatchedMutation, 6 - moviesControllerSearchMoviesOptions, 7 5 moviesControllerUnmarkWatchedMutation, 8 - showsControllerDiscoverShowsOptions, 9 - showsControllerSearchShowsOptions, 6 + showsControllerGetUserShowsOptions, 7 + showsControllerGetUserShowsQueryKey, 8 + showsControllerMarkShowWatchedMutation, 9 + showsControllerUnmarkWatchedMutation, 10 10 type TmdbMovieResultDto, 11 - type TmdbShowResultDto, 11 + type UnifiedSearchResultDto, 12 12 } from "@opnshelf/api"; 13 13 import { FlashList, type ListRenderItem } from "@shopify/flash-list"; 14 14 import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; 15 15 import { router } from "expo-router"; 16 16 import { useCallback, useEffect, useMemo, useRef, useState } from "react"; 17 - import { Pressable, StyleSheet, Text, View } from "react-native"; 17 + import { Dimensions, Pressable, StyleSheet, Text, View } from "react-native"; 18 18 import { SafeAreaView } from "react-native-safe-area-context"; 19 19 import { MovieItem } from "@/components/MovieItem"; 20 20 import { ShowItem } from "@/components/ShowItem"; ··· 27 27 import { createTitleSlug } from "@/lib/utils"; 28 28 29 29 const DEBOUNCE_MS = 300; 30 + const API_URL = process.env.EXPO_PUBLIC_API_URL || "http://localhost:3001"; 31 + const SCREEN_WIDTH = Dimensions.get("window").width; 32 + const GAP = spacing.md; 33 + const H_PADDING = spacing.lg; 34 + const COLUMNS = 2; 35 + const ITEM_MARGIN = GAP / 2; 36 + const ITEM_WIDTH = (SCREEN_WIDTH - H_PADDING * 2) / COLUMNS - ITEM_MARGIN * 2; 30 37 31 38 export default function SearchScreen() { 32 39 const [query, setQuery] = useState(""); ··· 61 68 enabled: !!user?.did, 62 69 }); 63 70 71 + const { data: trackedShows } = useQuery({ 72 + ...showsControllerGetUserShowsOptions({ 73 + path: { userDid: user?.did || "" }, 74 + }), 75 + enabled: !!user?.did, 76 + }); 77 + 64 78 const watchedMovieIds = useMemo(() => { 65 79 if (!trackedMovies) return new Set<string>(); 66 80 return new Set(trackedMovies.map((m) => m.movieId)); 67 81 }, [trackedMovies]); 68 82 69 - const { data, isLoading, error } = useQuery({ 70 - ...moviesControllerSearchMoviesOptions({ 71 - query: { query: debouncedQuery }, 72 - }), 73 - enabled: 74 - debouncedQuery.length > 0 && 75 - (mediaType === "all" || mediaType === "movies"), 83 + const watchedShowIds = useMemo(() => { 84 + if (!trackedShows) return new Set<string>(); 85 + return new Set(trackedShows.map((s) => s.showId)); 86 + }, [trackedShows]); 87 + 88 + const { 89 + data: searchData, 90 + isLoading: isSearchLoading, 91 + error: searchError, 92 + } = useQuery({ 93 + queryKey: ["search", "all", debouncedQuery], 94 + queryFn: async () => { 95 + const response = await fetch( 96 + `${API_URL}/search/all?query=${encodeURIComponent(debouncedQuery)}`, 97 + ); 98 + if (!response.ok) throw new Error("Search failed"); 99 + return response.json(); 100 + }, 101 + enabled: debouncedQuery.length > 0, 76 102 }); 77 103 78 104 const { 79 - data: showData, 80 - isLoading: isShowLoading, 81 - error: showError, 105 + data: discoverData, 106 + isLoading: isDiscoverLoading, 107 + error: discoverError, 82 108 } = useQuery({ 83 - ...showsControllerSearchShowsOptions({ 84 - query: { query: debouncedQuery }, 85 - }), 86 - enabled: 87 - debouncedQuery.length > 0 && 88 - (mediaType === "all" || mediaType === "shows"), 109 + queryKey: ["search", "discover"], 110 + queryFn: async () => { 111 + const response = await fetch(`${API_URL}/search/discover`); 112 + if (!response.ok) throw new Error("Discover failed"); 113 + return response.json(); 114 + }, 115 + enabled: debouncedQuery.length === 0, 89 116 }); 90 117 91 - const { data: discoverData, isLoading: isDiscoverLoading } = useQuery({ 92 - ...moviesControllerDiscoverMoviesOptions({}), 93 - enabled: 94 - debouncedQuery.length === 0 && 95 - (mediaType === "all" || mediaType === "movies"), 96 - }); 118 + const results: UnifiedSearchResultDto[] = useMemo(() => { 119 + const data = debouncedQuery.length > 0 ? searchData : discoverData; 120 + return data?.results ?? []; 121 + }, [debouncedQuery, searchData, discoverData]); 122 + 123 + const total = useMemo(() => { 124 + const data = debouncedQuery.length > 0 ? searchData : discoverData; 125 + return data?.total_results ?? results.length; 126 + }, [debouncedQuery, searchData, discoverData, results.length]); 97 127 98 - const { data: discoverShowsData, isLoading: isDiscoverShowsLoading } = 99 - useQuery({ 100 - ...showsControllerDiscoverShowsOptions({}), 101 - enabled: 102 - debouncedQuery.length === 0 && 103 - (mediaType === "all" || mediaType === "shows"), 104 - }); 128 + const showTotal = debouncedQuery.length > 0 && total > 0; 129 + 130 + const isLoading = 131 + debouncedQuery.length > 0 ? isSearchLoading : isDiscoverLoading; 132 + const error = debouncedQuery.length > 0 ? searchError : discoverError; 133 + 134 + const filteredResults = useMemo(() => { 135 + if (mediaType === "movies") { 136 + return results.filter((r) => r.media_type === "movie"); 137 + } 138 + if (mediaType === "shows") { 139 + return results.filter((r) => r.media_type === "tv"); 140 + } 141 + return results; 142 + }, [results, mediaType]); 105 143 106 144 const markMutation = useMutation({ 107 145 mutationKey: ["movies", "markWatched"], ··· 135 173 }, 136 174 }); 137 175 176 + const markShowMutation = useMutation({ 177 + mutationKey: ["shows", "markShowWatched"], 178 + ...showsControllerMarkShowWatchedMutation(), 179 + onSuccess: () => { 180 + queryClient.invalidateQueries({ 181 + queryKey: showsControllerGetUserShowsQueryKey({ 182 + path: { userDid: user?.did || "" }, 183 + }), 184 + }); 185 + showToast("Added to your shelf", "success"); 186 + }, 187 + onError: () => { 188 + showToast("Failed to add to shelf. Please try again.", "error"); 189 + }, 190 + }); 191 + 192 + const unmarkShowMutation = useMutation({ 193 + mutationKey: ["shows", "unmarkWatched"], 194 + ...showsControllerUnmarkWatchedMutation(), 195 + onSuccess: () => { 196 + queryClient.invalidateQueries({ 197 + queryKey: showsControllerGetUserShowsQueryKey({ 198 + path: { userDid: user?.did || "" }, 199 + }), 200 + }); 201 + showToast("Removed from your shelf", "success"); 202 + }, 203 + onError: () => { 204 + showToast("Failed to remove from shelf. Please try again.", "error"); 205 + }, 206 + }); 207 + 138 208 const handleToggleWatched = useCallback( 139 209 (movieId: string, isWatched: boolean) => { 140 210 if (!user) { ··· 150 220 } 151 221 }, 152 222 [user, markMutation, unmarkMutation, showToast], 223 + ); 224 + 225 + const handleToggleShowWatched = useCallback( 226 + (showId: string, isWatched: boolean) => { 227 + if (!user) { 228 + showToast("Sign in to track shows", "info"); 229 + router.push("/login"); 230 + return; 231 + } 232 + 233 + if (isWatched) { 234 + unmarkShowMutation.mutate({ path: { showId }, query: { mode: "all" } }); 235 + } else { 236 + markShowMutation.mutate({ body: { showId } }); 237 + } 238 + }, 239 + [user, markShowMutation, unmarkShowMutation, showToast], 153 240 ); 154 241 155 242 const handleMoviePress = useCallback( ··· 175 262 }); 176 263 }, []); 177 264 178 - const renderMovieItem: ListRenderItem<TmdbMovieResultDto> = useCallback( 265 + const renderItem: ListRenderItem<UnifiedSearchResultDto> = useCallback( 179 266 ({ item }) => { 180 - const movieId = item.id.toString(); 181 - const isWatched = watchedMovieIds.has(movieId); 182 - const isMarking = 183 - markMutation.isPending && 184 - markMutation.variables?.body?.movieId === movieId; 185 - const isUnmarking = 186 - unmarkMutation.isPending && 187 - unmarkMutation.variables?.path?.movieId === movieId; 267 + if (item.media_type === "movie") { 268 + const movie: TmdbMovieResultDto = { 269 + id: item.id, 270 + title: item.title ?? "", 271 + poster_path: item.poster_path, 272 + backdrop_path: item.backdrop_path, 273 + release_date: item.release_date, 274 + overview: item.overview, 275 + }; 276 + const movieId = item.id.toString(); 277 + const isWatched = watchedMovieIds.has(movieId); 278 + const isMarking = 279 + markMutation.isPending && 280 + markMutation.variables?.body?.movieId === movieId; 281 + const isUnmarking = 282 + unmarkMutation.isPending && 283 + unmarkMutation.variables?.path?.movieId === movieId; 284 + 285 + return ( 286 + <MovieItem 287 + movie={movie} 288 + isWatched={isWatched} 289 + isMarking={isMarking} 290 + isUnmarking={isUnmarking} 291 + onToggle={handleToggleWatched} 292 + onPress={() => handleMoviePress(movie)} 293 + width={ITEM_WIDTH} 294 + /> 295 + ); 296 + } else { 297 + const show = { 298 + id: item.id, 299 + name: item.name ?? "", 300 + poster_path: item.poster_path, 301 + backdrop_path: item.backdrop_path, 302 + first_air_date: item.first_air_date, 303 + overview: item.overview, 304 + }; 305 + const showId = item.id.toString(); 306 + const isWatched = watchedShowIds.has(showId); 307 + const isMarking = 308 + markShowMutation.isPending && 309 + markShowMutation.variables?.body?.showId === showId; 310 + const isUnmarking = 311 + unmarkShowMutation.isPending && 312 + unmarkShowMutation.variables?.path?.showId === showId; 188 313 189 - return ( 190 - <MovieItem 191 - movie={item} 192 - isWatched={isWatched} 193 - isMarking={isMarking} 194 - isUnmarking={isUnmarking} 195 - onToggle={handleToggleWatched} 196 - onPress={() => handleMoviePress(item)} 197 - /> 198 - ); 314 + return ( 315 + <ShowItem 316 + show={show} 317 + isWatched={isWatched} 318 + isMarking={isMarking} 319 + isUnmarking={isUnmarking} 320 + onToggle={handleToggleShowWatched} 321 + onPress={() => 322 + handleShowPress(show as { id: number; name: string }) 323 + } 324 + width={ITEM_WIDTH} 325 + /> 326 + ); 327 + } 199 328 }, 200 329 [ 201 330 watchedMovieIds, 202 331 markMutation, 203 332 unmarkMutation, 333 + watchedShowIds, 334 + markShowMutation, 335 + unmarkShowMutation, 204 336 handleToggleWatched, 337 + handleToggleShowWatched, 205 338 handleMoviePress, 339 + handleShowPress, 206 340 ], 207 341 ); 208 342 209 343 const keyExtractor = useCallback( 210 - (item: TmdbMovieResultDto) => item.id.toString(), 211 - [], 212 - ); 213 - const showKeyExtractor = useCallback( 214 - (item: TmdbShowResultDto) => item.id.toString(), 344 + (item: UnifiedSearchResultDto) => `${item.media_type}-${item.id}`, 215 345 [], 216 346 ); 217 347 218 - const renderShowItem: ListRenderItem<TmdbShowResultDto> = useCallback( 219 - ({ item }) => ( 220 - <ShowItem 221 - show={item} 222 - onPress={() => handleShowPress(item as TmdbShowResultDto)} 223 - /> 224 - ), 225 - [handleShowPress], 226 - ); 227 - 228 348 const renderSkeleton = () => ( 229 - <View style={styles.skeletonGrid}> 230 - {[...Array(10)].map((_, i) => ( 231 - <View key={i} style={styles.skeletonItem}> 232 - <Skeleton width="100%" height={210} borderRadius={borderRadius.lg} /> 233 - <View style={{ marginTop: spacing.sm }}> 234 - <Skeleton width="80%" height={16} /> 349 + <View style={styles.skeletonContainer}> 350 + <View style={styles.skeletonRow}> 351 + {[...Array(2)].map((_, i) => ( 352 + <View key={i} style={styles.skeletonItem}> 353 + <Skeleton 354 + width="100%" 355 + height={210} 356 + borderRadius={borderRadius.lg} 357 + /> 358 + <View style={{ marginTop: spacing.sm }}> 359 + <Skeleton width="80%" height={16} /> 360 + </View> 361 + <View style={{ marginTop: spacing.xs }}> 362 + <Skeleton width="50%" height={14} /> 363 + </View> 235 364 </View> 236 - <View style={{ marginTop: spacing.xs }}> 237 - <Skeleton width="50%" height={14} /> 365 + ))} 366 + </View> 367 + <View style={styles.skeletonRow}> 368 + {[...Array(2)].map((_, i) => ( 369 + <View key={i} style={styles.skeletonItem}> 370 + <Skeleton 371 + width="100%" 372 + height={210} 373 + borderRadius={borderRadius.lg} 374 + /> 375 + <View style={{ marginTop: spacing.sm }}> 376 + <Skeleton width="80%" height={16} /> 377 + </View> 378 + <View style={{ marginTop: spacing.xs }}> 379 + <Skeleton width="50%" height={14} /> 380 + </View> 238 381 </View> 239 - </View> 240 - ))} 382 + ))} 383 + </View> 241 384 </View> 242 385 ); 243 386 244 - const showResults = useMemo(() => showData?.results || [], [showData]); 245 - const discoverShowResults = useMemo( 246 - () => discoverShowsData?.results || [], 247 - [discoverShowsData], 248 - ); 249 - 250 387 return ( 251 388 <SafeAreaView 252 389 style={[styles.container, { backgroundColor: colors.background }]} ··· 254 391 > 255 392 <View style={styles.header}> 256 393 <Text style={[styles.title, { color: colors.onBackground }]}> 257 - Search Movies 394 + Search 258 395 </Text> 259 396 </View> 260 397 261 398 <SearchInput 262 399 value={query} 263 400 onChangeText={setQuery} 264 - placeholder="Search for a movie..." 401 + placeholder="Search movies and shows..." 265 402 containerStyle={styles.searchInput} 266 403 onClear={() => setQuery("")} 267 404 /> ··· 292 429 ))} 293 430 </View> 294 431 295 - {(isLoading || isShowLoading) && renderSkeleton()} 432 + {isLoading && renderSkeleton()} 296 433 297 - {(error || showError) && ( 434 + {error && ( 298 435 <View style={styles.centerContent}> 299 436 <Text style={[styles.errorText, { color: colors.error }]}> 300 - Error: {(error || showError)?.message} 437 + Error: {(error as Error).message} 301 438 </Text> 302 439 </View> 303 440 )} 304 441 305 - {data && data.results.length > 0 && ( 442 + {!isLoading && filteredResults.length > 0 && ( 306 443 <FlashList 307 - data={data.results} 308 - renderItem={renderMovieItem} 444 + data={filteredResults} 445 + renderItem={renderItem} 309 446 keyExtractor={keyExtractor} 310 447 numColumns={2} 311 448 contentContainerStyle={styles.listContent} 312 - extraData={watchedMovieIds} 449 + extraData={{ 450 + watchedMovieIds, 451 + watchedShowIds, 452 + markMutation, 453 + unmarkMutation, 454 + markShowMutation, 455 + unmarkShowMutation, 456 + }} 313 457 ListHeaderComponent={ 314 - <Text 315 - style={[styles.resultsCount, { color: colors.onSurfaceVariant }]} 316 - > 317 - Found {data.total_results.toLocaleString()} results 318 - </Text> 458 + showTotal ? ( 459 + <Text 460 + style={[ 461 + styles.resultsCount, 462 + { color: colors.onSurfaceVariant }, 463 + ]} 464 + > 465 + Found {total.toLocaleString()} results 466 + </Text> 467 + ) : null 319 468 } 320 469 /> 321 470 )} 322 471 323 - {showData && showResults.length > 0 && ( 324 - <FlashList 325 - data={showResults} 326 - renderItem={renderShowItem} 327 - keyExtractor={showKeyExtractor} 328 - numColumns={2} 329 - contentContainerStyle={styles.listContent} 330 - /> 331 - )} 332 - 333 - {data && data.results.length === 0 && debouncedQuery && ( 472 + {!isLoading && filteredResults.length === 0 && debouncedQuery && ( 334 473 <View style={styles.centerContent}> 335 474 <Text style={[styles.emptyText, { color: colors.onSurfaceVariant }]}> 336 475 No results found for &quot;{debouncedQuery}&quot; ··· 338 477 </View> 339 478 )} 340 479 341 - {!debouncedQuery && ( 342 - <View style={{ flex: 1 }}> 343 - <View style={styles.header}> 344 - <Text style={[styles.title, { color: colors.onBackground }]}> 345 - Popular Movies 346 - </Text> 347 - </View> 348 - {isDiscoverLoading && renderSkeleton()} 349 - {discoverData && discoverData.results.length > 0 && ( 350 - <FlashList 351 - data={discoverData.results} 352 - renderItem={renderMovieItem} 353 - keyExtractor={keyExtractor} 354 - numColumns={2} 355 - contentContainerStyle={styles.listContent} 356 - extraData={watchedMovieIds} 357 - /> 358 - )} 359 - {(mediaType === "all" || mediaType === "shows") && ( 360 - <> 361 - <View style={styles.header}> 362 - <Text style={[styles.title, { color: colors.onBackground }]}> 363 - Popular Shows 364 - </Text> 365 - </View> 366 - {isDiscoverShowsLoading && renderSkeleton()} 367 - {discoverShowResults.length > 0 && ( 368 - <FlashList 369 - data={discoverShowResults} 370 - renderItem={renderShowItem} 371 - keyExtractor={showKeyExtractor} 372 - numColumns={2} 373 - contentContainerStyle={styles.listContent} 374 - /> 375 - )} 376 - </> 377 - )} 480 + {!isLoading && !debouncedQuery && filteredResults.length === 0 && ( 481 + <View style={styles.centerContent}> 482 + <Text style={[styles.emptyText, { color: colors.onSurfaceVariant }]}> 483 + No popular content available 484 + </Text> 378 485 </View> 379 486 )} 380 487 </SafeAreaView> ··· 410 517 borderRadius: borderRadius.full, 411 518 }, 412 519 listContent: { 413 - padding: spacing.lg, 520 + paddingVertical: spacing.lg, 521 + paddingHorizontal: H_PADDING, 414 522 }, 415 523 resultsCount: { 416 524 fontSize: 14, 417 525 marginBottom: spacing.md, 526 + paddingHorizontal: spacing.sm, 418 527 }, 419 528 centerContent: { 420 529 flex: 1, ··· 430 539 fontSize: 14, 431 540 textAlign: "center", 432 541 }, 433 - skeletonGrid: { 542 + skeletonContainer: { 543 + paddingHorizontal: H_PADDING, 544 + }, 545 + skeletonRow: { 434 546 flexDirection: "row", 435 - flexWrap: "wrap", 436 - padding: spacing.lg, 437 - paddingTop: 0, 547 + gap: GAP, 438 548 }, 439 549 skeletonItem: { 440 550 flex: 1, 441 551 marginBottom: spacing.lg, 442 - marginHorizontal: spacing.sm, 443 - minWidth: 140, 444 - maxWidth: "47%", 445 552 }, 446 553 });
+5 -4
apps/mobile/components/MovieItem.tsx
··· 30 30 isUnmarking: boolean; 31 31 onToggle: (movieId: string, isWatched: boolean) => void; 32 32 onPress: () => void; 33 + width?: number; 33 34 } 34 35 35 36 const AnimatedPressable = Animated.createAnimatedComponent(Pressable); ··· 61 62 isUnmarking, 62 63 onToggle, 63 64 onPress, 65 + width, 64 66 }: MovieItemProps) { 65 67 const { colors } = useTheme(); 66 68 const scale = useSharedValue(1); ··· 101 103 () => 102 104 StyleSheet.create({ 103 105 movieItem: { 104 - flex: 1, 106 + width: width, 105 107 marginBottom: spacing.lg, 106 - marginHorizontal: spacing.sm, 107 - minWidth: 140, 108 - maxWidth: "47%", 108 + marginHorizontal: spacing.xs, 109 109 }, 110 110 posterContainer: { 111 111 aspectRatio: 2 / 3, ··· 181 181 }, 182 182 }), 183 183 [ 184 + width, 184 185 colors.surfaceContainer, 185 186 colors.surfaceContainerHigh, 186 187 colors.onSurfaceVariant,
+146 -7
apps/mobile/components/ShowItem.tsx
··· 1 - import { useMemo } from "react"; 1 + import { useCallback, useMemo } from "react"; 2 + import { Check, Loader2, Plus } from "lucide-react-native"; 3 + import * as Haptics from "expo-haptics"; 2 4 import { Image } from "expo-image"; 3 - import { Pressable, StyleSheet, Text, View } from "react-native"; 5 + import { 6 + type GestureResponderEvent, 7 + Platform, 8 + Pressable, 9 + StyleSheet, 10 + Text, 11 + View, 12 + } from "react-native"; 13 + import Animated, { 14 + Easing, 15 + useAnimatedStyle, 16 + useSharedValue, 17 + withRepeat, 18 + withSpring, 19 + withTiming, 20 + } from "react-native-reanimated"; 4 21 import { borderRadius, spacing } from "@/constants/spacing"; 5 22 import { useTheme } from "@/contexts/theme"; 6 23 import { getTmdbPosterUrl } from "@/lib/utils"; ··· 17 34 interface ShowItemProps { 18 35 show: ShowItemData; 19 36 onPress: () => void; 37 + onToggle?: (showId: string, isWatched: boolean) => void; 38 + isWatched?: boolean; 39 + isMarking?: boolean; 40 + isUnmarking?: boolean; 20 41 metaText?: string; 42 + width?: number; 21 43 } 22 44 23 - export function ShowItem({ show, onPress, metaText }: ShowItemProps) { 45 + const AnimatedPressable = Animated.createAnimatedComponent(Pressable); 46 + 47 + const SpinningLoader = ({ size, color }: { size: number; color: string }) => { 48 + const rotation = useSharedValue(0); 49 + 50 + rotation.value = withRepeat( 51 + withTiming(360, { duration: 1000, easing: Easing.linear }), 52 + -1, 53 + false, 54 + ); 55 + 56 + const animatedStyle = useAnimatedStyle(() => ({ 57 + transform: [{ rotate: `${rotation.value}deg` }], 58 + })); 59 + 60 + return ( 61 + <Animated.View style={animatedStyle}> 62 + <Loader2 size={size} color={color} /> 63 + </Animated.View> 64 + ); 65 + }; 66 + 67 + export function ShowItem({ 68 + show, 69 + onPress, 70 + onToggle, 71 + isWatched = false, 72 + isMarking = false, 73 + isUnmarking = false, 74 + metaText, 75 + width, 76 + }: ShowItemProps) { 24 77 const { colors } = useTheme(); 78 + const scale = useSharedValue(1); 79 + const opacity = useSharedValue(1); 80 + 81 + const handleToggle = useCallback( 82 + (e: GestureResponderEvent) => { 83 + e.stopPropagation(); 84 + if (!onToggle) return; 85 + 86 + if (Platform.OS !== "web") { 87 + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); 88 + } 89 + 90 + onToggle(show.id.toString(), isWatched); 91 + }, 92 + [show.id, isWatched, onToggle], 93 + ); 94 + 95 + const handlePressIn = useCallback(() => { 96 + scale.value = withSpring(0.95, { damping: 15, stiffness: 300 }); 97 + opacity.value = withTiming(0.8, { duration: 100 }); 98 + }, [scale, opacity]); 99 + 100 + const handlePressOut = useCallback(() => { 101 + scale.value = withSpring(1, { damping: 15, stiffness: 300 }); 102 + opacity.value = withTiming(1, { duration: 100 }); 103 + }, [scale, opacity]); 104 + 105 + const animatedButtonStyle = useAnimatedStyle(() => ({ 106 + transform: [{ scale: scale.value }], 107 + opacity: opacity.value, 108 + })); 109 + 110 + const isPending = isMarking || isUnmarking; 25 111 const posterUrl = getTmdbPosterUrl( 26 112 show.poster_path ?? show.posterPath ?? null, 27 113 ); 28 114 const firstAirDate = show.first_air_date ?? show.firstAirDate ?? null; 29 115 const year = firstAirDate ? firstAirDate.split("-")[0] : undefined; 116 + const hasToggle = !!onToggle; 30 117 31 118 const styles = useMemo( 32 119 () => 33 120 StyleSheet.create({ 34 121 showItem: { 35 - flex: 1, 122 + width: width, 36 123 marginBottom: spacing.lg, 37 - marginHorizontal: spacing.sm, 38 - minWidth: 140, 39 - maxWidth: "47%", 124 + marginHorizontal: spacing.xs, 40 125 }, 41 126 posterContainer: { 42 127 aspectRatio: 2 / 3, ··· 48 133 shadowOpacity: 0.3, 49 134 shadowRadius: 8, 50 135 elevation: 8, 136 + position: "relative", 51 137 }, 52 138 poster: { 53 139 width: "100%", ··· 63 149 fontSize: 12, 64 150 fontWeight: "500", 65 151 }, 152 + actionButton: { 153 + position: "absolute", 154 + top: spacing.sm, 155 + right: spacing.sm, 156 + width: 40, 157 + height: 40, 158 + borderRadius: borderRadius.full, 159 + backgroundColor: colors.primary, 160 + justifyContent: "center", 161 + alignItems: "center", 162 + shadowColor: "#000", 163 + shadowOffset: { width: 0, height: 3 }, 164 + shadowOpacity: 0.4, 165 + shadowRadius: 5, 166 + elevation: 5, 167 + }, 168 + actionButtonWatched: { 169 + backgroundColor: "#16a34a", 170 + }, 171 + iconContainer: { 172 + width: 20, 173 + height: 20, 174 + justifyContent: "center", 175 + alignItems: "center", 176 + }, 66 177 titleContainer: { 67 178 marginTop: spacing.sm, 68 179 minHeight: 40, ··· 84 195 }, 85 196 }), 86 197 [ 198 + width, 87 199 colors.surfaceContainer, 88 200 colors.surfaceContainerHigh, 89 201 colors.onSurfaceVariant, 202 + colors.primary, 203 + colors.tertiary, 90 204 colors.onSurface, 91 205 ], 92 206 ); ··· 105 219 <View style={[styles.poster, styles.noPoster]}> 106 220 <Text style={styles.noPosterText}>No poster</Text> 107 221 </View> 222 + )} 223 + 224 + {hasToggle && ( 225 + <AnimatedPressable 226 + onPress={handleToggle} 227 + onPressIn={handlePressIn} 228 + onPressOut={handlePressOut} 229 + disabled={isPending} 230 + hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} 231 + style={[ 232 + styles.actionButton, 233 + isWatched && styles.actionButtonWatched, 234 + animatedButtonStyle, 235 + ]} 236 + > 237 + <View style={styles.iconContainer}> 238 + {isPending ? ( 239 + <SpinningLoader size={16} color={colors.onSurface} /> 240 + ) : isWatched ? ( 241 + <Check size={16} color={colors.onSurface} strokeWidth={2.5} /> 242 + ) : ( 243 + <Plus size={16} color={colors.onSurface} strokeWidth={2.5} /> 244 + )} 245 + </View> 246 + </AnimatedPressable> 108 247 )} 109 248 </Pressable> 110 249 <Pressable onPress={onPress} style={styles.titleContainer}>
+11 -10
apps/mobile/components/ui/Input.tsx
··· 17 17 {icon ? <View style={styles.icon}>{icon}</View> : null} 18 18 <TextInput 19 19 style={[styles.input, icon ? styles.inputWithIcon : undefined, { color: colors.onSurface }, style]} 20 - placeholderTextColor={colors.onSurfaceVariant} 20 + placeholderTextColor="#e5e7eb" 21 21 {...props} 22 22 /> 23 23 </View> ··· 42 42 [colors.surfaceContainer, colors.outline, containerStyle], 43 43 ); 44 44 45 - const inputStyles = useMemo( 46 - () => [ 47 - styles.input, 48 - styles.inputWithIcon, 49 - { color: colors.onSurface, placeholderTextColor: colors.onSurfaceVariant }, 50 - hasValue ? styles.inputWithClear : null, 51 - ], 52 - [colors.onSurface, colors.onSurfaceVariant, hasValue], 45 + const inputTextStyle = useMemo( 46 + () => [styles.input, styles.inputWithIcon, hasValue ? styles.inputWithClear : null], 47 + [hasValue], 53 48 ); 54 49 55 50 return ( ··· 57 52 <View style={styles.icon}> 58 53 <Search size={20} color={colors.onSurfaceVariant} /> 59 54 </View> 60 - <TextInput style={inputStyles} value={value} {...props} /> 55 + <TextInput 56 + style={inputTextStyle} 57 + value={value} 58 + placeholderTextColor={colors.onSurfaceVariant} 59 + color={colors.onSurface} 60 + {...props} 61 + /> 61 62 {hasValue && onClear && ( 62 63 <TouchableOpacity style={styles.clearButton} onPress={onClear}> 63 64 <X size={20} color={colors.onSurfaceVariant} />
+114 -3
apps/web/src/components/ShowCard.tsx
··· 1 - import type { TmdbShowResultDto } from "@opnshelf/api"; 1 + import { 2 + showsControllerGetUserShowsQueryKey, 3 + showsControllerMarkShowWatchedMutation, 4 + showsControllerUnmarkWatchedMutation, 5 + type TmdbShowResultDto, 6 + type UserDto, 7 + } from "@opnshelf/api"; 8 + import { useMutation, useQueryClient } from "@tanstack/react-query"; 2 9 import { Link } from "@tanstack/react-router"; 10 + import { Check, Loader2, Plus } from "lucide-react"; 11 + import { toast } from "sonner"; 12 + import { Button } from "@/components/ui/button"; 13 + import { 14 + Tooltip, 15 + TooltipContent, 16 + TooltipProvider, 17 + TooltipTrigger, 18 + } from "@/components/ui/tooltip"; 3 19 import { createTitleSlug, getTmdbPosterUrl } from "@/lib/utils"; 4 20 5 21 interface ShowCardProps { 6 22 show: TmdbShowResultDto; 23 + user: UserDto | null | undefined; 24 + isWatched: boolean; 25 + showActions?: boolean; 7 26 } 8 27 9 - export function ShowCard({ show }: ShowCardProps) { 28 + export function ShowCard({ 29 + show, 30 + user, 31 + isWatched, 32 + showActions = true, 33 + }: ShowCardProps) { 34 + const queryClient = useQueryClient(); 35 + const showId = show.id.toString(); 36 + 37 + const markMutation = useMutation({ 38 + mutationKey: ["shows", showId, "markShowWatched"], 39 + ...showsControllerMarkShowWatchedMutation(), 40 + onSuccess: () => { 41 + queryClient.invalidateQueries({ 42 + queryKey: showsControllerGetUserShowsQueryKey({ 43 + path: { userDid: user?.did || "" }, 44 + }), 45 + }); 46 + toast.success("Added to your shelf"); 47 + }, 48 + onError: () => { 49 + toast.error("Failed to update. Please try again."); 50 + }, 51 + }); 52 + 53 + const unmarkMutation = useMutation({ 54 + mutationKey: ["shows", showId, "unmarkWatched"], 55 + ...showsControllerUnmarkWatchedMutation(), 56 + onSuccess: () => { 57 + queryClient.invalidateQueries({ 58 + queryKey: showsControllerGetUserShowsQueryKey({ 59 + path: { userDid: user?.did || "" }, 60 + }), 61 + }); 62 + toast.success("Removed from your shelf"); 63 + }, 64 + onError: () => { 65 + toast.error("Failed to update. Please try again."); 66 + }, 67 + }); 68 + 69 + const isMarkPending = 70 + markMutation.isPending && markMutation.variables?.body?.showId === showId; 71 + const isUnmarkPending = 72 + unmarkMutation.isPending && 73 + unmarkMutation.variables?.path?.showId === showId; 74 + const isPending = isMarkPending || isUnmarkPending; 75 + 10 76 const compatShow = show as TmdbShowResultDto & { 11 77 posterPath?: string | null; 12 78 firstAirDate?: string | null; 13 79 }; 14 - const showId = show.id.toString(); 15 80 const posterUrl = getTmdbPosterUrl( 16 81 show.poster_path ?? compatShow.posterPath ?? null, 17 82 ); ··· 34 99 ) : ( 35 100 <div className="w-full h-full flex items-center justify-center text-gray-600"> 36 101 No poster 102 + </div> 103 + )} 104 + {showActions && user && ( 105 + <div className="absolute top-2 right-2 z-10"> 106 + <TooltipProvider> 107 + <Tooltip> 108 + <TooltipTrigger asChild> 109 + <Button 110 + type="button" 111 + size="icon" 112 + variant="default" 113 + onClick={(e) => { 114 + e.preventDefault(); 115 + e.stopPropagation(); 116 + if (isWatched) { 117 + unmarkMutation.mutate({ 118 + path: { showId }, 119 + query: { mode: "all" }, 120 + }); 121 + } else { 122 + markMutation.mutate({ 123 + body: { showId }, 124 + }); 125 + } 126 + }} 127 + disabled={isPending} 128 + className={`${ 129 + isWatched 130 + ? "bg-green-600 hover:bg-red-600" 131 + : "bg-primary hover:bg-primary/80 [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover:opacity-100" 132 + } transition-opacity`} 133 + > 134 + {isPending ? ( 135 + <Loader2 className="w-4 h-4 animate-spin" /> 136 + ) : isWatched ? ( 137 + <Check className="w-4 h-4" /> 138 + ) : ( 139 + <Plus className="w-4 h-4" /> 140 + )} 141 + </Button> 142 + </TooltipTrigger> 143 + <TooltipContent> 144 + <p>{isWatched ? "Remove from shelf" : "Mark as watched"}</p> 145 + </TooltipContent> 146 + </Tooltip> 147 + </TooltipProvider> 37 148 </div> 38 149 )} 39 150 </Link>
+114 -141
apps/web/src/routes/search.tsx
··· 1 1 import { 2 2 authControllerMeOptions, 3 - moviesControllerDiscoverMoviesOptions, 4 3 moviesControllerGetUserMoviesOptions, 5 - moviesControllerSearchMoviesOptions, 6 - showsControllerDiscoverShowsOptions, 7 - showsControllerSearchShowsOptions, 4 + searchControllerDiscoverAllOptions, 5 + searchControllerSearchAllOptions, 6 + showsControllerGetUserShowsOptions, 8 7 type TmdbMovieResultDto, 9 - type TmdbShowResultDto, 8 + type UnifiedSearchResultDto, 10 9 } from "@opnshelf/api"; 11 10 import { useQuery } from "@tanstack/react-query"; 12 11 import { createFileRoute, useNavigate } from "@tanstack/react-router"; 13 12 import { Search, X } from "lucide-react"; 14 13 import { useEffect, useMemo, useRef, useState } from "react"; 15 - import { MovieGrid, MovieGridSkeleton } from "@/components/MovieGrid"; 16 - import { ShowGrid } from "@/components/ShowGrid"; 14 + import { MovieCard } from "@/components/MovieCard"; 15 + import { ShowCard } from "@/components/ShowCard"; 17 16 import { M3TextField } from "@/components/ui/m3-text-field"; 18 17 19 18 export const Route = createFileRoute("/search")({ ··· 28 27 }); 29 28 30 29 const DEBOUNCE_MS = 300; 31 - const ALL_PREVIEW_LIMIT = 12; 32 30 33 31 function SearchPage() { 34 32 const { q: searchQuery, type } = Route.useSearch(); ··· 50 48 enabled: !!user?.did, 51 49 }); 52 50 51 + const { data: trackedShows } = useQuery({ 52 + ...showsControllerGetUserShowsOptions({ 53 + path: { userDid: user?.did || "" }, 54 + }), 55 + enabled: !!user?.did, 56 + }); 57 + 53 58 const watchedMovieIds = useMemo(() => { 54 59 if (!trackedMovies) return new Set<string>(); 55 60 return new Set(trackedMovies.map((m: { movieId: string }) => m.movieId)); 56 61 }, [trackedMovies]); 62 + 63 + const watchedShowIds = useMemo(() => { 64 + if (!trackedShows) return new Set<string>(); 65 + return new Set(trackedShows.map((s: { showId: string }) => s.showId)); 66 + }, [trackedShows]); 57 67 58 68 useEffect(() => { 59 69 if (searchQuery !== lastNavigatedQueryRef.current) { ··· 87 97 }, [query, searchQuery, type, navigate]); 88 98 89 99 const hasQuery = searchQuery.length > 0; 90 - const isAll = type === "all"; 100 + const _isAll = type === "all"; 91 101 const isMovies = type === "movies"; 92 102 const isShows = type === "shows"; 93 103 94 104 const { 95 - data: movieSearchData, 96 - isLoading: isMovieSearchLoading, 97 - error: movieSearchError, 105 + data: searchData, 106 + isLoading: isSearchLoading, 107 + error: searchError, 98 108 } = useQuery({ 99 - ...moviesControllerSearchMoviesOptions({ 109 + ...searchControllerSearchAllOptions({ 100 110 query: { query: searchQuery }, 101 111 }), 102 - enabled: hasQuery && (isAll || isMovies), 112 + enabled: hasQuery, 103 113 }); 104 114 105 - const { 106 - data: showSearchData, 107 - isLoading: isShowSearchLoading, 108 - error: showSearchError, 109 - } = useQuery({ 110 - ...showsControllerSearchShowsOptions({ 111 - query: { query: searchQuery }, 112 - }), 113 - enabled: hasQuery && (isAll || isShows), 115 + const { data: discoverData, isLoading: isDiscoverLoading } = useQuery({ 116 + ...searchControllerDiscoverAllOptions({}), 117 + enabled: !hasQuery, 114 118 }); 115 119 116 - const { data: discoverMoviesData, isLoading: isDiscoverMoviesLoading } = 117 - useQuery({ 118 - ...moviesControllerDiscoverMoviesOptions({}), 119 - enabled: !hasQuery && (isAll || isMovies), 120 - }); 121 - 122 - const { data: discoverShowsData, isLoading: isDiscoverShowsLoading } = 123 - useQuery({ 124 - ...showsControllerDiscoverShowsOptions({}), 125 - enabled: !hasQuery && (isAll || isShows), 126 - }); 120 + const results: UnifiedSearchResultDto[] = hasQuery 121 + ? (searchData?.results ?? []) 122 + : (discoverData?.results ?? []); 127 123 128 - const movieResults: TmdbMovieResultDto[] = hasQuery 129 - ? (movieSearchData?.results ?? []) 130 - : (discoverMoviesData?.results ?? []); 131 - const showResults: TmdbShowResultDto[] = hasQuery 132 - ? (showSearchData?.results ?? []) 133 - : (discoverShowsData?.results ?? []); 124 + const total = hasQuery 125 + ? (searchData?.total_results ?? results.length) 126 + : results.length; 134 127 135 - const movieTotal = hasQuery 136 - ? (movieSearchData?.total_results ?? movieResults.length) 137 - : movieResults.length; 138 - const showTotal = hasQuery 139 - ? (showSearchData?.total_results ?? showResults.length) 140 - : showResults.length; 128 + const showTotal = hasQuery && total > 0; 141 129 142 - const movieLoading = hasQuery 143 - ? isMovieSearchLoading 144 - : isDiscoverMoviesLoading; 145 - const showLoading = hasQuery ? isShowSearchLoading : isDiscoverShowsLoading; 146 - const primaryError = movieSearchError || showSearchError; 130 + const loading = hasQuery ? isSearchLoading : isDiscoverLoading; 147 131 148 132 const switchType = (nextType: "all" | "movies" | "shows") => { 149 133 const trimmed = query.trim(); ··· 154 138 resetScroll: false, 155 139 }); 156 140 }; 141 + 142 + const movieResults = useMemo(() => { 143 + if (isShows) return []; 144 + return results.filter((r) => r.media_type === "movie"); 145 + }, [results, isShows]); 146 + 147 + const showResults = useMemo(() => { 148 + if (isMovies) return []; 149 + return results.filter((r) => r.media_type === "tv"); 150 + }, [results, isMovies]); 151 + 152 + const combinedResults = useMemo(() => { 153 + if (isMovies) return movieResults; 154 + if (isShows) return showResults; 155 + return results; 156 + }, [isMovies, isShows, movieResults, showResults, results]); 157 157 158 158 return ( 159 159 <div ··· 219 219 </div> 220 220 </div> 221 221 222 - {primaryError && ( 222 + {searchError && ( 223 223 <div 224 224 className="max-w-2xl p-4 rounded-lg mb-4" 225 225 style={{ ··· 228 228 }} 229 229 > 230 230 <p style={{ color: "var(--md-sys-color-on-error-container)" }}> 231 - Error: {primaryError.message} 231 + Error: {(searchError as Error).message} 232 232 </p> 233 233 </div> 234 234 )} 235 235 236 - {isAll && ( 237 - <div className="grid grid-cols-1 lg:grid-cols-2 gap-8"> 238 - <section> 239 - <div className="flex items-center justify-between mb-4"> 240 - <h2 className="md-title-large">Movies</h2> 241 - <span className="text-sm text-(--md-sys-color-on-surface-variant)"> 242 - {movieTotal.toLocaleString()} 243 - </span> 244 - </div> 245 - {movieLoading ? ( 246 - <MovieGridSkeleton count={6} /> 247 - ) : movieResults.length > 0 ? ( 248 - <MovieGrid 249 - movies={movieResults.slice(0, ALL_PREVIEW_LIMIT)} 250 - user={user} 251 - watchedMovieIds={watchedMovieIds} 252 - gridClassName="grid-cols-2 md:grid-cols-3" 253 - /> 254 - ) : ( 255 - <p className="text-sm text-(--md-sys-color-on-surface-variant) py-4"> 256 - No movie results. 257 - </p> 258 - )} 259 - </section> 260 - 261 - <section> 262 - <div className="flex items-center justify-between mb-4"> 263 - <h2 className="md-title-large">Shows</h2> 264 - <span className="text-sm text-(--md-sys-color-on-surface-variant)"> 265 - {showTotal.toLocaleString()} 266 - </span> 267 - </div> 268 - {showLoading ? ( 269 - <MovieGridSkeleton count={6} /> 270 - ) : showResults.length > 0 ? ( 271 - <ShowGrid 272 - shows={showResults.slice(0, ALL_PREVIEW_LIMIT)} 273 - gridClassName="grid-cols-2 md:grid-cols-3" 274 - /> 275 - ) : ( 276 - <p className="text-sm text-(--md-sys-color-on-surface-variant) py-4"> 277 - No show results. 278 - </p> 279 - )} 280 - </section> 236 + {loading ? ( 237 + <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"> 238 + {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => ( 239 + <div 240 + key={i} 241 + className="aspect-2/3 bg-gray-800 rounded-lg animate-pulse" 242 + /> 243 + ))} 281 244 </div> 282 - )} 283 - 284 - {isMovies && ( 285 - <section> 245 + ) : combinedResults.length > 0 ? ( 246 + <> 286 247 <div className="flex items-center justify-between mb-4"> 287 248 <h2 className="md-title-large"> 288 - {hasQuery ? "Movie Results" : "Popular Movies"} 249 + {hasQuery ? "Results" : "Popular"} 289 250 </h2> 290 - <span className="text-sm text-(--md-sys-color-on-surface-variant)"> 291 - {movieTotal.toLocaleString()} 292 - </span> 251 + {showTotal && ( 252 + <span className="text-sm text-(--md-sys-color-on-surface-variant)"> 253 + {total.toLocaleString()} results 254 + </span> 255 + )} 293 256 </div> 294 - {movieLoading ? ( 295 - <MovieGridSkeleton /> 296 - ) : movieResults.length > 0 ? ( 297 - <MovieGrid 298 - movies={movieResults} 299 - user={user} 300 - watchedMovieIds={watchedMovieIds} 301 - /> 302 - ) : ( 303 - <div className="text-center py-12 text-(--md-sys-color-on-surface-variant)"> 304 - No movie results found for &quot;{searchQuery}&quot; 305 - </div> 306 - )} 307 - </section> 308 - )} 309 - 310 - {isShows && ( 311 - <section> 312 - <div className="flex items-center justify-between mb-4"> 313 - <h2 className="md-title-large"> 314 - {hasQuery ? "Show Results" : "Popular Shows"} 315 - </h2> 316 - <span className="text-sm text-(--md-sys-color-on-surface-variant)"> 317 - {showTotal.toLocaleString()} 318 - </span> 257 + <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"> 258 + {combinedResults.map((item) => { 259 + if (item.media_type === "movie") { 260 + const movie: TmdbMovieResultDto = { 261 + id: item.id, 262 + title: item.title ?? "", 263 + poster_path: item.poster_path, 264 + backdrop_path: item.backdrop_path, 265 + release_date: item.release_date, 266 + overview: item.overview, 267 + }; 268 + return ( 269 + <MovieCard 270 + key={`movie-${item.id}`} 271 + movie={movie} 272 + user={user} 273 + isWatched={watchedMovieIds.has(item.id.toString())} 274 + /> 275 + ); 276 + } else { 277 + const show = { 278 + id: item.id, 279 + name: item.name ?? "", 280 + poster_path: item.poster_path, 281 + backdrop_path: item.backdrop_path, 282 + first_air_date: item.first_air_date, 283 + overview: item.overview, 284 + }; 285 + return ( 286 + <ShowCard 287 + key={`show-${item.id}`} 288 + show={show} 289 + user={user} 290 + isWatched={watchedShowIds.has(item.id.toString())} 291 + /> 292 + ); 293 + } 294 + })} 319 295 </div> 320 - {showLoading ? ( 321 - <MovieGridSkeleton /> 322 - ) : showResults.length > 0 ? ( 323 - <ShowGrid shows={showResults} /> 324 - ) : ( 325 - <div className="text-center py-12 text-(--md-sys-color-on-surface-variant)"> 326 - No show results found for &quot;{searchQuery}&quot; 327 - </div> 328 - )} 329 - </section> 296 + </> 297 + ) : ( 298 + <div className="text-center py-12 text-(--md-sys-color-on-surface-variant)"> 299 + {hasQuery 300 + ? `No results found for "${searchQuery}"` 301 + : "No popular content available"} 302 + </div> 330 303 )} 331 304 </div> 332 305 </div>
+2
backend/src/app.module.ts
··· 5 5 import { ListsModule } from "./lists/lists.module"; 6 6 import { MoviesModule } from "./movies/movies.module"; 7 7 import { PrismaModule } from "./prisma/prisma.module"; 8 + import { SearchModule } from "./search/search.module"; 8 9 import { ShelfModule } from "./shelf/shelf.module"; 9 10 import { ShowsModule } from "./shows/shows.module"; 10 11 import { UsersModule } from "./users/users.module"; ··· 20 21 ListsModule, 21 22 ShowsModule, 22 23 ShelfModule, 24 + SearchModule, 23 25 ], 24 26 }) 25 27 export class AppModule {}
+3
backend/src/movies/movies.service.ts
··· 17 17 backdrop_path?: string; 18 18 release_date?: string; 19 19 overview?: string; 20 + popularity: number; 21 + vote_average: number; 22 + vote_count: number; 20 23 } 21 24 22 25 export interface TMDBSearchResponse {
+105
backend/src/search/dto/search.dto.ts
··· 1 + import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; 2 + 3 + export type MediaType = "movie" | "tv"; 4 + 5 + export class UnifiedSearchResultDto { 6 + @ApiProperty() 7 + id: number; 8 + 9 + @ApiProperty({ enum: ["movie", "tv"] }) 10 + media_type: MediaType; 11 + 12 + @ApiPropertyOptional() 13 + title?: string; 14 + 15 + @ApiPropertyOptional() 16 + name?: string; 17 + 18 + @ApiPropertyOptional() 19 + poster_path?: string; 20 + 21 + @ApiPropertyOptional() 22 + backdrop_path?: string; 23 + 24 + @ApiPropertyOptional() 25 + release_date?: string; 26 + 27 + @ApiPropertyOptional() 28 + first_air_date?: string; 29 + 30 + @ApiPropertyOptional() 31 + overview?: string; 32 + 33 + @ApiProperty() 34 + popularity: number; 35 + 36 + @ApiProperty() 37 + vote_average: number; 38 + 39 + @ApiProperty() 40 + vote_count: number; 41 + 42 + @ApiPropertyOptional() 43 + original_language?: string; 44 + 45 + @ApiPropertyOptional() 46 + genre_ids?: number[]; 47 + 48 + @ApiPropertyOptional() 49 + original_title?: string; 50 + 51 + @ApiPropertyOptional() 52 + original_name?: string; 53 + 54 + @ApiPropertyOptional() 55 + adult?: boolean; 56 + 57 + @ApiPropertyOptional() 58 + video?: boolean; 59 + } 60 + 61 + export class UnifiedSearchResponseDto { 62 + @ApiProperty({ type: [UnifiedSearchResultDto] }) 63 + results: UnifiedSearchResultDto[]; 64 + 65 + @ApiProperty() 66 + total_results: number; 67 + 68 + @ApiProperty() 69 + page: number; 70 + } 71 + 72 + export class UnifiedDiscoverResponseDto { 73 + @ApiProperty({ type: [UnifiedSearchResultDto] }) 74 + results: UnifiedSearchResultDto[]; 75 + 76 + @ApiProperty() 77 + total_results: number; 78 + 79 + @ApiProperty() 80 + page: number; 81 + } 82 + 83 + export class DiscoverQueryDto { 84 + @ApiProperty({ 85 + required: false, 86 + enum: [ 87 + "popularity.desc", 88 + "popularity.asc", 89 + "vote_average.desc", 90 + "vote_average.asc", 91 + "release_date.desc", 92 + "release_date.asc", 93 + "primary_release_date.desc", 94 + "primary_release_date.asc", 95 + ], 96 + default: "popularity.desc", 97 + }) 98 + sortBy?: string; 99 + 100 + @ApiProperty({ required: false, minimum: 1, default: 1 }) 101 + page?: number; 102 + 103 + @ApiProperty({ required: false, minimum: 1900, maximum: 2100 }) 104 + year?: number; 105 + }
+35
backend/src/search/search.controller.ts
··· 1 + import { Controller, Get, Query } from "@nestjs/common"; 2 + import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger"; 3 + import { 4 + type DiscoverQueryDto, 5 + UnifiedDiscoverResponseDto, 6 + UnifiedSearchResponseDto, 7 + } from "./dto/search.dto"; 8 + import { SearchService } from "./search.service"; 9 + 10 + @ApiTags("search") 11 + @Controller("search") 12 + export class SearchController { 13 + constructor(private readonly searchService: SearchService) {} 14 + 15 + @Get("all") 16 + @ApiOperation({ summary: "Search movies and shows from TMDB" }) 17 + @ApiQuery({ name: "query", required: true, description: "Search term" }) 18 + @ApiQuery({ name: "page", required: false, description: "Page number" }) 19 + @ApiResponse({ status: 200, type: UnifiedSearchResponseDto }) 20 + async searchAll(@Query("query") query: string, @Query("page") page?: number) { 21 + return this.searchService.searchAll(query, page ?? 1); 22 + } 23 + 24 + @Get("discover") 25 + @ApiOperation({ 26 + summary: "Discover popular movies and shows from TMDB", 27 + }) 28 + @ApiQuery({ name: "sortBy", required: false, description: "Sort by" }) 29 + @ApiQuery({ name: "page", required: false, description: "Page number" }) 30 + @ApiQuery({ name: "year", required: false, description: "Year filter" }) 31 + @ApiResponse({ status: 200, type: UnifiedDiscoverResponseDto }) 32 + async discoverAll(@Query() query: DiscoverQueryDto) { 33 + return this.searchService.discoverAll(query); 34 + } 35 + }
+13
backend/src/search/search.module.ts
··· 1 + import { Module } from "@nestjs/common"; 2 + import { MoviesModule } from "../movies/movies.module"; 3 + import { ShowsModule } from "../shows/shows.module"; 4 + import { SearchController } from "./search.controller"; 5 + import { SearchService } from "./search.service"; 6 + 7 + @Module({ 8 + imports: [MoviesModule, ShowsModule], 9 + controllers: [SearchController], 10 + providers: [SearchService], 11 + exports: [SearchService], 12 + }) 13 + export class SearchModule {}
+139
backend/src/search/search.service.ts
··· 1 + import { Injectable } from "@nestjs/common"; 2 + import { MoviesService } from "../movies/movies.service"; 3 + import { ShowsService } from "../shows/shows.service"; 4 + import { 5 + type DiscoverQueryDto, 6 + type MediaType, 7 + type UnifiedDiscoverResponseDto, 8 + type UnifiedSearchResponseDto, 9 + type UnifiedSearchResultDto, 10 + } from "./dto/search.dto"; 11 + 12 + interface TMDBSearchItem { 13 + id: number; 14 + popularity: number; 15 + vote_average: number; 16 + vote_count: number; 17 + poster_path?: string; 18 + backdrop_path?: string; 19 + overview?: string; 20 + genre_ids?: number[]; 21 + original_language?: string; 22 + adult?: boolean; 23 + video?: boolean; 24 + } 25 + 26 + interface TMDBSearchMovie extends TMDBSearchItem { 27 + title: string; 28 + release_date?: string; 29 + original_title?: string; 30 + } 31 + 32 + interface TMDBSearchShow extends TMDBSearchItem { 33 + name: string; 34 + first_air_date?: string; 35 + original_name?: string; 36 + } 37 + 38 + @Injectable() 39 + export class SearchService { 40 + constructor( 41 + private moviesService: MoviesService, 42 + private showsService: ShowsService, 43 + ) {} 44 + 45 + async searchAll( 46 + query: string, 47 + page: number = 1, 48 + ): Promise<UnifiedSearchResponseDto> { 49 + const [movieResults, showResults] = await Promise.all([ 50 + this.moviesService.searchMovies(query, page), 51 + this.showsService.searchShows(query, page), 52 + ]); 53 + 54 + const unifiedResults = this.mergeResults( 55 + movieResults.results, 56 + showResults.results, 57 + ); 58 + 59 + return { 60 + results: unifiedResults, 61 + total_results: movieResults.total_results + showResults.total_results, 62 + page, 63 + }; 64 + } 65 + 66 + async discoverAll( 67 + query: DiscoverQueryDto, 68 + ): Promise<UnifiedDiscoverResponseDto> { 69 + const { sortBy = "popularity.desc", page = 1, year } = query; 70 + 71 + const [movieResults, showResults] = await Promise.all([ 72 + this.moviesService.discoverMovies(sortBy, page, year), 73 + this.showsService.discoverShows(sortBy, page, year), 74 + ]); 75 + 76 + const unifiedResults = this.mergeResults( 77 + movieResults.results, 78 + showResults.results, 79 + ); 80 + 81 + return { 82 + results: unifiedResults, 83 + total_results: movieResults.total_results + showResults.total_results, 84 + page, 85 + }; 86 + } 87 + 88 + private mergeResults( 89 + movieResults: TMDBSearchMovie[], 90 + showResults: TMDBSearchShow[], 91 + ): UnifiedSearchResultDto[] { 92 + const movieWithType: UnifiedSearchResultDto[] = movieResults.map((m) => ({ 93 + id: m.id, 94 + media_type: "movie", 95 + title: m.title, 96 + poster_path: m.poster_path, 97 + backdrop_path: m.backdrop_path, 98 + release_date: m.release_date, 99 + overview: m.overview, 100 + popularity: m.popularity, 101 + vote_average: m.vote_average, 102 + vote_count: m.vote_count, 103 + original_language: m.original_language, 104 + genre_ids: m.genre_ids, 105 + original_title: m.original_title, 106 + adult: m.adult, 107 + video: m.video, 108 + })); 109 + 110 + const showWithType: UnifiedSearchResultDto[] = showResults.map((s) => ({ 111 + id: s.id, 112 + media_type: "tv", 113 + name: s.name, 114 + poster_path: s.poster_path, 115 + backdrop_path: s.backdrop_path, 116 + first_air_date: s.first_air_date, 117 + overview: s.overview, 118 + popularity: s.popularity, 119 + vote_average: s.vote_average, 120 + vote_count: s.vote_count, 121 + original_language: s.original_language, 122 + genre_ids: s.genre_ids, 123 + original_name: s.original_name, 124 + adult: s.adult, 125 + video: s.video, 126 + })); 127 + 128 + const combined = [...movieWithType, ...showWithType]; 129 + 130 + combined.sort((a, b) => { 131 + if (b.popularity !== a.popularity) { 132 + return b.popularity - a.popularity; 133 + } 134 + return b.vote_count - a.vote_count; 135 + }); 136 + 137 + return combined; 138 + } 139 + }
+3
backend/src/shows/shows.service.ts
··· 24 24 genres?: Array<{ id: number; name: string }>; 25 25 number_of_seasons?: number; 26 26 number_of_episodes?: number; 27 + popularity: number; 28 + vote_average: number; 29 + vote_count: number; 27 30 } 28 31 29 32 export interface TMDBSearchResponse {
+92 -2
packages/api/src/generated/@tanstack/react-query.gen.ts
··· 3 3 import { type DefaultError, type InfiniteData, infiniteQueryOptions, queryOptions, type UseMutationOptions } from '@tanstack/react-query'; 4 4 5 5 import { client } from '../client.gen'; 6 - import { 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'; 7 - import type { AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerMeData, AuthControllerMeResponse, AuthControllerSuggestionsData, ListsControllerAddItemToListData, ListsControllerAddToListData, ListsControllerCreateListData, ListsControllerCreateListResponse, ListsControllerDeleteListData, ListsControllerGetListData, ListsControllerGetListResponse, ListsControllerGetListsForItemData, ListsControllerGetListsForItemResponse, ListsControllerGetListsForMovieData, ListsControllerGetUserListsData, ListsControllerGetUserListsResponse, ListsControllerInitDefaultListsData, ListsControllerInitDefaultListsResponse, ListsControllerRemoveFromListData, ListsControllerRemoveItemFromListData, ListsControllerUpdateListData, ListsControllerUpdateListResponse, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryResponse, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponse, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponse, MoviesControllerGetMovieResponse, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryResponse, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponse, MoviesControllerGetUserMoviesResponse, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedResponse, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedResponse, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponse, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryResponse, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponse, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponse, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponse, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponse, ShowsControllerGetShowResponse, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryResponse, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponse, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponse, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedResponse, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedResponse, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedResponse, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponse, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponse, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountResponse, UsersControllerGetMySettingsData, UsersControllerGetMySettingsResponse, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsResponse } from '../types.gen'; 6 + import { 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, searchControllerDiscoverAll, searchControllerSearchAll, shelfControllerGetUserShelf, showsControllerDeleteEpisodeWatchHistoryEntry, showsControllerDiscoverShows, showsControllerGetEpisodeDetails, showsControllerGetSeasonDetails, showsControllerGetShow, showsControllerGetShowDetails, showsControllerGetShowWatchHistory, showsControllerGetUserEpisodesPaginated, showsControllerGetUserShows, showsControllerMarkSeasonWatched, showsControllerMarkShowWatched, showsControllerMarkWatched, showsControllerSearchShows, showsControllerUnmarkWatched, usersControllerDeleteMyAccount, usersControllerGetMySettings, usersControllerUpdateMySettings } from '../sdk.gen'; 7 + import type { AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerMeData, AuthControllerMeResponse, AuthControllerSuggestionsData, ListsControllerAddItemToListData, ListsControllerAddToListData, ListsControllerCreateListData, ListsControllerCreateListResponse, ListsControllerDeleteListData, ListsControllerGetListData, ListsControllerGetListResponse, ListsControllerGetListsForItemData, ListsControllerGetListsForItemResponse, ListsControllerGetListsForMovieData, ListsControllerGetUserListsData, ListsControllerGetUserListsResponse, ListsControllerInitDefaultListsData, ListsControllerInitDefaultListsResponse, ListsControllerRemoveFromListData, ListsControllerRemoveItemFromListData, ListsControllerUpdateListData, ListsControllerUpdateListResponse, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryResponse, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponse, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponse, MoviesControllerGetMovieResponse, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryResponse, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponse, MoviesControllerGetUserMoviesResponse, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedResponse, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponse, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedResponse, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponse, SearchControllerSearchAllData, SearchControllerSearchAllResponse, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponse, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryResponse, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponse, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponse, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponse, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponse, ShowsControllerGetShowResponse, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryResponse, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponse, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponse, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedResponse, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedResponse, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedResponse, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponse, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponse, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountResponse, UsersControllerGetMySettingsData, UsersControllerGetMySettingsResponse, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsResponse } from '../types.gen'; 8 8 9 9 export type QueryKey<TOptions extends Options> = [ 10 10 Pick<TOptions, 'baseUrl' | 'body' | 'headers' | 'path' | 'query'> & { ··· 957 957 }, 958 958 queryKey: shelfControllerGetUserShelfInfiniteQueryKey(options) 959 959 }); 960 + 961 + export const searchControllerSearchAllQueryKey = (options: Options<SearchControllerSearchAllData>) => createQueryKey('searchControllerSearchAll', options); 962 + 963 + /** 964 + * Search movies and shows from TMDB 965 + */ 966 + export const searchControllerSearchAllOptions = (options: Options<SearchControllerSearchAllData>) => queryOptions<SearchControllerSearchAllResponse, DefaultError, SearchControllerSearchAllResponse, ReturnType<typeof searchControllerSearchAllQueryKey>>({ 967 + queryFn: async ({ queryKey, signal }) => { 968 + const { data } = await searchControllerSearchAll({ 969 + ...options, 970 + ...queryKey[0], 971 + signal, 972 + throwOnError: true 973 + }); 974 + return data; 975 + }, 976 + queryKey: searchControllerSearchAllQueryKey(options) 977 + }); 978 + 979 + export const searchControllerSearchAllInfiniteQueryKey = (options: Options<SearchControllerSearchAllData>): QueryKey<Options<SearchControllerSearchAllData>> => createQueryKey('searchControllerSearchAll', options, true); 980 + 981 + /** 982 + * Search movies and shows from TMDB 983 + */ 984 + export const searchControllerSearchAllInfiniteOptions = (options: Options<SearchControllerSearchAllData>) => infiniteQueryOptions<SearchControllerSearchAllResponse, DefaultError, InfiniteData<SearchControllerSearchAllResponse>, QueryKey<Options<SearchControllerSearchAllData>>, number | Pick<QueryKey<Options<SearchControllerSearchAllData>>[0], 'body' | 'headers' | 'path' | 'query'>>( 985 + // @ts-ignore 986 + { 987 + queryFn: async ({ pageParam, queryKey, signal }) => { 988 + // @ts-ignore 989 + const page: Pick<QueryKey<Options<SearchControllerSearchAllData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { 990 + query: { 991 + page: pageParam 992 + } 993 + }; 994 + const params = createInfiniteParams(queryKey, page); 995 + const { data } = await searchControllerSearchAll({ 996 + ...options, 997 + ...params, 998 + signal, 999 + throwOnError: true 1000 + }); 1001 + return data; 1002 + }, 1003 + queryKey: searchControllerSearchAllInfiniteQueryKey(options) 1004 + }); 1005 + 1006 + export const searchControllerDiscoverAllQueryKey = (options?: Options<SearchControllerDiscoverAllData>) => createQueryKey('searchControllerDiscoverAll', options); 1007 + 1008 + /** 1009 + * Discover popular movies and shows from TMDB 1010 + */ 1011 + export const searchControllerDiscoverAllOptions = (options?: Options<SearchControllerDiscoverAllData>) => queryOptions<SearchControllerDiscoverAllResponse, DefaultError, SearchControllerDiscoverAllResponse, ReturnType<typeof searchControllerDiscoverAllQueryKey>>({ 1012 + queryFn: async ({ queryKey, signal }) => { 1013 + const { data } = await searchControllerDiscoverAll({ 1014 + ...options, 1015 + ...queryKey[0], 1016 + signal, 1017 + throwOnError: true 1018 + }); 1019 + return data; 1020 + }, 1021 + queryKey: searchControllerDiscoverAllQueryKey(options) 1022 + }); 1023 + 1024 + export const searchControllerDiscoverAllInfiniteQueryKey = (options?: Options<SearchControllerDiscoverAllData>): QueryKey<Options<SearchControllerDiscoverAllData>> => createQueryKey('searchControllerDiscoverAll', options, true); 1025 + 1026 + /** 1027 + * Discover popular movies and shows from TMDB 1028 + */ 1029 + export const searchControllerDiscoverAllInfiniteOptions = (options?: Options<SearchControllerDiscoverAllData>) => infiniteQueryOptions<SearchControllerDiscoverAllResponse, DefaultError, InfiniteData<SearchControllerDiscoverAllResponse>, QueryKey<Options<SearchControllerDiscoverAllData>>, unknown | Pick<QueryKey<Options<SearchControllerDiscoverAllData>>[0], 'body' | 'headers' | 'path' | 'query'>>( 1030 + // @ts-ignore 1031 + { 1032 + queryFn: async ({ pageParam, queryKey, signal }) => { 1033 + // @ts-ignore 1034 + const page: Pick<QueryKey<Options<SearchControllerDiscoverAllData>>[0], 'body' | 'headers' | 'path' | 'query'> = typeof pageParam === 'object' ? pageParam : { 1035 + query: { 1036 + page: pageParam 1037 + } 1038 + }; 1039 + const params = createInfiniteParams(queryKey, page); 1040 + const { data } = await searchControllerDiscoverAll({ 1041 + ...options, 1042 + ...params, 1043 + signal, 1044 + throwOnError: true 1045 + }); 1046 + return data; 1047 + }, 1048 + queryKey: searchControllerDiscoverAllInfiniteQueryKey(options) 1049 + });
+2 -2
packages/api/src/generated/index.ts
··· 1 1 // This file is auto-generated by @hey-api/openapi-ts 2 2 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, 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'; 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, searchControllerDiscoverAll, searchControllerSearchAll, 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, 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, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponse, SearchControllerDiscoverAllResponses, SearchControllerSearchAllData, SearchControllerSearchAllResponse, SearchControllerSearchAllResponses, 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, UnifiedDiscoverResponseDto, UnifiedSearchResponseDto, UnifiedSearchResultDto, UpdateListDto, UpdateUserSettingsDto, UserDto, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponse, UsersControllerDeleteMyAccountResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponse, UsersControllerGetMySettingsResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponse, UsersControllerUpdateMySettingsResponses, UserSettingsDto, WatchHistoryItemDto } from './types.gen';
+11 -1
packages/api/src/generated/sdk.gen.ts
··· 2 2 3 3 import type { Client, Options as Options2, TDataShape } from './client'; 4 4 import { client } from './client.gen'; 5 - import type { AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponses, AuthControllerSuggestionsData, AuthControllerSuggestionsResponses, ListsControllerAddItemToListData, ListsControllerAddItemToListErrors, ListsControllerAddItemToListResponses, ListsControllerAddToListData, ListsControllerAddToListResponses, ListsControllerCreateListData, ListsControllerCreateListErrors, ListsControllerCreateListResponses, ListsControllerDeleteListData, ListsControllerDeleteListErrors, ListsControllerDeleteListResponses, ListsControllerGetListData, ListsControllerGetListErrors, ListsControllerGetListResponses, ListsControllerGetListsForItemData, ListsControllerGetListsForItemErrors, ListsControllerGetListsForItemResponses, ListsControllerGetListsForMovieData, ListsControllerGetListsForMovieResponses, ListsControllerGetUserListsData, ListsControllerGetUserListsErrors, ListsControllerGetUserListsResponses, ListsControllerInitDefaultListsData, ListsControllerInitDefaultListsErrors, ListsControllerInitDefaultListsResponses, ListsControllerRemoveFromListData, ListsControllerRemoveFromListResponses, ListsControllerRemoveItemFromListData, ListsControllerRemoveItemFromListErrors, ListsControllerRemoveItemFromListResponses, ListsControllerUpdateListData, ListsControllerUpdateListErrors, ListsControllerUpdateListResponses, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryErrors, MoviesControllerDeleteWatchHistoryEntryResponses, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponses, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponses, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryErrors, MoviesControllerGetMovieWatchHistoryResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponses, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponses, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponses, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryErrors, ShowsControllerDeleteEpisodeWatchHistoryEntryResponses, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponses, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponses, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponses, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponses, ShowsControllerGetShowResponses, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryErrors, ShowsControllerGetShowWatchHistoryResponses, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponses, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponses, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedErrors, ShowsControllerMarkSeasonWatchedResponses, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedErrors, ShowsControllerMarkShowWatchedResponses, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedErrors, ShowsControllerMarkWatchedResponses, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponses, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponses, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponses } from './types.gen'; 5 + import type { AuthControllerCallbackData, AuthControllerGetClientMetadataData, AuthControllerGetClientMetadataResponses, AuthControllerLoginData, AuthControllerLogoutData, AuthControllerLogoutResponses, AuthControllerMeData, AuthControllerMeErrors, AuthControllerMeResponses, AuthControllerSuggestionsData, AuthControllerSuggestionsResponses, ListsControllerAddItemToListData, ListsControllerAddItemToListErrors, ListsControllerAddItemToListResponses, ListsControllerAddToListData, ListsControllerAddToListResponses, ListsControllerCreateListData, ListsControllerCreateListErrors, ListsControllerCreateListResponses, ListsControllerDeleteListData, ListsControllerDeleteListErrors, ListsControllerDeleteListResponses, ListsControllerGetListData, ListsControllerGetListErrors, ListsControllerGetListResponses, ListsControllerGetListsForItemData, ListsControllerGetListsForItemErrors, ListsControllerGetListsForItemResponses, ListsControllerGetListsForMovieData, ListsControllerGetListsForMovieResponses, ListsControllerGetUserListsData, ListsControllerGetUserListsErrors, ListsControllerGetUserListsResponses, ListsControllerInitDefaultListsData, ListsControllerInitDefaultListsErrors, ListsControllerInitDefaultListsResponses, ListsControllerRemoveFromListData, ListsControllerRemoveFromListResponses, ListsControllerRemoveItemFromListData, ListsControllerRemoveItemFromListErrors, ListsControllerRemoveItemFromListResponses, ListsControllerUpdateListData, ListsControllerUpdateListErrors, ListsControllerUpdateListResponses, MoviesControllerDeleteWatchHistoryEntryData, MoviesControllerDeleteWatchHistoryEntryErrors, MoviesControllerDeleteWatchHistoryEntryResponses, MoviesControllerDiscoverMoviesData, MoviesControllerDiscoverMoviesResponses, MoviesControllerGetMovieData, MoviesControllerGetMovieDetailsData, MoviesControllerGetMovieDetailsResponses, MoviesControllerGetMovieResponses, MoviesControllerGetMovieWatchHistoryData, MoviesControllerGetMovieWatchHistoryErrors, MoviesControllerGetMovieWatchHistoryResponses, MoviesControllerGetUserMoviesData, MoviesControllerGetUserMoviesPaginatedData, MoviesControllerGetUserMoviesPaginatedResponses, MoviesControllerGetUserMoviesResponses, MoviesControllerMarkWatchedData, MoviesControllerMarkWatchedErrors, MoviesControllerMarkWatchedResponses, MoviesControllerSearchMoviesData, MoviesControllerSearchMoviesResponses, MoviesControllerUnmarkWatchedData, MoviesControllerUnmarkWatchedErrors, MoviesControllerUnmarkWatchedResponses, SearchControllerDiscoverAllData, SearchControllerDiscoverAllResponses, SearchControllerSearchAllData, SearchControllerSearchAllResponses, ShelfControllerGetUserShelfData, ShelfControllerGetUserShelfResponses, ShowsControllerDeleteEpisodeWatchHistoryEntryData, ShowsControllerDeleteEpisodeWatchHistoryEntryErrors, ShowsControllerDeleteEpisodeWatchHistoryEntryResponses, ShowsControllerDiscoverShowsData, ShowsControllerDiscoverShowsResponses, ShowsControllerGetEpisodeDetailsData, ShowsControllerGetEpisodeDetailsResponses, ShowsControllerGetSeasonDetailsData, ShowsControllerGetSeasonDetailsResponses, ShowsControllerGetShowData, ShowsControllerGetShowDetailsData, ShowsControllerGetShowDetailsResponses, ShowsControllerGetShowResponses, ShowsControllerGetShowWatchHistoryData, ShowsControllerGetShowWatchHistoryErrors, ShowsControllerGetShowWatchHistoryResponses, ShowsControllerGetUserEpisodesPaginatedData, ShowsControllerGetUserEpisodesPaginatedResponses, ShowsControllerGetUserShowsData, ShowsControllerGetUserShowsResponses, ShowsControllerMarkSeasonWatchedData, ShowsControllerMarkSeasonWatchedErrors, ShowsControllerMarkSeasonWatchedResponses, ShowsControllerMarkShowWatchedData, ShowsControllerMarkShowWatchedErrors, ShowsControllerMarkShowWatchedResponses, ShowsControllerMarkWatchedData, ShowsControllerMarkWatchedErrors, ShowsControllerMarkWatchedResponses, ShowsControllerSearchShowsData, ShowsControllerSearchShowsResponses, ShowsControllerUnmarkWatchedData, ShowsControllerUnmarkWatchedResponses, UsersControllerDeleteMyAccountData, UsersControllerDeleteMyAccountErrors, UsersControllerDeleteMyAccountResponses, UsersControllerGetMySettingsData, UsersControllerGetMySettingsErrors, UsersControllerGetMySettingsResponses, UsersControllerUpdateMySettingsData, UsersControllerUpdateMySettingsErrors, UsersControllerUpdateMySettingsResponses } from './types.gen'; 6 6 7 7 export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & { 8 8 /** ··· 310 310 * Get paginated shelf items for a user (movies and episodes) 311 311 */ 312 312 export const shelfControllerGetUserShelf = <ThrowOnError extends boolean = false>(options: Options<ShelfControllerGetUserShelfData, ThrowOnError>) => (options.client ?? client).get<ShelfControllerGetUserShelfResponses, unknown, ThrowOnError>({ url: '/shelf/user/{userDid}', ...options }); 313 + 314 + /** 315 + * Search movies and shows from TMDB 316 + */ 317 + export const searchControllerSearchAll = <ThrowOnError extends boolean = false>(options: Options<SearchControllerSearchAllData, ThrowOnError>) => (options.client ?? client).get<SearchControllerSearchAllResponses, unknown, ThrowOnError>({ url: '/search/all', ...options }); 318 + 319 + /** 320 + * Discover popular movies and shows from TMDB 321 + */ 322 + export const searchControllerDiscoverAll = <ThrowOnError extends boolean = false>(options?: Options<SearchControllerDiscoverAllData, ThrowOnError>) => (options?.client ?? client).get<SearchControllerDiscoverAllResponses, unknown, ThrowOnError>({ url: '/search/discover', ...options });
+81
packages/api/src/generated/types.gen.ts
··· 513 513 total: number; 514 514 }; 515 515 516 + export type UnifiedSearchResultDto = { 517 + id: number; 518 + media_type: 'movie' | 'tv'; 519 + title?: string; 520 + name?: string; 521 + poster_path?: string; 522 + backdrop_path?: string; 523 + release_date?: string; 524 + first_air_date?: string; 525 + overview?: string; 526 + popularity: number; 527 + vote_average: number; 528 + vote_count: number; 529 + original_language?: string; 530 + genre_ids?: Array<string>; 531 + original_title?: string; 532 + original_name?: string; 533 + adult?: boolean; 534 + video?: boolean; 535 + }; 536 + 537 + export type UnifiedSearchResponseDto = { 538 + results: Array<UnifiedSearchResultDto>; 539 + total_results: number; 540 + page: number; 541 + }; 542 + 543 + export type UnifiedDiscoverResponseDto = { 544 + results: Array<UnifiedSearchResultDto>; 545 + total_results: number; 546 + page: number; 547 + }; 548 + 516 549 export type MoviesControllerSearchMoviesData = { 517 550 body?: never; 518 551 path?: never; ··· 1504 1537 }; 1505 1538 1506 1539 export type ShelfControllerGetUserShelfResponse = ShelfControllerGetUserShelfResponses[keyof ShelfControllerGetUserShelfResponses]; 1540 + 1541 + export type SearchControllerSearchAllData = { 1542 + body?: never; 1543 + path?: never; 1544 + query: { 1545 + /** 1546 + * Search term 1547 + */ 1548 + query: string; 1549 + /** 1550 + * Page number 1551 + */ 1552 + page?: number; 1553 + }; 1554 + url: '/search/all'; 1555 + }; 1556 + 1557 + export type SearchControllerSearchAllResponses = { 1558 + 200: UnifiedSearchResponseDto; 1559 + }; 1560 + 1561 + export type SearchControllerSearchAllResponse = SearchControllerSearchAllResponses[keyof SearchControllerSearchAllResponses]; 1562 + 1563 + export type SearchControllerDiscoverAllData = { 1564 + body?: never; 1565 + path?: never; 1566 + query?: { 1567 + /** 1568 + * Year filter 1569 + */ 1570 + year?: unknown; 1571 + /** 1572 + * Page number 1573 + */ 1574 + page?: unknown; 1575 + /** 1576 + * Sort by 1577 + */ 1578 + sortBy?: unknown; 1579 + }; 1580 + url: '/search/discover'; 1581 + }; 1582 + 1583 + export type SearchControllerDiscoverAllResponses = { 1584 + 200: UnifiedDiscoverResponseDto; 1585 + }; 1586 + 1587 + export type SearchControllerDiscoverAllResponse = SearchControllerDiscoverAllResponses[keyof SearchControllerDiscoverAllResponses];