Skip to content

Commit

Permalink
feat(api): delete series if it has been deleted from TMDB
Browse files Browse the repository at this point in the history
This requires some refactoring of the TMDB request making as we want to
differentiate between a not found response and if parsing fails. If the
parsing fails, then that is not enough cause to delete the series as
there might just be some random breaking change that we would need to
handle. In the future, we should probably send a Sentry error or
something when parsing fails.

Related to #72
  • Loading branch information
JoosepAlviste committed Aug 13, 2023
1 parent ccdb299 commit 2748f51
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 22 deletions.
4 changes: 2 additions & 2 deletions apps/api/src/features/series/__tests__/scopes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import nock from 'nock'

import { config } from '@/config'
import {
type TMDbSeries,
type TMDbSeason,
type TMDbSearchResponse,
type TMDbSeriesResponse,
} from '@/features/tmdb'

export const mockTMDbSearchRequest = (
Expand All @@ -29,7 +29,7 @@ export const mockTMDbSearchRequest = (

export const mockTMDbDetailsRequest = (
tmdbId: number,
response: TMDbSeries,
response: TMDbSeriesResponse,
) => {
return nock(config.tmdb.url)
.get(`/3/tv/${tmdbId}`)
Expand Down
48 changes: 48 additions & 0 deletions apps/api/src/features/series/__tests__/series.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
createSeenEpisodesForUser,
createSeriesWithEpisodesAndSeasons,
} from '@/test/testUtils'
import { type LiterallyAnything } from '@/types/utils'

import { episodeFactory } from '../episode.factory'
import { seasonFactory } from '../season.factory'
Expand Down Expand Up @@ -265,6 +266,53 @@ describe('features/series/series.service', () => {
tmdbId: series.tmdbId,
})
})

it('deletes a series if it has been deleted in TMDB', async () => {
const series = await seriesFactory.create()

mockTMDbDetailsRequest(series.tmdbId, {
success: false,
status_code: 34,
status_message: 'The resource you requested could not be found.',
})

const returnedSeries = await syncSeriesDetails({
ctx: createContext(),
tmdbId: series.tmdbId,
})

expect(returnedSeries).toBe(null)

const seriesInDb = await db
.selectFrom('series')
.where('id', '=', series.id)
.selectAll()
.executeTakeFirst()
expect(seriesInDb).toBe(undefined)
})

it('does not delete the series if parsing the TMDB response fails', async () => {
const series = await seriesFactory.create()

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mockTMDbDetailsRequest(series.tmdbId, {
not: 'correct',
} as LiterallyAnything)

const returnedSeries = await syncSeriesDetails({
ctx: createContext(),
tmdbId: series.tmdbId,
})

expect(returnedSeries).toBe(null)

const seriesInDb = await db
.selectFrom('series')
.where('id', '=', series.id)
.selectAll()
.executeTakeFirst()
expect(seriesInDb).not.toBeFalsy()
})
})

