Compare commits

...

12 Commits

Author SHA1 Message Date
advplyr
e56b8edc0a Version bump 2.4.3 2023-09-17 16:13:19 -05:00
advplyr
8ab0a0a14d Update personalized shelves logs to dev logs 2023-09-17 16:09:21 -05:00
advplyr
4e01722ba6 Merge pull request #2103 from selfhost-alt/faster-scan-for-empty-series
Scan for empty book series more efficiently
2023-09-17 15:54:33 -05:00
advplyr
87eaacea22 Fix empty podcast and empty book queries when cleaning db on init 2023-09-17 15:53:25 -05:00
advplyr
3ad4f05449 Merge branch 'master' into faster-scan-for-empty-series 2023-09-17 15:47:06 -05:00
advplyr
817be40959 Merge pull request #2101 from selfhost-alt/fix-parse-full-name-typo
Fix typo in fixParsedNameCase
2023-09-17 15:43:25 -05:00
advplyr
d18592eaeb Fix:Duplicate series and authors being added on matches and scans #2106 2023-09-17 15:29:39 -05:00
advplyr
0aae672e19 Fix:Scanner purge cover cache when extracting from audio file 2023-09-17 14:53:25 -05:00
advplyr
cfd9a01da7 Fix:Server crash when removing item from playlist #2115 2023-09-17 12:40:13 -05:00
Selfhost Alt
19cf3bfb9f Fix query to actually return empty series 2023-09-15 13:32:21 -07:00
Selfhost Alt
8b39b01269 Scan for empty book series more efficiently 2023-09-14 22:35:33 -07:00
Selfhost Alt
f7849d2956 Fix typo in fixParsedNameCase 2023-09-14 22:12:22 -07:00
14 changed files with 57 additions and 74 deletions

View File

@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
"version": "2.4.2",
"version": "2.4.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
"version": "2.4.2",
"version": "2.4.3",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",

View File

