diff --git a/server/api/admin/movies/[id].put.ts b/server/api/admin/movies/[id].put.ts
index d27c5d0..92c34f6 100644
--- a/server/api/admin/movies/[id].put.ts
+++ b/server/api/admin/movies/[id].put.ts
@@ -32,6 +32,34 @@ export default defineEventHandler(async (event) => {
: null
}
+ if ('actors' in body) {
+ updates.actors = Array.isArray(body.actors) && body.actors.length
+ ? body.actors.filter((a: any) => a.name?.trim()).map((a: any) => ({
+ name: a.name.trim(),
+ originalName: a.originalName?.trim() || undefined,
+ role: a.role?.trim() || undefined,
+ avatar: a.avatar?.trim() || undefined,
+ }))
+ : null
+ }
+
+ if ('customServers' in body) {
+ updates.customServers = Array.isArray(body.customServers) && body.customServers.length
+ ? body.customServers
+ .filter((server: any) => server.name?.trim())
+ .map((server: any) => ({
+ name: server.name.trim(),
+ episodes: Array.isArray(server.episodes)
+ ? server.episodes.filter((ep: any) => ep.name?.trim()).map((ep: any) => ({
+ name: ep.name.trim(),
+ linkEmbed: ep.linkEmbed?.trim() || null,
+ linkM3u8: ep.linkM3u8?.trim() || null,
+ }))
+ : [],
+ }))
+ : null
+ }
+
if (!Object.keys(updates).length) {
throw createError({ statusCode: 400, message: 'Không có dữ liệu cập nhật' })
}
diff --git a/server/api/admin/movies/[id]/sources.get.ts b/server/api/admin/movies/[id]/sources.get.ts
new file mode 100644
index 0000000..46bf9b1
--- /dev/null
+++ b/server/api/admin/movies/[id]/sources.get.ts
@@ -0,0 +1,59 @@
+import { movies } from '../../../../database/schema'
+import { eq } from 'drizzle-orm'
+import { getOphimDetail, getNguoncDetail, getKkphimDetail, type NormalizedServer, type NormalizedActor } from '../../../../utils/movies'
+
+const detailFetchers = {
+ ophim: getOphimDetail,
+ nguonc: getNguoncDetail,
+ kkphim: getKkphimDetail,
+}
+
+export default defineEventHandler(async (event) => {
+ const db = useDb()
+ const id = Number(getRouterParam(event, 'id'))
+
+ if (!id) {
+ throw createError({ statusCode: 400, message: 'Thiếu ID phim' })
+ }
+
+ const result = await db.select().from(movies).where(eq(movies.id, id)).limit(1)
+ if (!result.length) {
+ throw createError({ statusCode: 404, message: 'Không tìm thấy phim' })
+ }
+
+ const movie = result[0]
+ const sourceRefs = (movie.sources || []).filter((ref: any) => ref.source && ref.slug)
+
+ if (!sourceRefs.length) {
+ return { movie, sources: [] }
+ }
+
+ const results = await Promise.allSettled(
+ sourceRefs.map((ref: any) => detailFetchers[ref.source as keyof typeof detailFetchers](ref.slug)),
+ )
+
+ const sourceServers: { source: string, slug: string, name: string, content: string, actors: NormalizedActor[], servers: NormalizedServer[] }[] = []
+
+ results.forEach((result, index) => {
+ const ref = sourceRefs[index]
+ if (result.status === 'fulfilled') {
+ sourceServers.push({
+ source: ref.source,
+ slug: ref.slug,
+ name: ref.name || result.value.name,
+ content: result.value.content || '',
+ actors: result.value.actors || [],
+ servers: (result.value.servers || []).map((server: NormalizedServer) => ({
+ ...server,
+ source: ref.source,
+ sourceSlug: ref.slug,
+ })),
+ })
+ }
+ })
+
+ return {
+ movie,
+ sources: sourceServers,
+ }
+})
diff --git a/server/api/admin/sync.post.ts b/server/api/admin/sync.post.ts
index 832058a..75b69e1 100644
--- a/server/api/admin/sync.post.ts
+++ b/server/api/admin/sync.post.ts
@@ -1,5 +1,5 @@
import { movies } from '../../database/schema'
-import { eq, and } from 'drizzle-orm'
+import { eq, and, or } from 'drizzle-orm'
import {
getOphimKoreanMovies,
getNguoncKoreanMovies,
@@ -7,6 +7,8 @@ import {
getOphimDetail,
getNguoncDetail,
getKkphimDetail,
+ groupMovies,
+ type NormalizedMovie,
} from '../../utils/movies'
const SYNC_PAGES = 10
@@ -19,28 +21,41 @@ const detailFetchers = {
kkphim: getKkphimDetail,
}
-async function fetchEpisodeTotal(source: string, slug: string): Promise {
+async function fetchMovieDetail(source: string, slug: string): Promise<{ episodeTotal?: string, actors?: { name: string, originalName?: string, role?: string, avatar?: string }[] } | undefined> {
const fetcher = detailFetchers[source as keyof typeof detailFetchers]
if (!fetcher) return undefined
try {
const detail = await fetcher(slug)
- return detail.episodeTotal || undefined
+ return {
+ episodeTotal: detail.episodeTotal || undefined,
+ actors: detail.actors?.length ? detail.actors.map((a) => ({
+ name: a.name,
+ originalName: a.originalName,
+ role: a.role,
+ avatar: a.avatar,
+ })) : undefined,
+ }
} catch {
return undefined
}
}
-async function enrichWithEpisodeTotal(moviesList: any[]): Promise {
- const moviesNeedTotal = moviesList.filter((m) => m.type !== 'single' && !m.episodeTotal && m.slug)
+async function enrichMovieDetails(moviesList: any[]): Promise {
+ const moviesNeedDetail = moviesList.filter((m) => m.slug && (m.type !== 'single' || !m.actors))
- for (let i = 0; i < moviesNeedTotal.length; i += DETAIL_CONCURRENCY) {
- const batch = moviesNeedTotal.slice(i, i + DETAIL_CONCURRENCY)
+ for (let i = 0; i < moviesNeedDetail.length; i += DETAIL_CONCURRENCY) {
+ const batch = moviesNeedDetail.slice(i, i + DETAIL_CONCURRENCY)
const results = await Promise.allSettled(
- batch.map((movie) => fetchEpisodeTotal(movie.source, movie.slug)),
+ batch.map((movie) => fetchMovieDetail(movie.source, movie.slug)),
)
results.forEach((result, idx) => {
if (result.status === 'fulfilled' && result.value) {
- batch[idx].episodeTotal = result.value
+ if (result.value.episodeTotal) {
+ batch[idx].episodeTotal = result.value.episodeTotal
+ }
+ if (result.value.actors) {
+ batch[idx].actors = result.value.actors
+ }
}
})
}
@@ -62,79 +77,94 @@ export default defineEventHandler(async (event) => {
nguonc: getNguoncKoreanMovies,
kkphim: getKkphimKoreanMovies,
}
+ const sourceStats: Record = {}
for (const sourceName of sources) {
const fetcher = fetchers[sourceName]
- if (!fetcher) continue
+ if (!fetcher) {
+ sourceStats[sourceName] = { fetched: 0, error: 'Không tìm thấy fetcher' }
+ continue
+ }
+ let fetched = 0
for (let page = 1; page <= SYNC_PAGES; page++) {
try {
const result = await fetcher(page)
for (const movie of result.items) {
allMovies.push({ ...movie, source: sourceName })
}
+ fetched += result.items.length
if (!result.items.length) break
- } catch {
+ } catch (err: any) {
+ sourceStats[sourceName] = { fetched, error: err?.message || `Lỗi ở trang ${page}` }
break
}
}
+ if (!sourceStats[sourceName]) {
+ sourceStats[sourceName] = { fetched }
+ }
}
- await enrichWithEpisodeTotal(allMovies)
+ await enrichMovieDetails(allMovies)
+
+ // Group movies across sources using the existing grouping logic
+ const grouped = groupMovies(allMovies as NormalizedMovie[])
let created = 0
let updated = 0
- for (const movie of allMovies) {
- const existing = await db
- .select()
- .from(movies)
- .where(and(eq(movies.source, movie.source), eq(movies.slug, movie.slug)))
- .limit(1)
+ for (const primary of grouped) {
+ const sourceRefs = (primary.sources || []).map((ref: any) => ({ source: ref.source, slug: ref.slug, name: ref.name || primary.name }))
+ const sourceSlugConditions = sourceRefs
+ .filter((ref: any) => ref.source && ref.slug)
+ .map((ref: any) => and(eq(movies.source, ref.source), eq(movies.slug, ref.slug)))
- if (existing.length) {
- await db
- .update(movies)
- .set({
- name: movie.name,
- originName: movie.originName || null,
- thumb: movie.thumb || null,
- poster: movie.poster || null,
- year: movie.year || null,
- time: movie.time || null,
- episode: movie.episode || null,
- episodeTotal: movie.episodeTotal || null,
- quality: movie.quality || null,
- lang: movie.lang || null,
- type: movie.type || null,
- rating: movie.rating || null,
- categories: movie.categories || null,
- countries: movie.countries || null,
- sources: movie.sources || null,
- apiUpdatedAt: movie.updatedAt ? new Date(movie.updatedAt) : null,
- syncedAt: new Date(),
- })
- .where(eq(movies.id, existing[0].id))
+ // Find existing record by any (source, slug) in the group, or same name/year
+ let existing = null
+ if (sourceSlugConditions.length) {
+ const existingBySource = await db.select().from(movies).where(or(...sourceSlugConditions)).limit(1)
+ if (existingBySource.length) existing = existingBySource[0]
+ }
+
+ if (!existing) {
+ const nameConditions: any[] = [eq(movies.name, primary.name)]
+ if (primary.year) nameConditions.push(eq(movies.year, primary.year))
+ const existingByName = await db
+ .select()
+ .from(movies)
+ .where(and(...nameConditions))
+ .limit(1)
+ if (existingByName.length) existing = existingByName[0]
+ }
+
+ const movieData = {
+ name: primary.name,
+ originName: primary.originName || null,
+ thumb: primary.thumb || null,
+ poster: primary.poster || null,
+ year: primary.year || null,
+ time: primary.time || null,
+ episode: primary.episode || null,
+ episodeTotal: primary.episodeTotal || null,
+ quality: primary.quality || null,
+ lang: primary.lang || null,
+ type: primary.type || null,
+ rating: primary.rating || null,
+ categories: primary.categories || null,
+ countries: primary.countries || null,
+ actors: (primary as any).actors || null,
+ sources: sourceRefs,
+ apiUpdatedAt: primary.updatedAt ? new Date(primary.updatedAt) : null,
+ syncedAt: new Date(),
+ }
+
+ if (existing) {
+ await db.update(movies).set(movieData).where(eq(movies.id, existing.id))
updated++
} else {
await db.insert(movies).values({
- source: movie.source,
- slug: movie.slug,
- name: movie.name,
- originName: movie.originName || null,
- thumb: movie.thumb || null,
- poster: movie.poster || null,
- year: movie.year || null,
- time: movie.time || null,
- episode: movie.episode || null,
- episodeTotal: movie.episodeTotal || null,
- quality: movie.quality || null,
- lang: movie.lang || null,
- type: movie.type || null,
- rating: movie.rating || null,
- categories: movie.categories || null,
- countries: movie.countries || null,
- sources: movie.sources || null,
- apiUpdatedAt: movie.updatedAt ? new Date(movie.updatedAt) : null,
+ source: primary.source,
+ slug: primary.slug,
+ ...movieData,
active: false,
})
created++
@@ -147,5 +177,6 @@ export default defineEventHandler(async (event) => {
total: allMovies.length,
created,
updated,
+ sourceStats,
}
})
diff --git a/server/api/movies/[slug].get.ts b/server/api/movies/[slug].get.ts
index bec2457..6fcd253 100644
--- a/server/api/movies/[slug].get.ts
+++ b/server/api/movies/[slug].get.ts
@@ -1,6 +1,53 @@
import { movies } from '../../database/schema'
import { eq, and } from 'drizzle-orm'
-import { getMovieDetailGroup, parseSourceRefs } from '../../utils/movies'
+import type { MovieDetail, NormalizedServer } from '../../utils/movies'
+
+function mapCustomServers(movie: any): NormalizedServer[] {
+ const servers = movie.customServers || movie.custom_servers
+ if (!Array.isArray(servers) || !servers.length) return []
+
+ return servers.map((server: any) => ({
+ name: String(server.name || 'Server'),
+ source: movie.source,
+ sourceSlug: movie.slug,
+ episodes: (server.episodes || []).map((ep: any) => ({
+ name: String(ep.name || ''),
+ linkEmbed: ep.linkEmbed || undefined,
+ linkM3u8: ep.linkM3u8 || undefined,
+ })).filter((ep: any) => ep.name || ep.linkEmbed || ep.linkM3u8),
+ })).filter((server: any) => server.episodes.length)
+}
+
+function mapMovieToDetail(movie: any): MovieDetail {
+ const servers = mapCustomServers(movie)
+
+ return {
+ id: `${movie.source}:${movie.slug}`,
+ source: movie.source,
+ name: movie.name,
+ originName: movie.originName || '',
+ slug: movie.slug,
+ thumb: movie.customThumb || movie.thumb || '',
+ poster: movie.customPoster || movie.poster || '',
+ year: movie.year || undefined,
+ time: movie.time || undefined,
+ episode: movie.episode || undefined,
+ episodeTotal: movie.episodeTotal || undefined,
+ quality: movie.quality || undefined,
+ lang: movie.lang || undefined,
+ type: movie.type || undefined,
+ rating: movie.rating || undefined,
+ updatedAt: movie.apiUpdatedAt ? new Date(movie.apiUpdatedAt).toISOString() : undefined,
+ categories: movie.categories || [],
+ countries: movie.countries || [],
+ sources: movie.sources || [],
+ content: movie.customContent || movie.content || '',
+ actors: movie.actors || [],
+ directors: [],
+ trailer: undefined,
+ servers,
+ }
+}
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
@@ -34,19 +81,5 @@ export default defineEventHandler(async (event) => {
recordToUpdate = fallback[0]
}
- const refs = parseSourceRefs(query.sources || query.srcs, query.source, slug)
-
- const detail = await getMovieDetailGroup(refs)
-
- if (detail.episodeTotal && recordToUpdate) {
- try {
- await db
- .update(movies)
- .set({ episodeTotal: detail.episodeTotal })
- .where(eq(movies.id, recordToUpdate.id))
- } catch {
- }
- }
-
- return detail
+ return mapMovieToDetail(recordToUpdate)
})
diff --git a/server/database/migrations/0013_add_custom_servers.sql b/server/database/migrations/0013_add_custom_servers.sql
new file mode 100644
index 0000000..5bfd2df
--- /dev/null
+++ b/server/database/migrations/0013_add_custom_servers.sql
@@ -0,0 +1 @@
+ALTER TABLE `movies` ADD `custom_servers` json;
diff --git a/server/database/migrations/0013_late_vapor.sql b/server/database/migrations/0013_late_vapor.sql
new file mode 100644
index 0000000..826a3d4
--- /dev/null
+++ b/server/database/migrations/0013_late_vapor.sql
@@ -0,0 +1 @@
+ALTER TABLE `movies` ADD `custom_servers` json;
\ No newline at end of file
diff --git a/server/database/migrations/meta/0013_snapshot.json b/server/database/migrations/meta/0013_snapshot.json
new file mode 100644
index 0000000..f2b7f2a
--- /dev/null
+++ b/server/database/migrations/meta/0013_snapshot.json
@@ -0,0 +1,573 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "0aab58f1-81f3-4328-8120-60b81eb7cbfd",
+ "prevId": "cdff5cf9-c17b-4367-a3db-46bd089b4b44",
+ "tables": {
+ "comment_votes": {
+ "name": "comment_votes",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "comment_id": {
+ "name": "comment_id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "vote": {
+ "name": "vote",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "idx_user_comment": {
+ "name": "idx_user_comment",
+ "columns": [
+ "user_id",
+ "comment_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "comment_votes_id": {
+ "name": "comment_votes_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "comments": {
+ "name": "comments",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "''"
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "movie_name": {
+ "name": "movie_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "parent_id": {
+ "name": "parent_id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "pinned": {
+ "name": "pinned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "spoiler": {
+ "name": "spoiler",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "anonymous": {
+ "name": "anonymous",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "like_count": {
+ "name": "like_count",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "dislike_count": {
+ "name": "dislike_count",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {
+ "idx_source_slug": {
+ "name": "idx_source_slug",
+ "columns": [
+ "source",
+ "slug"
+ ],
+ "isUnique": false
+ },
+ "idx_user_id": {
+ "name": "idx_user_id",
+ "columns": [
+ "user_id"
+ ],
+ "isUnique": false
+ },
+ "idx_parent_id": {
+ "name": "idx_parent_id",
+ "columns": [
+ "parent_id"
+ ],
+ "isUnique": false
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "comments_id": {
+ "name": "comments_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "movies": {
+ "name": "movies",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "origin_name": {
+ "name": "origin_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "thumb": {
+ "name": "thumb",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "poster": {
+ "name": "poster",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "year": {
+ "name": "year",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "time": {
+ "name": "time",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "episode": {
+ "name": "episode",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "episode_total": {
+ "name": "episode_total",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "quality": {
+ "name": "quality",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "lang": {
+ "name": "lang",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "rating": {
+ "name": "rating",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "views": {
+ "name": "views",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": 0
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "categories": {
+ "name": "categories",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "countries": {
+ "name": "countries",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "sources": {
+ "name": "sources",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_poster": {
+ "name": "custom_poster",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_thumb": {
+ "name": "custom_thumb",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_content": {
+ "name": "custom_content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_episodes": {
+ "name": "custom_episodes",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "custom_servers": {
+ "name": "custom_servers",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": false
+ },
+ "api_updated_at": {
+ "name": "api_updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "synced_at": {
+ "name": "synced_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "movies_id": {
+ "name": "movies_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {},
+ "checkConstraint": {}
+ },
+ "users": {
+ "name": "users",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "password": {
+ "name": "password",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "avatar": {
+ "name": "avatar",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "gender": {
+ "name": "gender",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reset_token": {
+ "name": "reset_token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "reset_token_expires": {
+ "name": "reset_token_expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "(now())"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "onUpdate": true,
+ "default": "(now())"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "users_id": {
+ "name": "users_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "columns": [
+ "email"
+ ]
+ }
+ },
+ "checkConstraint": {}
+ }
+ },
+ "views": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ },
+ "internal": {
+ "tables": {},
+ "indexes": {}
+ }
+}
\ No newline at end of file
diff --git a/server/database/migrations/meta/_journal.json b/server/database/migrations/meta/_journal.json
index 0c672af..706e4db 100644
--- a/server/database/migrations/meta/_journal.json
+++ b/server/database/migrations/meta/_journal.json
@@ -92,6 +92,13 @@
"when": 1784882595519,
"tag": "0012_bumpy_the_watchers",
"breakpoints": true
+ },
+ {
+ "idx": 13,
+ "version": "5",
+ "when": 1785165500506,
+ "tag": "0013_late_vapor",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/server/database/schema.ts b/server/database/schema.ts
index 880637b..e165c87 100644
--- a/server/database/schema.ts
+++ b/server/database/schema.ts
@@ -35,11 +35,13 @@ export const movies = mysqlTable('movies', {
content: text('content'),
categories: json('categories').$type(),
countries: json('countries').$type(),
+ actors: json('actors').$type<{ name: string, originalName?: string, role?: string, avatar?: string }[]>(),
sources: json('sources').$type<{ source: string, slug: string, name?: string }[]>(),
customPoster: text('custom_poster'),
customThumb: text('custom_thumb'),
customContent: text('custom_content'),
customEpisodes: json('custom_episodes').$type<{ name: string, linkEmbed?: string, linkM3u8?: string }[]>(),
+ customServers: json('custom_servers').$type<{ name: string, episodes: { name: string, linkEmbed?: string, linkM3u8?: string }[] }[]>(),
active: boolean('active').notNull().default(false),
apiUpdatedAt: timestamp('api_updated_at'),
syncedAt: timestamp('synced_at').notNull().defaultNow(),
diff --git a/server/utils/movies.ts b/server/utils/movies.ts
index a6750ef..dba660c 100644
--- a/server/utils/movies.ts
+++ b/server/utils/movies.ts
@@ -200,7 +200,7 @@ function interleaveMovies(groups: NormalizedMovie[][]) {
return interleaved
}
-function groupMovies(items: NormalizedMovie[]) {
+export function groupMovies(items: NormalizedMovie[]) {
const groups = new Map()
const exactAliases = new Map()
const looseAliases = new Map()
@@ -291,7 +291,7 @@ export function normalizeOphimMovie(movie: any, pathImage = OPHIM_IMAGE): Normal
originName: text(movie?.origin_name),
slug: text(movie?.slug),
thumb: joinOphimImage(pathImage, movie?.thumb_url),
- poster: joinOphimImage(pathImage, movie?.poster_url || movie?.thumb_url),
+ poster: joinOphimImage(pathImage, movie?.poster_url),
year: Number(movie?.year) || undefined,
time: text(movie?.time),
episode: text(movie?.episode_current),
@@ -314,7 +314,7 @@ export function normalizeKkphimMovie(movie: any, pathImage = KKPHIM_IMAGE): Norm
originName: text(movie?.origin_name),
slug: text(movie?.slug),
thumb: joinKkphimImage(pathImage, movie?.thumb_url),
- poster: joinKkphimImage(pathImage, movie?.poster_url || movie?.thumb_url),
+ poster: joinKkphimImage(pathImage, movie?.poster_url),
year: Number(movie?.year) || undefined,
time: text(movie?.time),
episode: text(movie?.episode_current),
@@ -339,7 +339,7 @@ function normalizeNguoncMovie(movie: any): NormalizedMovie {
originName: text(movie?.original_name || movie?.origin_name),
slug: text(movie?.slug),
thumb: joinImage('', image),
- poster: joinImage('', movie?.poster_url || image),
+ poster: joinImage('', movie?.poster_url),
year: Number(movie?.year || movie?.release_year) || undefined,
time: text(movie?.time || movie?.duration),
episode: text(movie?.episode_current || movie?.current_episode),