Compare commits
50 Commits
10a19a066d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d7270f8696 | |||
| bd66705eae | |||
| 7fb1d4f1f5 | |||
| 7f129a1b55 | |||
| 3d3a9af9dc | |||
| 90bf31a209 | |||
| 81cc1057b4 | |||
| 9dd02d097d | |||
| 6f754a878b | |||
| e3e4df35e2 | |||
| f3522f6cac | |||
| 745fd213f9 | |||
| dea8c6b7ce | |||
| c1f90c40f2 | |||
| d6fa5efaff | |||
| 4b9ade64c3 | |||
| 9afa0e62f9 | |||
| 9e487297cd | |||
| 5cb9b475be | |||
| 0e3b0a341f | |||
| aa6e3424d3 | |||
| 92620f35bc | |||
| bb6bed884a | |||
| 491d41e44d | |||
| 414cdffe1b | |||
| 9766a6cde6 | |||
| 38855a47c2 | |||
| d13de6ac98 | |||
| 0be954af01 | |||
| 589cf62a6e | |||
| c66731f07d | |||
| c94a7abfb2 | |||
| f9427c577c | |||
| 48b0f7b8a3 | |||
| 8c90df4618 | |||
| 83792848ed | |||
| 09c74b28ab | |||
| d233ec757c | |||
| dedafaa4de | |||
| e213dba0e5 | |||
| ae508ded6d | |||
| fd8f980d9a | |||
| 68fec64efc | |||
| 5a43769f69 | |||
| 6fff049c18 | |||
| 11a607b4da | |||
| ab0c8afca4 | |||
| 0521c27834 | |||
| ce56e13f30 | |||
| 4e8eda081c |
42
.gitea/workflows/ci.yml
Normal file
42
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
name: Integration Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
integration-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node (required by Prisma)
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "24.13.0"
|
||||||
|
|
||||||
|
- name: Setup runtime environment (Bun)
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Index route sync
|
||||||
|
run: bun run route:sync
|
||||||
|
|
||||||
|
- name: Linting test
|
||||||
|
run: bun run lint
|
||||||
|
|
||||||
|
- name: Create dummy system account
|
||||||
|
run: bun run dummy:systemaccount
|
||||||
|
|
||||||
|
- name: Generate prisma schema
|
||||||
|
run: bunx prisma generate
|
||||||
|
|
||||||
|
- name: Build test
|
||||||
|
run: bun run build
|
||||||
20
.gitea/workflows/sync-github.yml
Normal file
20
.gitea/workflows/sync-github.yml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
name: Sync to GitHub
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sync:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout from Gitea
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Push to GitHub mirror-main
|
||||||
|
run: |
|
||||||
|
git remote add github https://vivy-agent:${{ secrets.GH_TOKEN }}@github.com/rafiarrafif/SyzneTV-backend.git
|
||||||
|
git push github HEAD:mirror-main --force
|
||||||
23
.github/workflows/auto-pr-from-mirror.yaml
vendored
Normal file
23
.github/workflows/auto-pr-from-mirror.yaml
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
name: Auto PR from mirror-main
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- mirror-main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Create PR via GitHub CLI
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.BOT_PAT }}
|
||||||
|
run: |
|
||||||
|
gh pr create \
|
||||||
|
--base main \
|
||||||
|
--head mirror-main \
|
||||||
|
--title "Sync from Gitea main" \
|
||||||
|
--body "Automated PR created from Gitea mirror branch."
|
||||||
35
.github/workflows/ci.yaml
vendored
Normal file
35
.github/workflows/ci.yaml
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
name: Intergration Tests
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
jobs:
|
||||||
|
integration-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup runtime environment (Bun)
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install depedencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Index route sync
|
||||||
|
run: bun run route:sync
|
||||||
|
|
||||||
|
- name: Linting test
|
||||||
|
run: bun run lint
|
||||||
|
|
||||||
|
- name: Create dummy system account
|
||||||
|
run: bun run dummy:systemaccount
|
||||||
|
|
||||||
|
- name: Generate prisma schema
|
||||||
|
run: bunx prisma generate
|
||||||
|
|
||||||
|
- name: Build test
|
||||||
|
run: bun run build
|
||||||
@ -5,7 +5,7 @@ import { defineConfig } from "eslint/config";
|
|||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{
|
{
|
||||||
ignores: ["src/modules/debug/**"],
|
ignores: ["src/modules/debug/**", "src/helpers/characters/generateSlug.ts"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ["src/**/*.{js,mjs,cjs,ts,mts,cts}"],
|
files: ["src/**/*.{js,mjs,cjs,ts,mts,cts}"],
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
"prisma:dbml": "bunx prisma db pull && bunx prisma dbml --output ./prisma/dbml/schema.dbml",
|
"prisma:dbml": "bunx prisma db pull && bunx prisma dbml --output ./prisma/dbml/schema.dbml",
|
||||||
"prisma:reset": "bunx prisma db push --force-reset",
|
"prisma:reset": "bunx prisma db push --force-reset",
|
||||||
"prisma:seed": "bun run ./prisma/seed/index.ts",
|
"prisma:seed": "bun run ./prisma/seed/index.ts",
|
||||||
|
"dummy:systemaccount": "bun run ./scripts/create-dummy-system-account.ts",
|
||||||
"route:sync": "bun run ./scripts/sync-routes.ts",
|
"route:sync": "bun run ./scripts/sync-routes.ts",
|
||||||
"env:publish": "bun run ./scripts/create-example-env.ts"
|
"env:publish": "bun run ./scripts/create-example-env.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -181,6 +181,10 @@ Table videos {
|
|||||||
deletedAt DateTime
|
deletedAt DateTime
|
||||||
createdAt DateTime [default: `now()`, not null]
|
createdAt DateTime [default: `now()`, not null]
|
||||||
updatedAt DateTime [default: `now()`, not null]
|
updatedAt DateTime [default: `now()`, not null]
|
||||||
|
|
||||||
|
indexes {
|
||||||
|
(serviceId, code) [unique]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Table video_services {
|
Table video_services {
|
||||||
@ -191,6 +195,7 @@ Table video_services {
|
|||||||
hexColor String [not null]
|
hexColor String [not null]
|
||||||
endpointVideo String [not null]
|
endpointVideo String [not null]
|
||||||
endpointThumbnail String
|
endpointThumbnail String
|
||||||
|
endpointDownload String
|
||||||
creator users [not null]
|
creator users [not null]
|
||||||
createdBy String [not null]
|
createdBy String [not null]
|
||||||
deletedAt DateTime
|
deletedAt DateTime
|
||||||
|
|||||||
@ -218,6 +218,8 @@ model Video {
|
|||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
|
|
||||||
|
@@unique([serviceId, code])
|
||||||
@@map("videos")
|
@@map("videos")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,6 +231,7 @@ model VideoService {
|
|||||||
hexColor String @db.VarChar(10)
|
hexColor String @db.VarChar(10)
|
||||||
endpointVideo String @db.Text
|
endpointVideo String @db.Text
|
||||||
endpointThumbnail String? @db.Text
|
endpointThumbnail String? @db.Text
|
||||||
|
endpointDownload String?
|
||||||
creator User @relation("UserVideoServices", fields: [createdBy], references: [id])
|
creator User @relation("UserVideoServices", fields: [createdBy], references: [id])
|
||||||
createdBy String @db.Uuid
|
createdBy String @db.Uuid
|
||||||
deletedAt DateTime?
|
deletedAt DateTime?
|
||||||
|
|||||||
@ -7,7 +7,7 @@ async function main() {
|
|||||||
console.log("🔌 Connecting to database...");
|
console.log("🔌 Connecting to database...");
|
||||||
|
|
||||||
const userSystemSeedResult = await userSystemSeed();
|
const userSystemSeedResult = await userSystemSeed();
|
||||||
const userRoleSeedResult = await userRoleSeed(userSystemSeedResult.id);
|
await userRoleSeed(userSystemSeedResult.id);
|
||||||
|
|
||||||
console.log("🌳 All seeds completed");
|
console.log("🌳 All seeds completed");
|
||||||
}
|
}
|
||||||
|
|||||||
16
scripts/create-dummy-system-account.ts
Normal file
16
scripts/create-dummy-system-account.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { generateUUIDv7 } from "../src/helpers/databases/uuidv7";
|
||||||
|
import { createFile } from "../src/helpers/files/createFile";
|
||||||
|
|
||||||
|
const createDummySystemAccount = async () => {
|
||||||
|
const file = await createFile(
|
||||||
|
`export const SystemAccountId = "${generateUUIDv7()}";`,
|
||||||
|
{
|
||||||
|
fileName: "system.ts",
|
||||||
|
targetDir: "src/config/account",
|
||||||
|
overwriteIfExists: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log(`Dummy system account created with id in file: ${file}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
createDummySystemAccount();
|
||||||
@ -50,7 +50,7 @@ try {
|
|||||||
|
|
||||||
// Extract the key and the remainder after "="
|
// Extract the key and the remainder after "="
|
||||||
const key = line.substring(0, delimiterIndex).trim();
|
const key = line.substring(0, delimiterIndex).trim();
|
||||||
let remainder = line.substring(delimiterIndex + 1);
|
const remainder = line.substring(delimiterIndex + 1);
|
||||||
|
|
||||||
// Attempt to separate value and inline comment (if any)
|
// Attempt to separate value and inline comment (if any)
|
||||||
let value = remainder;
|
let value = remainder;
|
||||||
|
|||||||
@ -14,7 +14,7 @@ for (const remote of remotes) {
|
|||||||
try {
|
try {
|
||||||
execSync(`git push ${remote} main`, { stdio: "inherit" });
|
execSync(`git push ${remote} main`, { stdio: "inherit" });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`❌ Failed to push to ${remote}`);
|
console.error(`❌ Failed to push to ${remote}`, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,17 +14,13 @@ export async function generateSlug(
|
|||||||
const baseSlug = slugify(input, { lower: true, strict: true });
|
const baseSlug = slugify(input, { lower: true, strict: true });
|
||||||
let uniqueSlug = baseSlug;
|
let uniqueSlug = baseSlug;
|
||||||
|
|
||||||
// CASE 1: Tidak ada config → langsung return slug
|
|
||||||
if (!config) return uniqueSlug;
|
if (!config) return uniqueSlug;
|
||||||
|
|
||||||
const { model, target } = config;
|
const { model, target } = config;
|
||||||
|
|
||||||
// CASE 2: Validasi pasangan model-target
|
|
||||||
if (!model || !target) {
|
if (!model || !target) {
|
||||||
throw new Error(`Both "model" and "target" must be provided together.`);
|
throw new Error(`Both "model" and "target" must be provided together.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// CASE 3: Cek unique
|
|
||||||
const prismaModel = (prisma as any)[model];
|
const prismaModel = (prisma as any)[model];
|
||||||
if (!prismaModel) {
|
if (!prismaModel) {
|
||||||
throw new Error(`Model "${model as string}" not found in PrismaClient.`);
|
throw new Error(`Model "${model as string}" not found in PrismaClient.`);
|
||||||
|
|||||||
5
src/helpers/characters/serializeBigInt.ts
Normal file
5
src/helpers/characters/serializeBigInt.ts
Normal 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)),
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,8 +1,10 @@
|
|||||||
import { generateUUIDv7 } from "./uuidv7";
|
import { generateUUIDv7 } from "./uuidv7";
|
||||||
|
|
||||||
function createManyWithUUID<T extends { id?: string }>(items: T[]): T[] {
|
export const createManyWithUUID = <T extends { id?: string }>(
|
||||||
|
items: T[],
|
||||||
|
): T[] => {
|
||||||
return items.map((i) => ({
|
return items.map((i) => ({
|
||||||
...i,
|
...i,
|
||||||
id: i.id ?? generateUUIDv7(),
|
id: i.id ?? generateUUIDv7(),
|
||||||
}));
|
}));
|
||||||
}
|
};
|
||||||
|
|||||||
@ -25,4 +25,6 @@ export const createFile = async (content: string, config: CreateFileConfig) => {
|
|||||||
|
|
||||||
// Write content to the file
|
// Write content to the file
|
||||||
await fs.promises.writeFile(targetFile, content, "utf8");
|
await fs.promises.writeFile(targetFile, content, "utf8");
|
||||||
|
|
||||||
|
return targetFile;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||||
|
|
||||||
import { middleware } from "./middleware";
|
import { middleware } from "./middleware";
|
||||||
import { validateEnv } from "./utils/startups/validateEnv";
|
import { validateEnv } from "./utils/startups/validateEnv";
|
||||||
|
|
||||||
@ -12,7 +14,7 @@ async function bootstrap() {
|
|||||||
sentryInit();
|
sentryInit();
|
||||||
|
|
||||||
console.log("\x1b[1m\x1b[33m🚀 Starting backend services...\x1b[0m");
|
console.log("\x1b[1m\x1b[33m🚀 Starting backend services...\x1b[0m");
|
||||||
const app = new Elysia()
|
new Elysia()
|
||||||
.use(middleware)
|
.use(middleware)
|
||||||
.use(routes)
|
.use(routes)
|
||||||
.listen(process.env.APP_PORT || 3000);
|
.listen(process.env.APP_PORT || 3000);
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import Elysia, { Context } from "elysia";
|
import Elysia from "elysia";
|
||||||
import { returnErrorResponse } from "../../helpers/callback/httpResponse";
|
import { returnErrorResponse } from "../../helpers/callback/httpResponse";
|
||||||
|
|
||||||
export const appAccessTokenMiddleware = () =>
|
export const appAccessTokenMiddleware = () =>
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
export const isAdminMiddleware = (ctx: Context) => {
|
export const isAdminMiddleware = (ctx: Context) => {
|
||||||
//validate here
|
const isAdmin = ctx.headers["isAdmin"];
|
||||||
|
if (!isAdmin) {
|
||||||
|
ctx.set.status = 403;
|
||||||
|
return {
|
||||||
|
error: "Forbidden",
|
||||||
|
message: "You don't have access to this resource",
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { AppError } from "../../../../helpers/error/instances/app";
|
import { AppError } from "../../../../helpers/error/instances/app";
|
||||||
import { jwtDecode } from "../../../../helpers/http/jwt/decode";
|
import { jwtDecode } from "../../../../helpers/http/jwt/decode";
|
||||||
import { jwtEncode } from "../../../../helpers/http/jwt/encode";
|
|
||||||
|
|
||||||
export const tokenValidationService = (payload: string) => {
|
export const tokenValidationService = (payload: string) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
import { Context } from "elysia";
|
||||||
|
import { mainErrorHandler } from "../../../helpers/error/handler";
|
||||||
|
import { returnReadResponse } from "../../../helpers/callback/httpResponse";
|
||||||
|
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 (
|
||||||
|
ctx: Context & { params: { mediaSlug: string } },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const episodesData = await getAllEpisodeFromSpecificMediaService(
|
||||||
|
ctx.params.mediaSlug,
|
||||||
|
);
|
||||||
|
return returnReadResponse(
|
||||||
|
ctx.set,
|
||||||
|
200,
|
||||||
|
"Episodes fetched successfully",
|
||||||
|
episodesData,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return mainErrorHandler(ctx.set, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
3
src/modules/episode/episode.model.ts
Normal file
3
src/modules/episode/episode.model.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { prisma } from "../../utils/databases/prisma/connection";
|
||||||
|
|
||||||
|
export const episodeModel = prisma.episode;
|
||||||
7
src/modules/episode/index.ts
Normal file
7
src/modules/episode/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Elysia from "elysia";
|
||||||
|
import { getAllEpisodeFromSpecificMediaController } from "./controllers/getAllEpisodeFromSpecificMedia.controller";
|
||||||
|
import { getEpisodeDetailsController } from "./controllers/getEpisodeDetails.controller";
|
||||||
|
|
||||||
|
export const episodeModule = new Elysia({ prefix: "/episodes/:mediaSlug" })
|
||||||
|
.get("/", getAllEpisodeFromSpecificMediaController)
|
||||||
|
.get("/:episode", getEpisodeDetailsController);
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
import { AppError } from "../../../../helpers/error/instances/app";
|
||||||
|
import { mediaModel } from "../../../media/model";
|
||||||
|
|
||||||
|
export const getAllEpisodeFromMediaRepository = async (mediaSlug: string) => {
|
||||||
|
try {
|
||||||
|
return mediaModel.findUnique({
|
||||||
|
where: { slug: mediaSlug },
|
||||||
|
select: {
|
||||||
|
episodes: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
episode: true,
|
||||||
|
pictureThumbnail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new AppError(500, "Failed to fetch episodes from media", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
import { AppError } from "../../../../helpers/error/instances/app";
|
||||||
|
import { episodeModel } from "../../episode.model";
|
||||||
|
|
||||||
|
export const getAllEpisodeWithThumbnailLinkRepository = async (
|
||||||
|
serviceReferenceId: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await episodeModel.findMany({
|
||||||
|
where: {
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
episode: true,
|
||||||
|
videos: {
|
||||||
|
where: {
|
||||||
|
deletedAt: null,
|
||||||
|
serviceId: serviceReferenceId,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
code: true,
|
||||||
|
service: {
|
||||||
|
select: {
|
||||||
|
endpointThumbnail: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new AppError(500, "Failed to get all episode thumbnails", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { AppError } from "../../../../helpers/error/instances/app";
|
||||||
|
import { episodeModel } from "../../episode.model";
|
||||||
|
|
||||||
|
export const updateEpisodeRepository = async (
|
||||||
|
payload: Prisma.EpisodeUncheckedUpdateInput,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await episodeModel.update({
|
||||||
|
where: {
|
||||||
|
id: payload.id as string,
|
||||||
|
},
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new AppError(500, "Failed to edit episode", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import { AppError } from "../../../../helpers/error/instances/app";
|
||||||
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
|
import { getAllEpisodeFromMediaRepository } from "../../repositories/GET/getAllEpisodeFromMedia.repository";
|
||||||
|
|
||||||
|
export const getAllEpisodeFromSpecificMediaService = async (
|
||||||
|
mediaSlug: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const mediaData = await getAllEpisodeFromMediaRepository(mediaSlug);
|
||||||
|
if (!mediaData)
|
||||||
|
throw new AppError(404, `Media with slug ${mediaSlug} not found`);
|
||||||
|
return mediaData.episodes;
|
||||||
|
} catch (error) {
|
||||||
|
ErrorForwarder(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -3,7 +3,46 @@ import { mainErrorHandler } from "../../../helpers/error/handler";
|
|||||||
import { bulkInsertEpisodeService } from "../services/http/bulkInsertEpisode.service";
|
import { bulkInsertEpisodeService } from "../services/http/bulkInsertEpisode.service";
|
||||||
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
|
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
|
||||||
|
|
||||||
// add pagination query
|
/**
|
||||||
|
* @function bulkInsertMediaController
|
||||||
|
* @description Perform bulk insert of episodes for a specific media. This operation fetches episode data from external sources and inserts them into the database. The page parameter is optional; if not provided, the first page of episodes will be fetched.
|
||||||
|
*
|
||||||
|
* @param {Context & { body: { media_mal_id: number }; query: { page?: number } }} ctx
|
||||||
|
* The context object containing the request body.
|
||||||
|
* The body must include:
|
||||||
|
* - media_mal_id: number - The MyAnimeList ID of the media for which episodes will be inserted.
|
||||||
|
* The query may include:
|
||||||
|
* - page?: number - (Optional) The page number of episodes to fetch and insert. If not provided, defaults to the first page.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Request route: POST /internal/episode/bulk-insert
|
||||||
|
* Request body:
|
||||||
|
* {
|
||||||
|
* "media_mal_id": 12345
|
||||||
|
* }
|
||||||
|
* Query parameter:
|
||||||
|
* ?page=2 (Optional, specifies the page number of episodes to fetch and insert)
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
* A response object indicating success or failure.
|
||||||
|
* Return example:
|
||||||
|
* {
|
||||||
|
* success: true,
|
||||||
|
* status: 201,
|
||||||
|
* message: "Bulk insert episode operation completed successfully",
|
||||||
|
* data: { ...bulkInsertResult } // 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 bulkInsertEpisodeController = async (
|
export const bulkInsertEpisodeController = async (
|
||||||
ctx: Context & { body: { media_mal_id: number }; query: { page?: number } },
|
ctx: Context & { body: { media_mal_id: number }; query: { page?: number } },
|
||||||
) => {
|
) => {
|
||||||
@ -15,7 +54,7 @@ export const bulkInsertEpisodeController = async (
|
|||||||
return returnWriteResponse(
|
return returnWriteResponse(
|
||||||
ctx.set,
|
ctx.set,
|
||||||
201,
|
201,
|
||||||
"Success bulk insert for episode",
|
"Bulk insert episode operation completed successfully",
|
||||||
bulkInsertResult,
|
bulkInsertResult,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { bulkInsertAnimeService } from "../services/http/bulkInsertAnime.service
|
|||||||
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
|
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @function bulkInsertAnimeController
|
* @function bulkInsertMediaController
|
||||||
* @description Insert new anime to the database only with mal_id. This operation including inserting related data such as genres, studios, producers, licensors, themes, demographics, and relations.
|
* @description Insert new anime to the database only with mal_id. This operation including inserting related data such as genres, studios, producers, licensors, themes, demographics, and relations.
|
||||||
*
|
*
|
||||||
* @param {Context & { body: { mal_id: number } }} ctx
|
* @param {Context & { body: { mal_id: number } }} ctx
|
||||||
|
|||||||
@ -0,0 +1,87 @@
|
|||||||
|
import { Context } from "elysia";
|
||||||
|
import { mainErrorHandler } from "../../../helpers/error/handler";
|
||||||
|
import { bulkInsertVideoService } from "../services/http/bulkInsertVideo.service";
|
||||||
|
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
|
||||||
|
|
||||||
|
export interface BulkInsertVideoBodyRequest {
|
||||||
|
media_id: string;
|
||||||
|
data: Array<{
|
||||||
|
episode: number;
|
||||||
|
videos: Array<{
|
||||||
|
service_id: string;
|
||||||
|
code: string;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function bulkInsertVideoController
|
||||||
|
* @description 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.
|
||||||
|
*
|
||||||
|
* @param {Context & { body: BulkInsertVideoBodyRequest }} ctx
|
||||||
|
* The context object containing the request body.
|
||||||
|
* The body must include:
|
||||||
|
* - media_id: string - The ID of the media for which episodes will be inserted.
|
||||||
|
* - data: Array - An array of episode data, each containing:
|
||||||
|
* - episode: number - The episode number.
|
||||||
|
* - videos: Array - An array of video data for the episode, each containing:
|
||||||
|
* - service_id: string - The ID of the video service.
|
||||||
|
* - code: string - The code of the video on the service.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Request route: POST /internal/video/bulk-insert
|
||||||
|
* Request body:
|
||||||
|
* {
|
||||||
|
* "media_id": "019c064e-a03d-7cc3-b2ae-5d6850ea456b",
|
||||||
|
* "data": [
|
||||||
|
* {
|
||||||
|
* "episode": 1,
|
||||||
|
* "videos": [
|
||||||
|
* {
|
||||||
|
* "service_id": "019c0df6-f8fe-7565-82cd-9c29b20232ab",
|
||||||
|
* "code": "fzwu9n8ge2qt"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* },
|
||||||
|
* {
|
||||||
|
* "episode": 2,
|
||||||
|
* "videos": [
|
||||||
|
* {
|
||||||
|
* "service_id": "019c0df6-f8fe-7565-82cd-9c29b20232ab",
|
||||||
|
* "code": "w2maywh53rt8"
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* ]
|
||||||
|
* },
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
* A response object indicating success or failure.
|
||||||
|
* Return example:
|
||||||
|
* {
|
||||||
|
* success: true,
|
||||||
|
* status: 201,
|
||||||
|
* message: "Videos inserted",
|
||||||
|
* data: { ...insertedVideos } // 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 bulkInsertVideoController = async (
|
||||||
|
ctx: Context & { body: BulkInsertVideoBodyRequest },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const insertedVideos = await bulkInsertVideoService(ctx.body);
|
||||||
|
return returnWriteResponse(ctx.set, 201, "Videos inserted", insertedVideos);
|
||||||
|
} catch (error) {
|
||||||
|
throw mainErrorHandler(ctx.set, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,80 @@
|
|||||||
|
import { Context } from "elysia";
|
||||||
|
import { mainErrorHandler } from "../../../helpers/error/handler";
|
||||||
|
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
|
||||||
|
import { createVideoServiceInternalService } from "../services/http/createVideoService.service";
|
||||||
|
|
||||||
|
export interface CreateVideoServiceInternalBodyRequest {
|
||||||
|
name: string;
|
||||||
|
domain: string;
|
||||||
|
logo: string;
|
||||||
|
hexColor: string;
|
||||||
|
endpointVideo: string;
|
||||||
|
endpointThumbnail: string;
|
||||||
|
endpointDownload?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @function createVideoServiceInternalController
|
||||||
|
* @description Perform creation of a new video service. This operation adds a new video service to the database based on the provided data.
|
||||||
|
*
|
||||||
|
* @param {Context & { body: CreateVideoServiceInternalBodyRequest }} ctx
|
||||||
|
* The context object containing the request body.
|
||||||
|
* The body must include:
|
||||||
|
* - name: string - The name of the video service.
|
||||||
|
* - domain: string - The domain of the video service.
|
||||||
|
* - logo: string - The logo URL of the video service.
|
||||||
|
* - hexColor: string - The hex color associated with the video service.
|
||||||
|
* - endpointVideo: string - The endpoint URL for video streaming.
|
||||||
|
* - endpointThumbnail: string - The endpoint URL for thumbnails.
|
||||||
|
* - endpointDownload?: string - (Optional) The endpoint URL for downloads.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* Request route: POST /internal/video-service
|
||||||
|
* Request body:
|
||||||
|
* {
|
||||||
|
* "name": "Example Video Service",
|
||||||
|
* "domain": "example.com",
|
||||||
|
* "logo": "https://example.com/logo.png",
|
||||||
|
* "hexColor": "#FF5733",
|
||||||
|
* "endpointVideo": "https://api.example.com/videos",
|
||||||
|
* "endpointThumbnail": "https://api.example.com/thumbnails",
|
||||||
|
* "endpointDownload": "https://api.example.com/downloads"
|
||||||
|
* },
|
||||||
|
*
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
* A response object indicating success or failure.
|
||||||
|
* Return example:
|
||||||
|
* {
|
||||||
|
* success: true,
|
||||||
|
* status: 201,
|
||||||
|
* message: "Video service created",
|
||||||
|
* data: { ...createdVideoService } // 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 createVideoServiceInternalController = async (
|
||||||
|
ctx: Context & { body: CreateVideoServiceInternalBodyRequest },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const createdVideoService = await createVideoServiceInternalService(
|
||||||
|
ctx.body,
|
||||||
|
);
|
||||||
|
return returnWriteResponse(
|
||||||
|
ctx.set,
|
||||||
|
201,
|
||||||
|
"Video service created",
|
||||||
|
createdVideoService,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw mainErrorHandler(ctx.set, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
import { Context } from "elysia";
|
||||||
|
import { mainErrorHandler } from "../../../helpers/error/handler";
|
||||||
|
import { returnWriteResponse } from "../../../helpers/callback/httpResponse";
|
||||||
|
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 (
|
||||||
|
ctx: Context & { body: { service_reference_id: string } },
|
||||||
|
) => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,7 +1,13 @@
|
|||||||
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 { bulkInsertVideoController } from "./controllers/bulkInsertVideo.controller";
|
||||||
|
import { updateAllEpisodeThumbnailController } from "./controllers/updateAllEpisodeThumbnail.controller";
|
||||||
|
|
||||||
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)
|
||||||
|
.put("/episode/update-thumbnails", updateAllEpisodeThumbnailController)
|
||||||
|
.post("/video/bulk-insert", bulkInsertVideoController)
|
||||||
|
.post("/video-service", createVideoServiceInternalController);
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { AppError } from "../../../helpers/error/instances/app";
|
||||||
|
import { prisma } from "../../../utils/databases/prisma/connection";
|
||||||
|
import { generateUUIDv7 } from "../../../helpers/databases/uuidv7";
|
||||||
|
|
||||||
|
export const bulkInsertVideoRepository = async (
|
||||||
|
payload: Omit<Prisma.VideoUncheckedCreateInput, "id">,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await prisma.video.upsert({
|
||||||
|
where: {
|
||||||
|
serviceId_code: {
|
||||||
|
serviceId: payload.serviceId,
|
||||||
|
code: payload.code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: generateUUIDv7(),
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
|
update: payload,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new AppError(500, "Error inserting video", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { AppError } from "../../../helpers/error/instances/app";
|
||||||
|
import { prisma } from "../../../utils/databases/prisma/connection";
|
||||||
|
import { generateUUIDv7 } from "../../../helpers/databases/uuidv7";
|
||||||
|
|
||||||
|
export const createVideoServiceInternalRepository = async (
|
||||||
|
payload: Omit<Prisma.VideoServiceUncheckedCreateInput, "id">,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await prisma.videoService.upsert({
|
||||||
|
where: {
|
||||||
|
name: payload.name,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: generateUUIDv7(),
|
||||||
|
...payload,
|
||||||
|
},
|
||||||
|
update: payload,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new AppError(500, "Failed to create video service", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,4 +1,3 @@
|
|||||||
import { Prisma } from "@prisma/client";
|
|
||||||
import { getEpisodeReferenceAPI } from "../../../../config/apis/episode.reference";
|
import { getEpisodeReferenceAPI } from "../../../../config/apis/episode.reference";
|
||||||
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
import { MediaEpisodeInfoResponse } from "../../types/mediaEpisodeInfo.type";
|
import { MediaEpisodeInfoResponse } from "../../types/mediaEpisodeInfo.type";
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { SystemAccountId } from "../../../../config/account/system";
|
||||||
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
|
import { BulkInsertVideoBodyRequest } from "../../controllers/bulkInsertVideo.controller";
|
||||||
|
import { findEpisodeWithMediaIdRepository } from "../../repositories/findEpisodeWithMediaId.repository";
|
||||||
|
import { bulkInsertVideoRepository } from "../../repositories/bulkInsertVideo.repository";
|
||||||
|
|
||||||
|
export const bulkInsertVideoService = async (
|
||||||
|
body: BulkInsertVideoBodyRequest,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const insertedVideos: string[] = [];
|
||||||
|
for (const episodeData of body.data) {
|
||||||
|
const episodeId = await findEpisodeWithMediaIdRepository({
|
||||||
|
media: body.media_id,
|
||||||
|
episode: episodeData.episode,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const videoData of episodeData.videos) {
|
||||||
|
const insertedVideo = await bulkInsertVideoRepository({
|
||||||
|
pendingUpload: false,
|
||||||
|
episodeId: episodeId.id,
|
||||||
|
serviceId: videoData.service_id,
|
||||||
|
code: videoData.code,
|
||||||
|
uploadedBy: SystemAccountId,
|
||||||
|
});
|
||||||
|
|
||||||
|
insertedVideos.push(insertedVideo.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return insertedVideos;
|
||||||
|
} catch (error) {
|
||||||
|
ErrorForwarder(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { SystemAccountId } from "../../../../config/account/system";
|
||||||
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
|
import { CreateVideoServiceInternalBodyRequest } from "../../controllers/createVideoService.controller";
|
||||||
|
import { createVideoServiceInternalRepository } from "../../repositories/createVideoService.repository";
|
||||||
|
|
||||||
|
export const createVideoServiceInternalService = async (
|
||||||
|
body: CreateVideoServiceInternalBodyRequest,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
return await createVideoServiceInternalRepository({
|
||||||
|
name: body.name,
|
||||||
|
domain: body.domain,
|
||||||
|
logo: body.logo,
|
||||||
|
hexColor: body.hexColor,
|
||||||
|
endpointVideo: body.endpointVideo,
|
||||||
|
endpointThumbnail: body.endpointThumbnail,
|
||||||
|
endpointDownload: body.endpointDownload,
|
||||||
|
createdBy: SystemAccountId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
ErrorForwarder(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
import { AppError } from "../../../../helpers/error/instances/app";
|
||||||
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
|
import { updateEpisodeRepository } from "../../../episode/repositories/PUT/updateEpisode.repository";
|
||||||
|
import { getAllEpisodeWithThumbnailLinkRepository } from "../../../episode/repositories/GET/getAllEpisodeWithThumbnailLink.repository";
|
||||||
|
|
||||||
|
export const updateAllEpisodeThumbnailService = async (
|
||||||
|
serviceReferenceId: string,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (!serviceReferenceId)
|
||||||
|
throw new AppError(400, "Service Reference ID is required.");
|
||||||
|
|
||||||
|
const episodesData = await getAllEpisodeWithThumbnailLinkRepository(
|
||||||
|
serviceReferenceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
let updatedThumbnailsCount = 0;
|
||||||
|
for (const episode of episodesData) {
|
||||||
|
if (episode.videos.length === 0) continue;
|
||||||
|
await updateEpisodeRepository({
|
||||||
|
id: episode.id,
|
||||||
|
pictureThumbnail:
|
||||||
|
episode.videos[0].service.endpointThumbnail?.replace(
|
||||||
|
":code:",
|
||||||
|
episode.videos[0].code,
|
||||||
|
) || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
updatedThumbnailsCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updatedThumbnailsCount;
|
||||||
|
} catch (error) {
|
||||||
|
ErrorForwarder(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,6 +1,5 @@
|
|||||||
import { SystemAccountId } from "../../../../config/account/system";
|
import { SystemAccountId } from "../../../../config/account/system";
|
||||||
import { getContentReferenceAPI } from "../../../../config/apis/media.reference";
|
import { getContentReferenceAPI } from "../../../../config/apis/media.reference";
|
||||||
import { generateUUIDv7 } from "../../../../helpers/databases/uuidv7";
|
|
||||||
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
import { bulkInsertCharactersRepository } from "../../repositories/bulkInsertCharacters.repository";
|
import { bulkInsertCharactersRepository } from "../../repositories/bulkInsertCharacters.repository";
|
||||||
import { bulkInsertLangVARepository } from "../../repositories/bulkInsertLangVA.repository";
|
import { bulkInsertLangVARepository } from "../../repositories/bulkInsertLangVA.repository";
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { SystemAccountId } from "../../../../config/account/system";
|
import { SystemAccountId } from "../../../../config/account/system";
|
||||||
import { generateUUIDv7 } from "../../../../helpers/databases/uuidv7";
|
|
||||||
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
import { bulkInsertVoiceActorRepository } from "../../repositories/bulkInsertVoiceActor.repository";
|
import { bulkInsertVoiceActorRepository } from "../../repositories/bulkInsertVoiceActor.repository";
|
||||||
import { Person } from "../../types/mediaCharWithVAInfo";
|
import { Person } from "../../types/mediaCharWithVAInfo";
|
||||||
|
|||||||
@ -34,12 +34,12 @@ interface Data {
|
|||||||
year: number;
|
year: number;
|
||||||
broadcast: Broadcast;
|
broadcast: Broadcast;
|
||||||
producers: Genre[];
|
producers: Genre[];
|
||||||
licensors: any[];
|
licensors: unknown[];
|
||||||
studios: Genre[];
|
studios: Genre[];
|
||||||
genres: Genre[];
|
genres: Genre[];
|
||||||
explicit_genres: any[];
|
explicit_genres: unknown[];
|
||||||
themes: Genre[];
|
themes: Genre[];
|
||||||
demographics: any[];
|
demographics: unknown[];
|
||||||
relations: Relation[];
|
relations: Relation[];
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
external: External[];
|
external: External[];
|
||||||
|
|||||||
@ -10,7 +10,7 @@ interface Data {
|
|||||||
name: string;
|
name: string;
|
||||||
given_name: null;
|
given_name: null;
|
||||||
family_name: null;
|
family_name: null;
|
||||||
alternate_names: any[];
|
alternate_names: string[];
|
||||||
birthday: Date;
|
birthday: Date;
|
||||||
favorites: number;
|
favorites: number;
|
||||||
about: string;
|
about: string;
|
||||||
|
|||||||
20
src/modules/media/controllers/getAllMedia.controller.ts
Normal file
20
src/modules/media/controllers/getAllMedia.controller.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { Context } from "elysia";
|
||||||
|
import { mainErrorHandler } from "../../../helpers/error/handler";
|
||||||
|
import { getAllMediaService } from "../services/http/getAllMedia.service";
|
||||||
|
import { returnReadResponse } from "../../../helpers/callback/httpResponse";
|
||||||
|
|
||||||
|
export const getAllMediaController = async (
|
||||||
|
ctx: Context & { query: { page: string } },
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const mediaData = await getAllMediaService(ctx.query.page);
|
||||||
|
return returnReadResponse(
|
||||||
|
ctx.set,
|
||||||
|
200,
|
||||||
|
"Media fetched successfully",
|
||||||
|
mediaData,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
return mainErrorHandler(ctx.set, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
7
src/modules/media/index.ts
Normal file
7
src/modules/media/index.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import Elysia from "elysia";
|
||||||
|
import { getAllMediaController } from "./controllers/getAllMedia.controller";
|
||||||
|
|
||||||
|
export const mediaModule = new Elysia({ prefix: "/media" }).get(
|
||||||
|
"/",
|
||||||
|
getAllMediaController,
|
||||||
|
);
|
||||||
17
src/modules/media/repositories/GET/getAllMedia.repository.ts
Normal file
17
src/modules/media/repositories/GET/getAllMedia.repository.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { AppError } from "../../../../helpers/error/instances/app";
|
||||||
|
import { mediaModel } from "../../model";
|
||||||
|
|
||||||
|
export const getAllMediaRepository = async (page: number) => {
|
||||||
|
try {
|
||||||
|
const limit = 10;
|
||||||
|
return await mediaModel.findMany({
|
||||||
|
take: limit,
|
||||||
|
skip: (page - 1) * limit,
|
||||||
|
where: {
|
||||||
|
deletedAt: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw new AppError(500, "Failed to get all media from repository", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
15
src/modules/media/services/http/getAllMedia.service.ts
Normal file
15
src/modules/media/services/http/getAllMedia.service.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { ErrorForwarder } from "../../../../helpers/error/instances/forwarder";
|
||||||
|
import { getAllMediaRepository } from "../../repositories/GET/getAllMedia.repository";
|
||||||
|
|
||||||
|
export const getAllMediaService = async (pagination: string) => {
|
||||||
|
try {
|
||||||
|
const page =
|
||||||
|
/^\d+$/.test(pagination) && Number(pagination) > 0
|
||||||
|
? Number(pagination)
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
return getAllMediaRepository(page);
|
||||||
|
} catch (error) {
|
||||||
|
ErrorForwarder(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -6,14 +6,17 @@ const includeOptions = ["preference", "assignedRoles"] as const;
|
|||||||
export const getUserOptionsSchema = z.object({
|
export const getUserOptionsSchema = z.object({
|
||||||
verbosity: z.enum(
|
verbosity: z.enum(
|
||||||
["exists", "basic", "full"],
|
["exists", "basic", "full"],
|
||||||
"option: verbosity value must match with enum types"
|
"option: verbosity value must match with enum types",
|
||||||
),
|
),
|
||||||
include: z
|
include: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.transform((val) => val?.split(",") ?? [])
|
.transform((val) => val?.split(",") ?? [])
|
||||||
.refine(
|
.refine(
|
||||||
(arr) => arr.every((val) => includeOptions.includes(val.trim() as any)),
|
(arr) =>
|
||||||
"option: include value didn't match with enum types"
|
arr.every((val) =>
|
||||||
|
includeOptions.includes(val.trim() as typeof includeOptions[number]),
|
||||||
|
),
|
||||||
|
"option: include value didn't match with enum types",
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,26 +2,6 @@ import { Prisma } from "@prisma/client";
|
|||||||
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
|
import { ErrorForwarder } from "../../../helpers/error/instances/forwarder";
|
||||||
import { userSessionModel } from "../userSession.model";
|
import { userSessionModel } from "../userSession.model";
|
||||||
|
|
||||||
type CreateUserSessionResponse = Prisma.UserSessionGetPayload<{
|
|
||||||
select: {
|
|
||||||
id: true;
|
|
||||||
deviceType: true;
|
|
||||||
isAuthenticated: true;
|
|
||||||
validUntil: true;
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
id: true;
|
|
||||||
name: true;
|
|
||||||
email: true;
|
|
||||||
username: true;
|
|
||||||
avatar: true;
|
|
||||||
birthDate: true;
|
|
||||||
bioProfile: true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
|
|
||||||
export const createUserSessionRepository = async (
|
export const createUserSessionRepository = async (
|
||||||
data: Prisma.UserSessionUncheckedCreateInput,
|
data: Prisma.UserSessionUncheckedCreateInput,
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { minioBucketName, minioClient } from "../client";
|
import { minioClient } from "../client";
|
||||||
import { ensureBucketExists } from "../validations/ensureBucketExists";
|
import { ensureBucketExists } from "../validations/ensureBucketExists";
|
||||||
|
|
||||||
export const getStreamFile = async (filename: string) => {
|
export const getStreamFile = async (filename: string) => {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { minioBucketName, minioClient, minioProtocol } from "../client";
|
import { minioBucketName, minioClient } from "../client";
|
||||||
import { ensureBucketExists } from "../validations/ensureBucketExists";
|
import { ensureBucketExists } from "../validations/ensureBucketExists";
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ export const uploadFile = async (
|
|||||||
options?: {
|
options?: {
|
||||||
fileDir?: string;
|
fileDir?: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
}
|
},
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
// Ensure the target MinIO bucket exists before performing any upload
|
// Ensure the target MinIO bucket exists before performing any upload
|
||||||
await ensureBucketExists();
|
await ensureBucketExists();
|
||||||
|
|||||||
Reference in New Issue
Block a user