Merge branch 'master' into social

This commit is contained in:
Ben 2022-08-20 11:52:41 -04:00 committed by GitHub
commit 92c393d2b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
351 changed files with 53592 additions and 1765 deletions

View file

@ -1,5 +1,5 @@
const Path = require('path')
const date = require('date-and-time')
const date = require('../libs/dateAndTime')
const version = require('../../package.json').version
class Backup {

View file

@ -1,5 +1,5 @@
const Path = require('path')
const date = require('date-and-time')
const date = require('../libs/dateAndTime')
const fs = require('../libs/fsExtra')
const { readTextFile } = require('../utils/fileUtils')
const Logger = require('../Logger')

View file

@ -1,5 +1,8 @@
const { AudioMimeType } = require('../utils/constants')
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
const DEFAULT_EXPIRATION = 1000 * 60 * 60 // 60 minutes
const DEFAULT_TIMEOUT = 1000 * 60 * 30 // 30 minutes
class Download {
constructor(download) {
this.id = null
@ -32,16 +35,7 @@ class Download {
}
get mimeType() {
if (this.ext === '.mp3' || this.ext === '.m4b' || this.ext === '.m4a') {
return 'audio/mpeg'
} else if (this.ext === '.mp4') {
return 'audio/mp4'
} else if (this.ext === '.ogg') {
return 'audio/ogg'
} else if (this.ext === '.aac' || this.ext === '.m4p') {
return 'audio/aac'
}
return 'audio/mpeg'
return getAudioMimeTypeFromExtname(this.ext) || AudioMimeType.MP3
}
toJSON() {

View file

@ -1,5 +1,5 @@
const Path = require('path')
const date = require('date-and-time')
const date = require('../libs/dateAndTime')
const { secondsToTimestamp } = require('../utils/index')
class FeedEpisode {
@ -85,7 +85,8 @@ class FeedEpisode {
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
const audiobookPubDate = date.format(new Date(libraryItem.addedAt), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
const timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
const audiobookPubDate = date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
const media = libraryItem.media

View file

@ -1,4 +1,4 @@
const date = require('date-and-time')
const date = require('../libs/dateAndTime')
const { getId } = require('../utils/index')
const { PlayMethod } = require('../utils/constants')
const BookMetadata = require('./metadata/BookMetadata')

View file

@ -9,6 +9,7 @@ class PodcastEpisodeDownload {
this.url = null
this.libraryItem = null
this.isAutoDownload = false
this.isDownloading = false
this.isFinished = false
this.failed = false
@ -46,11 +47,12 @@ class PodcastEpisodeDownload {
return this.libraryItem ? this.libraryItem.id : null
}
setData(podcastEpisode, libraryItem) {
setData(podcastEpisode, libraryItem, isAutoDownload) {
this.id = getId('epdl')
this.podcastEpisode = podcastEpisode
this.url = podcastEpisode.enclosure.url
this.libraryItem = libraryItem
this.isAutoDownload = isAutoDownload
this.createdAt = Date.now()
}

View file

@ -1,4 +1,4 @@
const Ffmpeg = require('fluent-ffmpeg')
const Ffmpeg = require('../libs/fluentFfmpeg')
const EventEmitter = require('events')
const Path = require('path')
const fs = require('../libs/fsExtra')

View file

@ -1,3 +1,4 @@
const Path = require('path')
const { getId } = require('../../utils/index')
const AudioFile = require('../files/AudioFile')
const AudioTrack = require('../files/AudioTrack')
@ -126,7 +127,7 @@ class PodcastEpisode {
setDataFromAudioFile(audioFile, index) {
this.id = getId('ep')
this.audioFile = audioFile
this.title = audioFile.metadata.filename
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
this.index = index
this.addedAt = Date.now()
this.updatedAt = Date.now()

View file

@ -1,4 +1,3 @@
const { isNullOrNaN } = require('../../utils/index')
const { AudioMimeType } = require('../../utils/constants')
const AudioMetaTags = require('../metadata/AudioMetaTags')
const FileMetadata = require('../metadata/FileMetadata')
@ -111,7 +110,11 @@ class AudioFile {
}
}
// New scanner creates AudioFile from AudioFileScanner
get isValidTrack() {
return !this.invalid && !this.exclude
}
// New scanner creates AudioFile from MediaFileScanner
setDataFromProbe(libraryFile, probeData) {
this.ino = libraryFile.ino || null
@ -132,23 +135,6 @@ class AudioFile {
this.embeddedCoverArt = probeData.embeddedCoverArt
}
validateTrackIndex() {
var numFromMeta = isNullOrNaN(this.trackNumFromMeta) ? null : Number(this.trackNumFromMeta)
var numFromFilename = isNullOrNaN(this.trackNumFromFilename) ? null : Number(this.trackNumFromFilename)
if (numFromMeta !== null) return numFromMeta
if (numFromFilename !== null) return numFromFilename
this.invalid = true
this.error = 'Failed to get track number'
return null
}
setDuplicateTrackNumber(num) {
this.invalid = true
this.error = 'Duplicate track number "' + num + '"'
}
syncChapters(updatedChapters) {
if (this.chapters.length !== updatedChapters.length) {
this.chapters = updatedChapters.map(ch => ({ ...ch }))

View file

@ -276,13 +276,18 @@ class Book {
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
metadataUpdatePayload[key] = opfMetadata.genres
}
} else if (key === 'author') {
if (opfMetadata.author && (!this.metadata.authors.length || opfMetadataOverrideDetails)) {
metadataUpdatePayload.authors = this.metadata.parseAuthorsTag(opfMetadata.author)
} else if (key === 'authors') {
if (opfMetadata.authors && opfMetadata.authors.length && (!this.metadata.authors.length || opfMetadataOverrideDetails)) {
metadataUpdatePayload.authors = opfMetadata.authors.map(authorName => {
return {
id: `new-${Math.floor(Math.random() * 1000000)}`,
name: authorName
}
})
}
} else if (key === 'narrator') {
if (opfMetadata.narrator && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
metadataUpdatePayload.narrators = this.metadata.parseNarratorsTag(opfMetadata.narrator)
} else if (key === 'narrators') {
if (opfMetadata.narrators && opfMetadata.narrators.length && (!this.metadata.narrators.length || opfMetadataOverrideDetails)) {
metadataUpdatePayload.narrators = opfMetadata.narrators
}
} else if (key === 'series') {
if (opfMetadata.series && (!this.metadata.series.length || opfMetadataOverrideDetails)) {
@ -402,9 +407,12 @@ class Book {
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
// If overdrive media markers are present and preferred, use those instead
if (preferOverdriveMediaMarker && overdriveMediaMarkersExist(includedAudioFiles)) {
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
return this.chapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
if (preferOverdriveMediaMarker) {
var overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
if (overdriveChapters) {
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
return this.chapters = overdriveChapters
}
}
if (includedAudioFiles.length === 1) {
@ -420,10 +428,10 @@ class Book {
// If audio file has chapters use chapters
if (file.chapters && file.chapters.length) {
file.chapters.forEach((chapter) => {
if (chapter.start > this.duration) {
if (currStartTime > this.duration) {
Logger.warn(`[Book] Invalid chapter start time > duration`)
} else {
var chapterAlreadyExists = this.chapters.find(ch => ch.start === chapter.start)
var chapterAlreadyExists = this.chapters.find(ch => ch.start === currStartTime)
if (!chapterAlreadyExists) {
var chapterDuration = chapter.end - chapter.start
if (chapterDuration > 0) {

View file

@ -18,7 +18,9 @@ class Podcast {
this.episodes = []
this.autoDownloadEpisodes = false
this.autoDownloadSchedule = null
this.lastEpisodeCheck = 0
this.maxEpisodesToKeep = 0
this.lastCoverSearch = null
this.lastCoverSearchQuery = null
@ -39,7 +41,9 @@ class Podcast {
return podcastEpisode
})
this.autoDownloadEpisodes = !!podcast.autoDownloadEpisodes
this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
}
toJSON() {
@ -50,7 +54,9 @@ class Podcast {
tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSON()),
autoDownloadEpisodes: this.autoDownloadEpisodes,
lastEpisodeCheck: this.lastEpisodeCheck
autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep
}
}
@ -61,7 +67,9 @@ class Podcast {
tags: [...this.tags],
numEpisodes: this.episodes.length,
autoDownloadEpisodes: this.autoDownloadEpisodes,
autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
size: this.size
}
}
@ -74,7 +82,9 @@ class Podcast {
tags: [...this.tags],
episodes: this.episodes.map(e => e.toJSONExpanded()),
autoDownloadEpisodes: this.autoDownloadEpisodes,
autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
size: this.size
}
}
@ -113,6 +123,9 @@ class Podcast {
})
return largestPublishedAt
}
get episodesWithPubDate() {
return this.episodes.filter(ep => !!ep.publishedAt)
}
update(payload) {
var json = this.toJSON()
@ -157,14 +170,15 @@ class Podcast {
return null
}
setData(mediaMetadata) {
setData(mediaData) {
this.metadata = new PodcastMetadata()
if (mediaMetadata.metadata) {
this.metadata.setData(mediaMetadata.metadata)
if (mediaData.metadata) {
this.metadata.setData(mediaData.metadata)
}
this.coverPath = mediaMetadata.coverPath || null
this.autoDownloadEpisodes = !!mediaMetadata.autoDownloadEpisodes
this.coverPath = mediaData.coverPath || null
this.autoDownloadEpisodes = !!mediaData.autoDownloadEpisodes
this.autoDownloadSchedule = mediaData.autoDownloadSchedule || global.ServerSettings.podcastEpisodeSchedule
this.lastEpisodeCheck = Date.now() // Makes sure new episodes are after this
}

View file

@ -1,5 +1,5 @@
const Logger = require('../../Logger')
const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix } = require('../../utils/index')
const parseNameString = require('../../utils/parsers/parseNameString')
class BookMetadata {
constructor(metadata) {
@ -109,15 +109,7 @@ class BookMetadata {
}
get titleIgnorePrefix() {
if (!this.title) return ''
var prefixesToIgnore = global.ServerSettings.sortingPrefixes || []
for (const prefix of prefixesToIgnore) {
// e.g. for prefix "the". If title is "The Book Title" return "Book Title, The"
if (this.title.toLowerCase().startsWith(`${prefix} `)) {
return this.title.substr(prefix.length + 1) + `, ${prefix.substr(0, 1).toUpperCase() + prefix.substr(1)}`
}
}
return this.title
return getTitleIgnorePrefix(this.title)
}
get authorName() {
if (!this.authors.length) return ''
@ -134,6 +126,13 @@ class BookMetadata {
return `${se.name} #${se.sequence}`
}).join(', ')
}
get seriesNameIgnorePrefix() {
if (!this.series.length) return ''
return this.series.map(se => {
if (!se.sequence) return getTitleIgnorePrefix(se.name)
return `${getTitleIgnorePrefix(se.name)} #${se.sequence}`
}).join(', ')
}
get narratorName() {
return this.narrators.join(', ')
}
@ -191,6 +190,14 @@ class BookMetadata {
return true
}
replaceAuthor(oldAuthor, newAuthor) {
this.authors = this.authors.filter(au => au.id !== oldAuthor.id) // Remove old author
this.authors.push({
id: newAuthor.id,
name: newAuthor.name
})
}
setData(scanMediaData = {}) {
this.title = scanMediaData.title || null
this.subtitle = scanMediaData.subtitle || null

View file

@ -1,11 +1,12 @@
const { BookCoverAspectRatio } = require('../../utils/constants')
const Logger = require('../../Logger')
class LibrarySettings {
constructor(settings) {
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
this.disableWatcher = false
this.skipMatchingMediaWithAsin = false
this.skipMatchingMediaWithIsbn = false
this.autoScanCronExpression = null
if (settings) {
this.construct(settings)
@ -13,16 +14,20 @@ class LibrarySettings {
}
construct(settings) {
this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
this.disableWatcher = !!settings.disableWatcher
this.skipMatchingMediaWithAsin = !!settings.skipMatchingMediaWithAsin
this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn
this.autoScanCronExpression = settings.autoScanCronExpression || null
}
toJSON() {
return {
coverAspectRatio: this.coverAspectRatio,
disableWatcher: this.disableWatcher,
skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn,
autoScanCronExpression: this.autoScanCronExpression
}
}

View file

@ -1,9 +1,11 @@
const { BookCoverAspectRatio, BookshelfView } = require('../../utils/constants')
const { isNullOrNaN } = require('../../utils')
const Logger = require('../../Logger')
class ServerSettings {
constructor(settings) {
this.id = 'server-settings'
this.tokenSecret = null
// Scanner
this.scannerParseSubtitle = false
@ -14,6 +16,8 @@ class ServerSettings {
this.scannerPreferMatchedMetadata = false
this.scannerDisableWatcher = false
this.scannerPreferOverdriveMediaMarker = false
this.scannerUseSingleThreadedProber = true
this.scannerMaxThreads = 0 // 0 = defaults to CPUs * 2
// Metadata - choose to store inside users library item folder
this.storeCoverWithItem = false
@ -35,7 +39,11 @@ class ServerSettings {
this.loggerScannerLogsToKeep = 2
// Cover
// TODO: Remove after mobile apps are configured to use library server settings
this.coverAspectRatio = BookCoverAspectRatio.SQUARE
// Bookshelf Display
this.homeBookshelfView = BookshelfView.STANDARD
this.bookshelfView = BookshelfView.STANDARD
// Podcasts
@ -61,6 +69,7 @@ class ServerSettings {
}
construct(settings) {
this.tokenSecret = settings.tokenSecret
this.scannerFindCovers = !!settings.scannerFindCovers
this.scannerCoverProvider = settings.scannerCoverProvider || 'google'
this.scannerParseSubtitle = settings.scannerParseSubtitle
@ -69,15 +78,14 @@ class ServerSettings {
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker
this.scannerUseSingleThreadedProber = !!settings.scannerUseSingleThreadedProber
if (settings.scannerUseSingleThreadedProber === undefined) { // Default to original scanner
this.scannerUseSingleThreadedProber = true
}
this.scannerMaxThreads = isNullOrNaN(settings.scannerMaxThreads) ? 0 : Number(settings.scannerMaxThreads)
this.storeCoverWithItem = !!settings.storeCoverWithItem
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was old name of setting < v2
this.storeCoverWithItem = !!settings.storeCoverWithBook
}
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
if (settings.storeMetadataWithBook != undefined) { // storeMetadataWithBook was old name of setting < v2
this.storeMetadataWithItem = !!settings.storeMetadataWithBook
}
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
@ -91,6 +99,7 @@ class ServerSettings {
this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2
this.coverAspectRatio = !isNaN(settings.coverAspectRatio) ? settings.coverAspectRatio : BookCoverAspectRatio.SQUARE
this.homeBookshelfView = settings.homeBookshelfView || BookshelfView.STANDARD
this.bookshelfView = settings.bookshelfView || BookshelfView.STANDARD
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
@ -102,14 +111,26 @@ class ServerSettings {
this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null
// Migrations
if (settings.storeCoverWithBook != undefined) { // storeCoverWithBook was renamed to storeCoverWithItem in 2.0.0
this.storeCoverWithItem = !!settings.storeCoverWithBook
}
if (settings.storeMetadataWithBook != undefined) { // storeMetadataWithBook was renamed to storeMetadataWithItem in 2.0.0
this.storeMetadataWithItem = !!settings.storeMetadataWithBook
}
if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3
this.homeBookshelfView = settings.bookshelfView
}
if (this.logLevel !== Logger.logLevel) {
Logger.setLogLevel(this.logLevel)
}
}
toJSON() {
toJSON() { // Use toJSONForBrowser if sending to client
return {
id: this.id,
tokenSecret: this.tokenSecret, // Do not return to client
scannerFindCovers: this.scannerFindCovers,
scannerCoverProvider: this.scannerCoverProvider,
scannerParseSubtitle: this.scannerParseSubtitle,
@ -118,6 +139,8 @@ class ServerSettings {
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
scannerDisableWatcher: this.scannerDisableWatcher,
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
scannerUseSingleThreadedProber: this.scannerUseSingleThreadedProber,
scannerMaxThreads: this.scannerMaxThreads,
storeCoverWithItem: this.storeCoverWithItem,
storeMetadataWithItem: this.storeMetadataWithItem,
rateLimitLoginRequests: this.rateLimitLoginRequests,
@ -129,6 +152,7 @@ class ServerSettings {
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
coverAspectRatio: this.coverAspectRatio,
homeBookshelfView: this.homeBookshelfView,
bookshelfView: this.bookshelfView,
sortingIgnorePrefix: this.sortingIgnorePrefix,
sortingPrefixes: [...this.sortingPrefixes],
@ -141,6 +165,12 @@ class ServerSettings {
}
}
toJSONForBrowser() {
const json = this.toJSON()
delete json.tokenSecret
return json
}
update(payload) {
var hasUpdates = false
for (const key in payload) {

View file

@ -1,5 +1,3 @@
const Logger = require('../../Logger')
class MediaProgress {
constructor(progress) {
this.id = null