@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.4.2",
"version": "2.4.3",
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
"scripts": {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "audiobookshelf",
"version": "2.4.2",
"version": "2.4.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf",
"version": "2.4.2",
"version": "2.4.3",
"license": "GPL-3.0",
"dependencies": {
"axios": "^0.27.2",

View File

@@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "2.4.2",
"version": "2.4.3",
"description": "Self-hosted audiobook and podcast server",
"main": "index.js",
"scripts": {

View File

@@ -666,7 +666,11 @@ class Database {
async cleanDatabase() {
// Remove invalid Podcast records
const podcastsWithNoLibraryItem = await this.podcastModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM libraryItems li WHERE li.mediaId = podcast.id)`), 0)
include: {
model: this.libraryItemModel,
required: false
},
where: { '$libraryItem.id$': null }
})
for (const podcast of podcastsWithNoLibraryItem) {
Logger.warn(`Found podcast "${podcast.title}" with no libraryItem - removing it`)
@@ -675,7 +679,11 @@ class Database {
// Remove invalid Book records
const booksWithNoLibraryItem = await this.bookModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM libraryItems li WHERE li.mediaId = book.id)`), 0)
include: {
model: this.libraryItemModel,
required: false
},
where: { '$libraryItem.id$': null }
})
for (const book of booksWithNoLibraryItem) {
Logger.warn(`Found book "${book.title}" with no libraryItem - removing it`)
@@ -684,7 +692,11 @@ class Database {
// Remove empty series
const emptySeries = await this.seriesModel.findAll({
where: Sequelize.where(Sequelize.literal(`(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)`), 0)
include: {
model: this.bookSeriesModel,
required: false
},
where: { '$bookSeries.id$': null }
})
for (const series of emptySeries) {
Logger.warn(`Found series "${series.name}" with no books - removing it`)

View File

@@ -206,7 +206,7 @@ class PlaylistController {
await Database.createPlaylistMediaItem(playlistMediaItem)
const jsonExpanded = await req.playlist.getOldJsonExpanded()
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
res.json(jsonExpanded)
}
@@ -376,9 +376,9 @@ class PlaylistController {
if (!numMediaItems) {
Logger.info(`[PlaylistController] Playlist "${req.playlist.name}" has no more items - removing it`)
await req.playlist.destroy()
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', jsonExpanded)
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_removed', jsonExpanded)
} else {
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', jsonExpanded)
SocketAuthority.clientEmitter(jsonExpanded.userId, 'playlist_updated', jsonExpanded)
}
}
res.json(jsonExpanded)

View File

@@ -229,38 +229,6 @@ class CoverManager {
}
}
async saveEmbeddedCoverArt(libraryItem) {
let audioFileWithCover = null
if (libraryItem.mediaType === 'book') {
audioFileWithCover = libraryItem.media.audioFiles.find(af => af.embeddedCoverArt)
} else if (libraryItem.mediaType == 'podcast') {
const episodeWithCover = libraryItem.media.episodes.find(ep => ep.audioFile.embeddedCoverArt)
if (episodeWithCover) audioFileWithCover = episodeWithCover.audioFile
} else if (libraryItem.mediaType === 'music') {
audioFileWithCover = libraryItem.media.audioFile
}
if (!audioFileWithCover) return false
const coverDirPath = this.getCoverDirectory(libraryItem)
await fs.ensureDir(coverDirPath)
const coverFilename = audioFileWithCover.embeddedCoverArt === 'png' ? 'cover.png' : 'cover.jpg'
const coverFilePath = Path.join(coverDirPath, coverFilename)
const coverAlreadyExists = await fs.pathExists(coverFilePath)
if (coverAlreadyExists) {
Logger.warn(`[CoverManager] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`)
return false
}
const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
if (success) {
libraryItem.updateMediaCover(coverFilePath)
return coverFilePath
}
return false
}
/**
* Extract cover art from audio file and save for library item
* @param {import('../models/Book').AudioFileObject[]} audioFiles
@@ -268,7 +236,7 @@ class CoverManager {
* @param {string} [libraryItemPath] null for isFile library items
* @returns {Promise<string>} returns cover path
*/
async saveEmbeddedCoverArtNew(audioFiles, libraryItemId, libraryItemPath) {
async saveEmbeddedCoverArt(audioFiles, libraryItemId, libraryItemPath) {
let audioFileWithCover = audioFiles.find(af => af.embeddedCoverArt)
if (!audioFileWithCover) return null
@@ -291,6 +259,7 @@ class CoverManager {
const success = await extractCoverArt(audioFileWithCover.metadata.path, coverFilePath)
if (success) {
await CacheManager.purgeCoverCache(libraryItemId)
return coverFilePath
}
return null

View File

@@ -1,4 +1,4 @@
const { DataTypes, Model, literal } = require('sequelize')
const { DataTypes, Model, where, fn, col } = require('sequelize')
const oldAuthor = require('../objects/entities/Author')
@@ -114,14 +114,11 @@ class Author extends Model {
static async getOldByNameAndLibrary(authorName, libraryId) {
const author = (await this.findOne({
where: [
literal(`name = ':authorName' COLLATE NOCASE`),
where(fn('lower', col('name')), authorName.toLowerCase()),
{
libraryId
}
],
replacements: {
authorName
}
]
}))?.getOldAuthor()
return author
}

View File

@@ -536,7 +536,7 @@ class LibraryItem extends Model {
})
}
}
Logger.debug(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`)
Logger.dev(`Loaded ${itemsInProgressPayload.items.length} of ${itemsInProgressPayload.count} items for "Continue Listening/Reading" in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`)
let start = Date.now()
if (library.isBook) {
@@ -553,7 +553,7 @@ class LibraryItem extends Model {
total: continueSeriesPayload.count
})
}
Logger.debug(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
Logger.dev(`Loaded ${continueSeriesPayload.libraryItems.length} of ${continueSeriesPayload.count} items for "Continue Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
} else if (library.isPodcast) {
// "Newest Episodes" shelf
const newestEpisodesPayload = await libraryFilters.getNewestPodcastEpisodes(library, user, limit)
@@ -567,7 +567,7 @@ class LibraryItem extends Model {
total: newestEpisodesPayload.count
})
}
Logger.debug(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
Logger.dev(`Loaded ${newestEpisodesPayload.libraryItems.length} of ${newestEpisodesPayload.count} episodes for "Newest Episodes" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
}
start = Date.now()
@@ -583,7 +583,7 @@ class LibraryItem extends Model {
total: mostRecentPayload.count
})
}
Logger.debug(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
Logger.dev(`Loaded ${mostRecentPayload.libraryItems.length} of ${mostRecentPayload.count} items for "Recently Added" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
if (library.isBook) {
start = Date.now()
@@ -599,7 +599,7 @@ class LibraryItem extends Model {
total: seriesMostRecentPayload.count
})
}
Logger.debug(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
Logger.dev(`Loaded ${seriesMostRecentPayload.series.length} of ${seriesMostRecentPayload.count} series for "Recent Series" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
start = Date.now()
// "Discover" shelf
@@ -614,7 +614,7 @@ class LibraryItem extends Model {
total: discoverLibraryItemsPayload.count
})
}
Logger.debug(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
Logger.dev(`Loaded ${discoverLibraryItemsPayload.libraryItems.length} of ${discoverLibraryItemsPayload.count} items for "Discover" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
}
start = Date.now()
@@ -645,7 +645,7 @@ class LibraryItem extends Model {
})
}
}
Logger.debug(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
Logger.dev(`Loaded ${mediaFinishedPayload.items.length} of ${mediaFinishedPayload.count} items for "Listen/Read Again" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
if (library.isBook) {
start = Date.now()
@@ -661,7 +661,7 @@ class LibraryItem extends Model {
total: newestAuthorsPayload.count
})
}
Logger.debug(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
Logger.dev(`Loaded ${newestAuthorsPayload.authors.length} of ${newestAuthorsPayload.count} authors for "Newest Authors" in ${((Date.now() - start) / 1000).toFixed(2)}s`)
}
Logger.debug(`Loaded ${shelves.length} personalized shelves in ${((Date.now() - fullStart) / 1000).toFixed(2)}s`)

View File

@@ -1,4 +1,4 @@
const { DataTypes, Model, literal } = require('sequelize')
const { DataTypes, Model, where, fn, col } = require('sequelize')
const oldSeries = require('../objects/entities/Series')
@@ -105,14 +105,11 @@ class Series extends Model {
static async getOldByNameAndLibrary(seriesName, libraryId) {
const series = (await this.findOne({
where: [
literal(`name = ':seriesName' COLLATE NOCASE`),
where(fn('lower', col('name')), seriesName.toLowerCase()),
{
libraryId
}
],
replacements: {
seriesName
}
]
}))?.getOldSeries()
return series
}

View File

@@ -553,13 +553,17 @@ class ApiRouter {
continue
}
if (mediaMetadata.authors[i].id?.startsWith('new')) {
mediaMetadata.authors[i].id = null
}
// Ensure the ID for the author exists
if (mediaMetadata.authors[i].id && !(await Database.checkAuthorExists(libraryId, mediaMetadata.authors[i].id))) {
Logger.warn(`[ApiRouter] Author id "${mediaMetadata.authors[i].id}" does not exist`)
mediaMetadata.authors[i].id = null
}
if (!mediaMetadata.authors[i].id || mediaMetadata.authors[i].id.startsWith('new')) {
if (!mediaMetadata.authors[i].id) {
let author = await Database.authorModel.getOldByNameAndLibrary(authorName, libraryId)
if (!author) {
author = new Author()
@@ -590,13 +594,17 @@ class ApiRouter {
continue
}
if (mediaMetadata.series[i].id?.startsWith('new')) {
mediaMetadata.series[i].id = null
}
// Ensure the ID for the series exists
if (mediaMetadata.series[i].id && !(await Database.checkSeriesExists(libraryId, mediaMetadata.series[i].id))) {
Logger.warn(`[ApiRouter] Series id "${mediaMetadata.series[i].id}" does not exist`)
mediaMetadata.series[i].id = null
}
if (!mediaMetadata.series[i].id || mediaMetadata.series[i].id.startsWith('new')) {
if (!mediaMetadata.series[i].id) {
let seriesItem = await Database.seriesModel.getOldByNameAndLibrary(seriesName, libraryId)
if (!seriesItem) {
seriesItem = new Series()

View File

@@ -313,7 +313,7 @@ class BookScanner {
// If no cover then extract cover from audio file if available OR search for cover if enabled in server settings
if (!media.coverPath) {
const libraryItemDir = existingLibraryItem.isFile ? null : existingLibraryItem.path
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(media.audioFiles, existingLibraryItem.id, libraryItemDir)
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(media.audioFiles, existingLibraryItem.id, libraryItemDir)
if (extractedCoverPath) {
libraryScan.addLog(LogLevel.DEBUG, `Updating book "${bookMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
media.coverPath = extractedCoverPath
@@ -461,7 +461,7 @@ class BookScanner {
if (!bookObject.coverPath) {
const libraryItemDir = libraryItemObj.isFile ? null : libraryItemObj.path
// Extract and save embedded cover art
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(scannedAudioFiles, libraryItemObj.id, libraryItemDir)
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(scannedAudioFiles, libraryItemObj.id, libraryItemDir)
if (extractedCoverPath) {
bookObject.coverPath = extractedCoverPath
} else if (Database.serverSettings.scannerFindCovers) {

View File

@@ -178,7 +178,7 @@ class PodcastScanner {
// If no cover then extract cover from audio file if available
if (!media.coverPath && existingPodcastEpisodes.length) {
const audioFiles = existingPodcastEpisodes.map(ep => ep.audioFile)
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArtNew(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
const extractedCoverPath = await CoverManager.saveEmbeddedCoverArt(audioFiles, existingLibraryItem.id, existingLibraryItem.path)
if (extractedCoverPath) {
libraryScan.addLog(LogLevel.DEBUG, `Updating podcast "${podcastMetadata.title}" extracted embedded cover art from audio file to path "${extractedCoverPath}"`)
media.coverPath = extractedCoverPath
@@ -279,7 +279,7 @@ class PodcastScanner {
// If cover was not found in folder then check embedded covers in audio files
if (!podcastObject.coverPath && scannedAudioFiles.length) {
// Extract and save embedded cover art
podcastObject.coverPath = await CoverManager.saveEmbeddedCoverArtNew(scannedAudioFiles, libraryItemObj.id, libraryItemObj.path)
podcastObject.coverPath = await CoverManager.saveEmbeddedCoverArt(scannedAudioFiles, libraryItemObj.id, libraryItemObj.path)
}
libraryItemObj.podcast = podcastObject

View File

@@ -70,8 +70,8 @@ module.exports = (nameToParse, partToReturn, fixCase, stopOnError, useLongLists)
namePartWords[j].slice(3).toLowerCase();
} else if (
namePartLabels[j] === 'suffix' &&
nameParts[j].slice(-1) !== '.' &&
!suffixList.indexOf(nameParts[j].toLowerCase())
namePartWords[j].slice(-1) !== '.' &&
!suffixList.indexOf(namePartWords[j].toLowerCase())
) { // Convert suffix abbreviations to UPPER CASE
if (namePartWords[j] === namePartWords[j].toLowerCase()) {
namePartWords[j] = namePartWords[j].toUpperCase();
@@ -343,4 +343,4 @@ module.exports = (nameToParse, partToReturn, fixCase, stopOnError, useLongLists)
parsedName = fixParsedNameCase(parsedName, fixCase);
return partToReturn === 'all' ? parsedName : parsedName[partToReturn];
};
};