refactor: add media_source_id to relevant entities (#1106)

We should be referencing media_sources by their ID on programs,
external_ids, etc. This enables us to use proper foreign keys for
referential integrity at the DB level, not worry about unique names for
media sources, and simplifies a lot of the code relating to media source
deletion and the cleanup thereafter.

This change also introduces the DBContext, which should allow for
arbitrarily calling other DB accessor functions when within transactions
and not deadlocking the connection to the DB.
This commit is contained in:
Christian Benincasa
2025-02-28 12:53:29 -08:00
committed by GitHub
parent 9b0385e577
commit 769b05d201
34 changed files with 1315 additions and 331 deletions

View File

@@ -1,13 +1,12 @@
import { ProgramExternalIdType } from '@/db/custom_types/ProgramExternalIdType.js';
import type { NewProgramGroupingExternalId } from '@/db/schema/ProgramGroupingExternalId.js';
import type { NewSingleOrMultiProgramGroupingExternalId } from '@/db/schema/ProgramGroupingExternalId.js';
import { isNonEmptyString } from '@/util/index.js';
import type { ContentProgram } from '@tunarr/types';
import type { JellyfinItem } from '@tunarr/types/jellyfin';
import type { PlexEpisode, PlexMusicTrack } from '@tunarr/types/plex';
import dayjs from 'dayjs';
import { find, first } from 'lodash-es';
import { first } from 'lodash-es';
import type { MarkRequired } from 'ts-essentials';
import { P, match } from 'ts-pattern';
import { v4 } from 'uuid';
import type { Nullable } from '../../types/util.ts';
import {
@@ -68,14 +67,15 @@ export class ProgramGroupingMinter {
program: ContentProgram,
groupingId: string,
externalSourceId: string,
mediaSourceId: string,
relationType: 'parent' | 'grandparent',
): NewProgramGroupingExternalId[] {
): NewSingleOrMultiProgramGroupingExternalId[] {
if (program.subtype === 'movie') {
return [];
}
const now = +dayjs();
const parentExternalIds: NewProgramGroupingExternalId[] = [];
const parentExternalIds: NewSingleOrMultiProgramGroupingExternalId[] = [];
const ratingKey =
relationType === 'grandparent'
@@ -83,6 +83,7 @@ export class ProgramGroupingMinter {
: program.parent?.externalKey;
if (isNonEmptyString(ratingKey)) {
parentExternalIds.push({
type: 'multi',
uuid: v4(),
createdAt: now,
updatedAt: now,
@@ -90,6 +91,7 @@ export class ProgramGroupingMinter {
externalKey: ratingKey,
sourceType: ProgramExternalIdType.PLEX,
externalSourceId,
mediaSourceId,
groupUuid: groupingId,
});
}
@@ -101,89 +103,13 @@ export class ProgramGroupingMinter {
);
if (isNonEmptyString(guid)) {
parentExternalIds.push({
type: 'single',
uuid: v4(),
createdAt: now,
updatedAt: now,
externalFilePath: null,
externalKey: guid,
sourceType: ProgramExternalIdType.PLEX_GUID,
externalSourceId: null,
groupUuid: groupingId,
});
}
return parentExternalIds;
}
static mintGroupingExternalIdsForPlex(
plexItem: PlexEpisode | PlexMusicTrack,
groupingId: string,
externalSourceId: string,
relationType: 'parent' | 'grandparent',
): NewProgramGroupingExternalId[] {
const now = +dayjs();
const parentExternalIds: NewProgramGroupingExternalId[] = [];
const ratingKey = plexItem[`${relationType}RatingKey`];
if (isNonEmptyString(ratingKey)) {
parentExternalIds.push({
uuid: v4(),
createdAt: now,
updatedAt: now,
externalFilePath: null,
externalKey: ratingKey,
sourceType: ProgramExternalIdType.PLEX,
externalSourceId,
groupUuid: groupingId,
});
}
const guid = plexItem[`${relationType}Guid`];
if (isNonEmptyString(guid)) {
parentExternalIds.push({
uuid: v4(),
createdAt: now,
updatedAt: now,
externalFilePath: null,
externalKey: guid,
sourceType: ProgramExternalIdType.PLEX_GUID,
externalSourceId: null,
groupUuid: groupingId,
});
}
return parentExternalIds;
}
static mintGroupingExternalIdsForJellyfin(
jellyfinItem: JellyfinItem,
groupingId: string,
externalSourceId: string,
relationType: 'parent' | 'grandparent',
): NewProgramGroupingExternalId[] {
const now = +dayjs();
const parentExternalIds: NewProgramGroupingExternalId[] = [];
const jellyfinId = match([jellyfinItem, relationType] as const)
.with([{ Type: 'Episode' }, 'grandparent'], () => jellyfinItem.SeriesId)
.with(
[{ Type: 'Audio' }, 'parent'],
() =>
find(jellyfinItem.AlbumArtists, { Name: jellyfinItem.AlbumArtist })
?.Id,
)
.with([P._, 'parent'], () => jellyfinItem.ParentId)
.otherwise(() => null);
if (isNonEmptyString(jellyfinId)) {
parentExternalIds.push({
uuid: v4(),
createdAt: now,
updatedAt: now,
externalFilePath: null,
externalKey: jellyfinId,
sourceType: ProgramExternalIdType.JELLYFIN,
externalSourceId,
groupUuid: groupingId,
});
}