// app/services/episode.ts // Copyright (C) 2025 DTP Technologies, LLC // All Rights Reserved import mongoose, { Types } from "mongoose"; import Episode, { EpisodeStatus, IEpisode } from "../models/episode.js"; import { DtpPlatform, DtpService, DtpServiceUpdate, WebError, WebPaginationParameters, } from "../../lib/dtplib.js"; import TextService from "./text.js"; export interface EpisodeDefinition { title: string; description: string; } export interface EpisodeLibrary { episodes: Array; totalEpisodeCount: number; } export class EpisodeService extends DtpService { populateEpisode: Array; static get name() { return "EpisodeService"; } static get slug() { return "episode"; } constructor(platform: DtpPlatform) { super(platform, EpisodeService); this.populateEpisode = [ { path: "show", }, { path: "video", }, { path: "feedItems", }, ]; } async create(definition: EpisodeDefinition): Promise { const textService = this.getService("text"); const NOW = new Date(); const episode = new Episode(); episode.created = NOW; episode.title = textService.filter(definition.title); episode.description = textService.filter(definition.description); await episode.save(); return episode.toObject(); } async update( episode: IEpisode | Types.ObjectId, definition: EpisodeDefinition ): Promise { const textService = this.getService("text"); const update: DtpServiceUpdate = {}; update.$set = {}; update.$set.title = textService.filter(definition.title); update.$set.description = textService.filter(definition.description); const newEpisode = await Episode.findByIdAndUpdate(episode._id, update, { new: true, populate: this.populateEpisode, }).lean(); if (!newEpisode) { throw new WebError(404, "show does not exist"); } return newEpisode; } async getById(episodeId: Types.ObjectId): Promise { const show = await Episode.findById(episodeId) .populate(this.populateEpisode) .lean(); return show; } async getLive(pagination: WebPaginationParameters): Promise { const search = { status: EpisodeStatus.Live }; const episodes = await Episode.find(search) .sort({ created: -1 }) .skip(pagination.skip) .limit(pagination.cpp) .populate(this.populateEpisode) .lean(); const totalEpisodeCount = await Episode.countDocuments(search); return { episodes, totalEpisodeCount }; } async getAll(pagination: WebPaginationParameters): Promise { const episodes = await Episode.find() .sort({ created: -1 }) .skip(pagination.skip) .limit(pagination.cpp) .populate(this.populateEpisode) .lean(); 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;