Compare commits

..

9 Commits

Author SHA1 Message Date
d7270f8696 Merge pull request 'docs' (#12) from docs into main
All checks were successful
Sync to GitHub / sync (push) Successful in 7s
Reviewed-on: #12
2026-02-06 22:30:22 +07:00
bd66705eae 📝 docs: add documentation for get all episodes controller
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 57s
2026-02-06 22:28:14 +07:00
7fb1d4f1f5 📝 docs: add documentation for get episode detail controller 2026-02-06 22:26:11 +07:00
7f129a1b55 📝 docs: add documentation for bulk update thumbnail controller 2026-02-06 22:22:49 +07:00
3d3a9af9dc Merge pull request 'feat/episode-details' (#11) from feat/episode-details into main
All checks were successful
Sync to GitHub / sync (push) Successful in 9s
Reviewed-on: #11
2026-02-05 22:22:51 +07:00
90bf31a209 🐛 fix: correct payload for bulk video insert API
All checks were successful
Integration Tests / integration-tests (pull_request) Successful in 37s
2026-02-05 22:22:05 +07:00
81cc1057b4 🐛 fix: handle bigint with json serialize helper 2026-02-05 22:20:25 +07:00
9dd02d097d feat: add endpoint to get episode details 2026-02-05 21:47:02 +07:00
6f754a878b Merge pull request ' feat: add automatic thumbnail generation' (#10) from feat/thumbnail-generation into main
All checks were successful
Sync to GitHub / sync (push) Successful in 8s
Reviewed-on: #10
2026-02-05 21:04:53 +07:00
10 changed files with 244 additions and 5 deletions

View File

@ -0,0 +1,5 @@
export const serializeBigInt = <T>(data: T): T => {
return JSON.parse(
JSON.stringify(data, (_, v) => (typeof v === "bigint" ? Number(v) : v)),
);
};

View File

@ -3,6 +3,38 @@ import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnReadResponse } from "../../../helpers/callback/httpResponse"; import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { getAllEpisodeFromSpecificMediaService } from "../services/http/getAllEpisodeFromSpecificMedia.service"; import { getAllEpisodeFromSpecificMediaService } from "../services/http/getAllEpisodeFromSpecificMedia.service";
/**
* @function getAllEpisodeFromSpecificMediaController
* @description Controller to handle fetching all episodes associated with a specific media slug.
*
* @param {Context & { params: { mediaSlug: string } }} ctx
* The context object containing the request body.
* The params must include:
* - mediaSlug: string - The slug of the media to which the episode belongs.
*
* @example
* Request route: GET /episodes/:mediaSlug
*
* @returns {Promise<Object>}
* A response object indicating success or failure.
* Return example:
* {
* success: true,
* status: 200,
* message: "Episodes fetched successfully.",
* data: { ...episodeDetails } // Data returned only if the env run on development mode
* }
*
* @throws {Object}
* An error response object if validation fails or an error occurs during bulk insert operation.
* Return example:
* {
* success: false,
* status: <Status Code>,
* message: "<Error Message>",
* error: { ...errorDetails } // Additional error details if available and the env run on development mode
* }
*/
export const getAllEpisodeFromSpecificMediaController = async ( export const getAllEpisodeFromSpecificMediaController = async (
ctx: Context & { params: { mediaSlug: string } }, ctx: Context & { params: { mediaSlug: string } },
) => { ) => {

View File

@ -0,0 +1,58 @@
import { Context } from "elysia";
import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnReadResponse } from "../../../helpers/callback/httpResponse";
import { getEpisodeDetailsService } from "../services/http/getEpisodeDetails.service";
export interface GetEpisodeDetailsParams {
mediaSlug?: string;
episode?: string;
}
/**
* @function getEpisodeDetailsController
* @description Controller to handle fetching episode details based on provided parameters.
*
* @param {Context & { params: GetEpisodeDetailsParams }} ctx
* The context object containing the request body.
* The params must include:
* - mediaSlug?: string - The slug of the media to which the episode belongs.
* - episode?: string - The identifier of the episode.
*
* @example
* Request route: GET /episodes/:mediaSlug/:episode
*
* @returns {Promise<Object>}
* A response object indicating success or failure.
* Return example:
* {
* success: true,
* status: 200,
* message: "Episode details fetched successfully.",
* data: { ...episodeDetails } // Data returned only if the env run on development mode
* }
*
* @throws {Object}
* An error response object if validation fails or an error occurs during bulk insert operation.
* Return example:
* {
* success: false,
* status: <Status Code>,
* message: "<Error Message>",
* error: { ...errorDetails } // Additional error details if available and the env run on development mode
* }
*/
export const getEpisodeDetailsController = async (
ctx: Context & { params: GetEpisodeDetailsParams },
) => {
try {
const result = await getEpisodeDetailsService(ctx.params);
return returnReadResponse(
ctx.set,
200,
"Episode details fetched successfully.",
result,
);
} catch (error) {
return mainErrorHandler(ctx.set, error);
}
};

View File

@ -1,7 +1,7 @@
import Elysia from "elysia"; import Elysia from "elysia";
import { getAllEpisodeFromSpecificMediaController } from "./controllers/getAllEpisodeFromSpecificMedia.controller"; import { getAllEpisodeFromSpecificMediaController } from "./controllers/getAllEpisodeFromSpecificMedia.controller";
import { getEpisodeDetailsController } from "./controllers/getEpisodeDetails.controller";
export const episodeModule = new Elysia({ prefix: "/episodes/:mediaSlug" }).get( export const episodeModule = new Elysia({ prefix: "/episodes/:mediaSlug" })
"/", .get("/", getAllEpisodeFromSpecificMediaController)
getAllEpisodeFromSpecificMediaController, .get("/:episode", getEpisodeDetailsController);
);

View File

@ -0,0 +1,66 @@
import { serializeBigInt } from "../../../../helpers/characters/serializeBigInt";
import { AppError } from "../../../../helpers/error/instances/app";
import { episodeModel } from "../../episode.model";
export const getEpisodeDetailsRepository = async (payload: {
mediaId: string;
episode: number;
}) => {
try {
const result = await episodeModel.findUnique({
where: {
mediaId_episode: {
mediaId: payload.mediaId,
episode: payload.episode,
},
deletedAt: null,
},
select: {
episode: true,
name: true,
score: true,
pictureThumbnail: true,
viewed: true,
likes: true,
updatedAt: true,
uploader: {
select: {
name: true,
username: true,
},
},
videos: {
where: {
pendingUpload: false,
deletedAt: null,
},
select: {
code: true,
service: {
select: {
endpointThumbnail: true,
endpointVideo: true,
endpointDownload: true,
},
},
},
},
media: {
select: {
slug: true,
title: true,
_count: {
select: {
episodes: true,
},
},
},
},
},
});
return serializeBigInt(result);
} catch (error) {
throw new AppError(500, "Failed to fetch episode details.", error);
}
};

View File

@ -0,0 +1,27 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
import { getMediaIdFromSlugRepository } from "../../../media/repositories/GET/getMediaIdFromSlug.repository";
import { GetEpisodeDetailsParams } from "../../controllers/getEpisodeDetails.controller";
import { getEpisodeDetailsRepository } from "../../repositories/GET/getEpisodeDetails.repository";
export const getEpisodeDetailsService = async (
params: GetEpisodeDetailsParams,
) => {
try {
if (!params.mediaSlug || !params.episode)
throw new AppError(400, "Media slug and episode are required.");
const mediaId = await getMediaIdFromSlugRepository(params.mediaSlug);
if (!mediaId?.id) throw new AppError(404, "Media not found.");
const result = await getEpisodeDetailsRepository({
mediaId: mediaId.id,
episode: Number(params.episode),
});
if (!result) throw new AppError(404, "Episode not found.");
return result;
} catch (error) {
ErrorForwarder(error);
}
};

View File

@ -3,6 +3,41 @@ import { mainErrorHandler } from "../../../helpers/error/handler";
import { returnWriteResponse } from "../../../helpers/callback/httpResponse"; import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
import { updateAllEpisodeThumbnailService } from "../services/http/updateAllEpisodeThumbnail.service"; import { updateAllEpisodeThumbnailService } from "../services/http/updateAllEpisodeThumbnail.service";
/**
* @function updateAllEpisodeThumbnailController
* @description Controller to handle the bulk updating of episode thumbnails for all episodes associated with a specific service reference ID.
*
* @param {Context & { body: { service_reference_id: string } }} ctx
* The context object containing the request body.
* The body must include:
* - service_reference_id: string - The ID of the service to which the episodes belong.
*
* @example
* Request route: PUT /internal/episode/update-thumbnails
* Request body:
* {
* "service_reference_id": "019c0df6-f8fe-7565-82cd-9c29b20232ab"
* },
*
* @returns {Promise<Object>}
* A response object indicating success or failure.
* Return example:
* {
* success: true,
* status: 204,
* message: "Updating {newEpisodeThumbnailsCount} episode thumbnails successfully.",
* }
*
* @throws {Object}
* An error response object if validation fails or an error occurs during bulk insert operation.
* Return example:
* {
* success: false,
* status: <Status Code>,
* message: "<Error Message>",
* error: { ...errorDetails } // Additional error details if available and the env run on development mode
* }
*/
export const updateAllEpisodeThumbnailController = async ( export const updateAllEpisodeThumbnailController = async (
ctx: Context & { body: { service_reference_id: string } }, ctx: Context & { body: { service_reference_id: string } },
) => { ) => {

View File

@ -8,6 +8,6 @@ import { updateAllEpisodeThumbnailController } from "./controllers/updateAllEpis
export const internalModule = new Elysia({ prefix: "/internal" }) export const internalModule = new Elysia({ prefix: "/internal" })
.post("/media/bulk-insert", bulkInsertMediaController) .post("/media/bulk-insert", bulkInsertMediaController)
.post("/episode/bulk-insert", bulkInsertEpisodeController) .post("/episode/bulk-insert", bulkInsertEpisodeController)
.post("/episode/update-thumbnails", updateAllEpisodeThumbnailController) .put("/episode/update-thumbnails", updateAllEpisodeThumbnailController)
.post("/video/bulk-insert", bulkInsertVideoController) .post("/video/bulk-insert", bulkInsertVideoController)
.post("/video-service", createVideoServiceInternalController); .post("/video-service", createVideoServiceInternalController);

View File

@ -17,6 +17,7 @@ export const bulkInsertVideoService = async (
for (const videoData of episodeData.videos) { for (const videoData of episodeData.videos) {
const insertedVideo = await bulkInsertVideoRepository({ const insertedVideo = await bulkInsertVideoRepository({
pendingUpload: false,
episodeId: episodeId.id, episodeId: episodeId.id,
serviceId: videoData.service_id, serviceId: videoData.service_id,
code: videoData.code, code: videoData.code,

View File

@ -0,0 +1,15 @@
import { AppError } from "../../../../helpers/error/instances/app";
import { mediaModel } from "../../model";
export const getMediaIdFromSlugRepository = async (slug: string) => {
try {
return await mediaModel.findUnique({
where: { slug },
select: {
id: true,
},
});
} catch (error) {
throw new AppError(500, "Failed to fetch media ID from slug.", error);
}
};