Merge branch 'master' into social

This commit is contained in:
Ben 2022-11-17 21:06:18 -05:00 committed by GitHub
commit 12b7976ef3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
245 changed files with 11554 additions and 3676 deletions

View file

@ -1,7 +1,7 @@
const Logger = require('../Logger')
const { getId } = require('../utils/index')
class UserCollection {
class Collection {
constructor(collection) {
this.id = null
this.libraryId = null
@ -63,7 +63,7 @@ class UserCollection {
if (!data.userId || !data.libraryId || !data.name) {
return false
}
this.id = getId('usr')
this.id = getId('col')
this.userId = data.userId
this.libraryId = data.libraryId
this.name = data.name
@ -105,4 +105,4 @@ class UserCollection {
return hasUpdates
}
}
module.exports = UserCollection
module.exports = Collection

View file

@ -1,120 +0,0 @@
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
this.libraryItemId = null
this.type = null
this.dirpath = null
this.path = null
this.ext = null
this.filename = null
this.size = 0
this.userId = null
this.isReady = false
this.isTimedOut = false
this.startedAt = null
this.finishedAt = null
this.expiresAt = null
this.expirationTimeMs = 0
this.timeoutTimeMs = 0
this.timeoutTimer = null
this.expirationTimer = null
if (download) {
this.construct(download)
}
}
get mimeType() {
return getAudioMimeTypeFromExtname(this.ext) || AudioMimeType.MP3
}
toJSON() {
return {
id: this.id,
libraryItemId: this.libraryItemId,
type: this.type,
dirpath: this.dirpath,
path: this.path,
ext: this.ext,
filename: this.filename,
size: this.size,
userId: this.userId,
isReady: this.isReady,
startedAt: this.startedAt,
finishedAt: this.finishedAt,
expirationSeconds: this.expirationSeconds
}
}
construct(download) {
this.id = download.id
this.libraryItemId = download.libraryItemId
this.type = download.type
this.dirpath = download.dirpath
this.path = download.path
this.ext = download.ext
this.filename = download.filename
this.size = download.size || 0
this.userId = download.userId
this.isReady = !!download.isReady
this.startedAt = download.startedAt
this.finishedAt = download.finishedAt || null
this.expirationTimeMs = download.expirationTimeMs || DEFAULT_EXPIRATION
this.timeoutTimeMs = download.timeoutTimeMs || DEFAULT_TIMEOUT
this.expiresAt = download.expiresAt || null
}
setData(downloadData) {
downloadData.startedAt = Date.now()
downloadData.isProcessing = true
this.construct(downloadData)
}
setComplete(fileSize) {
this.finishedAt = Date.now()
this.size = fileSize
this.isReady = true
this.expiresAt = this.finishedAt + this.expirationTimeMs
}
setExpirationTimer(callback) {
this.expirationTimer = setTimeout(() => {
if (callback) {
callback(this)
}
}, this.expirationTimeMs)
}
setTimeoutTimer(callback) {
this.timeoutTimer = setTimeout(() => {
if (callback) {
this.isTimedOut = true
callback(this)
}
}, this.timeoutTimeMs)
}
clearTimeoutTimer() {
clearTimeout(this.timeoutTimer)
}
clearExpirationTimer() {
clearTimeout(this.expirationTimer)
}
}
module.exports = Download

View file

@ -8,7 +8,7 @@ class Library {
this.name = null
this.folders = []
this.displayOrder = 1
this.icon = 'database' // database, podcast, book, audiobook, comic
this.icon = 'database'
this.mediaType = 'book' // book, podcast
this.provider = 'google'
@ -46,14 +46,15 @@ class Library {
this.createdAt = library.createdAt
this.lastUpdate = library.lastUpdate
this.cleanOldValues() // mediaType changed for v2
this.cleanOldValues() // mediaType changed for v2 and icon change for v2.2.2
}
cleanOldValues() {
var availableIcons = ['database', 'audiobook', 'book', 'comic', 'podcast']
const availableIcons = ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
if (!availableIcons.includes(this.icon)) {
if (this.icon === 'default') this.icon = 'database'
else if (this.icon.endsWith('s') && availableIcons.includes(this.icon.slice(0, -1))) this.icon = this.icon.slice(0, -1)
if (this.icon === 'audiobook') this.icon = 'audiobookshelf'
else if (this.icon === 'book') this.icon = 'books-1'
else if (this.icon === 'comic') this.icon = 'file-picture'
else this.icon = 'database'
}
if (!this.mediaType || (this.mediaType !== 'podcast' && this.mediaType !== 'book' && this.mediaType !== 'video')) {

View file

@ -246,6 +246,7 @@ class LibraryItem {
setLastScan() {
this.lastScan = Date.now()
this.updatedAt = Date.now()
this.scanVersion = version
}

View file

@ -0,0 +1,133 @@
const { getId } = require('../utils/index')
class Notification {
constructor(notification = null) {
this.id = null
this.libraryId = null
this.eventName = ''
this.urls = []
this.titleTemplate = ''
this.bodyTemplate = ''
this.type = 'info'
this.enabled = false
this.lastFiredAt = null
this.lastAttemptFailed = false
this.numConsecutiveFailedAttempts = 0
this.numTimesFired = 0
this.createdAt = null
if (notification) {
this.construct(notification)
}
}
construct(notification) {
this.id = notification.id
this.libraryId = notification.libraryId || null
this.eventName = notification.eventName
this.urls = notification.urls || []
this.titleTemplate = notification.titleTemplate || ''
this.bodyTemplate = notification.bodyTemplate || ''
this.type = notification.type || 'info'
this.enabled = !!notification.enabled
this.lastFiredAt = notification.lastFiredAt || null
this.lastAttemptFailed = !!notification.lastAttemptFailed
this.numConsecutiveFailedAttempts = notification.numConsecutiveFailedAttempts || 0
this.numTimesFired = notification.numTimesFired || 0
this.createdAt = notification.createdAt
}
toJSON() {
return {
id: this.id,
libraryId: this.libraryId,
eventName: this.eventName,
urls: this.urls,
titleTemplate: this.titleTemplate,
bodyTemplate: this.bodyTemplate,
enabled: this.enabled,
type: this.type,
lastFiredAt: this.lastFiredAt,
lastAttemptFailed: this.lastAttemptFailed,
numConsecutiveFailedAttempts: this.numConsecutiveFailedAttempts,
numTimesFired: this.numTimesFired,
createdAt: this.createdAt
}
}
setData(payload) {
this.id = getId('noti')
this.libraryId = payload.libraryId || null
this.eventName = payload.eventName
this.urls = payload.urls
this.titleTemplate = payload.titleTemplate
this.bodyTemplate = payload.bodyTemplate
this.enabled = !!payload.enabled
this.type = payload.type || null
this.createdAt = Date.now()
}
update(payload) {
if (!this.enabled && payload.enabled) {
// Reset
this.lastFiredAt = null
this.lastAttemptFailed = false
this.numConsecutiveFailedAttempts = 0
}
const keysToUpdate = ['libraryId', 'eventName', 'urls', 'titleTemplate', 'bodyTemplate', 'enabled', 'type']
var hasUpdated = false
for (const key of keysToUpdate) {
if (payload[key] !== undefined) {
if (key === 'urls') {
if (payload[key].join(',') !== this.urls.join(',')) {
this.urls = [...payload[key]]
hasUpdated = true
}
} else if (payload[key] !== this[key]) {
this[key] = payload[key]
hasUpdated = true
}
}
}
return hasUpdated
}
updateNotificationFired(success) {
this.lastFiredAt = Date.now()
this.lastAttemptFailed = !success
this.numConsecutiveFailedAttempts = success ? 0 : this.numConsecutiveFailedAttempts + 1
this.numTimesFired++
}
replaceVariablesInTemplate(templateText, data) {
const ptrn = /{{ ?([a-zA-Z]+) ?}}/mg
var match
var updatedTemplate = templateText
while ((match = ptrn.exec(templateText)) != null) {
if (data[match[1]]) {
updatedTemplate = updatedTemplate.replace(match[0], data[match[1]])
}
}
return updatedTemplate
}
parseTitleTemplate(data) {
return this.replaceVariablesInTemplate(this.titleTemplate, data)
}
parseBodyTemplate(data) {
return this.replaceVariablesInTemplate(this.bodyTemplate, data)
}
getApprisePayload(data) {
return {
urls: this.urls,
title: this.parseTitleTemplate(data),
body: this.parseBodyTemplate(data)
}
}
}
module.exports = Notification

View file

@ -22,7 +22,7 @@ class PodcastEpisodeDownload {
toJSONForClient() {
return {
id: this.id,
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.bestFilename : null,
episodeDisplayTitle: this.podcastEpisode ? this.podcastEpisode.title : null,
url: this.url,
libraryItemId: this.libraryItem ? this.libraryItem.id : null,
isDownloading: this.isDownloading,
@ -35,7 +35,7 @@ class PodcastEpisodeDownload {
}
get targetFilename() {
return sanitizeFilename(`${this.podcastEpisode.bestFilename}.mp3`)
return sanitizeFilename(`${this.podcastEpisode.title}.mp3`)
}
get targetPath() {
return Path.join(this.libraryItem.path, this.targetFilename)

View file

@ -73,7 +73,9 @@ class Stream extends EventEmitter {
AudioMimeType.FLAC,
AudioMimeType.OPUS,
AudioMimeType.WMA,
AudioMimeType.AIFF
AudioMimeType.AIFF,
AudioMimeType.WEBM,
AudioMimeType.WEBMA
]
}
get userToken() {

56
server/objects/Task.js Normal file
View file

@ -0,0 +1,56 @@
const { getId } = require('../utils/index')
class Task {
constructor() {
this.id = null
this.action = null // e.g. embed-metadata, encode-m4b, etc
this.data = null // additional info for the action like libraryItemId
this.title = null
this.description = null
this.error = null
this.isFailed = false
this.isFinished = false
this.startedAt = null
this.finishedAt = null
}
toJSON() {
return {
id: this.id,
action: this.action,
data: this.data ? { ...this.data } : {},
title: this.title,
description: this.description,
error: this.error,
isFailed: this.isFailed,
isFinished: this.isFinished,
startedAt: this.startedAt,
finishedAt: this.finishedAt
}
}
setData(action, title, description, data = {}) {
this.id = getId(action)
this.action = action
this.data = { ...data }
this.title = title
this.description = description
this.startedAt = Date.now()
}
setFailed(message) {
this.error = message
this.isFailed = true
this.failedAt = Date.now()
this.setFinished()
}
setFinished() {
this.isFinished = true
this.finishedAt = Date.now()
}
}
module.exports = Task

View file

@ -1,5 +1,6 @@
const Logger = require('../../Logger')
const { getId } = require('../../utils/index')
const { checkNamesAreEqual } = require('../../utils/parsers/parseNameString')
class Author {
constructor(author) {
@ -86,7 +87,7 @@ class Author {
Logger.error(`[Author] Author name is null (${this.id})`)
return false
}
return this.name.toLowerCase() == name.toLowerCase().trim()
return checkNamesAreEqual(this.name, name)
}
}
module.exports = Author

View file

@ -103,9 +103,8 @@ class PodcastEpisode {
return this.audioFile.duration
}
get size() { return this.audioFile.metadata.size }
get bestFilename() {
if (this.episode) return `${this.episode} - ${this.title}`
return this.title
get enclosureUrl() {
return this.enclosure ? this.enclosure.url : null
}
setData(data, index = 1) {

View file

@ -47,6 +47,19 @@ class Series {
this.updatedAt = Date.now()
}
update(series) {
if (!series) return false
const keysToUpdate = ['name', 'description']
var hasUpdated = false
for (const key of keysToUpdate) {
if (series[key] !== undefined && series[key] !== this[key]) {
this[key] = series[key]
hasUpdated = true
}
}
return hasUpdated
}
checkNameEquals(name) {
if (!name || !this.name) return false
return this.name.toLowerCase() == name.toLowerCase().trim()

View file

@ -29,7 +29,7 @@ class AudioTrack {
this.startOffset = startOffset
this.duration = audioFile.duration
this.title = audioFile.metadata.filename || ''
this.contentUrl = Path.join(`/s/item/${itemId}`, encodeUriPath(audioFile.metadata.relPath))
this.contentUrl = Path.join(`${global.RouterBasePath}/s/item/${itemId}`, encodeUriPath(audioFile.metadata.relPath))
this.mimeType = audioFile.mimeType
this.metadata = audioFile.metadata.clone()
}

View file

@ -26,7 +26,7 @@ class VideoTrack {
this.index = videoFile.index
this.duration = videoFile.duration
this.title = videoFile.metadata.filename || ''
this.contentUrl = Path.join(`/s/item/${itemId}`, encodeUriPath(videoFile.metadata.relPath))
this.contentUrl = Path.join(`${global.RouterBasePath}/s/item/${itemId}`, encodeUriPath(videoFile.metadata.relPath))
this.mimeType = videoFile.mimeType
this.metadata = videoFile.metadata.clone()
}

View file

@ -3,7 +3,7 @@ const Logger = require('../../Logger')
const BookMetadata = require('../metadata/BookMetadata')
const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
const { overdriveMediaMarkersExist, parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers')
const { parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers')
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
const { readTextFile } = require('../../utils/fileUtils')
const AudioFile = require('../files/AudioFile')
@ -111,12 +111,15 @@ class Book {
get invalidAudioFiles() {
return this.audioFiles.filter(af => af.invalid)
}
get includedAudioFiles() {
return this.audioFiles.filter(af => !af.exclude && !af.invalid)
}
get hasIssues() {
return this.missingParts.length || this.invalidAudioFiles.length
}
get tracks() {
var startOffset = 0
return this.audioFiles.filter(af => !af.exclude && !af.invalid).map((af) => {
return this.includedAudioFiles.map((af) => {
var audioTrack = new AudioTrack()
audioTrack.setData(this.libraryItemId, af, startOffset)
startOffset += audioTrack.duration
@ -396,7 +399,7 @@ class Book {
var newMissingParts = (this.missingParts || []).join(',') || ''
var wasUpdated = newMissingParts !== currMissingParts
if (wasUpdated && this.missingParts.length) {
Logger.info(`[Audiobook] "${this.name}" has ${missingParts.length} missing parts`)
Logger.info(`[Audiobook] "${this.metadata.title}" has ${missingParts.length} missing parts`)
}
return wasUpdated
@ -405,54 +408,29 @@ class Book {
setChapters(preferOverdriveMediaMarker = false) {
// If 1 audio file without chapters, then no chapters will be set
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
if (!includedAudioFiles.length) return
// If overdrive media markers are present and preferred, use those instead
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
this.chapters = overdriveChapters
return
}
}
if (includedAudioFiles.length === 1) {
// 1 audio file with chapters
if (includedAudioFiles[0].chapters) {
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
}
} else {
// IF first audio file has embedded chapters then use embedded chapters
if (includedAudioFiles[0].chapters && includedAudioFiles[0].chapters.length) {
Logger.debug(`[Book] setChapters: Using embedded chapters in audio file ${includedAudioFiles[0].metadata.path}`)
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
} else if (includedAudioFiles.length > 1) {
// Build chapters from audio files
this.chapters = []
var currChapterId = 0
var currStartTime = 0
includedAudioFiles.forEach((file) => {
// If audio file has chapters use chapters
if (file.chapters && file.chapters.length) {
file.chapters.forEach((chapter) => {
if (currStartTime > this.duration) {
Logger.warn(`[Book] Invalid chapter start time > duration`)
} else {
var chapterAlreadyExists = this.chapters.find(ch => ch.start === currStartTime)
if (!chapterAlreadyExists) {
var chapterDuration = chapter.end - chapter.start
if (chapterDuration > 0) {
var title = `Chapter ${currChapterId}`
if (chapter.title) {
title += ` (${chapter.title})`
}
var endTime = Math.min(this.duration, currStartTime + chapterDuration)
this.chapters.push({
id: currChapterId++,
start: currStartTime,
end: endTime,
title
})
currStartTime += chapterDuration
}
}
}
})
} else if (file.duration) {
// Otherwise just use track has chapter
if (file.duration) {
this.chapters.push({
id: currChapterId++,
start: currStartTime,

View file

@ -21,6 +21,7 @@ class Podcast {
this.autoDownloadSchedule = null
this.lastEpisodeCheck = 0
this.maxEpisodesToKeep = 0
this.maxNewEpisodesToDownload = 3
this.lastCoverSearch = null
this.lastCoverSearchQuery = null
@ -44,6 +45,7 @@ class Podcast {
this.autoDownloadSchedule = podcast.autoDownloadSchedule || '0 * * * *' // Added in 2.1.3 so default to hourly
this.lastEpisodeCheck = podcast.lastEpisodeCheck || 0
this.maxEpisodesToKeep = podcast.maxEpisodesToKeep || 0
this.maxNewEpisodesToDownload = podcast.maxNewEpisodesToDownload || 3
}
toJSON() {
@ -56,7 +58,8 @@ class Podcast {
autoDownloadEpisodes: this.autoDownloadEpisodes,
autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep
maxEpisodesToKeep: this.maxEpisodesToKeep,
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload
}
}
@ -70,6 +73,7 @@ class Podcast {
autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
size: this.size
}
}
@ -85,6 +89,7 @@ class Podcast {
autoDownloadSchedule: this.autoDownloadSchedule,
lastEpisodeCheck: this.lastEpisodeCheck,
maxEpisodesToKeep: this.maxEpisodesToKeep,
maxNewEpisodesToDownload: this.maxNewEpisodesToDownload,
size: this.size
}
}
@ -228,6 +233,7 @@ class Podcast {
addPodcastEpisode(podcastEpisode) {
this.episodes.push(podcastEpisode)
this.reorderEpisodes()
}
addNewEpisodeFromAudioFile(audioFile, index) {
@ -241,15 +247,13 @@ class Podcast {
reorderEpisodes() {
var hasUpdates = false
// TODO: Sort by published date
this.episodes = naturalSort(this.episodes).asc((ep) => ep.bestFilename)
this.episodes = naturalSort(this.episodes).desc((ep) => ep.publishedAt)
for (let i = 0; i < this.episodes.length; i++) {
if (this.episodes[i].index !== (i + 1)) {
this.episodes[i].index = i + 1
hasUpdates = true
}
}
this.episodes.sort((a, b) => b.index - a.index)
return hasUpdates
}

View file

@ -87,6 +87,10 @@ class AudioMetaTags {
this.tagOverdriveMediaMarker = payload.file_tag_overdrive_media_marker || null
}
setDataFromTone(tags) {
// TODO: Implement
}
updateData(payload) {
const dataMap = {
tagAlbum: payload.file_tag_album || null,

View file

@ -1,5 +1,5 @@
const Logger = require('../../Logger')
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix } = require('../../utils/index')
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
const parseNameString = require('../../utils/parsers/parseNameString')
class BookMetadata {
constructor(metadata) {
@ -62,7 +62,7 @@ class BookMetadata {
toJSONMinified() {
return {
title: this.title,
titleIgnorePrefix: this.titleIgnorePrefix,
titleIgnorePrefix: this.titlePrefixAtEnd,
subtitle: this.subtitle,
authorName: this.authorName,
authorNameLF: this.authorNameLF,
@ -83,7 +83,7 @@ class BookMetadata {
toJSONExpanded() {
return {
title: this.title,
titleIgnorePrefix: this.titleIgnorePrefix,
titleIgnorePrefix: this.titlePrefixAtEnd,
subtitle: this.subtitle,
authors: this.authors.map(a => ({ ...a })), // Author JSONMinimal with name and id
narrators: [...this.narrators],
@ -111,6 +111,9 @@ class BookMetadata {
get titleIgnorePrefix() {
return getTitleIgnorePrefix(this.title)
}
get titlePrefixAtEnd() {
return getTitlePrefixAtEnd(this.title)
}
get authorName() {
if (!this.authors.length) return ''
return this.authors.map(au => au.name).join(', ')
@ -133,6 +136,21 @@ class BookMetadata {
return `${getTitleIgnorePrefix(se.name)} #${se.sequence}`
}).join(', ')
}
get seriesNamePrefixAtEnd() {
if (!this.series.length) return ''
return this.series.map(se => {
if (!se.sequence) return getTitlePrefixAtEnd(se.name)
return `${getTitlePrefixAtEnd(se.name)} #${se.sequence}`
}).join(', ')
}
get firstSeriesName() {
if (!this.series.length) return ''
return this.series[0].name
}
get firstSeriesSequence() {
if (!this.series.length) return ''
return this.series[0].sequence
}
get narratorName() {
return this.narrators.join(', ')
}
@ -226,6 +244,7 @@ class BookMetadata {
},
{
tag: 'tagDescription',
altTag: 'tagComment',
key: 'description'
},
{

View file

@ -1,5 +1,5 @@
const Logger = require('../../Logger')
const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
const { areEquivalent, copyValue, cleanStringForSearch, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
class PodcastMetadata {
constructor(metadata) {
@ -56,7 +56,7 @@ class PodcastMetadata {
toJSONMinified() {
return {
title: this.title,
titleIgnorePrefix: this.titleIgnorePrefix,
titleIgnorePrefix: this.titlePrefixAtEnd,
author: this.author,
description: this.description,
releaseDate: this.releaseDate,
@ -80,15 +80,11 @@ class PodcastMetadata {
}
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 titlePrefixAtEnd() {
return getTitlePrefixAtEnd(this.title)
}
searchQuery(query) { // Returns key if match is found

View file

@ -1,5 +1,5 @@
const Logger = require('../../Logger')
const { areEquivalent, copyValue } = require('../../utils/index')
const { areEquivalent, copyValue, getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
class VideoMetadata {
constructor(metadata) {
@ -32,7 +32,7 @@ class VideoMetadata {
toJSONMinified() {
return {
title: this.title,
titleIgnorePrefix: this.titleIgnorePrefix,
titleIgnorePrefix: this.titlePrefixAtEnd,
description: this.description,
explicit: this.explicit,
language: this.language
@ -48,15 +48,11 @@ class VideoMetadata {
}
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 titlePrefixAtEnd() {
return getTitlePrefixAtEnd(this.title)
}
searchQuery(query) { // Returns key if match is found

View file

@ -0,0 +1,106 @@
const Logger = require('../../Logger')
const Notification = require('../Notification')
const { isNullOrNaN } = require('../../utils')
class NotificationSettings {
constructor(settings = null) {
this.id = 'notification-settings'
this.appriseType = 'api'
this.appriseApiUrl = null
this.notifications = []
this.maxFailedAttempts = 5
this.maxNotificationQueue = 20 // once reached events will be ignored
this.notificationDelay = 1000 // ms delay between firing notifications
if (settings) {
this.construct(settings)
}
}
construct(settings) {
this.appriseType = settings.appriseType
this.appriseApiUrl = settings.appriseApiUrl || null
this.notifications = (settings.notifications || []).map(n => new Notification(n))
this.maxFailedAttempts = settings.maxFailedAttempts || 5
this.maxNotificationQueue = settings.maxNotificationQueue || 20
this.notificationDelay = settings.notificationDelay || 1000
}
toJSON() {
return {
id: this.id,
appriseType: this.appriseType,
appriseApiUrl: this.appriseApiUrl,
notifications: this.notifications.map(n => n.toJSON()),
maxFailedAttempts: this.maxFailedAttempts,
maxNotificationQueue: this.maxNotificationQueue,
notificationDelay: this.notificationDelay
}
}
get isUseable() {
return !!this.appriseApiUrl
}
getActiveNotificationsForEvent(eventName) {
return this.notifications.filter(n => n.eventName === eventName && n.enabled)
}
getNotification(id) {
return this.notifications.find(n => n.id === id)
}
removeNotification(id) {
if (this.notifications.some(n => n.id === id)) {
this.notifications = this.notifications.filter(n => n.id !== id)
return true
}
return false
}
update(payload) {
if (!payload) return false
var hasUpdates = false
if (payload.appriseApiUrl !== this.appriseApiUrl) {
this.appriseApiUrl = payload.appriseApiUrl || null
hasUpdates = true
}
const _maxFailedAttempts = isNullOrNaN(payload.maxFailedAttempts) ? 5 : Number(payload.maxFailedAttempts)
if (_maxFailedAttempts !== this.maxFailedAttempts) {
this.maxFailedAttempts = _maxFailedAttempts
hasUpdates = true
}
const _maxNotificationQueue = isNullOrNaN(payload.maxNotificationQueue) ? 20 : Number(payload.maxNotificationQueue)
if (_maxNotificationQueue !== this.maxNotificationQueue) {
this.maxNotificationQueue = _maxNotificationQueue
hasUpdates = true
}
return hasUpdates
}
createNotification(payload) {
if (!payload) return false
if (!payload.eventName || !payload.urls.length) return false
const notification = new Notification()
notification.setData(payload)
this.notifications.push(notification)
return true
}
updateNotification(payload) {
if (!payload) return false
const notification = this.notifications.find(n => n.id === payload.id)
if (!notification) {
Logger.error(`[NotificationSettings] updateNotification: Notification not found ${payload.id}`)
return false
}
return notification.update(payload)
}
}
module.exports = NotificationSettings

View file

@ -1,4 +1,4 @@
const { BookCoverAspectRatio, BookshelfView } = require('../../utils/constants')
const { BookshelfView } = require('../../utils/constants')
const { isNullOrNaN } = require('../../utils')
const Logger = require('../../Logger')
@ -17,7 +17,8 @@ class ServerSettings {
this.scannerDisableWatcher = false
this.scannerPreferOverdriveMediaMarker = false
this.scannerUseSingleThreadedProber = true
this.scannerMaxThreads = 0 // 0 = defaults to CPUs * 2
this.scannerMaxThreads = 0 // Currently not being used
this.scannerUseTone = false
// Metadata - choose to store inside users library item folder
this.storeCoverWithItem = false
@ -28,8 +29,7 @@ class ServerSettings {
this.rateLimitLoginWindow = 10 * 60 * 1000 // 10 Minutes
// Backups
// this.backupSchedule = '30 1 * * *' // If false then auto-backups are disabled (default every day at 1:30am)
this.backupSchedule = false
this.backupSchedule = false // If false then auto-backups are disabled
this.backupsToKeep = 2
this.maxBackupSize = 1
this.backupMetadataCovers = true
@ -38,26 +38,23 @@ class ServerSettings {
this.loggerDailyLogsToKeep = 7
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
this.bookshelfView = BookshelfView.DETAIL
// Podcasts
this.podcastEpisodeSchedule = '0 * * * *' // Every hour
// Sorting
this.sortingIgnorePrefix = false
this.sortingPrefixes = ['the', 'a']
this.sortingPrefixes = ['the']
// Misc Flags
this.chromecastEnabled = false
this.sharedListeningStats = false
this.enableEReader = false
this.dateFormat = 'MM/dd/yyyy'
this.language = 'en-us'
this.logLevel = Logger.logLevel
@ -83,6 +80,7 @@ class ServerSettings {
this.scannerUseSingleThreadedProber = true
}
this.scannerMaxThreads = isNullOrNaN(settings.scannerMaxThreads) ? 0 : Number(settings.scannerMaxThreads)
this.scannerUseTone = !!settings.scannerUseTone
this.storeCoverWithItem = !!settings.storeCoverWithItem
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
@ -98,16 +96,16 @@ class ServerSettings {
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
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
this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a']
this.sortingPrefixes = settings.sortingPrefixes || ['the']
this.chromecastEnabled = !!settings.chromecastEnabled
this.sharedListeningStats = !!settings.sharedListeningStats
this.enableEReader = !!settings.enableEReader
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
this.language = settings.language || 'en-us'
this.logLevel = settings.logLevel || Logger.logLevel
this.version = settings.version || null
@ -141,6 +139,7 @@ class ServerSettings {
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
scannerUseSingleThreadedProber: this.scannerUseSingleThreadedProber,
scannerMaxThreads: this.scannerMaxThreads,
scannerUseTone: this.scannerUseTone,
storeCoverWithItem: this.storeCoverWithItem,
storeMetadataWithItem: this.storeMetadataWithItem,
rateLimitLoginRequests: this.rateLimitLoginRequests,
@ -151,7 +150,6 @@ class ServerSettings {
backupMetadataCovers: this.backupMetadataCovers,
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
coverAspectRatio: this.coverAspectRatio,
homeBookshelfView: this.homeBookshelfView,
bookshelfView: this.bookshelfView,
sortingIgnorePrefix: this.sortingIgnorePrefix,
@ -160,6 +158,7 @@ class ServerSettings {
sharedListeningStats: this.sharedListeningStats,
enableEReader: this.enableEReader,
dateFormat: this.dateFormat,
language: this.language,
logLevel: this.logLevel,
version: this.version
}

View file

@ -8,6 +8,7 @@ class MediaProgress {
this.progress = null // 0 to 1
this.currentTime = null // seconds
this.isFinished = false
this.hideFromContinueListening = false
this.lastUpdate = null
this.startedAt = null
@ -27,6 +28,7 @@ class MediaProgress {
progress: this.progress,
currentTime: this.currentTime,
isFinished: this.isFinished,
hideFromContinueListening: this.hideFromContinueListening,
lastUpdate: this.lastUpdate,
startedAt: this.startedAt,
finishedAt: this.finishedAt
@ -41,6 +43,7 @@ class MediaProgress {
this.progress = progress.progress
this.currentTime = progress.currentTime
this.isFinished = !!progress.isFinished
this.hideFromContinueListening = !!progress.hideFromContinueListening
this.lastUpdate = progress.lastUpdate
this.startedAt = progress.startedAt
this.finishedAt = progress.finishedAt || null
@ -58,6 +61,7 @@ class MediaProgress {
this.progress = Math.min(1, (progress.progress || 0))
this.currentTime = progress.currentTime || 0
this.isFinished = !!progress.isFinished || this.progress == 1
this.hideFromContinueListening = !!progress.hideFromContinueListening
this.lastUpdate = Date.now()
this.startedAt = Date.now()
this.finishedAt = null
@ -102,9 +106,21 @@ class MediaProgress {
this.startedAt = Date.now()
}
if (hasUpdates) {
if (payload.hideFromContinueListening === undefined) {
// Reset this flag when the media progress is updated
this.hideFromContinueListening = false
}
this.lastUpdate = Date.now()
}
return hasUpdates
}
removeFromContinueListening() {
if (this.hideFromContinueListening) return false
this.hideFromContinueListening = true
return true
}
}
module.exports = MediaProgress

View file

@ -15,6 +15,7 @@ class User {
this.createdAt = null
this.mediaProgress = []
this.seriesHideFromContinueListening = [] // Series IDs that should not show on home page continue listening
this.bookmarks = []
this.settings = {}
@ -93,6 +94,7 @@ class User {
type: this.type,
token: this.token,
mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
seriesHideFromContinueListening: [...this.seriesHideFromContinueListening],
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
isActive: this.isActive,
isLocked: this.isLocked,
@ -112,6 +114,7 @@ class User {
type: this.type,
token: this.token,
mediaProgress: this.mediaProgress ? this.mediaProgress.map(li => li.toJSON()) : [],
seriesHideFromContinueListening: [...this.seriesHideFromContinueListening],
bookmarks: this.bookmarks ? this.bookmarks.map(b => b.toJSON()) : [],
isActive: this.isActive,
isLocked: this.isLocked,
@ -128,8 +131,8 @@ class User {
toJSONForPublic(sessions, libraryItems) {
var userSession = sessions ? sessions.find(s => s.userId === this.id) : null
var session = null
if (session) {
var libraryItem = libraryItems.find(li => li.id === session.libraryItemId)
if (userSession) {
var libraryItem = libraryItems.find(li => li.id === userSession.libraryItemId)
if (libraryItem) {
session = userSession.toJSONForClient(libraryItem)
}
@ -162,6 +165,9 @@ class User {
this.bookmarks = user.bookmarks.filter(bm => typeof bm.libraryItemId == 'string').map(bm => new AudioBookmark(bm))
}
this.seriesHideFromContinueListening = []
if (user.seriesHideFromContinueListening) this.seriesHideFromContinueListening = [...user.seriesHideFromContinueListening]
this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive
this.isLocked = user.type === 'root' ? false : !!user.isLocked
this.lastSeen = user.lastSeen || null
@ -197,6 +203,13 @@ class User {
}
})
if (payload.seriesHideFromContinueListening && Array.isArray(payload.seriesHideFromContinueListening)) {
if (this.seriesHideFromContinueListening.join(',') !== payload.seriesHideFromContinueListening.join(',')) {
hasUpdates = true
this.seriesHideFromContinueListening = [...payload.seriesHideFromContinueListening]
}
}
// And update permissions
if (payload.permissions) {
for (const key in payload.permissions) {
@ -254,16 +267,42 @@ class User {
return firstAccessibleLibrary.id
}
// Returns most recent media progress w/ `media` object and optionally an `episode` object
getMostRecentItemProgress(libraryItems) {
if (!this.mediaProgress.length) return null
var lip = this.mediaProgress.map(lip => lip.toJSON())
lip.sort((a, b) => b.lastUpdate - a.lastUpdate)
var mostRecentWithLip = lip.find(li => libraryItems.find(_li => _li.id === li.id))
if (!mostRecentWithLip) return null
var libraryItem = libraryItems.find(li => li.id === mostRecentWithLip.id)
var mediaProgressObjects = this.mediaProgress.map(lip => lip.toJSON())
mediaProgressObjects.sort((a, b) => b.lastUpdate - a.lastUpdate)
var libraryItemMedia = null
var progressEpisode = null
// Find the most recent progress that still has a libraryItem and episode
var mostRecentProgress = mediaProgressObjects.find((progress) => {
const libraryItem = libraryItems.find(li => li.id === progress.libraryItemId)
if (!libraryItem) {
Logger.warn('[User] Library item not found for users progress ' + progress.libraryItemId)
return false
} else if (progress.episodeId) {
const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(progress.episodeId) : null
if (!episode) {
Logger.warn(`[User] Episode ${progress.episodeId} not found for user media progress, podcast: ${libraryItem.media.metadata.title}`)
return false
} else {
libraryItemMedia = libraryItem.media.toJSONExpanded()
progressEpisode = episode.toJSON()
return true
}
} else {
libraryItemMedia = libraryItem.media.toJSONExpanded()
return true
}
})
if (!mostRecentProgress) return null
return {
...mostRecentWithLip,
media: libraryItem.media.toJSONExpanded()
...mostRecentProgress,
media: libraryItemMedia,
episode: progressEpisode
}
}
@ -298,7 +337,13 @@ class User {
return wasUpdated
}
removeMediaProgress(libraryItemId) {
removeMediaProgress(id) {
if (!this.mediaProgress.some(mp => mp.id === id)) return false
this.mediaProgress = this.mediaProgress.filter(mp => mp.id !== id)
return true
}
removeMediaProgressForLibraryItem(libraryItemId) {
if (!this.mediaProgress.some(lip => lip.libraryItemId == libraryItemId)) return false
this.mediaProgress = this.mediaProgress.filter(lip => lip.libraryItemId != libraryItemId)
return true
@ -379,5 +424,27 @@ class User {
removeBookmark(libraryItemId, time) {
this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time))
}
checkShouldHideSeriesFromContinueListening(seriesId) {
return this.seriesHideFromContinueListening.includes(seriesId)
}
addSeriesToHideFromContinueListening(seriesId) {
if (this.seriesHideFromContinueListening.includes(seriesId)) return false
this.seriesHideFromContinueListening.push(seriesId)
return true
}
removeSeriesFromHideFromContinueListening(seriesId) {
if (!this.seriesHideFromContinueListening.includes(seriesId)) return false
this.seriesHideFromContinueListening = this.seriesHideFromContinueListening.filter(sid => sid !== seriesId)
return true
}
removeProgressFromContinueListening(progressId) {
const progress = this.mediaProgress.find(mp => mp.id === progressId)
if (!progress) return false
return progress.removeFromContinueListening()
}
}
module.exports = User