Fix recently-added using cluster first-seen and add new-releases shelf.
This commit is contained in:
parent
0dbf2c3d7a
commit
06a022089b
6 changed files with 360 additions and 37 deletions
|
|
@ -0,0 +1,38 @@
|
|||
-- Cluster “date added” (title/episode) + TMDB release date for “Net uitgebracht”.
|
||||
|
||||
ALTER TABLE "movies" ADD COLUMN IF NOT EXISTS "release_date" TIMESTAMP(3);
|
||||
ALTER TABLE "movies" ADD COLUMN IF NOT EXISTS "library_added_at" TIMESTAMP(3);
|
||||
ALTER TABLE "episodes" ADD COLUMN IF NOT EXISTS "library_added_at" TIMESTAMP(3);
|
||||
|
||||
-- Prefer file mtime over scan flood: if last_seen is older than discovered_at, restore it.
|
||||
UPDATE "media_files"
|
||||
SET "discovered_at" = "last_seen"
|
||||
WHERE "last_seen" IS NOT NULL
|
||||
AND "last_seen" < "discovered_at";
|
||||
|
||||
-- First-seen in cluster = earliest file discovery for that title/episode.
|
||||
UPDATE "movies" m
|
||||
SET "library_added_at" = src.min_d
|
||||
FROM (
|
||||
SELECT movie_id, MIN(discovered_at) AS min_d
|
||||
FROM media_files
|
||||
WHERE movie_id IS NOT NULL AND available = true
|
||||
GROUP BY movie_id
|
||||
) src
|
||||
WHERE m.id = src.movie_id
|
||||
AND m.library_added_at IS NULL;
|
||||
|
||||
UPDATE "episodes" e
|
||||
SET "library_added_at" = src.min_d
|
||||
FROM (
|
||||
SELECT episode_id, MIN(discovered_at) AS min_d
|
||||
FROM media_files
|
||||
WHERE episode_id IS NOT NULL AND available = true
|
||||
GROUP BY episode_id
|
||||
) src
|
||||
WHERE e.id = src.episode_id
|
||||
AND e.library_added_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "movies_library_added_at_idx" ON "movies"("library_added_at");
|
||||
CREATE INDEX IF NOT EXISTS "movies_release_date_idx" ON "movies"("release_date");
|
||||
CREATE INDEX IF NOT EXISTS "episodes_library_added_at_idx" ON "episodes"("library_added_at");
|
||||
|
|
@ -422,6 +422,13 @@ model Movie {
|
|||
runtime Int?
|
||||
imdbId String? @unique @map("imdb_id")
|
||||
tmdbId Int? @unique @map("tmdb_id")
|
||||
/** TMDB release_date — for “Net uitgebracht”. */
|
||||
releaseDate DateTime? @map("release_date")
|
||||
/**
|
||||
* Eerste keer dat deze titel in het cluster beschikbaar kwam.
|
||||
* Bump’t niet als een tweede node dezelfde film aanlevert.
|
||||
*/
|
||||
libraryAddedAt DateTime? @map("library_added_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
|
|
@ -430,6 +437,8 @@ model Movie {
|
|||
subtitles LibrarySubtitle[]
|
||||
|
||||
@@index([adult])
|
||||
@@index([libraryAddedAt])
|
||||
@@index([releaseDate])
|
||||
@@map("movies")
|
||||
}
|
||||
|
||||
|
|
@ -482,6 +491,8 @@ model Episode {
|
|||
airDate DateTime? @map("air_date")
|
||||
imdbId String? @map("imdb_id")
|
||||
tmdbId Int? @map("tmdb_id")
|
||||
/** Eerste keer dat deze aflevering in het cluster kwam (geen bump door 2e node). */
|
||||
libraryAddedAt DateTime? @map("library_added_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
|
|
@ -491,6 +502,7 @@ model Episode {
|
|||
matches LibraryMatch[]
|
||||
|
||||
@@unique([seriesId, seasonNumber, episodeNumber])
|
||||
@@index([libraryAddedAt])
|
||||
@@map("episodes")
|
||||
}
|
||||
|
||||
|
|
@ -515,7 +527,7 @@ model MediaFile {
|
|||
path String? @map("path")
|
||||
available Boolean @default(true)
|
||||
lastSeen DateTime @default(now()) @map("last_seen")
|
||||
/// Eerste keer dat dit bestand in de library kwam (wijzigt niet bij rematch/upsert).
|
||||
/// Eerste indexering van dit bestand: bij create ≈ file-mtime (niet scan-now).
|
||||
discoveredAt DateTime @default(now()) @map("discovered_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
|
|
|||
|
|
@ -179,6 +179,56 @@ async function main() {
|
|||
console.warn("Could not ensure node media path columns:", err);
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(
|
||||
`ALTER TABLE "movies" ADD COLUMN IF NOT EXISTS "release_date" TIMESTAMP(3)`
|
||||
);
|
||||
await prisma.$executeRawUnsafe(
|
||||
`ALTER TABLE "movies" ADD COLUMN IF NOT EXISTS "library_added_at" TIMESTAMP(3)`
|
||||
);
|
||||
await prisma.$executeRawUnsafe(
|
||||
`ALTER TABLE "episodes" ADD COLUMN IF NOT EXISTS "library_added_at" TIMESTAMP(3)`
|
||||
);
|
||||
await prisma.$executeRawUnsafe(`
|
||||
UPDATE "media_files"
|
||||
SET "discovered_at" = "last_seen"
|
||||
WHERE "last_seen" IS NOT NULL AND "last_seen" < "discovered_at"
|
||||
`);
|
||||
await prisma.$executeRawUnsafe(`
|
||||
UPDATE "movies" m
|
||||
SET "library_added_at" = src.min_d
|
||||
FROM (
|
||||
SELECT movie_id, MIN(discovered_at) AS min_d
|
||||
FROM media_files
|
||||
WHERE movie_id IS NOT NULL AND available = true
|
||||
GROUP BY movie_id
|
||||
) src
|
||||
WHERE m.id = src.movie_id AND m.library_added_at IS NULL
|
||||
`);
|
||||
await prisma.$executeRawUnsafe(`
|
||||
UPDATE "episodes" e
|
||||
SET "library_added_at" = src.min_d
|
||||
FROM (
|
||||
SELECT episode_id, MIN(discovered_at) AS min_d
|
||||
FROM media_files
|
||||
WHERE episode_id IS NOT NULL AND available = true
|
||||
GROUP BY episode_id
|
||||
) src
|
||||
WHERE e.id = src.episode_id AND e.library_added_at IS NULL
|
||||
`);
|
||||
await prisma.$executeRawUnsafe(
|
||||
`CREATE INDEX IF NOT EXISTS "movies_library_added_at_idx" ON "movies"("library_added_at")`
|
||||
);
|
||||
await prisma.$executeRawUnsafe(
|
||||
`CREATE INDEX IF NOT EXISTS "movies_release_date_idx" ON "movies"("release_date")`
|
||||
);
|
||||
await prisma.$executeRawUnsafe(
|
||||
`CREATE INDEX IF NOT EXISTS "episodes_library_added_at_idx" ON "episodes"("library_added_at")`
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn("Could not ensure library_added_at / release_date:", err);
|
||||
}
|
||||
|
||||
try {
|
||||
await prisma.$executeRawUnsafe(`
|
||||
DO $$ BEGIN
|
||||
|
|
|
|||
|
|
@ -86,8 +86,26 @@ type TmdbMeta = {
|
|||
logoUrl?: string;
|
||||
imdbId?: string;
|
||||
adult?: boolean;
|
||||
/** YYYY-MM-DD from TMDB */
|
||||
releaseDate?: string;
|
||||
};
|
||||
|
||||
/** File mtime → discoveredAt (no future dates; invalid → now). */
|
||||
export function discoveredAtFromFileMtime(modifiedAt: string | Date | null | undefined): Date {
|
||||
const now = Date.now();
|
||||
const mtime = modifiedAt ? new Date(modifiedAt) : new Date(NaN);
|
||||
if (Number.isNaN(mtime.getTime())) return new Date(now);
|
||||
// >1 day in the future → treat as now (bad clocks / copy artefacts)
|
||||
if (mtime.getTime() > now + 86_400_000) return new Date(now);
|
||||
return mtime;
|
||||
}
|
||||
|
||||
function parseTmdbDate(raw: string | null | undefined): Date | null {
|
||||
if (!raw?.trim()) return null;
|
||||
const d = new Date(`${raw.trim()}T00:00:00.000Z`);
|
||||
return Number.isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
|
||||
/** Extra fields for Stremio meta detail pages. */
|
||||
export type StremioMetaExtras = {
|
||||
logo?: string;
|
||||
|
|
@ -341,6 +359,7 @@ export class MetadataService {
|
|||
const nextShelf = file.shelfId || null;
|
||||
const sizeBytes = BigInt(file.sizeBytes);
|
||||
const sizeChanged = existing.sizeBytes !== sizeBytes;
|
||||
const discoveredAt = sizeChanged ? new Date() : undefined;
|
||||
await prisma.mediaFile.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
|
|
@ -354,9 +373,16 @@ export class MetadataService {
|
|||
releaseName: file.releaseName,
|
||||
...(file.path ? { path: file.path } : {}),
|
||||
lastSeen: new Date(file.modifiedAt),
|
||||
...(sizeChanged ? { discoveredAt: new Date() } : {}),
|
||||
...(discoveredAt ? { discoveredAt } : {}),
|
||||
},
|
||||
});
|
||||
if (discoveredAt) {
|
||||
await this.ensureLibraryAddedAt({
|
||||
movieId: existing.movieId,
|
||||
episodeId: existing.episodeId,
|
||||
at: discoveredAt,
|
||||
});
|
||||
}
|
||||
return existing.movieId ?? existing.episodeId;
|
||||
}
|
||||
|
||||
|
|
@ -1975,6 +2001,7 @@ export class MetadataService {
|
|||
|
||||
/** TMDB upsert with safe IMDb linking (unique conflicts must not block file matching). */
|
||||
private async upsertMovieFromTmdb(tmdb: TmdbMeta): Promise<MovieRow> {
|
||||
const releaseDate = parseTmdbDate(tmdb.releaseDate);
|
||||
const data = {
|
||||
title: tmdb.title,
|
||||
year: tmdb.year,
|
||||
|
|
@ -1983,6 +2010,7 @@ export class MetadataService {
|
|||
backdropUrl: tmdb.backdropUrl,
|
||||
tmdbId: tmdb.id,
|
||||
adult: tmdb.adult === true,
|
||||
...(releaseDate ? { releaseDate } : {}),
|
||||
};
|
||||
try {
|
||||
return await prisma.movie.upsert({
|
||||
|
|
@ -2014,6 +2042,29 @@ export class MetadataService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zet libraryAddedAt één keer (cluster first-seen).
|
||||
* Extra node / tweede kopie bump’t niet.
|
||||
*/
|
||||
private async ensureLibraryAddedAt(opts: {
|
||||
movieId?: string | null;
|
||||
episodeId?: string | null;
|
||||
at: Date;
|
||||
}): Promise<void> {
|
||||
if (opts.movieId) {
|
||||
await prisma.movie.updateMany({
|
||||
where: { id: opts.movieId, libraryAddedAt: null },
|
||||
data: { libraryAddedAt: opts.at },
|
||||
});
|
||||
}
|
||||
if (opts.episodeId) {
|
||||
await prisma.episode.updateMany({
|
||||
where: { id: opts.episodeId, libraryAddedAt: null },
|
||||
data: { libraryAddedAt: opts.at },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async upsertMediaFile(
|
||||
nodeId: string,
|
||||
file: MediaFileInfo,
|
||||
|
|
@ -2026,6 +2077,8 @@ export class MetadataService {
|
|||
select: { sizeBytes: true },
|
||||
});
|
||||
const sizeChanged = existing != null && existing.sizeBytes !== sizeBytes;
|
||||
const discoveredAtCreate = discoveredAtFromFileMtime(file.modifiedAt);
|
||||
const discoveredAtUpdate = sizeChanged ? new Date() : undefined;
|
||||
|
||||
await prisma.mediaFile.upsert({
|
||||
where: {
|
||||
|
|
@ -2046,8 +2099,7 @@ export class MetadataService {
|
|||
path: file.path || null,
|
||||
available: true,
|
||||
lastSeen: mtime,
|
||||
// Eerste indexering = nu (ook als bestand-mtime ouder is)
|
||||
discoveredAt: new Date(),
|
||||
discoveredAt: discoveredAtCreate,
|
||||
},
|
||||
update: {
|
||||
...("movieId" in refs ? { movieId: refs.movieId ?? null } : {}),
|
||||
|
|
@ -2062,10 +2114,25 @@ export class MetadataService {
|
|||
...(file.path ? { path: file.path } : {}),
|
||||
available: true,
|
||||
lastSeen: mtime,
|
||||
// Alleen opnieuw “recent” als het bestand inhoudelijk vernieuwd is
|
||||
...(sizeChanged ? { discoveredAt: new Date() } : {}),
|
||||
...(discoveredAtUpdate ? { discoveredAt: discoveredAtUpdate } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
const at = discoveredAtUpdate ?? (existing ? null : discoveredAtCreate);
|
||||
if (at) {
|
||||
await this.ensureLibraryAddedAt({
|
||||
movieId: refs.movieId,
|
||||
episodeId: refs.episodeId,
|
||||
at,
|
||||
});
|
||||
} else if (refs.movieId || refs.episodeId) {
|
||||
// Existing file re-linked / rematched: still seed libraryAddedAt if missing.
|
||||
await this.ensureLibraryAddedAt({
|
||||
movieId: refs.movieId,
|
||||
episodeId: refs.episodeId,
|
||||
at: discoveredAtCreate,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async findExistingMovie(title: string, year?: number) {
|
||||
|
|
@ -2227,6 +2294,7 @@ export class MetadataService {
|
|||
: undefined,
|
||||
imdbId,
|
||||
adult: item.adult === true,
|
||||
releaseDate: item.release_date || undefined,
|
||||
};
|
||||
this.movieCache.set(tmdbId, meta);
|
||||
return meta;
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ const BASE_CATALOGS = [
|
|||
{ type: "series", id: "series", name: "Series", extra: SEARCH_SKIP_EXTRAS },
|
||||
{ type: "movie", id: "recent-movies", name: "Recent toegevoegd", extra: SKIP_EXTRA },
|
||||
{ type: "series", id: "recent-series", name: "Recent toegevoegde series", extra: SKIP_EXTRA },
|
||||
{ type: "movie", id: "new-releases", name: "Net uitgebracht", extra: SKIP_EXTRA },
|
||||
{ type: "movie", id: "4k-movies", name: "4K films", extra: SEARCH_SKIP_EXTRAS },
|
||||
];
|
||||
|
||||
|
|
@ -447,11 +448,13 @@ async function getMovieCatalog(
|
|||
|
||||
if (catalogId === "recent-movies" && !search) {
|
||||
const idRows = await prisma.$queryRaw<Array<{ movie_id: string }>>`
|
||||
SELECT mf.movie_id
|
||||
SELECT m.id AS movie_id
|
||||
FROM media_files mf
|
||||
INNER JOIN movies m ON m.id = mf.movie_id
|
||||
WHERE mf.available = true AND mf.movie_id IS NOT NULL
|
||||
GROUP BY mf.movie_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC
|
||||
GROUP BY m.id, m.library_added_at
|
||||
ORDER BY COALESCE(m.library_added_at, MAX(mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC
|
||||
OFFSET ${skip}
|
||||
LIMIT ${CATALOG_PAGE}
|
||||
`;
|
||||
|
|
@ -465,11 +468,49 @@ async function getMovieCatalog(
|
|||
return ids.map((id) => byId.get(id)).filter((m): m is NonNullable<typeof m> => !!m);
|
||||
})
|
||||
: [];
|
||||
} else if (catalogId === "new-releases") {
|
||||
if (!search) {
|
||||
const idRows = await prisma.$queryRaw<Array<{ movie_id: string }>>`
|
||||
SELECT m.id AS movie_id
|
||||
FROM movies m
|
||||
WHERE (
|
||||
(
|
||||
m.release_date IS NOT NULL
|
||||
AND m.release_date <= CURRENT_TIMESTAMP
|
||||
AND m.release_date >= (CURRENT_TIMESTAMP - interval '3 years')
|
||||
)
|
||||
OR (
|
||||
m.release_date IS NULL
|
||||
AND m.year IS NOT NULL
|
||||
AND m.year >= (EXTRACT(YEAR FROM CURRENT_TIMESTAMP)::int - 3)
|
||||
)
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM media_files mf
|
||||
WHERE mf.movie_id = m.id AND mf.available = true
|
||||
)
|
||||
ORDER BY COALESCE(m.release_date, make_date(m.year, 6, 1)) DESC NULLS LAST,
|
||||
m.title ASC
|
||||
OFFSET ${skip}
|
||||
LIMIT ${CATALOG_PAGE}
|
||||
`;
|
||||
const ids = idRows.map((r) => r.movie_id);
|
||||
movies = ids.length
|
||||
? await prisma.movie.findMany({
|
||||
where: { id: { in: ids } },
|
||||
include: { mediaFiles: { where: { available: true } } },
|
||||
}).then((rows) => {
|
||||
const byId = new Map(rows.map((m) => [m.id, m]));
|
||||
return ids.map((id) => byId.get(id)).filter((m): m is NonNullable<typeof m> => !!m);
|
||||
})
|
||||
: [];
|
||||
}
|
||||
} else if (catalogId === "4k-movies") {
|
||||
if (!search) {
|
||||
const idRows = await prisma.$queryRaw<Array<{ movie_id: string }>>`
|
||||
SELECT mf.movie_id
|
||||
SELECT m.id AS movie_id
|
||||
FROM media_files mf
|
||||
INNER JOIN movies m ON m.id = mf.movie_id
|
||||
WHERE mf.available = true
|
||||
AND mf.movie_id IS NOT NULL
|
||||
AND (
|
||||
|
|
@ -477,8 +518,9 @@ async function getMovieCatalog(
|
|||
OR mf.resolution ILIKE '%4K%'
|
||||
OR mf.resolution ILIKE '%UHD%'
|
||||
)
|
||||
GROUP BY mf.movie_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC
|
||||
GROUP BY m.id, m.library_added_at
|
||||
ORDER BY COALESCE(m.library_added_at, MAX(mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC
|
||||
OFFSET ${skip}
|
||||
LIMIT ${CATALOG_PAGE}
|
||||
`;
|
||||
|
|
@ -515,11 +557,13 @@ async function getMovieCatalog(
|
|||
}
|
||||
} else if (catalogId === "movies" && !search) {
|
||||
const idRows = await prisma.$queryRaw<Array<{ movie_id: string }>>`
|
||||
SELECT mf.movie_id
|
||||
SELECT m.id AS movie_id
|
||||
FROM media_files mf
|
||||
INNER JOIN movies m ON m.id = mf.movie_id
|
||||
WHERE mf.available = true AND mf.movie_id IS NOT NULL
|
||||
GROUP BY mf.movie_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC
|
||||
GROUP BY m.id, m.library_added_at
|
||||
ORDER BY COALESCE(m.library_added_at, MAX(mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC
|
||||
OFFSET ${skip}
|
||||
LIMIT ${CATALOG_PAGE}
|
||||
`;
|
||||
|
|
@ -589,7 +633,8 @@ async function getSeriesCatalog(
|
|||
INNER JOIN episodes e ON e.id = mf.episode_id
|
||||
WHERE mf.available = true AND mf.episode_id IS NOT NULL
|
||||
GROUP BY e.series_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC
|
||||
ORDER BY MAX(COALESCE(e.library_added_at, mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC
|
||||
OFFSET ${skip}
|
||||
LIMIT ${CATALOG_PAGE}
|
||||
`;
|
||||
|
|
@ -607,7 +652,8 @@ async function getSeriesCatalog(
|
|||
INNER JOIN episodes e ON e.id = mf.episode_id
|
||||
WHERE mf.available = true AND mf.episode_id IS NOT NULL
|
||||
GROUP BY e.series_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC
|
||||
ORDER BY MAX(COALESCE(e.library_added_at, mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC
|
||||
OFFSET ${skip}
|
||||
LIMIT ${CATALOG_PAGE}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -626,7 +626,7 @@ export class ViewerService {
|
|||
}
|
||||
|
||||
async home(profileId: string, kidsMode: boolean) {
|
||||
const [shelves, prefs, continueItems, recentMovies, recentSeries, fourK, spotlight] =
|
||||
const [shelves, prefs, continueItems, recentMovies, recentSeries, newReleases, fourK, spotlight] =
|
||||
await Promise.all([
|
||||
prisma.libraryShelf.findMany({
|
||||
where: { enabled: true },
|
||||
|
|
@ -636,6 +636,7 @@ export class ViewerService {
|
|||
this.continueWatching(profileId, kidsMode),
|
||||
this.recentMovies(48, 0, kidsMode),
|
||||
this.recentSeries(36, 0, kidsMode),
|
||||
this.newReleaseMovies(36, 0, kidsMode),
|
||||
this.fourKMovies(36, 0, kidsMode),
|
||||
this.spotlight(12, kidsMode),
|
||||
]);
|
||||
|
|
@ -695,6 +696,16 @@ export class ViewerService {
|
|||
items: recentSeries,
|
||||
});
|
||||
}
|
||||
if (newReleases.length) {
|
||||
rows.push({
|
||||
id: "system:new-releases",
|
||||
title: "Net uitgebracht",
|
||||
name: "Net uitgebracht",
|
||||
kind: "MOVIE",
|
||||
slug: "new-releases",
|
||||
items: newReleases,
|
||||
});
|
||||
}
|
||||
if (fourK.length) {
|
||||
rows.push({
|
||||
id: "system:4k-movies",
|
||||
|
|
@ -707,12 +718,14 @@ export class ViewerService {
|
|||
}
|
||||
|
||||
// Plank-items parallel laden (volgorde van `ordered` blijft behouden via Promise.all-index).
|
||||
// Favorites hangt niet van planken af → tegelijk ophalen i.p.v. erna.
|
||||
// Home: recent in library vooraan (niet A–Z). Favorites tegelijk.
|
||||
const [shelfPayloads, favorites] = await Promise.all([
|
||||
Promise.all(
|
||||
ordered.map(async (shelf) => {
|
||||
const items = await this.shelfItems(shelf.id, shelf.kind, 48, kidsMode);
|
||||
return { shelf, items };
|
||||
const page = await this.shelfItemsPage(shelf.id, shelf.kind, 0, 48, kidsMode, {
|
||||
sort: "recent",
|
||||
});
|
||||
return { shelf, items: page.items };
|
||||
})
|
||||
),
|
||||
this.listFavorites(profileId, kidsMode),
|
||||
|
|
@ -854,27 +867,30 @@ export class ViewerService {
|
|||
return this.addFavorite(profileId, mediaType, id);
|
||||
}
|
||||
|
||||
/** Nieuwste titels (op bestand-mtime van de node) voor de home billboard. */
|
||||
/** Nieuwste titels (cluster first-seen) voor de home billboard. */
|
||||
async spotlight(take: number, kidsMode = false) {
|
||||
const kidsMovie = kidsMode ? kidsMovieSql() : Prisma.empty;
|
||||
const kidsSeries = kidsMode ? kidsSeriesSql() : Prisma.empty;
|
||||
const movieIds = await prisma.$queryRaw<Array<{ movie_id: string; added_at: Date }>>`
|
||||
SELECT mf.movie_id, MAX(mf.discovered_at) AS added_at
|
||||
SELECT m.id AS movie_id,
|
||||
COALESCE(m.library_added_at, MIN(mf.discovered_at)) AS added_at
|
||||
FROM media_files mf
|
||||
INNER JOIN movies m ON m.id = mf.movie_id
|
||||
WHERE mf.available = true AND mf.movie_id IS NOT NULL
|
||||
${kidsMovie}
|
||||
GROUP BY mf.movie_id
|
||||
ORDER BY added_at DESC, MAX(mf.discovered_at) DESC
|
||||
GROUP BY m.id, m.library_added_at
|
||||
ORDER BY added_at DESC NULLS LAST
|
||||
LIMIT ${take * 2}
|
||||
`;
|
||||
const seriesIds = await prisma.$queryRaw<Array<{ series_id: string; added_at: Date }>>`
|
||||
SELECT e.series_id, MAX(mf.discovered_at) AS added_at
|
||||
SELECT e.series_id,
|
||||
MAX(COALESCE(e.library_added_at, mf.discovered_at)) AS added_at
|
||||
FROM media_files mf
|
||||
INNER JOIN episodes e ON e.id = mf.episode_id
|
||||
WHERE mf.available = true AND mf.episode_id IS NOT NULL
|
||||
${kidsSeries}
|
||||
GROUP BY e.series_id
|
||||
ORDER BY added_at DESC, MAX(mf.discovered_at) DESC
|
||||
ORDER BY added_at DESC NULLS LAST
|
||||
LIMIT ${take * 2}
|
||||
`;
|
||||
|
||||
|
|
@ -967,8 +983,8 @@ export class ViewerService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sorteer op discovered_at (eerste indexering). Niet op created_at:
|
||||
* die volgt na bulk-import vaak padvolgorde (W/Z vooraan).
|
||||
* Sorteer op library_added_at (cluster first-seen). Fallback discovered_at.
|
||||
* Tweede node bump’t niet; mtime bij eerste index voorkomt scan-flood.
|
||||
*/
|
||||
private async moviesByRecency(
|
||||
skip: number,
|
||||
|
|
@ -979,14 +995,73 @@ export class ViewerService {
|
|||
const cap = opts.cap;
|
||||
const kidsSql = opts.kidsMode ? kidsMovieSql() : Prisma.empty;
|
||||
const idRows = await prisma.$queryRaw<Array<{ movie_id: string }>>`
|
||||
SELECT mf.movie_id
|
||||
SELECT m.id AS movie_id
|
||||
FROM media_files mf
|
||||
INNER JOIN movies m ON m.id = mf.movie_id
|
||||
WHERE mf.available = true
|
||||
AND mf.movie_id IS NOT NULL
|
||||
${extraWhereSql}
|
||||
${kidsSql}
|
||||
GROUP BY mf.movie_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC, MIN(mf.release_name) ASC
|
||||
GROUP BY m.id, m.library_added_at
|
||||
ORDER BY COALESCE(m.library_added_at, MAX(mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC,
|
||||
MIN(mf.release_name) ASC
|
||||
${cap != null ? Prisma.sql`LIMIT ${cap}` : Prisma.empty}
|
||||
`;
|
||||
const allIds = idRows.map((r) => r.movie_id);
|
||||
const total = allIds.length;
|
||||
const ids = allIds.slice(skip, skip + take);
|
||||
if (!ids.length) return { total, items: [], offset: skip };
|
||||
|
||||
const movies = await prisma.movie.findMany({
|
||||
where: { id: { in: ids } },
|
||||
include: {
|
||||
mediaFiles: {
|
||||
where: { available: true },
|
||||
orderBy: [{ lastSeen: "desc" }, { createdAt: "desc" }],
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
return { total, items: this.mapMovies(ids, movies), offset: skip };
|
||||
}
|
||||
|
||||
/** Films gesorteerd op TMDB-releasedatum (net uitgebracht). */
|
||||
async newReleaseMovies(take: number, skip = 0, kidsMode = false) {
|
||||
const page = await this.moviesByReleaseDate(skip, take, { cap: RECENT_CATALOG_CAP, kidsMode });
|
||||
return page.items;
|
||||
}
|
||||
|
||||
private async moviesByReleaseDate(
|
||||
skip: number,
|
||||
take: number,
|
||||
opts: { cap?: number; kidsMode?: boolean; withinYears?: number } = {}
|
||||
) {
|
||||
const cap = opts.cap;
|
||||
const kidsSql = opts.kidsMode ? kidsMovieSql() : Prisma.empty;
|
||||
const withinYears = opts.withinYears ?? 3;
|
||||
const idRows = await prisma.$queryRaw<Array<{ movie_id: string }>>`
|
||||
SELECT m.id AS movie_id
|
||||
FROM movies m
|
||||
WHERE (
|
||||
(
|
||||
m.release_date IS NOT NULL
|
||||
AND m.release_date <= CURRENT_TIMESTAMP
|
||||
AND m.release_date >= (CURRENT_TIMESTAMP - (${withinYears}::text || ' years')::interval)
|
||||
)
|
||||
OR (
|
||||
m.release_date IS NULL
|
||||
AND m.year IS NOT NULL
|
||||
AND m.year >= (EXTRACT(YEAR FROM CURRENT_TIMESTAMP)::int - ${withinYears})
|
||||
)
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM media_files mf
|
||||
WHERE mf.movie_id = m.id AND mf.available = true
|
||||
${kidsSql}
|
||||
)
|
||||
ORDER BY COALESCE(m.release_date, make_date(m.year, 6, 1)) DESC NULLS LAST,
|
||||
m.title ASC
|
||||
${cap != null ? Prisma.sql`LIMIT ${cap}` : Prisma.empty}
|
||||
`;
|
||||
const allIds = idRows.map((r) => r.movie_id);
|
||||
|
|
@ -1118,7 +1193,8 @@ export class ViewerService {
|
|||
WHERE mf.available = true AND mf.episode_id IS NOT NULL
|
||||
${kidsSql}
|
||||
GROUP BY e.series_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC
|
||||
ORDER BY MAX(COALESCE(e.library_added_at, mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC
|
||||
${cap != null ? Prisma.sql`LIMIT ${cap}` : Prisma.empty}
|
||||
`;
|
||||
const allIds = idRows.map((r) => r.series_id);
|
||||
|
|
@ -1327,14 +1403,16 @@ export class ViewerService {
|
|||
LIMIT ${take}
|
||||
`
|
||||
: await prisma.$queryRaw<Array<{ movie_id: string }>>`
|
||||
SELECT mf.movie_id
|
||||
SELECT m.id AS movie_id
|
||||
FROM media_files mf
|
||||
INNER JOIN movies m ON m.id = mf.movie_id
|
||||
WHERE mf.shelf_id = ${shelfId}
|
||||
AND mf.available = true
|
||||
AND mf.movie_id IS NOT NULL
|
||||
${kidsMovie}
|
||||
GROUP BY mf.movie_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC
|
||||
GROUP BY m.id, m.library_added_at
|
||||
ORDER BY COALESCE(m.library_added_at, MAX(mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC
|
||||
OFFSET ${offset}
|
||||
LIMIT ${take}
|
||||
`;
|
||||
|
|
@ -1427,7 +1505,8 @@ export class ViewerService {
|
|||
AND mf.episode_id IS NOT NULL
|
||||
${kidsSeries}
|
||||
GROUP BY e.series_id
|
||||
ORDER BY MAX(mf.discovered_at) DESC, MAX(mf.created_at) DESC
|
||||
ORDER BY MAX(COALESCE(e.library_added_at, mf.discovered_at)) DESC NULLS LAST,
|
||||
MAX(mf.discovered_at) DESC
|
||||
OFFSET ${offset}
|
||||
LIMIT ${take}
|
||||
`;
|
||||
|
|
@ -1771,6 +1850,36 @@ export class ViewerService {
|
|||
};
|
||||
}
|
||||
|
||||
if (id === "new-releases" || id === "net-uitgebracht") {
|
||||
const sort = requestedSort ?? "recent";
|
||||
const page =
|
||||
sort === "title"
|
||||
? await this.moviesByTitle(
|
||||
skip,
|
||||
take,
|
||||
Prisma.sql`AND EXISTS (
|
||||
SELECT 1 FROM movies mx
|
||||
WHERE mx.id = mf.movie_id
|
||||
AND mx.release_date IS NOT NULL
|
||||
AND mx.release_date <= CURRENT_TIMESTAMP
|
||||
AND mx.release_date >= (CURRENT_TIMESTAMP - interval '3 years')
|
||||
)`,
|
||||
{ kidsMode, letter }
|
||||
)
|
||||
: await this.moviesByReleaseDate(skip, take, {
|
||||
cap: RECENT_CATALOG_CAP,
|
||||
kidsMode,
|
||||
});
|
||||
return {
|
||||
id: "new-releases",
|
||||
title: "Net uitgebracht",
|
||||
kind: "MOVIE" as const,
|
||||
total: page.total,
|
||||
offset: page.offset ?? skip,
|
||||
items: page.items,
|
||||
};
|
||||
}
|
||||
|
||||
if (id === "recent-series") {
|
||||
const sort = requestedSort ?? "recent";
|
||||
const page =
|
||||
|
|
|
|||
Loading…
Reference in a new issue