A virtual newsroom powered by RSS and AI.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

156 lines
4.0 KiB

// 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<IEpisode>;
totalEpisodeCount: number;
}
export class EpisodeService extends DtpService {
populateEpisode: Array<mongoose.PopulateOptions>;
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<IEpisode> {
const textService = this.getService<TextService>("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<IEpisode> {
const textService = this.getService<TextService>("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<IEpisode | null> {
const show = await Episode.findById(episodeId)
.populate(this.populateEpisode)
.lean();
return show;
}
async getLive(pagination: WebPaginationParameters): Promise<EpisodeLibrary> {
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<EpisodeLibrary> {
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<IEpisode|mongoose.Types.ObjectId> Array of episodes to
* exclude from the result set.
* @returns An array of recommended episodes for display.
*/
async getRecommended(
maxCount: number,
exclude?: Array<IEpisode | Types.ObjectId>
): Promise<Array<IEpisode>> {
const pipeline: Array<mongoose.PipelineStage> = [];
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;