mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-09 10:51:37 +00:00
added comments to books
This commit is contained in:
parent
acc3d253c5
commit
396ecfff4a
11 changed files with 583 additions and 28 deletions
|
|
@ -139,6 +139,51 @@
|
||||||
<tables-library-files-table v-if="libraryFiles.length" :library-item="libraryItem" class="mt-6" />
|
<tables-library-files-table v-if="libraryFiles.length" :library-item="libraryItem" class="mt-6" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Comments section -->
|
||||||
|
<div class="max-w-6xl mx-auto">
|
||||||
|
<div class="flex flex-col lg:flex-row">
|
||||||
|
<div class="w-full lg:w-52" style="min-width: 208px">
|
||||||
|
<!-- Spacer div to match the layout above -->
|
||||||
|
</div>
|
||||||
|
<div class="grow px-2 md:px-10">
|
||||||
|
<div class="mt-12">
|
||||||
|
<h3 class="text-xl font-semibold mb-4">{{ $strings.LabelComments }}</h3>
|
||||||
|
|
||||||
|
<!-- Add comment form -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="bg-bg rounded-lg border border-gray-600 p-4">
|
||||||
|
<textarea v-model="newCommentText" placeholder="Add a comment... (Ctrl+Enter to post)" class="w-full bg-bg text-white resize-y min-h-[80px] focus:outline-none" @keydown.ctrl.enter="logNewComment"></textarea>
|
||||||
|
<div class="flex justify-end mt-2">
|
||||||
|
<button class="px-4 py-2 bg-success text-white rounded-md hover:bg-success/80" @click="logNewComment">
|
||||||
|
{{ $strings.ButtonPost }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Comments list -->
|
||||||
|
<div v-if="libraryItem.comments && libraryItem.comments.length" class="space-y-4">
|
||||||
|
<div v-for="comment in libraryItem.comments" :key="comment.id" class="bg-bg rounded-lg p-4 border border-gray-600">
|
||||||
|
<div class="flex justify-between items-start mb-2">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">{{ comment.user.username }}</span>
|
||||||
|
<span class="text-gray-400 text-sm ml-2">{{ formatDate(comment.createdAt) }}</span>
|
||||||
|
</div>
|
||||||
|
<button v-if="canDeleteComment(comment)" @click="deleteComment(comment.id)" class="text-red-500 hover:text-red-400">
|
||||||
|
{{ $strings.ButtonDelete }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-200 whitespace-pre-wrap">{{ comment.text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-gray-400 text-center py-4">
|
||||||
|
{{ $strings.MessageNoComments }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" :download-queue="episodeDownloadsQueued" :episodes-downloading="episodesDownloading" />
|
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" :download-queue="episodeDownloadsQueued" :episodes-downloading="episodesDownloading" />
|
||||||
|
|
@ -148,6 +193,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
// Remove unused comments component registration
|
||||||
|
},
|
||||||
async asyncData({ store, params, app, redirect, route }) {
|
async asyncData({ store, params, app, redirect, route }) {
|
||||||
if (!store.state.user.user) {
|
if (!store.state.user.user) {
|
||||||
return redirect(`/login?redirect=${route.path}`)
|
return redirect(`/login?redirect=${route.path}`)
|
||||||
|
|
@ -182,7 +230,11 @@ export default {
|
||||||
episodeDownloadsQueued: [],
|
episodeDownloadsQueued: [],
|
||||||
showBookmarksModal: false,
|
showBookmarksModal: false,
|
||||||
isDescriptionClamped: false,
|
isDescriptionClamped: false,
|
||||||
showFullDescription: false
|
showFullDescription: false,
|
||||||
|
newCommentText: '',
|
||||||
|
editingComment: null,
|
||||||
|
isAddingComment: false,
|
||||||
|
isUpdatingComment: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
|
@ -431,6 +483,9 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
return items
|
return items
|
||||||
|
},
|
||||||
|
currentUser() {
|
||||||
|
return this.$store.state.user.user
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -777,6 +832,65 @@ export default {
|
||||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setShareModal', this.mediaItemShare)
|
this.$store.commit('globals/setShareModal', this.mediaItemShare)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async logNewComment() {
|
||||||
|
if (!this.newCommentText.trim()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Save the comment first
|
||||||
|
const savedComment = await this.$axios.$post(`/api/items/${this.libraryItemId}/comments`, {
|
||||||
|
text: this.newCommentText
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize comments array if it doesn't exist
|
||||||
|
if (!this.libraryItem.comments) {
|
||||||
|
this.libraryItem.comments = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the saved comment to the beginning of the array
|
||||||
|
this.libraryItem.comments.unshift(savedComment)
|
||||||
|
|
||||||
|
// Clear the input after successful save
|
||||||
|
this.newCommentText = ''
|
||||||
|
this.$toast.success(this.$strings.MessageCommentAdded)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving comment:', error)
|
||||||
|
this.$toast.error(this.$strings.ErrorAddingComment)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
formatDate(dateString) {
|
||||||
|
return new Date(dateString).toLocaleDateString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
canDeleteComment(comment) {
|
||||||
|
return this.currentUser.id === comment.userId || this.currentUser.isAdmin
|
||||||
|
},
|
||||||
|
async deleteComment(commentId) {
|
||||||
|
if (!confirm(this.$strings.ConfirmDeleteComment)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Remove comment from the array
|
||||||
|
const index = this.libraryItem.comments.findIndex((c) => c.id === commentId)
|
||||||
|
if (index !== -1) {
|
||||||
|
this.libraryItem.comments.splice(index, 1)
|
||||||
|
|
||||||
|
// Delete the comment
|
||||||
|
await this.$axios.$delete(`/api/items/${this.libraryItemId}/comments/${commentId}`)
|
||||||
|
this.$toast.success(this.$strings.MessageCommentDeleted)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting comment:', error)
|
||||||
|
this.$toast.error(this.$strings.ErrorDeletingComment)
|
||||||
|
// Restore the comment if the delete failed
|
||||||
|
if (this.editingComment) {
|
||||||
|
this.libraryItem.comments.splice(index, 0, this.editingComment)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|
|
||||||
|
|
@ -1103,5 +1103,17 @@
|
||||||
"ToastUserPasswordChangeSuccess": "Password changed successfully",
|
"ToastUserPasswordChangeSuccess": "Password changed successfully",
|
||||||
"ToastUserPasswordMismatch": "Passwords do not match",
|
"ToastUserPasswordMismatch": "Passwords do not match",
|
||||||
"ToastUserPasswordMustChange": "New password cannot match old password",
|
"ToastUserPasswordMustChange": "New password cannot match old password",
|
||||||
"ToastUserRootRequireName": "Must enter a root username"
|
"ToastUserRootRequireName": "Must enter a root username",
|
||||||
|
"LabelComments": "Comments",
|
||||||
|
"PlaceholderAddComment": "Add a comment... (Ctrl+Enter to post)",
|
||||||
|
"ButtonPost": "Post",
|
||||||
|
"MessageNoComments": "No comments yet. Be the first to comment!",
|
||||||
|
"MessageCommentAdded": "Comment added successfully",
|
||||||
|
"MessageCommentUpdated": "Comment updated successfully",
|
||||||
|
"MessageCommentDeleted": "Comment deleted successfully",
|
||||||
|
"ErrorLoadingComments": "Error loading comments",
|
||||||
|
"ErrorAddingComment": "Error adding comment",
|
||||||
|
"ErrorUpdatingComment": "Error updating comment",
|
||||||
|
"ErrorDeletingComment": "Error deleting comment",
|
||||||
|
"ConfirmDeleteComment": "Are you sure you want to delete this comment?"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
client/strings/en.json
Normal file
18
client/strings/en.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"LabelComments": "Comments",
|
||||||
|
"PlaceholderAddComment": "Add a comment... (Ctrl+Enter to post)",
|
||||||
|
"ButtonPost": "Post",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
|
"ButtonDelete": "Delete",
|
||||||
|
"ButtonCancel": "Cancel",
|
||||||
|
"ButtonSave": "Save",
|
||||||
|
"MessageNoComments": "No comments yet. Be the first to comment!",
|
||||||
|
"MessageCommentAdded": "Comment added successfully",
|
||||||
|
"MessageCommentUpdated": "Comment updated successfully",
|
||||||
|
"MessageCommentDeleted": "Comment deleted successfully",
|
||||||
|
"ErrorLoadingComments": "Error loading comments",
|
||||||
|
"ErrorAddingComment": "Error adding comment",
|
||||||
|
"ErrorUpdatingComment": "Error updating comment",
|
||||||
|
"ErrorDeletingComment": "Error deleting comment",
|
||||||
|
"ConfirmDeleteComment": "Are you sure you want to delete this comment?"
|
||||||
|
}
|
||||||
Binary file not shown.
|
|
@ -152,6 +152,11 @@ class Database {
|
||||||
return this.models.device
|
return this.models.device
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @type {typeof import('./models/Comment')} */
|
||||||
|
get commentModel() {
|
||||||
|
return this.models.comment
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if db file exists
|
* Check if db file exists
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
|
@ -333,6 +338,14 @@ class Database {
|
||||||
require('./models/Setting').init(this.sequelize)
|
require('./models/Setting').init(this.sequelize)
|
||||||
require('./models/CustomMetadataProvider').init(this.sequelize)
|
require('./models/CustomMetadataProvider').init(this.sequelize)
|
||||||
require('./models/MediaItemShare').init(this.sequelize)
|
require('./models/MediaItemShare').init(this.sequelize)
|
||||||
|
require('./models/Comment').init(this.sequelize)
|
||||||
|
|
||||||
|
// Set up associations after all models are initialized
|
||||||
|
Object.values(this.sequelize.models).forEach(model => {
|
||||||
|
if (typeof model.associate === 'function') {
|
||||||
|
model.associate(this.sequelize.models)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return this.sequelize.sync({ force, alter: false })
|
return this.sequelize.sync({ force, alter: false })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,18 @@ class LibraryItemController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Include comments
|
||||||
|
const comments = await Database.commentModel.findAll({
|
||||||
|
where: { libraryItemId: req.params.id },
|
||||||
|
include: [{
|
||||||
|
model: Database.userModel,
|
||||||
|
as: 'user',
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
}],
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
})
|
||||||
|
item.comments = comments
|
||||||
|
|
||||||
return res.json(item)
|
return res.json(item)
|
||||||
}
|
}
|
||||||
res.json(req.libraryItem.toOldJSON())
|
res.json(req.libraryItem.toOldJSON())
|
||||||
|
|
@ -1193,5 +1205,121 @@ class LibraryItemController {
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/items/:id/comments
|
||||||
|
* Get comments for a library item
|
||||||
|
*
|
||||||
|
* @param {LibraryItemControllerRequest} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
|
async getComments(req, res) {
|
||||||
|
try {
|
||||||
|
const comments = await Database.commentModel.findAll({
|
||||||
|
where: { libraryItemId: req.params.id },
|
||||||
|
include: [{
|
||||||
|
model: Database.userModel,
|
||||||
|
as: 'user',
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
}],
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
})
|
||||||
|
res.json(comments)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[LibraryItemController] getComments error:', error)
|
||||||
|
res.status(500).json({ error: 'Failed to get comments' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: /api/items/:id/comments
|
||||||
|
* Add a comment to a library item
|
||||||
|
*
|
||||||
|
* @param {LibraryItemControllerRequest} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
|
async addComment(req, res) {
|
||||||
|
try {
|
||||||
|
const comment = await Database.commentModel.create({
|
||||||
|
text: req.body.text,
|
||||||
|
libraryItemId: req.params.id,
|
||||||
|
userId: req.user.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const commentWithUser = await Database.commentModel.findByPk(comment.id, {
|
||||||
|
include: [{
|
||||||
|
model: Database.userModel,
|
||||||
|
as: 'user',
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json(commentWithUser)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[LibraryItemController] addComment error:', error)
|
||||||
|
res.status(500).json({ error: 'Failed to add comment' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PATCH: /api/items/:id/comments/:commentId
|
||||||
|
* Update a comment
|
||||||
|
*
|
||||||
|
* @param {LibraryItemControllerRequest} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
|
async updateComment(req, res) {
|
||||||
|
try {
|
||||||
|
const comment = await Database.commentModel.findByPk(req.params.commentId, {
|
||||||
|
include: [{
|
||||||
|
model: Database.userModel,
|
||||||
|
as: 'user',
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!comment) {
|
||||||
|
return res.status(404).json({ error: 'Comment not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow comment owner or admin to update
|
||||||
|
if (comment.userId !== req.user.id && !req.user.isAdmin) {
|
||||||
|
return res.status(403).json({ error: 'Not authorized to update this comment' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await comment.update({ text: req.body.text })
|
||||||
|
res.json(comment)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[LibraryItemController] updateComment error:', error)
|
||||||
|
res.status(500).json({ error: 'Failed to update comment' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE: /api/items/:id/comments/:commentId
|
||||||
|
* Delete a comment
|
||||||
|
*
|
||||||
|
* @param {LibraryItemControllerRequest} req
|
||||||
|
* @param {Response} res
|
||||||
|
*/
|
||||||
|
async deleteComment(req, res) {
|
||||||
|
try {
|
||||||
|
const comment = await Database.commentModel.findByPk(req.params.commentId)
|
||||||
|
if (!comment) {
|
||||||
|
return res.status(404).json({ error: 'Comment not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow comment owner or admin to delete
|
||||||
|
if (comment.userId !== req.user.id && !req.user.isAdmin) {
|
||||||
|
return res.status(403).json({ error: 'Not authorized to delete this comment' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await comment.destroy()
|
||||||
|
res.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error('[LibraryItemController] deleteComment error:', error)
|
||||||
|
res.status(500).json({ error: 'Failed to delete comment' })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new LibraryItemController()
|
module.exports = new LibraryItemController()
|
||||||
|
|
|
||||||
61
server/migrations/v2.20.1-add-comments.js
Normal file
61
server/migrations/v2.20.1-add-comments.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
const { DataTypes } = require('sequelize')
|
||||||
|
|
||||||
|
async function up({ context: { queryInterface, logger } }) {
|
||||||
|
logger.info('[2.20.1 migration] Creating comments table')
|
||||||
|
|
||||||
|
await queryInterface.createTable('comments', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
libraryItemId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'libraryItems',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE'
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add indexes for faster lookups
|
||||||
|
await queryInterface.addIndex('comments', ['libraryItemId'])
|
||||||
|
await queryInterface.addIndex('comments', ['userId'])
|
||||||
|
|
||||||
|
logger.info('[2.20.1 migration] Comments table created successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function down({ context: { queryInterface, logger } }) {
|
||||||
|
logger.info('[2.20.1 migration] Dropping comments table')
|
||||||
|
await queryInterface.dropTable('comments')
|
||||||
|
logger.info('[2.20.1 migration] Comments table dropped successfully')
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { up, down }
|
||||||
83
server/models/Comment.js
Normal file
83
server/models/Comment.js
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
const { Model, DataTypes } = require('sequelize')
|
||||||
|
|
||||||
|
class Comment extends Model {
|
||||||
|
static init(sequelize) {
|
||||||
|
return super.init(
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
primaryKey: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
libraryItemId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'libraryItems',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE'
|
||||||
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
references: {
|
||||||
|
model: 'users',
|
||||||
|
key: 'id'
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
onUpdate: 'CASCADE'
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: DataTypes.NOW
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sequelize,
|
||||||
|
modelName: 'comment',
|
||||||
|
tableName: 'comments',
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static associate(models) {
|
||||||
|
Comment.belongsTo(models.user, {
|
||||||
|
foreignKey: 'userId',
|
||||||
|
as: 'user'
|
||||||
|
})
|
||||||
|
Comment.belongsTo(models.libraryItem, {
|
||||||
|
foreignKey: 'libraryItemId',
|
||||||
|
as: 'libraryItem'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
text: this.text,
|
||||||
|
userId: this.userId,
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
createdAt: this.createdAt,
|
||||||
|
updatedAt: this.updatedAt,
|
||||||
|
user: this.user ? {
|
||||||
|
id: this.user.id,
|
||||||
|
username: this.user.username
|
||||||
|
} : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Comment
|
||||||
|
|
@ -191,7 +191,20 @@ class LibraryItem extends Model {
|
||||||
static async getExpandedById(libraryItemId) {
|
static async getExpandedById(libraryItemId) {
|
||||||
if (!libraryItemId) return null
|
if (!libraryItemId) return null
|
||||||
|
|
||||||
const libraryItem = await this.findByPk(libraryItemId)
|
const libraryItem = await this.findByPk(libraryItemId, {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: this.sequelize.models.comment,
|
||||||
|
as: 'comments',
|
||||||
|
include: [{
|
||||||
|
model: this.sequelize.models.user,
|
||||||
|
as: 'user',
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
}],
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
Logger.error(`[LibraryItem] Library item not found with id "${libraryItemId}"`)
|
Logger.error(`[LibraryItem] Library item not found with id "${libraryItemId}"`)
|
||||||
return null
|
return null
|
||||||
|
|
@ -734,31 +747,6 @@ class LibraryItem extends Model {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const { library, libraryFolder, book, podcast } = sequelize.models
|
|
||||||
library.hasMany(LibraryItem)
|
|
||||||
LibraryItem.belongsTo(library)
|
|
||||||
|
|
||||||
libraryFolder.hasMany(LibraryItem)
|
|
||||||
LibraryItem.belongsTo(libraryFolder)
|
|
||||||
|
|
||||||
book.hasOne(LibraryItem, {
|
|
||||||
foreignKey: 'mediaId',
|
|
||||||
constraints: false,
|
|
||||||
scope: {
|
|
||||||
mediaType: 'book'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false })
|
|
||||||
|
|
||||||
podcast.hasOne(LibraryItem, {
|
|
||||||
foreignKey: 'mediaId',
|
|
||||||
constraints: false,
|
|
||||||
scope: {
|
|
||||||
mediaType: 'podcast'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false })
|
|
||||||
|
|
||||||
LibraryItem.addHook('afterFind', (findResult) => {
|
LibraryItem.addHook('afterFind', (findResult) => {
|
||||||
if (!findResult) return
|
if (!findResult) return
|
||||||
|
|
||||||
|
|
@ -786,6 +774,40 @@ class LibraryItem extends Model {
|
||||||
media.destroy()
|
media.destroy()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
static associate(models) {
|
||||||
|
const { library, libraryFolder, book, podcast, comment } = models
|
||||||
|
library.hasMany(LibraryItem)
|
||||||
|
LibraryItem.belongsTo(library)
|
||||||
|
|
||||||
|
libraryFolder.hasMany(LibraryItem)
|
||||||
|
LibraryItem.belongsTo(libraryFolder)
|
||||||
|
|
||||||
|
book.hasOne(LibraryItem, {
|
||||||
|
foreignKey: 'mediaId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaType: 'book'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
LibraryItem.belongsTo(book, { foreignKey: 'mediaId', constraints: false })
|
||||||
|
|
||||||
|
podcast.hasOne(LibraryItem, {
|
||||||
|
foreignKey: 'mediaId',
|
||||||
|
constraints: false,
|
||||||
|
scope: {
|
||||||
|
mediaType: 'podcast'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false })
|
||||||
|
|
||||||
|
LibraryItem.hasMany(comment, {
|
||||||
|
foreignKey: 'libraryItemId',
|
||||||
|
as: 'comments'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get isBook() {
|
get isBook() {
|
||||||
|
|
|
||||||
|
|
@ -126,6 +126,12 @@ class ApiRouter {
|
||||||
this.router.get('/items/:id/ebook/:fileid?', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this))
|
this.router.get('/items/:id/ebook/:fileid?', LibraryItemController.middleware.bind(this), LibraryItemController.getEBookFile.bind(this))
|
||||||
this.router.patch('/items/:id/ebook/:fileid/status', LibraryItemController.middleware.bind(this), LibraryItemController.updateEbookFileStatus.bind(this))
|
this.router.patch('/items/:id/ebook/:fileid/status', LibraryItemController.middleware.bind(this), LibraryItemController.updateEbookFileStatus.bind(this))
|
||||||
|
|
||||||
|
// Comment routes
|
||||||
|
this.router.get('/items/:id/comments', LibraryItemController.middleware.bind(this), LibraryItemController.getComments.bind(this))
|
||||||
|
this.router.post('/items/:id/comments', LibraryItemController.middleware.bind(this), LibraryItemController.addComment.bind(this))
|
||||||
|
this.router.patch('/items/:id/comments/:commentId', LibraryItemController.middleware.bind(this), LibraryItemController.updateComment.bind(this))
|
||||||
|
this.router.delete('/items/:id/comments/:commentId', LibraryItemController.middleware.bind(this), LibraryItemController.deleteComment.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// User Routes
|
// User Routes
|
||||||
//
|
//
|
||||||
|
|
|
||||||
98
server/routers/LibraryItemRouter.js
Normal file
98
server/routers/LibraryItemRouter.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
const express = require('express')
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
// Get library item by id
|
||||||
|
router.get('/:id', async (req, res) => {
|
||||||
|
const libraryItem = await req.ctx.models.libraryItem.getExpandedById(req.params.id)
|
||||||
|
if (!libraryItem) {
|
||||||
|
return res.status(404).send({ error: 'Library item not found' })
|
||||||
|
}
|
||||||
|
res.json(libraryItem.toOldJSONExpanded())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get comments for a library item
|
||||||
|
router.get('/:id/comments', async (req, res) => {
|
||||||
|
const libraryItem = await req.ctx.models.libraryItem.findByPk(req.params.id)
|
||||||
|
if (!libraryItem) {
|
||||||
|
return res.status(404).send({ error: 'Library item not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const comments = await req.ctx.models.comment.findAll({
|
||||||
|
where: { libraryItemId: req.params.id },
|
||||||
|
include: [{
|
||||||
|
model: req.ctx.models.user,
|
||||||
|
as: 'user',
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
}],
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json(comments)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add a comment to a library item
|
||||||
|
router.post('/:id/comments', async (req, res) => {
|
||||||
|
const libraryItem = await req.ctx.models.libraryItem.findByPk(req.params.id)
|
||||||
|
if (!libraryItem) {
|
||||||
|
return res.status(404).send({ error: 'Library item not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const comment = await req.ctx.models.comment.create({
|
||||||
|
text: req.body.text,
|
||||||
|
libraryItemId: req.params.id,
|
||||||
|
userId: req.user.id
|
||||||
|
})
|
||||||
|
|
||||||
|
const commentWithUser = await req.ctx.models.comment.findByPk(comment.id, {
|
||||||
|
include: [{
|
||||||
|
model: req.ctx.models.user,
|
||||||
|
as: 'user',
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json(commentWithUser)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update a comment
|
||||||
|
router.patch('/:itemId/comments/:commentId', async (req, res) => {
|
||||||
|
const comment = await req.ctx.models.comment.findByPk(req.params.commentId)
|
||||||
|
if (!comment) {
|
||||||
|
return res.status(404).send({ error: 'Comment not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.userId !== req.user.id && !req.user.isAdmin) {
|
||||||
|
return res.status(403).send({ error: 'Not authorized to update this comment' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await comment.update({
|
||||||
|
text: req.body.text
|
||||||
|
})
|
||||||
|
|
||||||
|
const updatedComment = await req.ctx.models.comment.findByPk(comment.id, {
|
||||||
|
include: [{
|
||||||
|
model: req.ctx.models.user,
|
||||||
|
as: 'user',
|
||||||
|
attributes: ['id', 'username']
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json(updatedComment)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete a comment
|
||||||
|
router.delete('/:itemId/comments/:commentId', async (req, res) => {
|
||||||
|
const comment = await req.ctx.models.comment.findByPk(req.params.commentId)
|
||||||
|
if (!comment) {
|
||||||
|
return res.status(404).send({ error: 'Comment not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comment.userId !== req.user.id && !req.user.isAdmin) {
|
||||||
|
return res.status(403).send({ error: 'Not authorized to delete this comment' })
|
||||||
|
}
|
||||||
|
|
||||||
|
await comment.destroy()
|
||||||
|
res.json({ success: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
Loading…
Add table
Add a link
Reference in a new issue