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: add Movies module with search functionality and integrate @nestjs/config

+273 -2
+1
backend/package.json
··· 24 24 "@atproto/api": "^0.18.18", 25 25 "@atproto/sync": "^0.1.39", 26 26 "@nestjs/common": "^11.0.1", 27 + "@nestjs/config": "^4.0.2", 27 28 "@nestjs/core": "^11.0.1", 28 29 "@nestjs/platform-express": "^11.0.1", 29 30 "@nestjs/swagger": "^11.2.5",
+3 -1
backend/src/app.module.ts
··· 2 2 import { AppController } from './app.controller'; 3 3 import { AppService } from './app.service'; 4 4 import { PrismaModule } from './prisma/prisma.module'; 5 + import { MoviesModule } from './movies/movies.module'; 6 + import { ConfigModule } from '@nestjs/config'; 5 7 6 8 @Module({ 7 - imports: [PrismaModule], 9 + imports: [ConfigModule.forRoot({ isGlobal: true }), PrismaModule, MoviesModule], 8 10 controllers: [AppController], 9 11 providers: [AppService], 10 12 })
+109
backend/src/movies/dto/movie.dto.ts
··· 1 + import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; 2 + import { IsString, IsOptional, IsInt, IsDateString } from 'class-validator'; 3 + 4 + export class MovieDto { 5 + @ApiProperty() 6 + @IsString() 7 + movieId: string; 8 + 9 + @ApiProperty() 10 + @IsString() 11 + title: string; 12 + 13 + @ApiPropertyOptional() 14 + @IsOptional() 15 + @IsString() 16 + posterPath?: string; 17 + 18 + @ApiPropertyOptional() 19 + @IsOptional() 20 + @IsString() 21 + backdropPath?: string; 22 + 23 + @ApiPropertyOptional() 24 + @IsOptional() 25 + @IsInt() 26 + releaseYear?: number; 27 + 28 + @ApiPropertyOptional() 29 + @IsOptional() 30 + @IsDateString() 31 + releaseDate?: string; 32 + 33 + @ApiPropertyOptional() 34 + @IsOptional() 35 + @IsString() 36 + overview?: string; 37 + } 38 + 39 + export class TrackedMovieDto { 40 + @ApiProperty() 41 + id: string; 42 + 43 + @ApiProperty() 44 + rkey: string; 45 + 46 + @ApiProperty() 47 + uri: string; 48 + 49 + @ApiProperty() 50 + cid: string; 51 + 52 + @ApiProperty() 53 + userDid: string; 54 + 55 + @ApiProperty() 56 + movieId: string; 57 + 58 + @ApiProperty() 59 + status: string; 60 + 61 + @ApiPropertyOptional() 62 + watchedDate?: string; 63 + 64 + @ApiProperty() 65 + createdAt: string; 66 + 67 + @ApiProperty() 68 + updatedAt: string; 69 + 70 + @ApiProperty({ type: MovieDto }) 71 + movie: MovieDto; 72 + } 73 + 74 + export class SearchMoviesDto { 75 + @ApiProperty() 76 + @IsString() 77 + query: string; 78 + } 79 + 80 + export class TMDBMovieResultDto { 81 + @ApiProperty() 82 + id: number; 83 + 84 + @ApiProperty() 85 + title: string; 86 + 87 + @ApiPropertyOptional() 88 + poster_path?: string; 89 + 90 + @ApiPropertyOptional() 91 + backdrop_path?: string; 92 + 93 + @ApiPropertyOptional() 94 + release_date?: string; 95 + 96 + @ApiPropertyOptional() 97 + overview?: string; 98 + } 99 + 100 + export class SearchResultsDto { 101 + @ApiProperty({ type: [TMDBMovieResultDto] }) 102 + results: TMDBMovieResultDto[]; 103 + 104 + @ApiProperty() 105 + total_results: number; 106 + 107 + @ApiProperty() 108 + page: number; 109 + }
+44
backend/src/movies/movies.controller.ts
··· 1 + import { Controller, Get, Query, Param } from '@nestjs/common'; 2 + import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; 3 + import { MoviesService } from './movies.service'; 4 + import { 5 + SearchMoviesDto, 6 + SearchResultsDto, 7 + TrackedMovieDto, 8 + MovieDto 9 + } from './dto/movie.dto'; 10 + 11 + @ApiTags('movies') 12 + @Controller('movies') 13 + export class MoviesController { 14 + constructor(private readonly moviesService: MoviesService) {} 15 + 16 + @Get('search') 17 + @ApiOperation({ summary: 'Search movies from TMDB' }) 18 + @ApiQuery({ name: 'query', required: true, description: 'Search term' }) 19 + @ApiResponse({ status: 200, type: SearchResultsDto }) 20 + async searchMovies(@Query() searchDto: SearchMoviesDto) { 21 + return this.moviesService.searchMovies(searchDto.query); 22 + } 23 + 24 + @Get('tmdb/:movieId') 25 + @ApiOperation({ summary: 'Get movie details from TMDB' }) 26 + @ApiResponse({ status: 200 }) 27 + async getMovieDetails(@Param('movieId') movieId: string) { 28 + return this.moviesService.getMovieDetails(movieId); 29 + } 30 + 31 + @Get('user/:userDid') 32 + @ApiOperation({ summary: 'Get tracked movies for a user' }) 33 + @ApiResponse({ status: 200, type: [TrackedMovieDto] }) 34 + async getUserMovies(@Param('userDid') userDid: string) { 35 + return this.moviesService.getUserMovies(userDid); 36 + } 37 + 38 + @Get(':movieId') 39 + @ApiOperation({ summary: 'Get movie from database' }) 40 + @ApiResponse({ status: 200, type: MovieDto }) 41 + async getMovie(@Param('movieId') movieId: string) { 42 + return this.moviesService.getMovieByTMDBId(movieId); 43 + } 44 + }
+12
backend/src/movies/movies.module.ts
··· 1 + import { Module } from '@nestjs/common'; 2 + import { MoviesController } from './movies.controller'; 3 + import { MoviesService } from './movies.service'; 4 + import { PrismaModule } from '../prisma/prisma.module'; 5 + 6 + @Module({ 7 + imports: [PrismaModule], 8 + controllers: [MoviesController], 9 + providers: [MoviesService], 10 + exports: [MoviesService], 11 + }) 12 + export class MoviesModule {}
+77
backend/src/movies/movies.service.ts
··· 1 + import { Injectable } from '@nestjs/common'; 2 + import { PrismaService } from '../prisma/prisma.service'; 3 + import { ConfigService } from '@nestjs/config'; 4 + 5 + @Injectable() 6 + export class MoviesService { 7 + private readonly tmdbApiKey: string; 8 + private readonly tmdbBaseUrl = 'https://api.themoviedb.org/3'; 9 + 10 + constructor( 11 + private prisma: PrismaService, 12 + private config: ConfigService, 13 + ) { 14 + this.tmdbApiKey = this.config.get('TMDB_API_KEY') ?? ''; 15 + } 16 + 17 + async searchMovies(query: string, page: number = 1) { 18 + const response = await fetch( 19 + `${this.tmdbBaseUrl}/search/movie?api_key=${this.tmdbApiKey}&query=${encodeURIComponent(query)}&page=${page}` 20 + ); 21 + 22 + if (!response.ok) { 23 + throw new Error('Failed to search movies'); 24 + } 25 + 26 + return response.json(); 27 + } 28 + 29 + async getMovieDetails(movieId: string) { 30 + const response = await fetch( 31 + `${this.tmdbBaseUrl}/movie/${movieId}?api_key=${this.tmdbApiKey}` 32 + ); 33 + 34 + if (!response.ok) { 35 + throw new Error('Movie not found'); 36 + } 37 + 38 + return response.json(); 39 + } 40 + 41 + async getUserMovies(userDid: string) { 42 + return this.prisma.trackedMovie.findMany({ 43 + where: { userDid }, 44 + include: { movie: true }, 45 + orderBy: { createdAt: 'desc' }, 46 + }); 47 + } 48 + 49 + async getMovieByTMDBId(movieId: string) { 50 + return this.prisma.movie.findUnique({ 51 + where: { movieId }, 52 + }); 53 + } 54 + 55 + async upsertMovie(movieData: any) { 56 + return this.prisma.movie.upsert({ 57 + where: { movieId: movieData.id.toString() }, 58 + create: { 59 + movieId: movieData.id.toString(), 60 + title: movieData.title, 61 + posterPath: movieData.poster_path, 62 + backdropPath: movieData.backdrop_path, 63 + releaseYear: movieData.release_date ? new Date(movieData.release_date).getFullYear() : null, 64 + releaseDate: movieData.release_date ? new Date(movieData.release_date) : null, 65 + overview: movieData.overview, 66 + }, 67 + update: { 68 + title: movieData.title, 69 + posterPath: movieData.poster_path, 70 + backdropPath: movieData.backdrop_path, 71 + releaseYear: movieData.release_date ? new Date(movieData.release_date).getFullYear() : null, 72 + releaseDate: movieData.release_date ? new Date(movieData.release_date) : null, 73 + overview: movieData.overview, 74 + }, 75 + }); 76 + } 77 + }
+2 -1
backend/src/prisma/prisma.module.ts
··· 2 2 import { PrismaService } from './prisma.service'; 3 3 4 4 @Module({ 5 - providers: [PrismaService] 5 + providers: [PrismaService], 6 + exports: [PrismaService], 6 7 }) 7 8 export class PrismaModule {}
+25
pnpm-lock.yaml
··· 159 159 '@nestjs/common': 160 160 specifier: ^11.0.1 161 161 version: 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) 162 + '@nestjs/config': 163 + specifier: ^4.0.2 164 + version: 4.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) 162 165 '@nestjs/core': 163 166 specifier: ^11.0.1 164 167 version: 11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2) ··· 1702 1705 optional: true 1703 1706 class-validator: 1704 1707 optional: true 1708 + 1709 + '@nestjs/config@4.0.2': 1710 + resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==} 1711 + peerDependencies: 1712 + '@nestjs/common': ^10.0.0 || ^11.0.0 1713 + rxjs: ^7.1.0 1705 1714 1706 1715 '@nestjs/core@11.1.12': 1707 1716 resolution: {integrity: sha512-97DzTYMf5RtGAVvX1cjwpKRiCUpkeQ9CCzSAenqkAhOmNVVFaApbhuw+xrDt13rsCa2hHVOYPrV4dBgOYMJjsA==} ··· 3951 3960 3952 3961 dotenv-expand@11.0.7: 3953 3962 resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} 3963 + engines: {node: '>=12'} 3964 + 3965 + dotenv-expand@12.0.1: 3966 + resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==} 3954 3967 engines: {node: '>=12'} 3955 3968 3956 3969 dotenv@16.4.7: ··· 9118 9131 transitivePeerDependencies: 9119 9132 - supports-color 9120 9133 9134 + '@nestjs/config@4.0.2(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': 9135 + dependencies: 9136 + '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) 9137 + dotenv: 16.4.7 9138 + dotenv-expand: 12.0.1 9139 + lodash: 4.17.21 9140 + rxjs: 7.8.2 9141 + 9121 9142 '@nestjs/core@11.1.12(@nestjs/common@11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.12)(reflect-metadata@0.2.2)(rxjs@7.8.2)': 9122 9143 dependencies: 9123 9144 '@nestjs/common': 11.1.12(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) ··· 11431 11452 domhandler: 5.0.3 11432 11453 11433 11454 dotenv-expand@11.0.7: 11455 + dependencies: 11456 + dotenv: 16.6.1 11457 + 11458 + dotenv-expand@12.0.1: 11434 11459 dependencies: 11435 11460 dotenv: 16.6.1 11436 11461