From fbd6cd8ff586ebaa6aae13587b2989ded26051fe Mon Sep 17 00:00:00 2001 From: Rob Colbert Date: Mon, 3 Feb 2025 16:11:13 -0500 Subject: [PATCH] Episode updates --- src/app/controllers/episode.ts | 72 ++++++++++++++++++++++++++++++++++ src/app/models/episode.ts | 10 +++-- src/app/services/episode.ts | 33 ++++++++++++++++ 3 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 src/app/controllers/episode.ts diff --git a/src/app/controllers/episode.ts b/src/app/controllers/episode.ts new file mode 100644 index 0000000..5dbb2ef --- /dev/null +++ b/src/app/controllers/episode.ts @@ -0,0 +1,72 @@ +// app/controllers/episode.ts +// Copyright (C) 2025 DTP Technologies, LLC +// All Rights Reserved + +import { NextFunction, Request, Response } from "express"; + +import { WebServer, WebController } from "../../lib/dtplib.js"; + +import { EpisodeService } from "../services/episode.js"; +import { SidebarService } from "../services/sidebar.js"; + +export class EpisodeController extends WebController { + static get name(): string { + return "EpisodeController"; + } + static get slug(): string { + return "episode"; + } + + constructor(server: WebServer) { + super(server, EpisodeController); + } + + get route(): string { + return "/episode"; + } + + async start(): Promise { + const sidebarService = this.getService("sidebar"); + const sidebarMiddleware = sidebarService.middleware(); + + this.router.get("/:episodeId", sidebarMiddleware, this.getView.bind(this)); + this.router.get("/", sidebarMiddleware, this.getHome.bind(this)); + } + + async getView( + _req: Request, + res: Response, + next: NextFunction + ): Promise { + const episodeService = this.getService("episode"); + try { + res.locals.recommended = await episodeService.getRecommended(3, [ + res.locals.episode, + ]); + res.render("episode/view"); + } catch (error) { + this.log.error("failed to present the view", { error }); + return next(error); + } + } + + async getHome( + req: Request, + res: Response, + next: NextFunction + ): Promise { + const episodeService = this.getService("episode"); + try { + res.locals.pagination = this.getPaginationParameters(req, 20); + res.locals.episodeLibrary = await episodeService.getLive( + res.locals.pagination + ); + return res.render("episode/home"); + } catch (error) { + this.log.error("failed to present the Home view", { error }); + return next(error); + } + } +} + +export default EpisodeController; diff --git a/src/app/models/episode.ts b/src/app/models/episode.ts index 69af36f..9110347 100644 --- a/src/app/models/episode.ts +++ b/src/app/models/episode.ts @@ -1,4 +1,4 @@ -// app/models/feed.ts +// app/models/episode.ts // Copyright (C) 2025 DTP Technologies, LLC // All Rights Reserved @@ -15,6 +15,10 @@ export enum EpisodeStatus { Offline = "offline", Removed = "removed", } + +/** + * Defines an Episode in DTP Newsroom. + */ export interface IEpisode { _id: Types.ObjectId; // MongoDB concern __v: number; // MongoDB concern @@ -25,8 +29,8 @@ export interface IEpisode { title: string; description: string; - video?: IVideo | Types.ObjectId; feedItems?: Array; + video?: IVideo | Types.ObjectId; } export const EpisodeSchema = new Schema({ @@ -46,8 +50,8 @@ export const EpisodeSchema = new Schema({ }, title: { type: String, required: true }, description: { type: String, required: true }, - video: { type: Schema.ObjectId, ref: "Video" }, feedItems: { type: [Schema.ObjectId], ref: "FeedItem" }, + video: { type: Schema.ObjectId, ref: "Video" }, }); export const Episode = model("Episode", EpisodeSchema); diff --git a/src/app/services/episode.ts b/src/app/services/episode.ts index d54f25e..fd36349 100644 --- a/src/app/services/episode.ts +++ b/src/app/services/episode.ts @@ -118,6 +118,39 @@ export class EpisodeService extends DtpService { const totalEpisodeCount = await Episode.estimatedDocumentCount(); return { episodes, totalEpisodeCount }; } + + /** + * For now just fetches a sample of the live list minus any episodes specified + * for exclusion (such as the one(s) on the page right now). + * @param maxCount number The maximum number of episodes to return. + * @param exclude Array Array of episodes to + * exclude from the result set. + * @returns An array of recommended episodes for display. + */ + async getRecommended( + maxCount: number, + exclude?: Array + ): Promise> { + const pipeline: Array = []; + + if (exclude) { + if (!Array.isArray(exclude)) { + exclude = [exclude]; + } + pipeline.push({ + $match: { + _id: { $nin: exclude.map((show) => show._id) }, + }, + }); + } + + pipeline.push({ + $sample: { size: maxCount }, + }); + + const shows = await Episode.aggregate(pipeline); + return await Episode.populate(shows, this.populateEpisode); + } } export default EpisodeService;