Merge branch 'master' into binary-manager

This commit is contained in:
advplyr 2024-01-02 14:16:27 -06:00
commit aa63aa6cf3
18 changed files with 395 additions and 157 deletions

View file

@ -139,15 +139,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'))
@ -287,7 +288,7 @@ class Server {
await this.stop()
Logger.info('Server stopped. Exiting.')
} else {
Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.')
Logger.info('SIGINT (Ctrl+C) received again. Exiting immediately.')
}
process.exit(0)
})
@ -398,13 +399,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 {

View file

@ -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

View file

@ -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)
}

View file

@ -152,7 +152,12 @@ class PodcastEpisode extends Model {
extraData: DataTypes.JSON
}, {
sequelize,
modelName: 'podcastEpisode'
modelName: 'podcastEpisode',
indexes: [
{
fields: ['createdAt']
}
]
})
const { podcast } = sequelize.models

View file

@ -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
}

View file

@ -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 }))

View file

@ -81,7 +81,12 @@ module.exports.getFileSize = async (path) => {
* @returns {Promise<number>} 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
}
}
/**