Merge branch 'master' into openid_signing_algorithm

This commit is contained in:
advplyr 2024-04-21 15:38:33 -05:00
commit af856ce1ec
56 changed files with 2150 additions and 118 deletions

View file

@ -117,16 +117,20 @@ class LibraryItemController {
zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
}
//
// PATCH: will create new authors & series if in payload
//
/**
* PATCH: /items/:id/media
* Update media for a library item. Will create new authors & series when necessary
*
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
async updateMedia(req, res) {
const libraryItem = req.libraryItem
const mediaPayload = req.body
if (mediaPayload.url) {
await LibraryItemController.prototype.uploadCover.bind(this)(req, res, false)
if (res.writableEnded) return
if (res.writableEnded || res.headersSent) return
}
// Book specific

View file

@ -284,7 +284,7 @@ class MiscController {
}
res.json({
tags: tags
tags: tags.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()))
})
}
@ -329,6 +329,7 @@ class MiscController {
await libraryItem.media.update({
tags: libraryItem.media.tags
})
await libraryItem.saveMetadataFile()
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++
@ -370,6 +371,7 @@ class MiscController {
await libraryItem.media.update({
tags: libraryItem.media.tags
})
await libraryItem.saveMetadataFile()
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++
@ -462,6 +464,7 @@ class MiscController {
await libraryItem.media.update({
genres: libraryItem.media.genres
})
await libraryItem.saveMetadataFile()
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++
@ -503,6 +506,7 @@ class MiscController {
await libraryItem.media.update({
genres: libraryItem.media.genres
})
await libraryItem.saveMetadataFile()
const oldLibraryItem = Database.libraryItemModel.getOldLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', oldLibraryItem.toJSONExpanded())
numItemsUpdated++

View file

@ -1,8 +1,12 @@
const Path = require('path')
const { DataTypes, Model } = require('sequelize')
const fsExtra = require('../libs/fsExtra')
const Logger = require('../Logger')
const oldLibraryItem = require('../objects/LibraryItem')
const libraryFilters = require('../utils/queries/libraryFilters')
const { areEquivalent } = require('../utils/index')
const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
const LibraryFile = require('../objects/files/LibraryFile')
const Book = require('./Book')
const Podcast = require('./Podcast')
@ -828,6 +832,147 @@ class LibraryItem extends Model {
return this[mixinMethodName](options)
}
/**
*
* @returns {Promise<Book|Podcast>}
*/
getMediaExpanded() {
if (this.mediaType === 'podcast') {
return this.getMedia({
include: [
{
model: this.sequelize.models.podcastEpisode
}
]
})
} else {
return this.getMedia({
include: [
{
model: this.sequelize.models.author,
through: {
attributes: []
}
},
{
model: this.sequelize.models.series,
through: {
attributes: ['sequence']
}
}
],
order: [
[this.sequelize.models.author, this.sequelize.models.bookAuthor, 'createdAt', 'ASC'],
[this.sequelize.models.series, 'bookSeries', 'createdAt', 'ASC']
]
})
}
}
/**
*
* @returns {Promise}
*/
async saveMetadataFile() {
let metadataPath = Path.join(global.MetadataPath, 'items', this.id)
let storeMetadataWithItem = global.ServerSettings.storeMetadataWithItem
if (storeMetadataWithItem && !this.isFile) {
metadataPath = this.path
} else {
// Make sure metadata book dir exists
storeMetadataWithItem = false
await fsExtra.ensureDir(metadataPath)
}
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
// Expanded with series, authors, podcastEpisodes
const mediaExpanded = this.media || await this.getMediaExpanded()
let jsonObject = {}
if (this.mediaType === 'book') {
jsonObject = {
tags: mediaExpanded.tags || [],
chapters: mediaExpanded.chapters?.map(c => ({ ...c })) || [],
title: mediaExpanded.title,
subtitle: mediaExpanded.subtitle,
authors: mediaExpanded.authors.map(a => a.name),
narrators: mediaExpanded.narrators,
series: mediaExpanded.series.map(se => {
const sequence = se.bookSeries?.sequence || ''
if (!sequence) return se.name
return `${se.name} #${sequence}`
}),
genres: mediaExpanded.genres || [],
publishedYear: mediaExpanded.publishedYear,
publishedDate: mediaExpanded.publishedDate,
publisher: mediaExpanded.publisher,
description: mediaExpanded.description,
isbn: mediaExpanded.isbn,
asin: mediaExpanded.asin,
language: mediaExpanded.language,
explicit: !!mediaExpanded.explicit,
abridged: !!mediaExpanded.abridged
}
} else {
jsonObject = {
tags: mediaExpanded.tags || [],
title: mediaExpanded.title,
author: mediaExpanded.author,
description: mediaExpanded.description,
releaseDate: mediaExpanded.releaseDate,
genres: mediaExpanded.genres || [],
feedURL: mediaExpanded.feedURL,
imageURL: mediaExpanded.imageURL,
itunesPageURL: mediaExpanded.itunesPageURL,
itunesId: mediaExpanded.itunesId,
itunesArtistId: mediaExpanded.itunesArtistId,
asin: mediaExpanded.asin,
language: mediaExpanded.language,
explicit: !!mediaExpanded.explicit,
podcastType: mediaExpanded.podcastType
}
}
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => {
// Add metadata.json to libraryFiles array if it is new
let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
if (storeMetadataWithItem) {
if (!metadataLibraryFile) {
const newLibraryFile = new LibraryFile()
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
metadataLibraryFile = newLibraryFile.toJSON()
this.libraryFiles.push(metadataLibraryFile)
} else {
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
if (fileTimestamps) {
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
metadataLibraryFile.metadata.size = fileTimestamps.size
metadataLibraryFile.ino = fileTimestamps.ino
}
}
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
if (libraryItemDirTimestamps) {
this.mtime = libraryItemDirTimestamps.mtimeMs
this.ctime = libraryItemDirTimestamps.ctimeMs
let size = 0
this.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
this.size = size
await this.save()
}
}
Logger.debug(`Success saving abmetadata to "${metadataFilePath}"`)
return metadataLibraryFile
}).catch((error) => {
Logger.error(`Failed to save json file at "${metadataFilePath}"`, error)
return null
})
}
/**
* Initialize model
* @param {import('../Database').sequelize} sequelize

View file

@ -195,7 +195,7 @@ class Stream extends EventEmitter {
var current_chunk = []
var last_seg_in_chunk = -1
var segments = Array.from(this.segmentsCreated).sort((a, b) => a - b);
var segments = Array.from(this.segmentsCreated).sort((a, b) => a - b)
var lastSegment = segments[segments.length - 1]
if (lastSegment > this.furthestSegmentCreated) {
this.furthestSegmentCreated = lastSegment
@ -342,7 +342,7 @@ class Stream extends EventEmitter {
Logger.error('Ffmpeg Err', '"' + err.message + '"')
// Temporary workaround for https://github.com/advplyr/audiobookshelf/issues/172 and https://github.com/advplyr/audiobookshelf/issues/2157
const aacErrorMsg = 'ffmpeg exited with code 1:'
const aacErrorMsg = 'ffmpeg exited with code 1'
if (audioCodec === 'copy' && this.isAACEncodable && err.message?.startsWith(aacErrorMsg)) {
Logger.info(`[Stream] Re-attempting stream with AAC encode`)
this.transcodeOptions.forceAAC = true

View file

@ -79,12 +79,19 @@ class Audible {
}
}
/**
* Test if a search title matches an ASIN. Supports lowercase letters
*
* @param {string} title
* @returns {boolean}
*/
isProbablyAsin(title) {
return /^[0-9A-Z]{10}$/.test(title)
return /^[0-9A-Za-z]{10}$/.test(title)
}
asinSearch(asin, region) {
asin = encodeURIComponent(asin)
if (!asin) return []
asin = encodeURIComponent(asin.toUpperCase())
var regionQuery = region ? `?region=${region}` : ''
var url = `https://api.audnex.us/books/${asin}${regionQuery}`
Logger.debug(`[Audible] ASIN url: ${url}`)
@ -124,7 +131,7 @@ class Audible {
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
Logger.debug(`[Audible] Search url: ${url}`)
items = await axios.get(url).then((res) => {
if (!res || !res.data || !res.data.products) return null
if (!res?.data?.products) return null
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin, region)))
}).catch(error => {
Logger.error('[Audible] query search error', error)

View file

@ -43,7 +43,7 @@ class CustomProviderAdapter {
}
}
const matches = await axios.get(`${provider.url}/search?${queryString}}`, axiosOptions).then((res) => {
const matches = await axios.get(`${provider.url}/search?${queryString}`, axiosOptions).then((res) => {
if (!res?.data || !Array.isArray(res.data.matches)) return null
return res.data.matches
}).catch(error => {

View file

@ -378,7 +378,7 @@ class AudioFileScanner {
const MetadataMapArray = [
{
tag: 'tagComment',
altTag: 'tagSubtitle',
altTag: 'tagDescription',
key: 'description'
},
{

View file

@ -359,7 +359,7 @@ class Scanner {
}
offset += limit
hasMoreChunks = libraryItems.length < limit
hasMoreChunks = libraryItems.length === limit
let oldLibraryItems = libraryItems.map(li => Database.libraryItemModel.getOldLibraryItem(li))
const shouldContinue = await this.matchLibraryItemsChunk(library, oldLibraryItems, libraryScan)

View file

@ -104,7 +104,8 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
const ffmpeg = Ffmpeg(response.data)
ffmpeg.addOption('-loglevel debug') // Debug logs printed on error
ffmpeg.outputOptions(
'-c', 'copy',
'-c:a', 'copy',
'-map', '0:a',
'-metadata', 'podcast=1'
)

View file

@ -59,7 +59,7 @@ async function getFileTimestampsWithIno(path) {
ino: String(stat.ino)
}
} catch (err) {
Logger.error('[fileUtils] Failed to getFileTimestampsWithIno', err)
Logger.error(`[fileUtils] Failed to getFileTimestampsWithIno for path "${path}"`, err)
return false
}
}

View file

@ -18,7 +18,10 @@ async function extractFileFromEpub(epubPath, filepath) {
Logger.error(`[parseEpubMetadata] Failed to extract ${filepath} from epub at "${epubPath}"`, error)
})
const filedata = data?.toString('utf8')
await zip.close()
await zip.close().catch((error) => {
Logger.error(`[parseEpubMetadata] Failed to close zip`, error)
})
return filedata
}
@ -68,6 +71,9 @@ async function parse(ebookFile) {
Logger.debug(`Parsing metadata from epub at "${epubPath}"`)
// Entrypoint of the epub that contains the filepath to the package document (opf file)
const containerJson = await extractXmlToJson(epubPath, 'META-INF/container.xml')
if (!containerJson) {
return null
}
// Get package document opf filepath from container.xml
const packageDocPath = containerJson.container?.rootfiles?.[0]?.rootfile?.[0]?.$?.['full-path']

View file

@ -451,7 +451,7 @@ module.exports = {
libraryId: libraryId
}
},
attributes: ['tags', 'genres']
attributes: ['tags', 'genres', 'language']
})
for (const podcast of podcasts) {
if (podcast.tags?.length) {
@ -460,6 +460,9 @@ module.exports = {
if (podcast.genres?.length) {
podcast.genres.forEach((genre) => data.genres.add(genre))
}
if (podcast.language) {
data.languages.add(podcast.language)
}
}
} else {
const books = await Database.bookModel.findAll({

View file

@ -34,6 +34,10 @@ module.exports = {
attributes: ['sequence']
}
}
],
order: [
[Database.authorModel, Database.bookAuthorModel, 'createdAt', 'ASC'],
[Database.seriesModel, 'bookSeries', 'createdAt', 'ASC']
]
})
for (const book of booksWithTag) {
@ -68,7 +72,7 @@ module.exports = {
/**
* Get all library items that have genres
* @param {string[]} genres
* @returns {Promise<LibraryItem[]>}
* @returns {Promise<import('../../models/LibraryItem')[]>}
*/
async getAllLibraryItemsWithGenres(genres) {
const libraryItems = []

View file

@ -51,6 +51,8 @@ module.exports = {
[Sequelize.Op.gte]: 1
})
replacements.filterValue = value
} else if (group === 'languages') {
mediaWhere['language'] = value
}
return {