@@ -136,7 +136,7 @@
-
+
diff --git a/client/pages/library/_library/podcast/latest.vue b/client/pages/library/_library/podcast/latest.vue
index d056581651..e69e055fce 100644
--- a/client/pages/library/_library/podcast/latest.vue
+++ b/client/pages/library/_library/podcast/latest.vue
@@ -155,7 +155,9 @@ export default {
if (this.episodeIdStreaming === episode.id) return this.streamIsPlaying ? 'Streaming' : 'Play'
if (!episode.progress) return this.$elapsedPretty(episode.duration)
if (episode.progress.isFinished) return 'Finished'
- var remaining = Math.floor(episode.progress.duration - episode.progress.currentTime)
+
+ const duration = episode.progress.duration || episode.duration
+ const remaining = Math.floor(duration - episode.progress.currentTime)
return `${this.$elapsedPretty(remaining)} left`
},
playClick(episodeToPlay) {
diff --git a/client/store/libraries.js b/client/store/libraries.js
index fd8af4aeaf..8771ebcf5a 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -80,13 +80,11 @@ export const actions = {
return state.folders
}
}
- console.log('Loading folders')
commit('setFoldersLastUpdate')
return this.$axios
.$get('/api/filesystem')
.then((res) => {
- console.log('Settings folders', res)
commit('setFolders', res.directories)
return res.directories
})
@@ -119,15 +117,16 @@ export const actions = {
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
+ if (libraryChanging) {
+ commit('setCollections', [])
+ commit('setUserPlaylists', [])
+ }
+
commit('addUpdate', library)
commit('setLibraryIssues', issues)
commit('setLibraryFilterData', filterData)
commit('setNumUserPlaylists', numUserPlaylists)
commit('setCurrentLibrary', libraryId)
- if (libraryChanging) {
- commit('setCollections', [])
- commit('setUserPlaylists', [])
- }
return data
})
.catch((error) => {
diff --git a/package-lock.json b/package-lock.json
index 8bd0b115cb..a54cc61717 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
- "version": "2.7.0",
+ "version": "2.7.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
- "version": "2.7.0",
+ "version": "2.7.1",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",
diff --git a/package.json b/package.json
index 33f483b01a..dca9710bc9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
- "version": "2.7.0",
+ "version": "2.7.1",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
diff --git a/server/Server.js b/server/Server.js
index 5e8cab7606..3aed50e0f1 100644
--- a/server/Server.js
+++ b/server/Server.js
@@ -136,15 +136,16 @@ class Server {
/**
* @temporary
- * This is necessary for the ebook API endpoint in the mobile apps
+ * This is necessary for the ebook & cover API endpoint in the mobile apps
* The mobile app ereader is using fetch api in Capacitor that is currently difficult to switch to native requests
* so we have to allow cors for specific origins to the /api/items/:id/ebook endpoint
+ * The cover image is fetched with XMLHttpRequest in the mobile apps to load into a canvas and extract colors
* @see https://ionicframework.com/docs/troubleshooting/cors
*
* Running in development allows cors to allow testing the mobile apps in the browser
*/
app.use((req, res, next) => {
- if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/ebook(\/[0-9]+)?/)) {
+ if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
if (Logger.isDev || allowedOrigins.some(o => o === req.get('origin'))) {
res.header('Access-Control-Allow-Origin', req.get('origin'))
@@ -276,6 +277,19 @@ class Server {
})
app.get('/healthcheck', (req, res) => res.sendStatus(200))
+ let sigintAlreadyReceived = false
+ process.on('SIGINT', async () => {
+ if (!sigintAlreadyReceived) {
+ sigintAlreadyReceived = true
+ Logger.info('SIGINT (Ctrl+C) received. Shutting down...')
+ await this.stop()
+ Logger.info('Server stopped. Exiting.')
+ } else {
+ Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.')
+ }
+ process.exit(0)
+ })
+
this.server.listen(this.Port, this.Host, () => {
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
else Logger.info(`Listening on port :${this.Port}`)
@@ -382,12 +396,17 @@ class Server {
res.sendStatus(200)
}
+ /**
+ * Gracefully stop server
+ * Stops watcher and socket server
+ */
async stop() {
+ Logger.info('=== Stopping Server ===')
await this.watcher.close()
Logger.info('Watcher Closed')
return new Promise((resolve) => {
- this.server.close((err) => {
+ SocketAuthority.close((err) => {
if (err) {
Logger.error('Failed to close server', err)
} else {
diff --git a/server/SocketAuthority.js b/server/SocketAuthority.js
index da17f5df1a..00f0a63ecd 100644
--- a/server/SocketAuthority.js
+++ b/server/SocketAuthority.js
@@ -73,6 +73,20 @@ class SocketAuthority {
}
}
+ /**
+ * Closes the Socket.IO server and disconnect all clients
+ *
+ * @param {Function} callback
+ */
+ close(callback) {
+ Logger.info('[SocketAuthority] Shutting down')
+ // This will close all open socket connections, and also close the underlying http server
+ if (this.io)
+ this.io.close(callback)
+ else
+ callback()
+ }
+
initialize(Server) {
this.Server = Server
diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js
index b6f2f28582..67e9abfbfc 100644
--- a/server/models/LibraryItem.js
+++ b/server/models/LibraryItem.js
@@ -419,40 +419,45 @@ class LibraryItem extends Model {
*/
static async getOldById(libraryItemId) {
if (!libraryItemId) return null
- const libraryItem = await this.findByPk(libraryItemId, {
- include: [
- {
- model: this.sequelize.models.book,
- include: [
- {
- model: this.sequelize.models.author,
- through: {
- attributes: []
- }
- },
- {
- model: this.sequelize.models.series,
- through: {
- attributes: ['sequence']
- }
+
+ const libraryItem = await this.findByPk(libraryItemId)
+ if (!libraryItem) {
+ Logger.error(`[LibraryItem] Library item not found with id "${libraryItemId}"`)
+ return null
+ }
+
+ if (libraryItem.mediaType === 'podcast') {
+ libraryItem.media = await libraryItem.getMedia({
+ include: [
+ {
+ model: this.sequelize.models.podcastEpisode
+ }
+ ]
+ })
+ } else {
+ libraryItem.media = await libraryItem.getMedia({
+ include: [
+ {
+ model: this.sequelize.models.author,
+ through: {
+ attributes: []
}
- ]
- },
- {
- model: this.sequelize.models.podcast,
- include: [
- {
- model: this.sequelize.models.podcastEpisode
+ },
+ {
+ model: this.sequelize.models.series,
+ through: {
+ attributes: ['sequence']
}
- ]
- }
- ],
- order: [
- [this.sequelize.models.book, this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
- [this.sequelize.models.book, this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
- ]
- })
- if (!libraryItem) return null
+ }
+ ],
+ order: [
+ [this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
+ [this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
+ ]
+ })
+ }
+
+ if (!libraryItem.media) return null
return this.getOldLibraryItem(libraryItem)
}
diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js
index 55b2f9d403..2fdefb86b6 100644
--- a/server/models/PodcastEpisode.js
+++ b/server/models/PodcastEpisode.js
@@ -152,7 +152,12 @@ class PodcastEpisode extends Model {
extraData: DataTypes.JSON
}, {
sequelize,
- modelName: 'podcastEpisode'
+ modelName: 'podcastEpisode',
+ indexes: [
+ {
+ fields: ['createdAt']
+ }
+ ]
})
const { podcast } = sequelize.models
diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js
index 1452b7b59a..cc8b8d9bc3 100644
--- a/server/objects/entities/PodcastEpisode.js
+++ b/server/objects/entities/PodcastEpisode.js
@@ -48,12 +48,14 @@ class PodcastEpisode {
this.guid = episode.guid || null
this.pubDate = episode.pubDate
this.chapters = episode.chapters?.map(ch => ({ ...ch })) || []
- this.audioFile = new AudioFile(episode.audioFile)
+ this.audioFile = episode.audioFile ? new AudioFile(episode.audioFile) : null
this.publishedAt = episode.publishedAt
this.addedAt = episode.addedAt
this.updatedAt = episode.updatedAt
- this.audioFile.index = 1 // Only 1 audio file per episode
+ if (this.audioFile) {
+ this.audioFile.index = 1 // Only 1 audio file per episode
+ }
}
toJSON() {
@@ -73,7 +75,7 @@ class PodcastEpisode {
guid: this.guid,
pubDate: this.pubDate,
chapters: this.chapters.map(ch => ({ ...ch })),
- audioFile: this.audioFile.toJSON(),
+ audioFile: this.audioFile?.toJSON() || null,
publishedAt: this.publishedAt,
addedAt: this.addedAt,
updatedAt: this.updatedAt
@@ -97,8 +99,8 @@ class PodcastEpisode {
guid: this.guid,
pubDate: this.pubDate,
chapters: this.chapters.map(ch => ({ ...ch })),
- audioFile: this.audioFile.toJSON(),
- audioTrack: this.audioTrack.toJSON(),
+ audioFile: this.audioFile?.toJSON() || null,
+ audioTrack: this.audioTrack?.toJSON() || null,
publishedAt: this.publishedAt,
addedAt: this.addedAt,
updatedAt: this.updatedAt,
@@ -108,6 +110,7 @@ class PodcastEpisode {
}
get audioTrack() {
+ if (!this.audioFile) return null
const audioTrack = new AudioTrack()
audioTrack.setData(this.libraryItemId, this.audioFile, 0)
return audioTrack
@@ -116,9 +119,9 @@ class PodcastEpisode {
return [this.audioTrack]
}
get duration() {
- return this.audioFile.duration
+ return this.audioFile?.duration || 0
}
- get size() { return this.audioFile.metadata.size }
+ get size() { return this.audioFile?.metadata.size || 0 }
get enclosureUrl() {
return this.enclosure?.url || null
}
diff --git a/server/scanner/AudioFileScanner.js b/server/scanner/AudioFileScanner.js
index ddd994d031..89951025db 100644
--- a/server/scanner/AudioFileScanner.js
+++ b/server/scanner/AudioFileScanner.js
@@ -468,7 +468,7 @@ class AudioFileScanner {
audioFiles.length === 1 ||
audioFiles.length > 1 &&
audioFiles[0].chapters.length === audioFiles[1].chapters?.length &&
- audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title)
+ audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title && c.start === audioFiles[1].chapters[i].start)
) {
libraryScan.addLog(LogLevel.DEBUG, `setChapters: Using embedded chapters in first audio file ${audioFiles[0].metadata?.path}`)
chapters = audioFiles[0].chapters.map((c) => ({ ...c }))
diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js
index ebad97dbc9..7ef5320dd7 100644
--- a/server/utils/fileUtils.js
+++ b/server/utils/fileUtils.js
@@ -81,7 +81,12 @@ module.exports.getFileSize = async (path) => {
* @returns {Promise
} epoch timestamp
*/
module.exports.getFileMTimeMs = async (path) => {
- return (await getFileStat(path))?.mtimeMs || 0
+ try {
+ return (await getFileStat(path))?.mtimeMs || 0
+ } catch (err) {
+ Logger.error(`[fileUtils] Failed to getFileMtimeMs`, err)
+ return 0
+ }
}
/**
diff --git a/server/utils/podcastUtils.js b/server/utils/podcastUtils.js
index 819ec91411..4e01c92b13 100644
--- a/server/utils/podcastUtils.js
+++ b/server/utils/podcastUtils.js
@@ -233,7 +233,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
method: 'GET',
timeout: 12000,
responseType: 'arraybuffer',
- headers: { Accept: 'application/rss+xml' },
+ headers: { Accept: 'application/rss+xml, application/xhtml+xml, application/xml' },
httpAgent: ssrfFilter(feedUrl),
httpsAgent: ssrfFilter(feedUrl)
}).then(async (data) => {