Compare commits

..

7 Commits

17 changed files with 450 additions and 409 deletions

View File

@ -169,10 +169,11 @@ Table user_watch_histories {
episode episodes [not null] episode episodes [not null]
updated_at DateTime [not null] updated_at DateTime [not null]
user_id String [not null] user_id String [not null]
episode_id String [not null] episode_number Int [not null]
media_id String [not null]
indexes { indexes {
(user_id, episode_id) [pk] (user_id, episode_number, media_id) [pk]
} }
} }
@ -254,6 +255,7 @@ Table medias {
related_media media_relations [not null] related_media media_relations [not null]
updated_by_id String updated_by_id String
deleted_by_id String deleted_by_id String
episodes episodes [not null]
home_media_banners home_media_banners [not null] home_media_banners home_media_banners [not null]
saved_to_collections media_collections [not null] saved_to_collections media_collections [not null]
@ -454,13 +456,16 @@ Table media_external_links {
} }
Table media_characters { Table media_characters {
id String [pk]
media medias [not null] media medias [not null]
character characters [not null] character characters [not null]
voice_actors voice_actors [not null] voice_actors voice_actors [not null]
role character_role [not null] role character_role [not null]
media_id String [not null] media_id String [not null]
character_id String [not null] character_id String [not null]
indexes {
(character_id, media_id) [pk]
}
} }
Table characters { Table characters {
@ -480,11 +485,16 @@ Table characters {
Table voice_actors { Table voice_actors {
id String [pk] id String [pk]
media_character_id String [not null]
language String [not null] language String [not null]
actor_staff staff [not null] actor_staff staff [not null]
staff_id String [not null] staff_id String [not null]
media_id String [not null]
character_id String [not null]
media_character media_characters [not null] media_character media_characters [not null]
indexes {
(media_id, character_id, staff_id, language) [unique]
}
} }
Table staff { Table staff {
@ -499,9 +509,7 @@ Table staff {
} }
Table episodes { Table episodes {
id String [pk] episode_number Int [not null]
media_id String [not null]
episode Int [not null]
mal_url String mal_url String
forum_url String forum_url String
title String [not null] title String [not null]
@ -520,12 +528,19 @@ Table episodes {
created_by_id String [not null] created_by_id String [not null]
comments comments [not null] comments comments [not null]
watch_histories user_watch_histories [not null] watch_histories user_watch_histories [not null]
media medias [not null]
media_id String [not null]
indexes {
(media_id, episode_number) [pk]
}
} }
Table videos { Table videos {
id String [pk] id String [pk]
service video_services [not null] video_service video_services [not null]
Episode episodes [not null] episode episodes [not null]
priority Int
video_code String [not null] video_code String [not null]
short_code String short_code String
thumbnail_code String thumbnail_code String
@ -533,9 +548,15 @@ Table videos {
created_at DateTime [default: `now()`, not null] created_at DateTime [default: `now()`, not null]
deleted_at DateTime deleted_at DateTime
updated_at DateTime [not null] updated_at DateTime [not null]
episode_id String [not null] episode_number Int [not null]
media_id String [not null]
created_by_id String [not null] created_by_id String [not null]
video_submission video_submissions video_submission video_submissions
video_service_id String [not null]
indexes {
(media_id, episode_number, priority) [unique]
}
} }
Table video_submissions { Table video_submissions {
@ -595,7 +616,8 @@ Table comments {
updated_at DateTime [not null] updated_at DateTime [not null]
deleted_at DateTime deleted_at DateTime
user_id String [not null] user_id String [not null]
episode_id String [not null] episode_number Int [not null]
media_id String [not null]
likes comment_likes [not null] likes comment_likes [not null]
audit_logs comment_audit_logs [not null] audit_logs comment_audit_logs [not null]
reports comment_reports [not null] reports comment_reports [not null]
@ -679,11 +701,6 @@ Table home_media_banners {
created_by_id String [not null] created_by_id String [not null]
} }
Table VideoToVideoService {
serviceId String [ref: > video_services.id]
videosId String [ref: > videos.id]
}
Enum user_role { Enum user_role {
user user
contributor contributor
@ -787,7 +804,7 @@ Ref: user_follows.following_id > users.id
Ref: user_watch_histories.user_id > users.id Ref: user_watch_histories.user_id > users.id
Ref: user_watch_histories.episode_id > episodes.id Ref: user_watch_histories.(episode_number, media_id) > episodes.(episode_number, media_id)
Ref: collection_members.collection_id > collections.id Ref: collection_members.collection_id > collections.id
@ -865,11 +882,15 @@ Ref: media_characters.character_id > characters.id
Ref: voice_actors.staff_id > staff.id Ref: voice_actors.staff_id > staff.id
Ref: voice_actors.media_character_id > media_characters.id Ref: voice_actors.(media_id, character_id) > media_characters.(media_id, character_id)
Ref: episodes.created_by_id > users.id Ref: episodes.created_by_id > users.id
Ref: videos.episode_id > episodes.id Ref: episodes.media_id > medias.id
Ref: videos.video_service_id > video_services.id
Ref: videos.(episode_number, media_id) > episodes.(episode_number, media_id)
Ref: video_submissions.created_by_id > users.id Ref: video_submissions.created_by_id > users.id
@ -885,7 +906,7 @@ Ref: video_service_submissions.video_service_id - video_services.id
Ref: comments.user_id > users.id Ref: comments.user_id > users.id
Ref: comments.episode_id > episodes.id Ref: comments.(episode_number, media_id) > episodes.(episode_number, media_id)
Ref: comment_likes.user_id > users.id Ref: comment_likes.user_id > users.id

View File

@ -279,13 +279,14 @@ model UserFollow {
model UserWatchHistory { model UserWatchHistory {
user User @relation(fields: [user_id], references: [id]) user User @relation(fields: [user_id], references: [id])
episode Episode @relation(fields: [episode_id], references: [id]) episode Episode @relation(fields: [episode_number, media_id], references: [episode_number, media_id])
updated_at DateTime @updatedAt @db.Timestamptz() updated_at DateTime @updatedAt @db.Timestamptz()
user_id String @db.Uuid user_id String @db.Uuid
episode_id String @db.Uuid episode_number Int @db.SmallInt
media_id String @db.Uuid
@@id([user_id, episode_id]) @@id([user_id, episode_number, media_id])
@@map("user_watch_histories") @@map("user_watch_histories")
} }
@ -376,6 +377,7 @@ model Media {
related_media MediaRelation[] @relation("MediaRelationRelatedMedia") related_media MediaRelation[] @relation("MediaRelationRelatedMedia")
updated_by_id String? @db.Uuid updated_by_id String? @db.Uuid
deleted_by_id String? @db.Uuid deleted_by_id String? @db.Uuid
episodes Episode[]
home_media_banners HomeMediaBanner[] home_media_banners HomeMediaBanner[]
saved_to_collections MediaCollection[] saved_to_collections MediaCollection[]
@ -640,8 +642,8 @@ model VoiceActor {
character_id String @db.Uuid character_id String @db.Uuid
media_character MediaCharacter @relation(fields: [media_id, character_id], references: [media_id, character_id]) media_character MediaCharacter @relation(fields: [media_id, character_id], references: [media_id, character_id])
@@map("voice_actors")
@@unique([media_id, character_id, staff_id, language]) @@unique([media_id, character_id, staff_id, language])
@@map("voice_actors")
} }
model Staff { model Staff {
@ -659,49 +661,53 @@ model Staff {
} }
model Episode { model Episode {
id String @id @default(uuid(7)) @db.Uuid episode_number Int @db.SmallInt
media_id String @db.Uuid mal_url String? @db.VarChar(255)
episode Int @db.SmallInt forum_url String? @db.VarChar(255)
mal_url String? @db.VarChar(255) title String @db.VarChar(155)
forum_url String? @db.VarChar(255) title_origin String? @db.VarChar(155)
title String @db.VarChar(155) title_romanji String? @db.VarChar(155)
title_origin String? @db.VarChar(155) aired_at DateTime? @db.Date
title_romanji String? @db.VarChar(155) filler Boolean
aired_at DateTime? @db.Date recap Boolean
filler Boolean total_score Int @default(0)
recap Boolean score_count Int @default(0)
total_score Int @default(0) deleted_at DateTime? @db.Timestamptz()
score_count Int @default(0) updated_at DateTime @updatedAt @db.Timestamptz()
deleted_at DateTime? @db.Timestamptz() created_at DateTime @default(now()) @db.Timestamptz()
updated_at DateTime @updatedAt @db.Timestamptz() created_by User @relation(fields: [created_by_id], references: [id])
created_at DateTime @default(now()) @db.Timestamptz()
created_by User @relation(fields: [created_by_id], references: [id])
videos Video[] videos Video[]
created_by_id String @db.Uuid created_by_id String @db.Uuid
comments Comment[] comments Comment[]
watch_histories UserWatchHistory[] watch_histories UserWatchHistory[]
media Media @relation(fields: [media_id], references: [id])
media_id String @db.Uuid
@@index([media_id, episode]) @@id([media_id, episode_number])
@@map("episodes") @@map("episodes")
} }
model Video { model Video {
id String @id @default(uuid(7)) @db.Uuid id String @id @default(uuid(7)) @db.Uuid
service VideoService[] video_service VideoService @relation(fields: [video_service_id], references: [id])
Episode Episode @relation(fields: [episode_id], references: [id]) episode Episode @relation(fields: [episode_number, media_id], references: [episode_number, media_id])
video_code String @db.VarChar(255) priority Int? @db.SmallInt
short_code String? @db.VarChar(255) video_code String @db.VarChar(255)
thumbnail_code String? @db.VarChar(255) short_code String? @db.VarChar(255)
download_code String? @db.VarChar(255) thumbnail_code String? @db.VarChar(255)
created_at DateTime @default(now()) @db.Timestamptz() download_code String? @db.VarChar(255)
deleted_at DateTime? @db.Timestamptz() created_at DateTime @default(now()) @db.Timestamptz()
updated_at DateTime @updatedAt @db.Timestamptz() deleted_at DateTime? @db.Timestamptz()
updated_at DateTime @updatedAt @db.Timestamptz()
episode_id String @db.Uuid episode_number Int @db.SmallInt
media_id String @db.Uuid
created_by_id String @db.Uuid created_by_id String @db.Uuid
video_submission VideoSubmission? video_submission VideoSubmission?
video_service_id String @db.Uuid
@@unique([media_id, episode_number, priority])
@@map("videos") @@map("videos")
} }
@ -762,17 +768,18 @@ model VideoServiceSubmission {
model Comment { model Comment {
id String @id @default(uuid(7)) @db.Uuid id String @id @default(uuid(7)) @db.Uuid
user User @relation(fields: [user_id], references: [id]) user User @relation(fields: [user_id], references: [id])
episode Episode @relation(fields: [episode_id], references: [id]) episode Episode @relation(fields: [episode_number, media_id], references: [episode_number, media_id])
content String @db.Text content String @db.Text
created_at DateTime @default(now()) @db.Timestamptz() created_at DateTime @default(now()) @db.Timestamptz()
updated_at DateTime @updatedAt @db.Timestamptz() updated_at DateTime @updatedAt @db.Timestamptz()
deleted_at DateTime? @db.Timestamptz() deleted_at DateTime? @db.Timestamptz()
user_id String @db.Uuid user_id String @db.Uuid
episode_id String @db.Uuid episode_number Int @db.SmallInt
likes CommentLike[] media_id String @db.Uuid
audit_logs CommentAuditLog[] likes CommentLike[]
reports CommentReport[] audit_logs CommentAuditLog[]
reports CommentReport[]
@@map("comments") @@map("comments")
} }

View File

@ -1,25 +0,0 @@
import { Context, Static } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { updateAllEpisodeThumbnailService } from "../services/http/updateAllEpisodeThumbnail.service";
import { updateAllEpisodeThumbnailSchema } from "../schemas/updateAllEpisodeThumbnail.schema";
/**
* Updating all episode thumbnails for a specific target service reference ID.
*
* This controller handles the bulk update of episode thumbnails for all episodes associated with a specific service reference ID.
* It fetches the latest thumbnail data from external sources and updates the existing episode records in the database accordingly.
*
* See OpenAPI documentation for request/response schema.
*/
export const updateAllEpisodeThumbnailController = async (ctx: {
set: Context["set"];
body: Static<typeof updateAllEpisodeThumbnailSchema.body>;
}) => {
try {
const newEpisodeThumbnailsCount = await updateAllEpisodeThumbnailService(ctx.body.service_reference_id);
return returnWriteResponse(ctx.set, 204, `Updating ${newEpisodeThumbnailsCount} episode thumbnails successfully.`);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -1,27 +1,24 @@
import Elysia from "elysia"; import Elysia from "elysia";
import { bulkInsertEpisodeController } from "./controllers/bulkInsertEpisode.controller"; import {bulkInsertEpisodeController} from "./controllers/bulkInsertEpisode.controller";
import { bulkInsertMediaController } from "./controllers/bulkInsertMedia.controller"; import {bulkInsertMediaController} from "./controllers/bulkInsertMedia.controller";
import { createVideoServiceInternalController } from "./controllers/createVideoService.controller"; import {createVideoServiceInternalController} from "./controllers/createVideoService.controller";
import { bulkInsertVideoController } from "./controllers/bulkInsertVideo.controller"; import {bulkInsertVideoController} from "./controllers/bulkInsertVideo.controller";
import { updateAllEpisodeThumbnailController } from "./controllers/updateAllEpisodeThumbnail.controller"; import {purgeUnusedSessionController} from "./controllers/purgeUnusedSession.controller";
import { purgeUnusedSessionController } from "./controllers/purgeUnusedSession.controller"; import {createHeroBannerController} from "./controllers/createHeroBanner.controller";
import { createHeroBannerController } from "./controllers/createHeroBanner.controller"; import {bulkInsertMediaSchema} from "./schemas/bulkInsertMedia.schema";
import { bulkInsertMediaSchema } from "./schemas/bulkInsertMedia.schema"; import {bulkInsertEpisodeSchema} from "./schemas/bulkInsertEpisode.schema";
import { bulkInsertEpisodeSchema } from "./schemas/bulkInsertEpisode.schema"; import {bulkInsertVideoSchema} from "./schemas/bulkInsertVideo.schema";
import { updateAllEpisodeThumbnailSchema } from "./schemas/updateAllEpisodeThumbnail.schema"; import {createVideoServiceInternalSchema} from "./schemas/createVideoServiceInternal.schema";
import { bulkInsertVideoSchema } from "./schemas/bulkInsertVideo.schema"; import {purgeUnusedSessionSchema} from "./schemas/purgeUnusedSession.schema";
import { createVideoServiceInternalSchema } from "./schemas/createVideoServiceInternal.schema"; import {createHeroBannerSchema} from "./schemas/createHeroBanner.schema";
import { purgeUnusedSessionSchema } from "./schemas/purgeUnusedSession.schema";
import { createHeroBannerSchema } from "./schemas/createHeroBanner.schema";
export const internalModule = new Elysia({ export const internalModule = new Elysia({
prefix: "/internal", prefix: "/internal",
tags: ["Internal"], tags: ["Internal"],
}) })
.post("/media/bulk-insert", bulkInsertMediaController, bulkInsertMediaSchema) .post("/media/bulk-insert", bulkInsertMediaController, bulkInsertMediaSchema)
.post("/episode/bulk-insert", bulkInsertEpisodeController, bulkInsertEpisodeSchema) .post("/episode/bulk-insert", bulkInsertEpisodeController, bulkInsertEpisodeSchema)
.put("/episode/update-thumbnails", updateAllEpisodeThumbnailController, updateAllEpisodeThumbnailSchema) .post("/video/bulk-insert", bulkInsertVideoController, bulkInsertVideoSchema)
.post("/video/bulk-insert", bulkInsertVideoController, bulkInsertVideoSchema) .post("/video-service", createVideoServiceInternalController, createVideoServiceInternalSchema)
.post("/video-service", createVideoServiceInternalController, createVideoServiceInternalSchema) .post("/user-session/purge-unused", purgeUnusedSessionController, purgeUnusedSessionSchema)
.post("/user-session/purge-unused", purgeUnusedSessionController, purgeUnusedSessionSchema) .post("/hero-banner", createHeroBannerController, createHeroBannerSchema);
.post("/hero-banner", createHeroBannerController, createHeroBannerSchema);

View File

@ -1,26 +1,46 @@
import { Prisma } from "@prisma/client"; import {AppError} from "../../../helpers/error/instances/app";
import { AppError } from "../../../helpers/error/instances/app"; import {prisma} from "../../../utils/databases/prisma/connection";
import { prisma } from "../../../utils/databases/prisma/connection"; import {SystemAccountId} from "../../../config/account/system";
import { generateUUIDv7 } from "../../../helpers/databases/uuidv7";
export interface BulkInsertEpisodesPayload {
media_id: string
episode_number: number
title: string
title_romanji: string
title_origin: string
aired_at: Date
score: number
filler: boolean
recap: boolean
forum_url: string
created_by_id: string
}
export const bulkInsertEpisodesRepository = async ( export const bulkInsertEpisodesRepository = async (
payload: Omit<Prisma.EpisodeUncheckedCreateInput, "id">, payload: BulkInsertEpisodesPayload[]
) => { ) => {
try { try {
return await prisma.episode.upsert({ await prisma.$transaction(async (tx) => {
where: { await Promise.all(
mediaId_episode: { payload.map(async (episode) =>
mediaId: payload.mediaId as string, await tx.episode.upsert({
episode: payload.episode as number, where: {
}, media_id_episode_number: {
}, media_id: episode.media_id,
update: payload, episode_number: episode.episode_number
create: { }
id: generateUUIDv7(), },
...payload, update: episode,
}, create: {
}); ...episode,
} catch (err) { created_by_id: SystemAccountId
throw new AppError(500, "Failed to bulk insert episodes", err); }
} })
)
)
})
} catch (err) {
throw new AppError(500, "Failed to bulk insert episodes", err);
}
}; };

View File

@ -1,10 +1,123 @@
import {AppError} from "../../../helpers/error/instances/app"; import {AppError} from "../../../helpers/error/instances/app";
import {MediaChar} from "../types/mediaCharacters"; import {MediaChar} from "../types/mediaCharacters";
import {prisma} from "../../../utils/databases/prisma/connection";
import {character_role} from "@prisma/client";
export const bulkInsertMediaCharacterRepository = async (animeMalId: number, characters: MediaChar[]) => { export const bulkInsertMediaCharacterRepository = async (
mediaId: string,
characters: MediaChar[]
) => {
try { try {
return characters[0].character.name; const chars = characters.map(c => ({
mal_id: c.character.mal_id,
name: c.character.name,
image: c.character.images.webp.image_url,
small_image: c.character.images.webp.small_image_url,
fanpage_url: c.character.url
}));
const staffs = characters.flatMap(c =>
c.voice_actors.map(v => ({
mal_id: v.person.mal_id,
name: v.person.name,
image: v.person.images.jpg.image_url
}))
);
await prisma.$transaction(async (tx) => {
// Insert Character
await tx.character.createMany({
data: chars,
skipDuplicates: true
});
// Insert Staff
await tx.staff.createMany({
data: staffs,
skipDuplicates: true
});
// Get inserted characters
const insertedChar = await tx.character.findMany({
where: {
mal_id: {
in: chars.map(c => c.mal_id)
}
},
select: {
id: true,
mal_id: true
}
});
// Get inserted staffs
const insertedStaff = await tx.staff.findMany({
where: {
mal_id: {
in: staffs.map(s => s.mal_id)
}
},
select: {
id: true,
mal_id: true
}
});
// Build lookup map
const characterMap = new Map(
insertedChar.map(c => [c.mal_id!, c.id])
);
const staffMap = new Map(
insertedStaff.map(s => [s.mal_id!, s.id])
);
// Connect media with characters
const mediaCharacters = characters.map(c => {
const characterId = characterMap.get(c.character.mal_id);
if (!characterId)
throw new AppError(500, `Character ${c.character.mal_id} not found`);
return {
media_id: mediaId,
character_id: characterId,
role: c.role.toLowerCase() as character_role
};
});
await tx.mediaCharacter.createMany({
data: mediaCharacters,
skipDuplicates: true
});
// Insert all voice actor of characters
const voiceActors = characters.flatMap(c => {
const characterId = characterMap.get(c.character.mal_id);
if (!characterId)
throw new AppError(500, `Character ${c.character.mal_id} not found`);
return c.voice_actors.map(v => {
const staffId = staffMap.get(v.person.mal_id);
if (!staffId) throw new AppError(500, `Staff ${v.person.mal_id} not found`);
return {
media_id: mediaId,
character_id: characterId,
staff_id: staffId,
language: v.language
};
});
});
await tx.voiceActor.createMany({
data: voiceActors,
skipDuplicates: true
});
});
} catch (error) { } catch (error) {
throw new AppError(500, "Failed to bulk insert media characters", error); throw new AppError(
500,
"Failed to bulk insert media characters",
error
);
} }
} };

View File

@ -0,0 +1,36 @@
import {AppError} from "../../../helpers/error/instances/app";
import {MediaFullInfoResponse} from "../types/mediaFullInfo.type";
import {prisma} from "../../../utils/databases/prisma/connection";
import slugify from "slugify";
export const bulkInsertMediaGenreRepository = async (mediaData: MediaFullInfoResponse, mediaId: string) => {
try {
await prisma.$transaction(async (tx) => {
const createdGenres = await tx.genre.createManyAndReturn({
data: mediaData.data.genres.map((genre) => ({
name: genre.name,
mal_id: genre.mal_id,
slug: slugify(genre.name, {
lower: true,
strict: true,
})
})),
skipDuplicates: true,
select: {
mal_id: true,
id: true
}
})
await tx.mediaGenre.createMany({
data: createdGenres.map((genre) => ({
media_id: mediaId,
genre_id: genre.id
})),
skipDuplicates: true
})
})
} catch (error) {
throw new AppError(500, "Failed to bulk insert media genre", error);
}
};

View File

@ -1,28 +0,0 @@
import { AppError } from "../../../helpers/error/instances/app";
import { prisma } from "../../../utils/databases/prisma/connection";
export const findEpisodeWithMediaIdRepository = async ({
media,
episode,
}: {
media: string;
episode: number;
}) => {
try {
const foundEpisode = await prisma.episode.findUnique({
where: {
mediaId_episode: {
mediaId: media,
episode: episode,
},
},
select: {
id: true,
},
});
if (!foundEpisode) throw new AppError(404, "Episode not found");
return foundEpisode;
} catch (error) {
throw new AppError(500, "Error finding episode with media id", error);
}
};

View File

@ -0,0 +1,14 @@
import {AppError} from "../../../helpers/error/instances/app";
import {prisma} from "../../../utils/databases/prisma/connection";
export const findMediaWithMalIdRepository = async (malId: number) => {
try {
return await prisma.media.findUnique({
where: {
mal_id: malId
}
});
} catch (error) {
throw new AppError(500, "Failed to find media with malId", error)
}
};

View File

@ -1,63 +1,76 @@
import { t } from "elysia"; import {t} from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema"; import {AppRouteSchema} from "../../../helpers/types/AppRouteSchema";
export const bulkInsertVideoSchema = { export const bulkInsertVideoSchema = {
body: t.Object({ body: t.Object({
media_id: t.String({ media_id: t.String({
description: "The ID of the media for which episodes will be inserted", description: "The ID of the media for which episodes will be inserted",
}),
data: t.Array(
t.Object({
episode: t.Number({
description: "The episode number",
}), }),
videos: t.Array( data: t.Array(
t.Object({ t.Object({
service_id: t.String({ episode: t.Number({
description: "The ID of the video service", description: "The episode number",
}),
videos: t.Array(
t.Object({
service_id: t.String({
description: "The ID of the video service",
}),
priority: t.Optional(t.Number({
description: "The priority of the video (can't be duplicate)",
})),
video_code: t.String({
description: "The code of the video on the service",
}),
short_code: t.Optional(
t.String({
description: "The code of the preview video on the service",
}),
),
thumbnail_code: t.Optional(
t.String({
description: "The code of the thumbnail for the video on the service",
}),
),
download_code: t.Optional(
t.String({
description: "The code of the download link for the video on the service",
})
)
}),
),
}), }),
video_code: t.String({
description: "The code of the video on the service",
}),
thumbnail_code: t.Optional(
t.String({
description: "The code of the thumbnail for the video on the service",
}),
),
}),
), ),
}), }),
), detail: {
}), summary: "Bulk insert videos for a media episode",
detail: { description:
summary: "Bulk insert videos for a media episode", "Perform bulk insert of videos for specific episodes of a media. This operation inserts multiple videos associated with different episodes into the database based on the provided data.",
description: responses: {
"Perform bulk insert of videos for specific episodes of a media. This operation inserts multiple videos associated with different episodes into the database based on the provided data.", 201: {
responses: { description: "Videos inserted successfully",
201: { content: {
description: "Videos inserted successfully", "application/json": {
content: { schema: {
"application/json": { type: "object",
schema: { properties: {
type: "object", success: {type: "boolean", default: true},
properties: { status: {type: "integer", default: 201},
success: { type: "boolean", default: true }, message: {type: "string", default: "Videos inserted successfully"},
status: { type: "integer", default: 201 }, data: {
message: { type: "string", default: "Videos inserted successfully" }, type: "array",
data: { default: ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
type: "array", description: "An array of IDs of the inserted videos",
default: ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"], items: {
description: "An array of IDs of the inserted videos", type: "string",
items: { description: "The ID of the inserted video",
type: "string", },
description: "The ID of the inserted video", },
}, },
},
},
}, },
},
}, },
},
}, },
},
}, },
},
} satisfies AppRouteSchema; } satisfies AppRouteSchema;

View File

@ -1,35 +0,0 @@
import { t } from "elysia";
import { AppRouteSchema } from "../../../helpers/types/AppRouteSchema";
export const updateAllEpisodeThumbnailSchema = {
body: t.Object({
service_reference_id: t.String({
description: "The ID of the service to which the target of episode thumbnails belong",
}),
}),
detail: {
summary: "Bulk update episode thumbnails",
description:
"Perform bulk update of episode thumbnails for all episodes associated with a specific service reference ID. This operation fetches the latest thumbnail data from external sources and updates the existing episode records in the database accordingly.",
responses: {
204: {
description: "Updating episode thumbnails operation completed successfully",
content: {
"application/json": {
schema: {
type: "object",
properties: {
success: { type: "boolean", default: true },
status: { type: "integer", default: 204 },
message: {
type: "string",
default: "Updating {newEpisodeThumbnailsCount} episode thumbnails operation completed successfully",
},
},
},
},
},
},
},
},
} satisfies AppRouteSchema;

View File

@ -4,20 +4,24 @@ import {getContentReferenceAPI} from "../../../../config/apis/jikan/media.refere
import {bulkInsertMediaCharacterRepository} from "../../repositories/bulkInsertMediaCharacter.repository"; import {bulkInsertMediaCharacterRepository} from "../../repositories/bulkInsertMediaCharacter.repository";
import {MediaFullInfoResponse} from "../../types/mediaFullInfo.type"; import {MediaFullInfoResponse} from "../../types/mediaFullInfo.type";
import {MediaCharacters} from "../../types/mediaCharacters"; import {MediaCharacters} from "../../types/mediaCharacters";
import {bulkInsertMediaGenreRepository} from "../../repositories/bulkInsertMediaGenre.repository";
export const bulkInsertAnimeService = async (malId: number) => { export const bulkInsertAnimeService = async (malId: number) => {
try { try {
const {baseURL, getMediaFullInfo, getMediaCharacters} = getContentReferenceAPI(malId); const {baseURL, getMediaFullInfo, getMediaCharacters} = getContentReferenceAPI(malId);
const mediaFullInfo = (await fetch(baseURL + getMediaFullInfo).then((res) => res.json())) as MediaFullInfoResponse; const mediaFullInfo = (await fetch(baseURL + getMediaFullInfo).then((res) => res.json())) as MediaFullInfoResponse;
// Inserting Media and Producers (Producer, Studio, Licensor)
const insertedMedia = await InsertMediaRepository({ const insertedMedia = await InsertMediaRepository({
payload: mediaFullInfo.data, payload: mediaFullInfo.data,
}); });
// Inserting Characters, Staff, and Voice Actors
// await bulkInsertMediaCharacterRepository(insertedMedia.mal_id)
const mediaChar = await fetch(baseURL + getMediaCharacters).then((res) => res.json()) as MediaCharacters; const mediaChar = await fetch(baseURL + getMediaCharacters).then((res) => res.json()) as MediaCharacters;
await bulkInsertMediaCharacterRepository(insertedMedia.mal_id, mediaChar.data); await bulkInsertMediaCharacterRepository(insertedMedia.id, mediaChar.data);
// Inserting Genres and Demographics
await bulkInsertMediaGenreRepository(mediaFullInfo, insertedMedia.id)
return insertedMedia.id; return insertedMedia.id;
} catch (error) { } catch (error) {

View File

@ -1,35 +1,37 @@
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import {MediaEpisodeInfoResponse} from "../../types/mediaEpisodeInfo.type";
import { MediaEpisodeInfoResponse } from "../../types/mediaEpisodeInfo.type"; import {AppError} from "../../../../helpers/error/instances/app";
import { AppError } from "../../../../helpers/error/instances/app"; import {SystemAccountId} from "../../../../config/account/system";
import { SystemAccountId } from "../../../../config/account/system"; import {ErrorForwarder} from "../../../../helpers/error/instances/forwarder";
import { bulkInsertEpisodesRepository } from "../../repositories/bulkInsertEpisodes.repository"; import {bulkInsertEpisodesRepository} from "../../repositories/bulkInsertEpisodes.repository";
import { getEpisodeReferenceAPI } from "../../../../config/apis/jikan/episode.reference"; import {getEpisodeReferenceAPI} from "../../../../config/apis/jikan/episode.reference";
import { selectMediaByMalIdRepository } from "../../../media/repositories/SELECT/selectMediaByMalId.repository"; import {findMediaWithMalIdRepository} from "../../repositories/findMediaWithMalId.repository";
export const bulkInsertEpisodeService = async (mal_id: number, page: number = 1) => { export const bulkInsertEpisodeService = async (mal_id: number, page: number = 1) => {
try { try {
const episodeAPI = getEpisodeReferenceAPI(mal_id); const episodeAPI = getEpisodeReferenceAPI(mal_id);
const episodeData: MediaEpisodeInfoResponse = await fetch( const episodeData: MediaEpisodeInfoResponse = await fetch(
`${episodeAPI.baseURL}${episodeAPI.getEpisodeList}?page=${page}`, `${episodeAPI.baseURL}${episodeAPI.getEpisodeList}?page=${page}`,
).then((res) => res.json()); ).then((res) => res.json()) as MediaEpisodeInfoResponse;
const mediaData = await selectMediaByMalIdRepository(mal_id); const mediaData = await findMediaWithMalIdRepository(mal_id)
if (!mediaData) throw new AppError(404, `Media with Mal ID ${mal_id} not found in database`); if (!mediaData) throw new AppError(404, "Media not found");
const insertedEpisodeData = []; const constructedInput = episodeData.data.map(c => ({
episodeData.data.forEach(async (episode) => { media_id: mediaData.id,
insertedEpisodeData.push( episode_number: c.mal_id,
await bulkInsertEpisodesRepository({ title: c.title,
mediaId: mediaData.id!, title_romanji: c.title_romanji,
episode: episode.mal_id, title_origin: c.title_japanese,
name: episode.title, aired_at: c.aired,
score: episode.score, score: c.score,
uploadedBy: SystemAccountId, filler: c.filler,
}), recap: c.recap,
); forum_url: c.forum_url,
}); created_by_id: SystemAccountId
return episodeData; }))
} catch (err) { const insertedEpisodes = await bulkInsertEpisodesRepository(constructedInput)
ErrorForwarder(err); return episodeData;
} } catch (err) {
ErrorForwarder(err);
}
}; };

View File

@ -1,35 +1,28 @@
import { SystemAccountId } from "../../../../config/account/system"; import {SystemAccountId} from "../../../../config/account/system";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder"; import {ErrorForwarder} from "../../../../helpers/error/instances/forwarder";
import { findEpisodeWithMediaIdRepository } from "../../repositories/findEpisodeWithMediaId.repository"; import {bulkInsertVideoRepository} from "../../repositories/bulkInsertVideo.repository";
import { bulkInsertVideoRepository } from "../../repositories/bulkInsertVideo.repository"; import {Static} from "elysia";
import { Static } from "elysia"; import {bulkInsertVideoSchema} from "../../schemas/bulkInsertVideo.schema";
import { bulkInsertVideoSchema } from "../../schemas/bulkInsertVideo.schema"; import {Prisma} from "@prisma/client";
export const bulkInsertVideoService = async (body: Static<typeof bulkInsertVideoSchema.body>) => { export const bulkInsertVideoService = async (body: Static<typeof bulkInsertVideoSchema.body>) => {
try { try {
const insertedVideos: string[] = []; const constructedInput: Prisma.VideoCreateManyInput[] = body.data.flatMap((d) => (
for (const episodeData of body.data) { d.videos.flatMap((v) => (
const episodeId = await findEpisodeWithMediaIdRepository({ {
media: body.media_id, created_by_id: SystemAccountId,
episode: episodeData.episode, media_id: body.media_id,
}); episode_number: d.episode,
video_service_id: v.service_id,
for (const videoData of episodeData.videos) { video_code: v.video_code,
const insertedVideo = await bulkInsertVideoRepository({ short_code: v.short_code,
pendingUpload: false, thumbnail_code: v.thumbnail_code,
episodeId: episodeId.id, download_code: v.download_code
serviceId: videoData.service_id, }
videoCode: videoData.video_code, ))
thumbnailCode: videoData.thumbnail_code, ));
uploadedBy: SystemAccountId, return constructedInput
}); } catch (error) {
ErrorForwarder(error);
insertedVideos.push(insertedVideo.id);
}
} }
return insertedVideos;
} catch (error) {
ErrorForwarder(error);
}
}; };

View File

@ -1,40 +0,0 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { bulkUpdateThumbnailRepository } from "../../../episode/repositories/PUT/bulkUpdateThumbnail.repository";
import { getAllVideoServiceWithEpisodeRepository } from "../../../videoService/repositories/GET/getAllVideoServiceWithEpisode.repository";
export const updateAllEpisodeThumbnailService = async (
serviceReferenceId?: string,
) => {
try {
if (!serviceReferenceId)
throw new AppError(400, "Service Reference ID is required.");
const videosData = await getAllVideoServiceWithEpisodeRepository(
serviceReferenceId,
);
if (!videosData || videosData.length === 0)
throw new AppError(
404,
"No episode with no thumbnail found in the specified video service.",
);
const updatePayload = videosData.flatMap((videoService) => {
const { endpointThumbnail, videos } = videoService;
return videos.map((video) => ({
episodeId: video.episode.id,
thumbnailCode: endpointThumbnail!.replace(
":code:",
video.thumbnailCode || video.videoCode,
),
}));
});
await bulkUpdateThumbnailRepository(updatePayload);
return updatePayload.length;
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -1,13 +1,12 @@
import {character_role} from "@prisma/client";
interface StaffVA { interface StaffVA {
mal_id: number; mal_id: number;
url: string; url: string;
name: string;
images: { images: {
jpg: { jpg: {
image_url: string; image_url: string;
},
webp: {
image_url: string;
small_image_url: string;
} }
} }
} }
@ -32,15 +31,9 @@ interface Character {
} }
} }
enum Role {
Main = "Main",
Supporting = "Supporting",
Background = "Background",
}
export interface MediaChar { export interface MediaChar {
character: Character; character: Character;
role: Role; role: character_role;
favorites: number; favorites: number;
voice_actors: voiceActor[]; voice_actors: voiceActor[];
} }

View File

@ -1,44 +0,0 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { videoServiceModel } from "../../model";
export const getAllVideoServiceWithEpisodeRepository = async (
videoServiceId: string,
) => {
try {
return await videoServiceModel.findMany({
where: {
id: videoServiceId,
endpointThumbnail: {
not: null,
},
videos: {
some: {
episode: {
pictureThumbnail: null,
},
},
},
},
select: {
endpointThumbnail: true,
videos: {
select: {
thumbnailCode: true,
videoCode: true,
episode: {
select: {
id: true,
},
},
},
},
},
});
} catch (error) {
throw new AppError(
500,
"An error occurred while fetching video services with episodes.",
error,
);
}
};