describe('findStatusForSeries', () => {
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/features/series/series.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,13 @@ export const updateOneByTMDbId = ({
.returningAll()
.executeTakeFirst()
}

export const deleteOne = ({
ctx,
tmdbId,
}: {
ctx: DBContext
tmdbId: number
}) => {
return ctx.db.deleteFrom('series').where('tmdbId', '=', tmdbId).execute()
}
22 changes: 20 additions & 2 deletions apps/api/src/features/series/series.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ export const syncSeasonsAndEpisodes = async ({
* Update the details of the series with the given TMDB ID from the TMDB API.
* This also syncs the seasons and episodes from TMDB, saving them into the
* database if needed.
*
* If the series does not exist on TMDB, then it will be deleted from the
* database.
*/
export const syncSeriesDetails = async ({
ctx,
Expand All @@ -194,10 +197,20 @@ export const syncSeriesDetails = async ({
tmdbId: number
}) => {
const {
parsed,
found,
series: newSeries,
totalSeasons,
seasons,
} = await tmdbService.fetchSeriesDetails({ tmdbId })
if (!parsed) {
return null
}

if (!found) {
await seriesRepository.deleteOne({ ctx, tmdbId })
return null
}

const savedSeries = await seriesRepository.updateOneByTMDbId({
ctx,
Expand All @@ -209,7 +222,7 @@ export const syncSeriesDetails = async ({
},
})
if (!savedSeries) {
throw new NotFoundError()
return null
}

if (totalSeasons) {
Expand Down Expand Up @@ -242,7 +255,12 @@ export const getSeriesByIdAndFetchDetailsFromTMDB = async ({
return series
}

return await syncSeriesDetails({ ctx, tmdbId: series.tmdbId })
const syncedSeries = await syncSeriesDetails({ ctx, tmdbId: series.tmdbId })
if (!syncedSeries) {
throw new NotFoundError()
}

return series
}

export const updateSeriesStatusForUser = async ({
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/features/tmdb/tmdb.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export const tmdbStatusSchema = z.enum([
'Canceled',
])

export const tmdbSeriesNotFoundSchema = z.object({
success: z.literal(false),
status_code: z.literal(34),
status_message: z.string(),
})

export const tmdbSeriesSchema = z.object({
id: z.number(),
name: z.string(),
Expand All @@ -30,6 +36,10 @@ export const tmdbSeriesSchema = z.object({
),
})

export const tmdbSeriesResponseSchema = tmdbSeriesSchema.or(
tmdbSeriesNotFoundSchema,
)

export const tmdbSearchSeriesSchema = tmdbSeriesSchema.pick({
id: true,
overview: true,
Expand Down
38 changes: 20 additions & 18 deletions apps/api/src/features/tmdb/tmdb.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { log } from '@/lib/logger'

import {
tmdbSeasonSchema,
tmdbSeriesSchema,
tmdbSeriesResponseSchema,
tmdbSeriesSearchResponseSchema,
} from './tmdb.schemas'
import { type TMDbSeries, type TMDbSearchSeries } from './types'
Expand All @@ -33,7 +33,7 @@ const makeTMDbRequest = async <T>(
const json = (await res.json()) as T

try {
return schema.parse(json)
return { parsed: true, response: schema.parse(json) }
} catch (e) {
log.warn(
{
Expand All @@ -44,7 +44,7 @@ const makeTMDbRequest = async <T>(
'TMDB API response did not match the schema.',
)

return null
return { parsed: false, response: null }
}
}

Expand Down Expand Up @@ -79,34 +79,36 @@ export const searchSeries = async ({
}: {
keyword: string
}): Promise<Insertable<Series>[]> => {
const seriesSearchResponse = await makeTMDbRequest(
const { response } = await makeTMDbRequest(
'search/tv',
{ query: keyword },
tmdbSeriesSearchResponseSchema,
)

if (!seriesSearchResponse) {
if (!response) {
// No result found or other error
return []
}

return seriesSearchResponse.results.map(parseSeriesFromTMDbResponse)
return response.results.map(parseSeriesFromTMDbResponse)
}

export const fetchSeriesDetails = async ({ tmdbId }: { tmdbId: number }) => {
const seriesDetails = await makeTMDbRequest(
const { parsed, response } = await makeTMDbRequest(
`tv/${tmdbId}`,
{ append_to_response: 'external_ids' },
tmdbSeriesSchema,
tmdbSeriesResponseSchema,
)
if (!seriesDetails) {
throw new NotFoundError()
if (!response || 'success' in response) {
return { parsed, found: false, series: null, totalSeasons: 0, seasons: [] }
}

return {
series: parseSeriesFromTMDbResponse(seriesDetails),
totalSeasons: seriesDetails.number_of_seasons,
seasons: seriesDetails.seasons.map(
found: true,
parsed: true,
series: parseSeriesFromTMDbResponse(response),
totalSeasons: response.number_of_seasons,
seasons: response.seasons.map(
(season): Omit<Insertable<Season>, 'seriesId'> => ({
tmdbId: season.id,
number: season.season_number,
Expand All @@ -123,19 +125,19 @@ export const fetchEpisodesForSeason = async ({
tmdbId: number
seasonNumber: number
}) => {
const season = await makeTMDbRequest(
const { response } = await makeTMDbRequest(
`tv/${tmdbId}/season/${seasonNumber}`,
{},
tmdbSeasonSchema,
)
if (!season) {
if (!response) {
throw new NotFoundError()
}

return {
seasonNumber: season.season_number,
seasonTitle: season.name,
episodes: season.episodes.map((episode) => ({
seasonNumber: response.season_number,
seasonTitle: response.name,
episodes: response.episodes.map((episode) => ({
tmdbId: episode.id,
number: episode.episode_number,
title: episode.name,
Expand Down
3 changes: 3 additions & 0 deletions apps/api/src/features/tmdb/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type tmdbEpisodeSchema,
type tmdbSeasonSchema,
type tmdbSeriesSearchResponseSchema,
type tmdbSeriesResponseSchema,
} from './tmdb.schemas'

export type TMDbSearchResponse = z.infer<typeof tmdbSeriesSearchResponseSchema>
Expand All @@ -14,6 +15,8 @@ export type TMDbSearchSeries = z.infer<typeof tmdbSearchSeriesSchema>

export type TMDbSeries = z.infer<typeof tmdbSeriesSchema>

export type TMDbSeriesResponse = z.infer<typeof tmdbSeriesResponseSchema>

export type TMDbSeason = z.infer<typeof tmdbSeasonSchema>

export type TMDbEpisode = z.infer<typeof tmdbEpisodeSchema>

0 comments on commit 2748f51

Please sign in to comment.