mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-10 23:31:08 +00:00
Add TmdbClient
This commit is contained in:
32
src/app/clients/tmdb/TmdbClient.ts
Normal file
32
src/app/clients/tmdb/TmdbClient.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ApiClient } from '../../../core/http/ApiClient';
|
||||
import type { TMDbMovie } from '../../models';
|
||||
import { SearchMovieEndpoint } from './endpoints/SearchMovieEndpoint';
|
||||
import { RecommendedMoviesEndpoint, SimilarMoviesEndpoint } from './endpoints/MovieListEndpoint';
|
||||
|
||||
const TMDB_BASE_URL = 'https://api.themoviedb.org';
|
||||
|
||||
export class TmdbClient extends ApiClient {
|
||||
private readonly searchEndpoint = new SearchMovieEndpoint();
|
||||
private readonly similarEndpoint = new SimilarMoviesEndpoint();
|
||||
private readonly recommendedEndpoint = new RecommendedMoviesEndpoint();
|
||||
|
||||
constructor(
|
||||
private readonly apiKey: string,
|
||||
baseUrl: string = TMDB_BASE_URL
|
||||
) {
|
||||
super(baseUrl);
|
||||
}
|
||||
|
||||
async searchMovie(title: string, year?: number): Promise<TMDbMovie | undefined> {
|
||||
const results = await this.request(this.searchEndpoint, { apiKey: this.apiKey, title, year });
|
||||
return results[0];
|
||||
}
|
||||
|
||||
getSimilar(tmdbId: number): Promise<TMDbMovie[]> {
|
||||
return this.request(this.similarEndpoint, { apiKey: this.apiKey, tmdbId });
|
||||
}
|
||||
|
||||
getRecommendations(tmdbId: number): Promise<TMDbMovie[]> {
|
||||
return this.request(this.recommendedEndpoint, { apiKey: this.apiKey, tmdbId });
|
||||
}
|
||||
}
|
||||
44
src/app/clients/tmdb/endpoints/MovieListEndpoint.ts
Normal file
44
src/app/clients/tmdb/endpoints/MovieListEndpoint.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Endpoint, type HttpMethod } from '../../../../core/http/Endpoint';
|
||||
import type { TMDbMovie } from '../../../models';
|
||||
import { toTMDbMovie } from './SearchMovieEndpoint';
|
||||
|
||||
interface TMDbResult {
|
||||
id: number;
|
||||
title: string;
|
||||
release_date: string;
|
||||
}
|
||||
|
||||
interface TMDbListResponse {
|
||||
results: TMDbResult[];
|
||||
}
|
||||
|
||||
export interface MovieListParams {
|
||||
apiKey: string;
|
||||
tmdbId: number;
|
||||
}
|
||||
|
||||
export class SimilarMoviesEndpoint extends Endpoint<TMDbMovie[], MovieListParams> {
|
||||
readonly method: HttpMethod = 'GET';
|
||||
path(params: MovieListParams): string {
|
||||
return `/3/movie/${params.tmdbId}/similar`;
|
||||
}
|
||||
query(params: MovieListParams): Record<string, string> {
|
||||
return { api_key: params.apiKey };
|
||||
}
|
||||
parseResponse(json: unknown): TMDbMovie[] {
|
||||
return (json as TMDbListResponse).results.map(toTMDbMovie);
|
||||
}
|
||||
}
|
||||
|
||||
export class RecommendedMoviesEndpoint extends Endpoint<TMDbMovie[], MovieListParams> {
|
||||
readonly method: HttpMethod = 'GET';
|
||||
path(params: MovieListParams): string {
|
||||
return `/3/movie/${params.tmdbId}/recommendations`;
|
||||
}
|
||||
query(params: MovieListParams): Record<string, string> {
|
||||
return { api_key: params.apiKey };
|
||||
}
|
||||
parseResponse(json: unknown): TMDbMovie[] {
|
||||
return (json as TMDbListResponse).results.map(toTMDbMovie);
|
||||
}
|
||||
}
|
||||
41
src/app/clients/tmdb/endpoints/SearchMovieEndpoint.ts
Normal file
41
src/app/clients/tmdb/endpoints/SearchMovieEndpoint.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Endpoint, type HttpMethod } from '../../../../core/http/Endpoint';
|
||||
import type { TMDbMovie } from '../../../models';
|
||||
|
||||
interface TMDbResult {
|
||||
id: number;
|
||||
title: string;
|
||||
release_date: string;
|
||||
}
|
||||
|
||||
interface TMDbSearchResponse {
|
||||
results: TMDbResult[];
|
||||
}
|
||||
|
||||
export interface SearchMovieParams {
|
||||
apiKey: string;
|
||||
title: string;
|
||||
year?: number;
|
||||
}
|
||||
|
||||
export function toTMDbMovie(result: TMDbResult): TMDbMovie {
|
||||
const year = result.release_date && result.release_date.length >= 4 ? Number(result.release_date.slice(0, 4)) : 0;
|
||||
return { id: result.id, title: result.title, year };
|
||||
}
|
||||
|
||||
export class SearchMovieEndpoint extends Endpoint<TMDbMovie[], SearchMovieParams> {
|
||||
readonly method: HttpMethod = 'GET';
|
||||
|
||||
path(): string {
|
||||
return '/3/search/movie';
|
||||
}
|
||||
|
||||
query(params: SearchMovieParams): Record<string, string> {
|
||||
const query: Record<string, string> = { api_key: params.apiKey, query: params.title };
|
||||
if (params.year) query.year = String(params.year);
|
||||
return query;
|
||||
}
|
||||
|
||||
parseResponse(json: unknown): TMDbMovie[] {
|
||||
return (json as TMDbSearchResponse).results.map(toTMDbMovie);
|
||||
}
|
||||
}
|
||||
52
test/app/clients/tmdb/TmdbClient.test.ts
Normal file
52
test/app/clients/tmdb/TmdbClient.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { TmdbClient } from '../../../../src/app/clients/tmdb/TmdbClient';
|
||||
|
||||
describe('TmdbClient', () => {
|
||||
const fetchMock = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
fetchMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('searchMovie returns the first result mapped to a TMDbMovie', async () => {
|
||||
fetchMock.mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: async () =>
|
||||
JSON.stringify({ results: [{ id: 438631, title: 'Dune', release_date: '2021-09-15' }] }),
|
||||
});
|
||||
|
||||
const client = new TmdbClient('tmdb-key', 'https://api.themoviedb.org');
|
||||
const result = await client.searchMovie('Dune', 2021);
|
||||
|
||||
expect(result).toEqual({ id: 438631, title: 'Dune', year: 2021 });
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(url).toContain('/3/search/movie');
|
||||
expect(url).toContain('query=Dune');
|
||||
expect(url).toContain('year=2021');
|
||||
});
|
||||
|
||||
it('searchMovie returns undefined when there are no results', async () => {
|
||||
fetchMock.mockResolvedValue({ ok: true, status: 200, text: async () => JSON.stringify({ results: [] }) });
|
||||
|
||||
const client = new TmdbClient('tmdb-key', 'https://api.themoviedb.org');
|
||||
const result = await client.searchMovie('Nonexistent');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('getSimilar and getRecommendations hit the right paths', async () => {
|
||||
fetchMock.mockResolvedValue({ ok: true, status: 200, text: async () => JSON.stringify({ results: [] }) });
|
||||
|
||||
const client = new TmdbClient('tmdb-key', 'https://api.themoviedb.org');
|
||||
await client.getSimilar(438631);
|
||||
await client.getRecommendations(438631);
|
||||
|
||||
expect(fetchMock.mock.calls[0][0]).toContain('/3/movie/438631/similar');
|
||||
expect(fetchMock.mock.calls[1][0]).toContain('/3/movie/438631/recommendations');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user