feat: Add AudioClip to database proper

Also: API endpoints and controller implementations
This commit is contained in:
Jozsef Kiraly 2025-11-27 11:55:52 +00:00
parent 657cb075ee
commit cdb0bbb4d2
No known key found for this signature in database
4 changed files with 983 additions and 0 deletions

View file

@ -162,6 +162,11 @@ class Database {
return this.models.device
}
/** @type {typeof import('./models/AudioClip')} */
get audioClipModel() {
return this.models.audioClip
}
/**
* Check if db file exists
* @returns {boolean}
@ -345,6 +350,7 @@ class Database {
require('./models/Setting').init(this.sequelize)
require('./models/CustomMetadataProvider').init(this.sequelize)
require('./models/MediaItemShare').init(this.sequelize)
require('./models/AudioClip').init(this.sequelize)
return this.sequelize.sync({ force, alter: false })
}

View file

@ -473,5 +473,171 @@ class MeController {
const data = await userStats.getStatsForYear(req.user.id, year)
res.json(data)
}
/**
* GET: /api/me/clips
* Get all clips for the authenticated user
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getClips(req, res) {
try {
const clips = await Database.audioClipModel.getClipsForUser(req.user.id)
res.json({ clips: clips.map((c) => c.toJSON()) })
} catch (error) {
Logger.error(`[MeController] Failed to get clips:`, error)
res.status(500).send('Failed to get clips')
}
}
/**
* GET: /api/me/items/:id/clips
* Get all clips for a specific library item
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async getItemClips(req, res) {
if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404)
try {
const episodeId = req.query.episodeId || null
const clips = await Database.audioClipModel.getClipsForItem(req.user.id, req.params.id, episodeId)
res.json({ clips: clips.map((c) => c.toJSON()) })
} catch (error) {
Logger.error(`[MeController] Failed to get clips for item:`, error)
res.status(500).send('Failed to get clips')
}
}
/**
* POST: /api/me/items/:id/clips
* Create a new clip for a library item
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async createClip(req, res) {
if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404)
const { startTime, endTime, title, note, episodeId } = req.body
// Validate required fields
if (isNullOrNaN(startTime)) {
Logger.error(`[MeController] createClip invalid startTime`, startTime)
return res.status(400).send('Invalid start time')
}
if (isNullOrNaN(endTime)) {
Logger.error(`[MeController] createClip invalid endTime`, endTime)
return res.status(400).send('Invalid end time')
}
if (!title || typeof title !== 'string') {
Logger.error(`[MeController] createClip invalid title`, title)
return res.status(400).send('Invalid title')
}
try {
const clip = await Database.audioClipModel.createClip(req.user.id, req.params.id, startTime, endTime, title, note, episodeId)
SocketAuthority.clientEmitter(req.user.id, 'clip_created', clip.toJSON())
res.json(clip.toJSON())
} catch (error) {
Logger.error(`[MeController] Failed to create clip:`, error)
res.status(400).send(error.message || 'Failed to create clip')
}
}
/**
* PATCH: /api/me/clips/:clipId
* Update an existing clip
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async updateClip(req, res) {
const clipId = req.params.clipId
// Check if clip exists and belongs to user
const existingClip = await Database.audioClipModel.findByPk(clipId)
if (!existingClip) {
Logger.error(`[MeController] updateClip not found for clip id "${clipId}"`)
return res.sendStatus(404)
}
if (existingClip.userId !== req.user.id) {
Logger.error(`[MeController] updateClip forbidden - clip does not belong to user`)
return res.sendStatus(403)
}
const { startTime, endTime, title, note } = req.body
const updates = {}
if (startTime !== undefined) {
if (isNullOrNaN(startTime)) {
Logger.error(`[MeController] updateClip invalid startTime`, startTime)
return res.status(400).send('Invalid start time')
}
updates.startTime = startTime
}
if (endTime !== undefined) {
if (isNullOrNaN(endTime)) {
Logger.error(`[MeController] updateClip invalid endTime`, endTime)
return res.status(400).send('Invalid end time')
}
updates.endTime = endTime
}
if (title !== undefined) {
if (typeof title !== 'string') {
Logger.error(`[MeController] updateClip invalid title`, title)
return res.status(400).send('Invalid title')
}
updates.title = title
}
if (note !== undefined) {
updates.note = note
}
try {
const clip = await Database.audioClipModel.updateClip(clipId, updates)
SocketAuthority.clientEmitter(req.user.id, 'clip_updated', clip.toJSON())
res.json(clip.toJSON())
} catch (error) {
Logger.error(`[MeController] Failed to update clip:`, error)
res.status(400).send(error.message || 'Failed to update clip')
}
}
/**
* DELETE: /api/me/clips/:clipId
* Delete a clip
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async deleteClip(req, res) {
const clipId = req.params.clipId
// Check if clip exists and belongs to user
const existingClip = await Database.audioClipModel.findByPk(clipId)
if (!existingClip) {
Logger.error(`[MeController] deleteClip not found for clip id "${clipId}"`)
return res.sendStatus(404)
}
if (existingClip.userId !== req.user.id) {
Logger.error(`[MeController] deleteClip forbidden - clip does not belong to user`)
return res.sendStatus(403)
}
try {
await Database.audioClipModel.deleteClip(clipId)
SocketAuthority.clientEmitter(req.user.id, 'clip_removed', { id: clipId })
res.sendStatus(200)
} catch (error) {
Logger.error(`[MeController] Failed to delete clip:`, error)
res.status(500).send('Failed to delete clip')
}
}
}
module.exports = new MeController()

View file

@ -182,6 +182,11 @@ class ApiRouter {
this.router.post('/me/item/:id/bookmark', MeController.createBookmark.bind(this))
this.router.patch('/me/item/:id/bookmark', MeController.updateBookmark.bind(this))
this.router.delete('/me/item/:id/bookmark/:time', MeController.removeBookmark.bind(this))
this.router.get('/me/clips', MeController.getClips.bind(this))
this.router.get('/me/item/:id/clips', MeController.getItemClips.bind(this))
this.router.post('/me/item/:id/clip', MeController.createClip.bind(this))
this.router.patch('/me/clip/:clipId', MeController.updateClip.bind(this))
this.router.delete('/me/clip/:clipId', MeController.deleteClip.bind(this))
this.router.patch('/me/password', this.auth.authRateLimiter, MeController.updatePassword.bind(this))
this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this))
this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this))