diff --git a/prisma/dbml/schema.dbml b/prisma/dbml/schema.dbml index 4a08353..b14d343 100644 --- a/prisma/dbml/schema.dbml +++ b/prisma/dbml/schema.dbml @@ -90,6 +90,7 @@ Table characters { deletedAt DateTime createdAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null] + voice_actors lang_va_char [not null] } Table voice_actors { @@ -106,6 +107,24 @@ Table voice_actors { deletedAt DateTime createdAt DateTime [default: `now()`, not null] updatedAt DateTime [default: `now()`, not null] + characters lang_va_char [not null] +} + +Table lang_va_char { + id String [pk] + language String [not null] + voiceActor voice_actors [not null] + vaId String [not null] + character characters [not null] + charId String [not null] + createdBy users [not null] + creatorId String [not null] + createdAt DateTime [default: `now()`, not null] + updatedAt DateTime [default: `now()`, not null] + + indexes { + (language, vaId, charId) [unique] + } } Table episodes { @@ -205,6 +224,7 @@ Table users { studios studios [not null] characters characters [not null] voice_actor voice_actors [not null] + lang_va_char lang_va_char [not null] episodes episodes [not null] episode_likes episode_likes [not null] videos videos [not null] @@ -647,6 +667,12 @@ Ref: characters.creatorId > users.id Ref: voice_actors.creatorId > users.id +Ref: lang_va_char.vaId > voice_actors.id + +Ref: lang_va_char.charId > characters.id + +Ref: lang_va_char.creatorId > users.id + Ref: episodes.mediaId > medias.id Ref: episodes.uploadedBy > users.id diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c3a78ea..11407cc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -124,6 +124,7 @@ model Character { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + voice_actors LangVAChar[] @relation("CharVALanguage") @@map("characters") } @@ -142,9 +143,26 @@ model VoiceActor { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt + characters LangVAChar[] @relation("VACharLanguage") @@map("voice_actors") } +model LangVAChar { + id String @id @default(uuid()) @db.Uuid + language String + voiceActor VoiceActor @relation("VACharLanguage", fields: [vaId], references: [id]) + vaId String @db.Uuid + character Character @relation("CharVALanguage", fields: [charId], references: [id]) + charId String @db.Uuid + createdBy User @relation("UserCreatedLangVAChar", fields: [creatorId], references: [id]) + creatorId String + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@unique([language, vaId, charId]) + @@map("lang_va_char") +} + model Episode { id String @id @default(uuid()) media Media @relation("MediaEpisodes", fields: [mediaId], references: [id]) @@ -249,6 +267,7 @@ model User { studios Studio[] @relation("UserCreatedStudios") characters Character[] @relation("UserCreatedCharacters") voice_actor VoiceActor[] @relation("UserCreatedVoiceActors") + lang_va_char LangVAChar[] @relation("UserCreatedLangVAChar") episodes Episode[] @relation("UserEpisodes") episode_likes EpisodeLike[] @relation("UserEpisodeLikes") videos Video[] @relation("UserVideos") diff --git a/src/config/account/system.ts b/src/config/account/system.ts new file mode 100644 index 0000000..f8ece61 --- /dev/null +++ b/src/config/account/system.ts @@ -0,0 +1 @@ +export const SystemAccountId = "b734b9bc-b4ea-408f-a80e-0a837ce884da"; diff --git a/src/config/apis/baseUrl.ts b/src/config/apis/baseUrl.ts new file mode 100644 index 0000000..839951a --- /dev/null +++ b/src/config/apis/baseUrl.ts @@ -0,0 +1 @@ +export const baseURL = "https://api.jikan.moe/v4"; diff --git a/src/config/apis/media.reference.ts b/src/config/apis/media.reference.ts index e077bb8..4d7d2ac 100644 --- a/src/config/apis/media.reference.ts +++ b/src/config/apis/media.reference.ts @@ -1,6 +1,8 @@ +import { baseURL } from "./baseUrl"; + export const getContentReferenceAPI = (malId: number) => { return { - baseURL: "https://api.jikan.moe/v4", + baseURL, getMediaFullInfo: `/anime/${malId}/full`, getMediaCharactersWithVA: `/anime/${malId}/characters`, }; diff --git a/src/config/apis/people.reference.ts b/src/config/apis/people.reference.ts new file mode 100644 index 0000000..cfbb14d --- /dev/null +++ b/src/config/apis/people.reference.ts @@ -0,0 +1,8 @@ +import { baseURL } from "./baseUrl"; + +export const getPeopleAPI = (malId: number) => { + return { + baseURL, + getPeopleInfo: `/people/${malId}`, + }; +}; diff --git a/src/modules/internal/repositories/bulkInsertCharacters.repository.ts b/src/modules/internal/repositories/bulkInsertCharacters.repository.ts new file mode 100644 index 0000000..1520c66 --- /dev/null +++ b/src/modules/internal/repositories/bulkInsertCharacters.repository.ts @@ -0,0 +1,18 @@ +import { Prisma } from "@prisma/client"; +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; + +export const bulkInsertCharactersRepository = async ( + payload: Prisma.CharacterUpsertArgs["create"], +) => { + try { + return await prisma.character.upsert({ + where: { malId: payload.malId }, + create: payload, + update: payload, + select: { id: true }, + }); + } catch (error) { + throw new AppError(500, "Failed to bulk insert characters", error); + } +}; diff --git a/src/modules/internal/repositories/bulkInsertLangVA.repository.ts b/src/modules/internal/repositories/bulkInsertLangVA.repository.ts new file mode 100644 index 0000000..f9dddeb --- /dev/null +++ b/src/modules/internal/repositories/bulkInsertLangVA.repository.ts @@ -0,0 +1,24 @@ +import { Prisma } from "@prisma/client"; +import { AppError } from "../../../helpers/error/instances/app"; +import { prisma } from "../../../utils/databases/prisma/connection"; + +export const bulkInsertLangVARepository = async ( + payload: Prisma.LangVACharUpsertArgs["create"], +) => { + try { + const insertedVA = await prisma.langVAChar.upsert({ + where: { + language_vaId_charId: { + language: payload.language, + vaId: payload.vaId!, + charId: payload.charId!, + }, + }, + create: payload, + update: {}, + }); + return insertedVA.id; + } catch (error) { + throw new AppError(500, "Failed to bulk insert VAs", error); + } +}; diff --git a/src/modules/internal/repositories/bulkInsertVA.repository.ts b/src/modules/internal/repositories/bulkInsertVoiceActor.repository.ts similarity index 66% rename from src/modules/internal/repositories/bulkInsertVA.repository.ts rename to src/modules/internal/repositories/bulkInsertVoiceActor.repository.ts index a07f236..61bfcb0 100644 --- a/src/modules/internal/repositories/bulkInsertVA.repository.ts +++ b/src/modules/internal/repositories/bulkInsertVoiceActor.repository.ts @@ -2,18 +2,17 @@ import { Prisma } from "@prisma/client"; import { AppError } from "../../../helpers/error/instances/app"; import { prisma } from "../../../utils/databases/prisma/connection"; -export const bulkInsertVARepository = async ( +export const bulkInsertVoiceActorRepository = async ( payload: Prisma.VoiceActorUpsertArgs["create"], ) => { try { - const insertedVA = await prisma.voiceActor.upsert({ + return await prisma.voiceActor.upsert({ where: { malId: payload.malId }, create: payload, update: payload, select: { id: true }, }); - return insertedVA.id; } catch (error) { - throw new AppError(500, "Failed to bulk insert VAs", error); + throw new AppError(500, "Failed to bulk insert voice actor", error); } }; diff --git a/src/modules/internal/services/internal/bulkInsertCharWithVA.service.ts b/src/modules/internal/services/internal/bulkInsertCharWithVA.service.ts index 3ab13d6..c5aea7f 100644 --- a/src/modules/internal/services/internal/bulkInsertCharWithVA.service.ts +++ b/src/modules/internal/services/internal/bulkInsertCharWithVA.service.ts @@ -1,6 +1,10 @@ +import { SystemAccountId } from "../../../../config/account/system"; import { getContentReferenceAPI } from "../../../../config/apis/media.reference"; import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; +import { bulkInsertCharactersRepository } from "../../repositories/bulkInsertCharacters.repository"; +import { bulkInsertLangVARepository } from "../../repositories/bulkInsertLangVA.repository"; import { MediaCharWithVAInfo } from "../../types/mediaCharWithVAInfo"; +import { bulkInsertStaffOrPeopleService } from "./bulkInsertStaffOrPeople.service"; export const bulkInsertCharWithVAService = async (malId: number) => { try { @@ -9,7 +13,45 @@ export const bulkInsertCharWithVAService = async (malId: number) => { `${baseURL}${getMediaCharactersWithVA}`, ).then((res) => res.json())) as MediaCharWithVAInfo; - return; + const insertedCharacters = []; + for (const charEntry of charactersWithVAData.data) { + // Insert character if not exists + const characterInsertedId = await bulkInsertCharactersRepository({ + malId: charEntry.character.mal_id, + name: charEntry.character.name, + role: charEntry.role, + favorites: charEntry.favorites, + imageUrl: charEntry.character.images.webp.image_url, + smallImageUrl: charEntry.character.images.webp.small_image_url, + creatorId: SystemAccountId, + }); + + // Insert character voice actors if not exists + const insertedVAs: { staffId: string; lang: string }[] = []; + for (const VAEntries of charEntry.voice_actors) { + const insertedVAId = await bulkInsertStaffOrPeopleService( + VAEntries.person.mal_id, + ); + insertedVAs.push({ + staffId: insertedVAId.id, + lang: VAEntries.language, + }); + } + + // Link character with inserted VAs + for (const langVA of insertedVAs) { + await bulkInsertLangVARepository({ + language: langVA.lang, + vaId: langVA.staffId, + charId: characterInsertedId.id, + creatorId: SystemAccountId, + }); + } + + insertedCharacters.push(characterInsertedId); + } + + return insertedCharacters; } catch (error) { ErrorForwarder(error); } diff --git a/src/modules/internal/services/internal/bulkInsertStaffOrPeople.service.ts b/src/modules/internal/services/internal/bulkInsertStaffOrPeople.service.ts new file mode 100644 index 0000000..df46384 --- /dev/null +++ b/src/modules/internal/services/internal/bulkInsertStaffOrPeople.service.ts @@ -0,0 +1,27 @@ +import { SystemAccountId } from "../../../../config/account/system"; +import { getPeopleAPI } from "../../../../config/apis/people.reference"; +import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; +import { bulkInsertVoiceActorRepository } from "../../repositories/bulkInsertVoiceActor.repository"; +import { PeopleInfoResponse } from "../../types/peopleInfo"; + +export const bulkInsertStaffOrPeopleService = async (malId: number) => { + try { + const { baseURL, getPeopleInfo } = getPeopleAPI(malId); + const peopleData = (await fetch(baseURL + getPeopleInfo).then((res) => + res.json(), + )) as PeopleInfoResponse; + + return await bulkInsertVoiceActorRepository({ + malId: peopleData.data.mal_id, + name: peopleData.data.name, + birthday: peopleData.data.birthday, + description: peopleData.data.about, + aboutUrl: peopleData.data.url, + imageUrl: peopleData.data.images.jpg.image_url, + websiteUrl: peopleData.data.website_url, + creatorId: SystemAccountId, + }); + } catch (error) { + ErrorForwarder(error); + } +}; diff --git a/src/modules/internal/types/peopleInfo.ts b/src/modules/internal/types/peopleInfo.ts new file mode 100644 index 0000000..1e2d22d --- /dev/null +++ b/src/modules/internal/types/peopleInfo.ts @@ -0,0 +1,25 @@ +export interface PeopleInfoResponse { + data: Data; +} + +interface Data { + mal_id: number; + url: string; + website_url: null; + images: Images; + name: string; + given_name: null; + family_name: null; + alternate_names: any[]; + birthday: Date; + favorites: number; + about: string; +} + +interface Images { + jpg: Jpg; +} + +interface Jpg { + image_url: string; +}