mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-27 03:31:46 +00:00
Merge branch 'master' into feat-forwardauth
This commit is contained in:
commit
8558baf30a
214 changed files with 14835 additions and 6306 deletions
|
|
@ -183,16 +183,19 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
|||
return false
|
||||
}
|
||||
|
||||
// Max is actually 255-260 for windows but this leaves padding incase ext wasnt put on yet
|
||||
const MAX_FILENAME_LEN = 240
|
||||
// Most file systems use number of bytes for max filename
|
||||
// to support most filesystems we will use max of 255 bytes in utf-16
|
||||
// Ref: https://doc.owncloud.com/server/next/admin_manual/troubleshooting/path_filename_length.html
|
||||
// Issue: https://github.com/advplyr/audiobookshelf/issues/1261
|
||||
const MAX_FILENAME_BYTES = 255
|
||||
|
||||
var replacement = ''
|
||||
var illegalRe = /[\/\?<>\\:\*\|"]/g
|
||||
var controlRe = /[\x00-\x1f\x80-\x9f]/g
|
||||
var reservedRe = /^\.+$/
|
||||
var windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
||||
var windowsTrailingRe = /[\. ]+$/
|
||||
var lineBreaks = /[\n\r]/g
|
||||
const replacement = ''
|
||||
const illegalRe = /[\/\?<>\\:\*\|"]/g
|
||||
const controlRe = /[\x00-\x1f\x80-\x9f]/g
|
||||
const reservedRe = /^\.+$/
|
||||
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i
|
||||
const windowsTrailingRe = /[\. ]+$/
|
||||
const lineBreaks = /[\n\r]/g
|
||||
|
||||
sanitized = filename
|
||||
.replace(':', colonReplacement) // Replace first occurrence of a colon
|
||||
|
|
@ -203,12 +206,25 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
|||
.replace(windowsReservedRe, replacement)
|
||||
.replace(windowsTrailingRe, replacement)
|
||||
|
||||
if (sanitized.length > MAX_FILENAME_LEN) {
|
||||
var lenToRemove = sanitized.length - MAX_FILENAME_LEN
|
||||
var ext = Path.extname(sanitized)
|
||||
var basename = Path.basename(sanitized, ext)
|
||||
basename = basename.slice(0, basename.length - lenToRemove)
|
||||
sanitized = basename + ext
|
||||
// Check if basename is too many bytes
|
||||
const ext = Path.extname(sanitized) // separate out file extension
|
||||
const basename = Path.basename(sanitized, ext)
|
||||
const extByteLength = Buffer.byteLength(ext, 'utf16le')
|
||||
const basenameByteLength = Buffer.byteLength(basename, 'utf16le')
|
||||
if (basenameByteLength + extByteLength > MAX_FILENAME_BYTES) {
|
||||
const MaxBytesForBasename = MAX_FILENAME_BYTES - extByteLength
|
||||
let totalBytes = 0
|
||||
let trimmedBasename = ''
|
||||
|
||||
// Add chars until max bytes is reached
|
||||
for (const char of basename) {
|
||||
totalBytes += Buffer.byteLength(char, 'utf16le')
|
||||
if (totalBytes > MaxBytesForBasename) break
|
||||
else trimmedBasename += char
|
||||
}
|
||||
|
||||
trimmedBasename = trimmedBasename.trim()
|
||||
sanitized = trimmedBasename + ext
|
||||
}
|
||||
|
||||
return sanitized
|
||||
|
|
|
|||
|
|
@ -140,14 +140,24 @@ module.exports.cleanStringForSearch = (str) => {
|
|||
return str.toLowerCase().replace(/[\'\.\`\",]/g, '').trim()
|
||||
}
|
||||
|
||||
module.exports.getTitleIgnorePrefix = (title) => {
|
||||
if (!title) return ''
|
||||
const getTitleParts = (title) => {
|
||||
if (!title) return ['', null]
|
||||
var prefixesToIgnore = global.ServerSettings.sortingPrefixes || []
|
||||
prefixes = []
|
||||
for (const prefix of prefixesToIgnore) {
|
||||
// e.g. for prefix "the". If title is "The Book" return "Book, The"
|
||||
if (title.toLowerCase().startsWith(`${prefix} `)) {
|
||||
return title.substr(prefix.length + 1) + `, ${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`
|
||||
return [title.substr(prefix.length + 1), `${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`]
|
||||
}
|
||||
}
|
||||
return title
|
||||
return [title, null]
|
||||
}
|
||||
|
||||
module.exports.getTitleIgnorePrefix = (title) => {
|
||||
return getTitleParts(title)[0]
|
||||
}
|
||||
|
||||
module.exports.getTitlePrefixAtEnd = (title) => {
|
||||
let [sort, prefix] = getTitleParts(title)
|
||||
return prefix ? `${sort}, ${prefix}` : title
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
const { sort, createNewSortInstance } = require('../libs/fastSort')
|
||||
const { getTitleIgnorePrefix, isNullOrNaN } = require('../utils/index')
|
||||
const { getTitlePrefixAtEnd, isNullOrNaN, getTitleIgnorePrefix } = require('../utils/index')
|
||||
const naturalSort = createNewSortInstance({
|
||||
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
||||
})
|
||||
|
|
@ -10,53 +10,57 @@ module.exports = {
|
|||
},
|
||||
|
||||
getFilteredLibraryItems(libraryItems, filterBy, user, feedsArray) {
|
||||
var filtered = libraryItems
|
||||
let filtered = libraryItems
|
||||
|
||||
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'missing', 'languages']
|
||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'missing', 'languages', 'tracks']
|
||||
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
if (group) {
|
||||
var filterVal = filterBy.replace(`${group}.`, '')
|
||||
var filter = this.decode(filterVal)
|
||||
const filterVal = filterBy.replace(`${group}.`, '')
|
||||
const filter = this.decode(filterVal)
|
||||
if (group === 'genres') filtered = filtered.filter(li => li.media.metadata && li.media.metadata.genres.includes(filter))
|
||||
else if (group === 'tags') filtered = filtered.filter(li => li.media.tags.includes(filter))
|
||||
else if (group === 'series') {
|
||||
if (filter === 'No Series') filtered = filtered.filter(li => li.mediaType === 'book' && !li.media.metadata.series.length)
|
||||
if (filter === 'no-series') filtered = filtered.filter(li => li.isBook && !li.media.metadata.series.length)
|
||||
else {
|
||||
filtered = filtered.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(filter))
|
||||
filtered = filtered.filter(li => li.isBook && li.media.metadata.hasSeries(filter))
|
||||
}
|
||||
}
|
||||
else if (group === 'authors') filtered = filtered.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(filter))
|
||||
else if (group === 'narrators') filtered = filtered.filter(li => li.mediaType === 'book' && li.media.metadata.hasNarrator(filter))
|
||||
else if (group === 'authors') filtered = filtered.filter(li => li.isBook && li.media.metadata.hasAuthor(filter))
|
||||
else if (group === 'narrators') filtered = filtered.filter(li => li.isBook && li.media.metadata.hasNarrator(filter))
|
||||
else if (group === 'progress') {
|
||||
filtered = filtered.filter(li => {
|
||||
var itemProgress = user.getMediaProgress(li.id)
|
||||
if (filter === 'Finished' && (itemProgress && itemProgress.isFinished)) return true
|
||||
if (filter === 'Not Started' && !itemProgress) return true
|
||||
if (filter === 'Not Finished' && (!itemProgress || !itemProgress.isFinished)) return true
|
||||
if (filter === 'In Progress' && (itemProgress && itemProgress.inProgress)) return true
|
||||
const itemProgress = user.getMediaProgress(li.id)
|
||||
if (filter === 'finished' && (itemProgress && itemProgress.isFinished)) return true
|
||||
if (filter === 'not-started' && !itemProgress) return true
|
||||
if (filter === 'not-finished' && (!itemProgress || !itemProgress.isFinished)) return true
|
||||
if (filter === 'in-progress' && (itemProgress && itemProgress.inProgress)) return true
|
||||
return false
|
||||
})
|
||||
} else if (group == 'missing') {
|
||||
filtered = filtered.filter(li => {
|
||||
if (li.mediaType === 'book') {
|
||||
if (filter === 'ASIN' && li.media.metadata.asin === null) return true;
|
||||
if (filter === 'ISBN' && li.media.metadata.isbn === null) return true;
|
||||
if (filter === 'Subtitle' && li.media.metadata.subtitle === null) return true;
|
||||
if (filter === 'Author' && li.media.metadata.authors.length === 0) return true;
|
||||
if (filter === 'Publish Year' && li.media.metadata.publishedYear === null) return true;
|
||||
if (filter === 'Series' && li.media.metadata.series.length === 0) return true;
|
||||
if (filter === 'Description' && li.media.metadata.description === null) return true;
|
||||
if (filter === 'Genres' && li.media.metadata.genres.length === 0) return true;
|
||||
if (filter === 'Tags' && li.media.tags.length === 0) return true;
|
||||
if (filter === 'Narrator' && li.media.metadata.narrators.length === 0) return true;
|
||||
if (filter === 'Publisher' && li.media.metadata.publisher === null) return true;
|
||||
if (filter === 'Language' && li.media.metadata.language === null) return true;
|
||||
if (li.isBook) {
|
||||
if (filter === 'asin' && !li.media.metadata.asin) return true
|
||||
if (filter === 'isbn' && !li.media.metadata.isbn) return true
|
||||
if (filter === 'subtitle' && !li.media.metadata.subtitle) return true
|
||||
if (filter === 'authors' && !li.media.metadata.authors.length) return true
|
||||
if (filter === 'publishedYear' && !li.media.metadata.publishedYear) return true
|
||||
if (filter === 'series' && !li.media.metadata.series.length) return true
|
||||
if (filter === 'description' && !li.media.metadata.description) return true
|
||||
if (filter === 'genres' && !li.media.metadata.genres.length) return true
|
||||
if (filter === 'tags' && !li.media.tags.length) return true
|
||||
if (filter === 'narrators' && !li.media.metadata.narrators.length) return true
|
||||
if (filter === 'publisher' && !li.media.metadata.publisher) return true
|
||||
if (filter === 'language' && !li.media.metadata.language) return true
|
||||
if (filter === 'cover' && !li.media.coverPath) return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
} else if (group === 'languages') {
|
||||
filtered = filtered.filter(li => li.media.metadata && li.media.metadata.language === filter)
|
||||
} else if (group === 'tracks') {
|
||||
if (filter === 'single') filtered = filtered.filter(li => li.isBook && li.media.numTracks === 1)
|
||||
else if (filter === 'multi') filtered = filtered.filter(li => li.isBook && li.media.numTracks > 1)
|
||||
}
|
||||
} else if (filterBy === 'issues') {
|
||||
filtered = filtered.filter(li => li.hasIssues)
|
||||
|
|
@ -153,7 +157,7 @@ module.exports = {
|
|||
return data
|
||||
},
|
||||
|
||||
getSeriesFromBooks(books, allSeries, filterBy, user, minified = false) {
|
||||
getSeriesFromBooks(books, allSeries, filterSeries, filterBy, user, minified = false) {
|
||||
const _series = {}
|
||||
const seriesToFilterOut = {}
|
||||
books.forEach((libraryItem) => {
|
||||
|
|
@ -179,11 +183,15 @@ module.exports = {
|
|||
|
||||
const abJson = minified ? libraryItem.toJSONMinified() : libraryItem.toJSONExpanded()
|
||||
abJson.sequence = bookSeriesObj.sequence
|
||||
if (filterSeries) {
|
||||
abJson.filterSeriesSequence = libraryItem.media.metadata.getSeries(filterSeries).sequence
|
||||
}
|
||||
if (!_series[bookSeriesObj.id]) {
|
||||
_series[bookSeriesObj.id] = {
|
||||
id: bookSeriesObj.id,
|
||||
name: bookSeriesObj.name,
|
||||
nameIgnorePrefix: getTitleIgnorePrefix(bookSeriesObj.name),
|
||||
nameIgnorePrefix: getTitlePrefixAtEnd(bookSeriesObj.name),
|
||||
nameIgnorePrefixSort: getTitleIgnorePrefix(bookSeriesObj.name),
|
||||
type: 'series',
|
||||
books: [abJson],
|
||||
addedAt: series ? series.addedAt : 0,
|
||||
|
|
@ -279,35 +287,34 @@ module.exports = {
|
|||
return totalSize
|
||||
},
|
||||
|
||||
collapseBookSeries(libraryItems, series) {
|
||||
var seriesObjects = this.getSeriesFromBooks(libraryItems, series, null, null, true)
|
||||
var seriesToUse = {}
|
||||
var libraryItemIdsToHide = []
|
||||
seriesObjects.forEach((series) => {
|
||||
series.firstBook = series.books.find(b => !seriesToUse[b.id]) // Find first book not already used
|
||||
if (series.firstBook) {
|
||||
seriesToUse[series.firstBook.id] = series
|
||||
libraryItemIdsToHide = libraryItemIdsToHide.concat(series.books.filter(b => !seriesToUse[b.id]).map(b => b.id))
|
||||
}
|
||||
})
|
||||
|
||||
return libraryItems.map((li) => {
|
||||
collapseBookSeries(libraryItems, series, filterSeries) {
|
||||
// Get series from the library items. If this list is being collapsed after filtering for a series,
|
||||
// don't collapse that series, only books that are in other series.
|
||||
var seriesObjects = this
|
||||
.getSeriesFromBooks(libraryItems, series, filterSeries, null, null, true)
|
||||
.filter(s => s.id != filterSeries)
|
||||
|
||||
var filteredLibraryItems = []
|
||||
|
||||
libraryItems.forEach((li) => {
|
||||
if (li.mediaType != 'book') return
|
||||
var libraryItemJson = li.toJSONMinified()
|
||||
if (libraryItemIdsToHide.includes(li.id)) {
|
||||
return null
|
||||
}
|
||||
if (seriesToUse[li.id]) {
|
||||
libraryItemJson.collapsedSeries = {
|
||||
id: seriesToUse[li.id].id,
|
||||
name: seriesToUse[li.id].name,
|
||||
nameIgnorePrefix: seriesToUse[li.id].nameIgnorePrefix,
|
||||
libraryItemIds: seriesToUse[li.id].books.map(b => b.id),
|
||||
numBooks: seriesToUse[li.id].books.length
|
||||
}
|
||||
}
|
||||
return libraryItemJson
|
||||
}).filter(li => li)
|
||||
|
||||
// Handle when this is the first book in a series
|
||||
seriesObjects.filter(s => s.books[0].id == li.id).forEach(series => {
|
||||
// Clone the library item as we need to attach data to it, but don't
|
||||
// want to change the global copy of the library item
|
||||
filteredLibraryItems.push(Object.assign(
|
||||
Object.create(Object.getPrototypeOf(li)),
|
||||
li, { collapsedSeries: series }))
|
||||
});
|
||||
|
||||
// Only included books not contained in series
|
||||
if (!seriesObjects.some(s => s.books.some(b => b.id == li.id)))
|
||||
filteredLibraryItems.push(li)
|
||||
});
|
||||
|
||||
return filteredLibraryItems
|
||||
},
|
||||
|
||||
buildPersonalizedShelves(user, libraryItems, mediaType, allSeries, allAuthors, maxEntitiesPerShelf = 10) {
|
||||
|
|
@ -317,6 +324,7 @@ module.exports = {
|
|||
{
|
||||
id: 'continue-listening',
|
||||
label: 'Continue Listening',
|
||||
labelStringKey: 'LabelContinueListening',
|
||||
type: isPodcastLibrary ? 'episode' : mediaType,
|
||||
entities: [],
|
||||
category: 'recentlyListened'
|
||||
|
|
@ -324,6 +332,7 @@ module.exports = {
|
|||
{
|
||||
id: 'continue-series',
|
||||
label: 'Continue Series',
|
||||
labelStringKey: 'LabelContinueSeries',
|
||||
type: mediaType,
|
||||
entities: [],
|
||||
category: 'continueSeries'
|
||||
|
|
@ -331,6 +340,7 @@ module.exports = {
|
|||
{
|
||||
id: 'recently-added',
|
||||
label: 'Recently Added',
|
||||
labelStringKey: 'LabelRecentlyAdded',
|
||||
type: mediaType,
|
||||
entities: [],
|
||||
category: 'newestItems'
|
||||
|
|
@ -338,6 +348,7 @@ module.exports = {
|
|||
{
|
||||
id: 'listen-again',
|
||||
label: 'Listen Again',
|
||||
labelStringKey: 'LabelListenAgain',
|
||||
type: isPodcastLibrary ? 'episode' : mediaType,
|
||||
entities: [],
|
||||
category: 'recentlyFinished'
|
||||
|
|
@ -345,6 +356,7 @@ module.exports = {
|
|||
{
|
||||
id: 'recent-series',
|
||||
label: 'Recent Series',
|
||||
labelStringKey: 'LabelRecentSeries',
|
||||
type: 'series',
|
||||
entities: [],
|
||||
category: 'newestSeries'
|
||||
|
|
@ -352,6 +364,7 @@ module.exports = {
|
|||
{
|
||||
id: 'newest-authors',
|
||||
label: 'Newest Authors',
|
||||
labelStringKey: 'LabelNewestAuthors',
|
||||
type: 'authors',
|
||||
entities: [],
|
||||
category: 'newestAuthors'
|
||||
|
|
@ -359,6 +372,7 @@ module.exports = {
|
|||
{
|
||||
id: 'episodes-recently-added',
|
||||
label: 'Newest Episodes',
|
||||
labelStringKey: 'LabelNewestEpisodes',
|
||||
type: 'episode',
|
||||
entities: [],
|
||||
category: 'newestEpisodes'
|
||||
|
|
|
|||
|
|
@ -96,13 +96,11 @@ function extractEpisodeData(item) {
|
|||
}
|
||||
|
||||
if (item['pubDate']) {
|
||||
if (typeof item['pubDate'] === 'string') {
|
||||
episode.pubDate = item['pubDate']
|
||||
} else if (Array.isArray(item['pubDate'])) {
|
||||
const pubDateObj = extractFirstArrayItem(item, 'pubDate')
|
||||
if (pubDateObj && typeof pubDateObj._ === 'string') {
|
||||
episode.pubDate = pubDateObj._
|
||||
}
|
||||
const pubDate = extractFirstArrayItem(item, 'pubDate')
|
||||
if (typeof pubDate === 'string') {
|
||||
episode.pubDate = pubDate
|
||||
} else if (pubDate && typeof pubDate._ === 'string') {
|
||||
episode.pubDate = pubDate._
|
||||
} else {
|
||||
Logger.error(`[podcastUtils] Invalid pubDate ${item['pubDate']} for ${episode.enclosure.url}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,70 @@ module.exports.getToneMetadataObject = (libraryItem, chaptersFile) => {
|
|||
return metadataObject
|
||||
}
|
||||
|
||||
module.exports.writeToneMetadataJsonFile = (libraryItem, chapters, filePath, trackTotal) => {
|
||||
const bookMetadata = libraryItem.media.metadata
|
||||
const coverPath = libraryItem.media.coverPath
|
||||
|
||||
const metadataObject = {
|
||||
'album': bookMetadata.title || '',
|
||||
'title': bookMetadata.title || '',
|
||||
'trackTotal': trackTotal,
|
||||
'additionalFields': {}
|
||||
}
|
||||
if (bookMetadata.subtitle) {
|
||||
metadataObject['subtitle'] = bookMetadata.subtitle
|
||||
}
|
||||
if (bookMetadata.authorName) {
|
||||
metadataObject['artist'] = bookMetadata.authorName
|
||||
metadataObject['albumArtist'] = bookMetadata.authorName
|
||||
}
|
||||
if (bookMetadata.description) {
|
||||
metadataObject['comment'] = bookMetadata.description
|
||||
metadataObject['description'] = bookMetadata.description
|
||||
}
|
||||
if (bookMetadata.narratorName) {
|
||||
metadataObject['narrator'] = bookMetadata.narratorName
|
||||
metadataObject['composer'] = bookMetadata.narratorName
|
||||
}
|
||||
if (bookMetadata.firstSeriesName) {
|
||||
metadataObject['movementName'] = bookMetadata.firstSeriesName
|
||||
}
|
||||
if (bookMetadata.firstSeriesSequence) {
|
||||
metadataObject['movement'] = bookMetadata.firstSeriesSequence
|
||||
}
|
||||
if (bookMetadata.genres.length) {
|
||||
metadataObject['genre'] = bookMetadata.genres.join('/')
|
||||
}
|
||||
if (bookMetadata.publisher) {
|
||||
metadataObject['publisher'] = bookMetadata.publisher
|
||||
}
|
||||
if (bookMetadata.asin) {
|
||||
metadataObject.additionalFields['asin'] = bookMetadata.asin
|
||||
}
|
||||
if (bookMetadata.isbn) {
|
||||
metadataObject.additionalFields['isbn'] = bookMetadata.isbn
|
||||
}
|
||||
if (coverPath) {
|
||||
metadataObject['coverFile'] = coverPath
|
||||
}
|
||||
if (parsePublishedYear(bookMetadata.publishedYear)) {
|
||||
metadataObject['publishingDate'] = parsePublishedYear(bookMetadata.publishedYear)
|
||||
}
|
||||
if (chapters && chapters.length > 0) {
|
||||
let metadataChapters = []
|
||||
for (const chapter of chapters) {
|
||||
metadataChapters.push({
|
||||
start: Math.round(chapter.start * 1000),
|
||||
length: Math.round((chapter.end - chapter.start) * 1000),
|
||||
title: chapter.title,
|
||||
})
|
||||
}
|
||||
metadataObject['chapters'] = metadataChapters
|
||||
}
|
||||
|
||||
return fs.writeFile(filePath, JSON.stringify({ meta: metadataObject }, null, 2))
|
||||
}
|
||||
|
||||
module.exports.tagAudioFile = (filePath, payload) => {
|
||||
return tone.tag(filePath, payload).then((data) => {
|
||||
return true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue