mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-30 06:49:39 +00:00
Merge remote-tracking branch 'origin/master' into auth_passportjs
This commit is contained in:
commit
dd9a3858d7
249 changed files with 15582 additions and 7835 deletions
|
|
@ -5,8 +5,8 @@ const version = require('../../package.json').version
|
|||
class Backup {
|
||||
constructor(data = null) {
|
||||
this.id = null
|
||||
this.key = null // Special key for pre-version checks
|
||||
this.datePretty = null
|
||||
this.backupMetadataCovers = null
|
||||
|
||||
this.backupDirPath = null
|
||||
this.filename = null
|
||||
|
|
@ -23,9 +23,9 @@ class Backup {
|
|||
}
|
||||
|
||||
get detailsString() {
|
||||
var details = []
|
||||
const details = []
|
||||
details.push(this.id)
|
||||
details.push(this.backupMetadataCovers ? '1' : '0')
|
||||
details.push(this.key)
|
||||
details.push(this.createdAt)
|
||||
details.push(this.serverVersion)
|
||||
return details.join('\n')
|
||||
|
|
@ -33,7 +33,9 @@ class Backup {
|
|||
|
||||
construct(data) {
|
||||
this.id = data.details[0]
|
||||
this.backupMetadataCovers = data.details[1] === '1'
|
||||
this.key = data.details[1]
|
||||
if (this.key == 1) this.key = null // v2.2.23 and below backups stored '1' here
|
||||
|
||||
this.createdAt = Number(data.details[2])
|
||||
this.serverVersion = data.details[3] || null
|
||||
|
||||
|
|
@ -48,7 +50,7 @@ class Backup {
|
|||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
backupMetadataCovers: this.backupMetadataCovers,
|
||||
key: this.key,
|
||||
backupDirPath: this.backupDirPath,
|
||||
datePretty: this.datePretty,
|
||||
fullPath: this.fullPath,
|
||||
|
|
@ -60,13 +62,12 @@ class Backup {
|
|||
}
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
setData(backupDirPath) {
|
||||
this.id = date.format(new Date(), 'YYYY-MM-DD[T]HHmm')
|
||||
this.key = 'sqlite'
|
||||
this.datePretty = date.format(new Date(), 'ddd, MMM D YYYY HH:mm')
|
||||
|
||||
this.backupMetadataCovers = data.backupMetadataCovers
|
||||
|
||||
this.backupDirPath = data.backupDirPath
|
||||
this.backupDirPath = backupDirPath
|
||||
|
||||
this.filename = this.id + '.audiobookshelf'
|
||||
this.path = Path.join('backups', this.filename)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Collection {
|
||||
constructor(collection) {
|
||||
this.id = null
|
||||
this.libraryId = null
|
||||
this.userId = null
|
||||
|
||||
this.name = null
|
||||
this.description = null
|
||||
|
|
@ -25,7 +24,6 @@ class Collection {
|
|||
return {
|
||||
id: this.id,
|
||||
libraryId: this.libraryId,
|
||||
userId: this.userId,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
cover: this.cover,
|
||||
|
|
@ -60,7 +58,6 @@ class Collection {
|
|||
construct(collection) {
|
||||
this.id = collection.id
|
||||
this.libraryId = collection.libraryId
|
||||
this.userId = collection.userId
|
||||
this.name = collection.name
|
||||
this.description = collection.description || null
|
||||
this.cover = collection.cover || null
|
||||
|
|
@ -71,11 +68,10 @@ class Collection {
|
|||
}
|
||||
|
||||
setData(data) {
|
||||
if (!data.userId || !data.libraryId || !data.name) {
|
||||
if (!data.libraryId || !data.name) {
|
||||
return false
|
||||
}
|
||||
this.id = getId('col')
|
||||
this.userId = data.userId
|
||||
this.id = uuidv4()
|
||||
this.libraryId = data.libraryId
|
||||
this.name = data.name
|
||||
this.description = data.description || null
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class DeviceInfo {
|
||||
constructor(deviceInfo = null) {
|
||||
this.id = null
|
||||
this.userId = null
|
||||
this.deviceId = null
|
||||
this.ipAddress = null
|
||||
|
||||
|
|
@ -16,7 +20,8 @@ class DeviceInfo {
|
|||
this.model = null
|
||||
this.sdkVersion = null // Android Only
|
||||
|
||||
this.serverVersion = null
|
||||
this.clientName = null
|
||||
this.deviceName = null
|
||||
|
||||
if (deviceInfo) {
|
||||
this.construct(deviceInfo)
|
||||
|
|
@ -33,6 +38,8 @@ class DeviceInfo {
|
|||
|
||||
toJSON() {
|
||||
const obj = {
|
||||
id: this.id,
|
||||
userId: this.userId,
|
||||
deviceId: this.deviceId,
|
||||
ipAddress: this.ipAddress,
|
||||
browserName: this.browserName,
|
||||
|
|
@ -44,7 +51,8 @@ class DeviceInfo {
|
|||
manufacturer: this.manufacturer,
|
||||
model: this.model,
|
||||
sdkVersion: this.sdkVersion,
|
||||
serverVersion: this.serverVersion
|
||||
clientName: this.clientName,
|
||||
deviceName: this.deviceName
|
||||
}
|
||||
for (const key in obj) {
|
||||
if (obj[key] === null || obj[key] === undefined) {
|
||||
|
|
@ -65,6 +73,7 @@ class DeviceInfo {
|
|||
// When client doesn't send a device id
|
||||
getTempDeviceId() {
|
||||
const keys = [
|
||||
this.userId,
|
||||
this.browserName,
|
||||
this.browserVersion,
|
||||
this.osName,
|
||||
|
|
@ -78,8 +87,10 @@ class DeviceInfo {
|
|||
return 'temp-' + Buffer.from(keys.join('-'), 'utf-8').toString('base64')
|
||||
}
|
||||
|
||||
setData(ip, ua, clientDeviceInfo, serverVersion) {
|
||||
this.deviceId = clientDeviceInfo?.deviceId || null
|
||||
setData(ip, ua, clientDeviceInfo, serverVersion, userId) {
|
||||
this.id = uuidv4()
|
||||
this.userId = userId
|
||||
this.deviceId = clientDeviceInfo?.deviceId || this.id
|
||||
this.ipAddress = ip || null
|
||||
|
||||
this.browserName = ua?.browser.name || null
|
||||
|
|
@ -88,16 +99,54 @@ class DeviceInfo {
|
|||
this.osVersion = ua?.os.version || null
|
||||
this.deviceType = ua?.device.type || null
|
||||
|
||||
this.clientVersion = clientDeviceInfo?.clientVersion || null
|
||||
this.clientVersion = clientDeviceInfo?.clientVersion || serverVersion
|
||||
this.manufacturer = clientDeviceInfo?.manufacturer || null
|
||||
this.model = clientDeviceInfo?.model || null
|
||||
this.sdkVersion = clientDeviceInfo?.sdkVersion || null
|
||||
|
||||
this.serverVersion = serverVersion || null
|
||||
this.clientName = clientDeviceInfo?.clientName || null
|
||||
if (this.sdkVersion) {
|
||||
if (!this.clientName) this.clientName = 'Abs Android'
|
||||
this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}`
|
||||
} else if (this.model) {
|
||||
if (!this.clientName) this.clientName = 'Abs iOS'
|
||||
this.deviceName = `${this.manufacturer || 'Unknown'} ${this.model || ''}`
|
||||
} else if (this.osName && this.browserName) {
|
||||
if (!this.clientName) this.clientName = 'Abs Web'
|
||||
this.deviceName = `${this.osName} ${this.osVersion || 'N/A'} ${this.browserName}`
|
||||
} else if (!this.clientName) {
|
||||
this.clientName = 'Unknown'
|
||||
}
|
||||
|
||||
if (!this.deviceId) {
|
||||
this.deviceId = this.getTempDeviceId()
|
||||
}
|
||||
}
|
||||
|
||||
update(deviceInfo) {
|
||||
const deviceInfoJson = deviceInfo.toJSON ? deviceInfo.toJSON() : deviceInfo
|
||||
const existingDeviceInfoJson = this.toJSON()
|
||||
|
||||
let hasUpdates = false
|
||||
for (const key in deviceInfoJson) {
|
||||
if (['id', 'deviceId'].includes(key)) continue
|
||||
|
||||
if (deviceInfoJson[key] !== existingDeviceInfoJson[key]) {
|
||||
this[key] = deviceInfoJson[key]
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in existingDeviceInfoJson) {
|
||||
if (['id', 'deviceId'].includes(key)) continue
|
||||
|
||||
if (existingDeviceInfoJson[key] && !deviceInfoJson[key]) {
|
||||
this[key] = null
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpdates
|
||||
}
|
||||
}
|
||||
module.exports = DeviceInfo
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
const uuidv4 = require("uuid").v4
|
||||
const FeedMeta = require('./FeedMeta')
|
||||
const FeedEpisode = require('./FeedEpisode')
|
||||
const RSS = require('../libs/rss')
|
||||
|
|
@ -39,6 +40,7 @@ class Feed {
|
|||
this.userId = feed.userId
|
||||
this.entityType = feed.entityType
|
||||
this.entityId = feed.entityId
|
||||
this.entityUpdatedAt = feed.entityUpdatedAt
|
||||
this.coverPath = feed.coverPath
|
||||
this.serverAddress = feed.serverAddress
|
||||
this.feedUrl = feed.feedUrl
|
||||
|
|
@ -77,7 +79,6 @@ class Feed {
|
|||
|
||||
getEpisodePath(id) {
|
||||
var episode = this.episodes.find(ep => ep.id === id)
|
||||
console.log('getEpisodePath=', id, episode)
|
||||
if (!episode) return null
|
||||
return episode.fullPath
|
||||
}
|
||||
|
|
@ -90,7 +91,7 @@ class Feed {
|
|||
const feedUrl = `${serverAddress}/feed/${slug}`
|
||||
const author = isPodcast ? mediaMetadata.author : mediaMetadata.authorName
|
||||
|
||||
this.id = slug
|
||||
this.id = uuidv4()
|
||||
this.slug = slug
|
||||
this.userId = userId
|
||||
this.entityType = 'libraryItem'
|
||||
|
|
@ -179,7 +180,7 @@ class Feed {
|
|||
const itemsWithTracks = collectionExpanded.books.filter(libraryItem => libraryItem.media.tracks.length)
|
||||
const firstItemWithCover = itemsWithTracks.find(item => item.media.coverPath)
|
||||
|
||||
this.id = slug
|
||||
this.id = uuidv4()
|
||||
this.slug = slug
|
||||
this.userId = userId
|
||||
this.entityType = 'collection'
|
||||
|
|
@ -253,7 +254,7 @@ class Feed {
|
|||
const libraryId = itemsWithTracks[0].libraryId
|
||||
const firstItemWithCover = itemsWithTracks.find(li => li.media.coverPath)
|
||||
|
||||
this.id = slug
|
||||
this.id = uuidv4()
|
||||
this.slug = slug
|
||||
this.userId = userId
|
||||
this.entityType = 'series'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const Path = require('path')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const date = require('../libs/dateAndTime')
|
||||
const { secondsToTimestamp } = require('../utils/index')
|
||||
|
||||
|
|
@ -98,13 +98,11 @@ class FeedEpisode {
|
|||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = null) {
|
||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||
let timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
||||
let episodeId = String(audioTrack.index)
|
||||
let episodeId = uuidv4()
|
||||
|
||||
// Additional offset can be used for collections/series
|
||||
if (additionalOffset !== null && !isNaN(additionalOffset)) {
|
||||
timeOffset += Number(additionalOffset) * 1000
|
||||
|
||||
episodeId = String(additionalOffset) + '-' + episodeId
|
||||
}
|
||||
|
||||
// e.g. Track 1 will have a pub date before Track 2
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { getId } = require("../utils")
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Folder {
|
||||
constructor(folder = null) {
|
||||
|
|
@ -29,7 +29,7 @@ class Folder {
|
|||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = data.id ? data.id : getId('fol')
|
||||
this.id = data.id || uuidv4()
|
||||
this.fullPath = data.fullPath
|
||||
this.libraryId = data.libraryId
|
||||
this.addedAt = Date.now()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
const uuidv4 = require("uuid").v4
|
||||
const Folder = require('./Folder')
|
||||
const LibrarySettings = require('./settings/LibrarySettings')
|
||||
const { getId } = require('../utils/index')
|
||||
const { filePathToPOSIX } = require('../utils/fileUtils')
|
||||
|
||||
class Library {
|
||||
constructor(library = null) {
|
||||
this.id = null
|
||||
this.oldLibraryId = null // TODO: Temp
|
||||
this.name = null
|
||||
this.folders = []
|
||||
this.displayOrder = 1
|
||||
|
|
@ -33,9 +34,13 @@ class Library {
|
|||
get isMusic() {
|
||||
return this.mediaType === 'music'
|
||||
}
|
||||
get isBook() {
|
||||
return this.mediaType === 'book'
|
||||
}
|
||||
|
||||
construct(library) {
|
||||
this.id = library.id
|
||||
this.oldLibraryId = library.oldLibraryId
|
||||
this.name = library.name
|
||||
this.folders = (library.folders || []).map(f => new Folder(f))
|
||||
this.displayOrder = library.displayOrder || 1
|
||||
|
|
@ -71,6 +76,7 @@ class Library {
|
|||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
oldLibraryId: this.oldLibraryId,
|
||||
name: this.name,
|
||||
folders: (this.folders || []).map(f => f.toJSON()),
|
||||
displayOrder: this.displayOrder,
|
||||
|
|
@ -84,7 +90,7 @@ class Library {
|
|||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = data.id ? data.id : getId('lib')
|
||||
this.id = data.id || uuidv4()
|
||||
this.name = data.name
|
||||
if (data.folder) {
|
||||
this.folders = [
|
||||
|
|
|
|||
|
|
@ -1,20 +1,22 @@
|
|||
const uuidv4 = require("uuid").v4
|
||||
const fs = require('../libs/fsExtra')
|
||||
const Path = require('path')
|
||||
const { version } = require('../../package.json')
|
||||
const Logger = require('../Logger')
|
||||
const abmetadataGenerator = require('../utils/abmetadataGenerator')
|
||||
const abmetadataGenerator = require('../utils/generators/abmetadataGenerator')
|
||||
const LibraryFile = require('./files/LibraryFile')
|
||||
const Book = require('./mediaTypes/Book')
|
||||
const Podcast = require('./mediaTypes/Podcast')
|
||||
const Video = require('./mediaTypes/Video')
|
||||
const Music = require('./mediaTypes/Music')
|
||||
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
|
||||
const { areEquivalent, copyValue, cleanStringForSearch } = require('../utils/index')
|
||||
const { filePathToPOSIX } = require('../utils/fileUtils')
|
||||
|
||||
class LibraryItem {
|
||||
constructor(libraryItem = null) {
|
||||
this.id = null
|
||||
this.ino = null // Inode
|
||||
this.oldLibraryItemId = null
|
||||
|
||||
this.libraryId = null
|
||||
this.folderId = null
|
||||
|
|
@ -51,6 +53,7 @@ class LibraryItem {
|
|||
construct(libraryItem) {
|
||||
this.id = libraryItem.id
|
||||
this.ino = libraryItem.ino || null
|
||||
this.oldLibraryItemId = libraryItem.oldLibraryItemId
|
||||
this.libraryId = libraryItem.libraryId
|
||||
this.folderId = libraryItem.folderId
|
||||
this.path = libraryItem.path
|
||||
|
|
@ -80,12 +83,23 @@ class LibraryItem {
|
|||
this.media.libraryItemId = this.id
|
||||
|
||||
this.libraryFiles = libraryItem.libraryFiles.map(f => new LibraryFile(f))
|
||||
|
||||
// Migration for v2.2.23 to set ebook library files as supplementary
|
||||
if (this.isBook && this.media.ebookFile) {
|
||||
for (const libraryFile of this.libraryFiles) {
|
||||
if (libraryFile.isEBookFile && libraryFile.isSupplementary === null) {
|
||||
libraryFile.isSupplementary = this.media.ebookFile.ino !== libraryFile.ino
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
ino: this.ino,
|
||||
oldLibraryItemId: this.oldLibraryItemId,
|
||||
libraryId: this.libraryId,
|
||||
folderId: this.folderId,
|
||||
path: this.path,
|
||||
|
|
@ -110,6 +124,7 @@ class LibraryItem {
|
|||
return {
|
||||
id: this.id,
|
||||
ino: this.ino,
|
||||
oldLibraryItemId: this.oldLibraryItemId,
|
||||
libraryId: this.libraryId,
|
||||
folderId: this.folderId,
|
||||
path: this.path,
|
||||
|
|
@ -134,6 +149,7 @@ class LibraryItem {
|
|||
return {
|
||||
id: this.id,
|
||||
ino: this.ino,
|
||||
oldLibraryItemId: this.oldLibraryItemId,
|
||||
libraryId: this.libraryId,
|
||||
folderId: this.folderId,
|
||||
path: this.path,
|
||||
|
|
@ -181,7 +197,7 @@ class LibraryItem {
|
|||
|
||||
// Data comes from scandir library item data
|
||||
setData(libraryMediaType, payload) {
|
||||
this.id = getId('li')
|
||||
this.id = uuidv4()
|
||||
this.mediaType = libraryMediaType
|
||||
if (libraryMediaType === 'video') {
|
||||
this.media = new Video()
|
||||
|
|
@ -192,6 +208,7 @@ class LibraryItem {
|
|||
} else if (libraryMediaType === 'music') {
|
||||
this.media = new Music()
|
||||
}
|
||||
this.media.id = uuidv4()
|
||||
this.media.libraryItemId = this.id
|
||||
|
||||
for (const key in payload) {
|
||||
|
|
@ -432,21 +449,41 @@ class LibraryItem {
|
|||
}
|
||||
|
||||
// Set metadata from files
|
||||
async syncFiles(preferOpfMetadata) {
|
||||
async syncFiles(preferOpfMetadata, librarySettings) {
|
||||
let hasUpdated = false
|
||||
|
||||
if (this.mediaType === 'book') {
|
||||
// Add/update ebook file (ebooks that were removed are removed in checkScanData)
|
||||
this.libraryFiles.forEach((lf) => {
|
||||
if (lf.fileType === 'ebook') {
|
||||
if (!this.media.ebookFile) {
|
||||
this.media.setEbookFile(lf)
|
||||
hasUpdated = true
|
||||
} else if (this.media.ebookFile.ino == lf.ino && this.media.ebookFile.updateFromLibraryFile(lf)) { // Update existing ebookFile
|
||||
hasUpdated = true
|
||||
}
|
||||
if (this.isBook) {
|
||||
// Add/update ebook files (ebooks that were removed are removed in checkScanData)
|
||||
if (librarySettings.audiobooksOnly) {
|
||||
hasUpdated = this.media.ebookFile
|
||||
if (hasUpdated) {
|
||||
// If library was set to audiobooks only then set primary ebook as supplementary
|
||||
Logger.info(`[LibraryItem] Library is audiobooks only so setting ebook "${this.media.ebookFile.metadata.filename}" as supplementary`)
|
||||
}
|
||||
})
|
||||
this.setPrimaryEbook(null)
|
||||
} else if (this.media.ebookFile) {
|
||||
const matchingLibraryFile = this.libraryFiles.find(lf => lf.ino === this.media.ebookFile.ino)
|
||||
if (matchingLibraryFile && this.media.ebookFile.updateFromLibraryFile(matchingLibraryFile)) {
|
||||
hasUpdated = true
|
||||
}
|
||||
// Set any other ebook files as supplementary
|
||||
const suppEbookLibraryFiles = this.libraryFiles.filter(lf => lf.isEBookFile && !lf.isSupplementary && this.media.ebookFile.ino !== lf.ino)
|
||||
if (suppEbookLibraryFiles.length) {
|
||||
for (const libraryFile of suppEbookLibraryFiles) {
|
||||
libraryFile.isSupplementary = true
|
||||
}
|
||||
hasUpdated = true
|
||||
}
|
||||
} else {
|
||||
const ebookLibraryFiles = this.libraryFiles.filter(lf => lf.isEBookFile && !lf.isSupplementary)
|
||||
|
||||
// Prefer epub ebook then fallback to first other ebook file
|
||||
const ebookLibraryFile = ebookLibraryFiles.find(lf => lf.metadata.format === 'epub') || ebookLibraryFiles[0]
|
||||
if (ebookLibraryFile) {
|
||||
this.setPrimaryEbook(ebookLibraryFile)
|
||||
hasUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set cover image if not set
|
||||
|
|
@ -486,7 +523,10 @@ class LibraryItem {
|
|||
return this.media.getDirectPlayTracklist(episodeId)
|
||||
}
|
||||
|
||||
// Saves metadata.abs file
|
||||
/**
|
||||
* Save metadata.json/metadata.abs file
|
||||
* @returns {boolean} true if saved
|
||||
*/
|
||||
async saveMetadata() {
|
||||
if (this.mediaType === 'video' || this.mediaType === 'music') return
|
||||
|
||||
|
|
@ -519,6 +559,7 @@ class LibraryItem {
|
|||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||
this.libraryFiles.push(newLibraryFile)
|
||||
}
|
||||
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
|
||||
|
||||
return true
|
||||
}).catch((error) => {
|
||||
|
|
@ -562,5 +603,20 @@ class LibraryItem {
|
|||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the EBookFile from a LibraryFile
|
||||
* If null then ebookFile will be removed from the book
|
||||
* all ebook library files that are not primary are marked as supplementary
|
||||
*
|
||||
* @param {LibraryFile} [libraryFile]
|
||||
*/
|
||||
setPrimaryEbook(ebookLibraryFile = null) {
|
||||
const ebookLibraryFiles = this.libraryFiles.filter(lf => lf.isEBookFile)
|
||||
for (const libraryFile of ebookLibraryFiles) {
|
||||
libraryFile.isSupplementary = ebookLibraryFile?.ino !== libraryFile.ino
|
||||
}
|
||||
this.media.setEbookFile(ebookLibraryFile)
|
||||
}
|
||||
}
|
||||
module.exports = LibraryItem
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Notification {
|
||||
constructor(notification = null) {
|
||||
|
|
@ -57,7 +57,7 @@ class Notification {
|
|||
}
|
||||
|
||||
setData(payload) {
|
||||
this.id = getId('noti')
|
||||
this.id = uuidv4()
|
||||
this.libraryId = payload.libraryId || null
|
||||
this.eventName = payload.eventName
|
||||
this.urls = payload.urls
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const date = require('../libs/dateAndTime')
|
||||
const { getId } = require('../utils/index')
|
||||
const { PlayMethod } = require('../utils/constants')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const serverVersion = require('../../package.json').version
|
||||
const BookMetadata = require('./metadata/BookMetadata')
|
||||
const PodcastMetadata = require('./metadata/PodcastMetadata')
|
||||
const DeviceInfo = require('./DeviceInfo')
|
||||
|
|
@ -12,6 +12,7 @@ class PlaybackSession {
|
|||
this.userId = null
|
||||
this.libraryId = null
|
||||
this.libraryItemId = null
|
||||
this.bookId = null
|
||||
this.episodeId = null
|
||||
|
||||
this.mediaType = null
|
||||
|
|
@ -25,6 +26,7 @@ class PlaybackSession {
|
|||
this.playMethod = null
|
||||
this.mediaPlayer = null
|
||||
this.deviceInfo = null
|
||||
this.serverVersion = null
|
||||
|
||||
this.date = null
|
||||
this.dayOfWeek = null
|
||||
|
|
@ -53,6 +55,7 @@ class PlaybackSession {
|
|||
userId: this.userId,
|
||||
libraryId: this.libraryId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
bookId: this.bookId,
|
||||
episodeId: this.episodeId,
|
||||
mediaType: this.mediaType,
|
||||
mediaMetadata: this.mediaMetadata?.toJSON() || null,
|
||||
|
|
@ -64,6 +67,7 @@ class PlaybackSession {
|
|||
playMethod: this.playMethod,
|
||||
mediaPlayer: this.mediaPlayer,
|
||||
deviceInfo: this.deviceInfo?.toJSON() || null,
|
||||
serverVersion: this.serverVersion,
|
||||
date: this.date,
|
||||
dayOfWeek: this.dayOfWeek,
|
||||
timeListening: this.timeListening,
|
||||
|
|
@ -80,6 +84,7 @@ class PlaybackSession {
|
|||
userId: this.userId,
|
||||
libraryId: this.libraryId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
bookId: this.bookId,
|
||||
episodeId: this.episodeId,
|
||||
mediaType: this.mediaType,
|
||||
mediaMetadata: this.mediaMetadata?.toJSON() || null,
|
||||
|
|
@ -91,6 +96,7 @@ class PlaybackSession {
|
|||
playMethod: this.playMethod,
|
||||
mediaPlayer: this.mediaPlayer,
|
||||
deviceInfo: this.deviceInfo?.toJSON() || null,
|
||||
serverVersion: this.serverVersion,
|
||||
date: this.date,
|
||||
dayOfWeek: this.dayOfWeek,
|
||||
timeListening: this.timeListening,
|
||||
|
|
@ -109,12 +115,31 @@ class PlaybackSession {
|
|||
this.userId = session.userId
|
||||
this.libraryId = session.libraryId || null
|
||||
this.libraryItemId = session.libraryItemId
|
||||
this.bookId = session.bookId || null
|
||||
this.episodeId = session.episodeId
|
||||
this.mediaType = session.mediaType
|
||||
this.duration = session.duration
|
||||
this.playMethod = session.playMethod
|
||||
this.mediaPlayer = session.mediaPlayer || null
|
||||
this.deviceInfo = new DeviceInfo(session.deviceInfo)
|
||||
|
||||
// Temp do not store old IDs
|
||||
if (this.libraryId?.startsWith('lib_')) {
|
||||
this.libraryId = null
|
||||
}
|
||||
if (this.libraryItemId?.startsWith('li_') || this.libraryItemId?.startsWith('local_')) {
|
||||
this.libraryItemId = null
|
||||
}
|
||||
if (this.episodeId?.startsWith('ep_') || this.episodeId?.startsWith('local_')) {
|
||||
this.episodeId = null
|
||||
}
|
||||
|
||||
if (session.deviceInfo instanceof DeviceInfo) {
|
||||
this.deviceInfo = new DeviceInfo(session.deviceInfo.toJSON())
|
||||
} else {
|
||||
this.deviceInfo = new DeviceInfo(session.deviceInfo)
|
||||
}
|
||||
|
||||
this.serverVersion = session.serverVersion
|
||||
this.chapters = session.chapters || []
|
||||
|
||||
this.mediaMetadata = null
|
||||
|
|
@ -152,7 +177,7 @@ class PlaybackSession {
|
|||
}
|
||||
|
||||
get deviceId() {
|
||||
return this.deviceInfo?.deviceId
|
||||
return this.deviceInfo?.id
|
||||
}
|
||||
|
||||
get deviceDescription() {
|
||||
|
|
@ -170,10 +195,11 @@ class PlaybackSession {
|
|||
}
|
||||
|
||||
setData(libraryItem, user, mediaPlayer, deviceInfo, startTime, episodeId = null) {
|
||||
this.id = getId('play')
|
||||
this.id = uuidv4()
|
||||
this.userId = user.id
|
||||
this.libraryId = libraryItem.libraryId
|
||||
this.libraryItemId = libraryItem.id
|
||||
this.bookId = episodeId ? null : libraryItem.media.id
|
||||
this.episodeId = episodeId
|
||||
this.mediaType = libraryItem.mediaType
|
||||
this.mediaMetadata = libraryItem.media.metadata.clone()
|
||||
|
|
@ -190,6 +216,7 @@ class PlaybackSession {
|
|||
|
||||
this.mediaPlayer = mediaPlayer
|
||||
this.deviceInfo = deviceInfo || new DeviceInfo()
|
||||
this.serverVersion = serverVersion
|
||||
|
||||
this.timeListening = 0
|
||||
this.startTime = startTime
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
const Logger = require('../Logger')
|
||||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Playlist {
|
||||
constructor(playlist) {
|
||||
|
|
@ -88,7 +87,7 @@ class Playlist {
|
|||
if (!data.userId || !data.libraryId || !data.name) {
|
||||
return false
|
||||
}
|
||||
this.id = getId('pl')
|
||||
this.id = uuidv4()
|
||||
this.userId = data.userId
|
||||
this.libraryId = data.libraryId
|
||||
this.name = data.name
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
const Path = require('path')
|
||||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { sanitizeFilename } = require('../utils/fileUtils')
|
||||
const globals = require('../utils/globals')
|
||||
|
||||
|
|
@ -15,6 +15,8 @@ class PodcastEpisodeDownload {
|
|||
this.isFinished = false
|
||||
this.failed = false
|
||||
|
||||
this.appendEpisodeId = false
|
||||
|
||||
this.startedAt = null
|
||||
this.createdAt = null
|
||||
this.finishedAt = null
|
||||
|
|
@ -29,6 +31,7 @@ class PodcastEpisodeDownload {
|
|||
libraryId: this.libraryId || null,
|
||||
isFinished: this.isFinished,
|
||||
failed: this.failed,
|
||||
appendEpisodeId: this.appendEpisodeId,
|
||||
startedAt: this.startedAt,
|
||||
createdAt: this.createdAt,
|
||||
finishedAt: this.finishedAt,
|
||||
|
|
@ -52,7 +55,9 @@ class PodcastEpisodeDownload {
|
|||
}
|
||||
|
||||
get targetFilename() {
|
||||
return sanitizeFilename(`${this.podcastEpisode.title}.${this.fileExtension}`)
|
||||
const appendage = this.appendEpisodeId ? ` (${this.podcastEpisode.id})` : ''
|
||||
const filename = `${this.podcastEpisode.title}${appendage}.${this.fileExtension}`
|
||||
return sanitizeFilename(filename)
|
||||
}
|
||||
get targetPath() {
|
||||
return Path.join(this.libraryItem.path, this.targetFilename)
|
||||
|
|
@ -65,7 +70,7 @@ class PodcastEpisodeDownload {
|
|||
}
|
||||
|
||||
setData(podcastEpisode, libraryItem, isAutoDownload, libraryId) {
|
||||
this.id = getId('epdl')
|
||||
this.id = uuidv4()
|
||||
this.podcastEpisode = podcastEpisode
|
||||
|
||||
const url = podcastEpisode.enclosure.url
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const Ffmpeg = require('../libs/fluentFfmpeg')
|
|||
const { secondsToTimestamp } = require('../utils/index')
|
||||
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
||||
const { AudioMimeType } = require('../utils/constants')
|
||||
const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
|
||||
const hlsPlaylistGenerator = require('../utils/generators/hlsPlaylistGenerator')
|
||||
const AudioTrack = require('./files/AudioTrack')
|
||||
|
||||
class Stream extends EventEmitter {
|
||||
|
|
@ -83,7 +83,8 @@ class Stream extends EventEmitter {
|
|||
AudioMimeType.AIFF,
|
||||
AudioMimeType.WEBM,
|
||||
AudioMimeType.WEBMA,
|
||||
AudioMimeType.AWB
|
||||
AudioMimeType.AWB,
|
||||
AudioMimeType.CAF
|
||||
]
|
||||
}
|
||||
get codecsToForceAAC() {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const { getId } = require('../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class Task {
|
||||
constructor() {
|
||||
|
|
@ -9,6 +9,7 @@ class Task {
|
|||
this.title = null
|
||||
this.description = null
|
||||
this.error = null
|
||||
this.showSuccess = false // If true client side should keep the task visible after success
|
||||
|
||||
this.isFailed = false
|
||||
this.isFinished = false
|
||||
|
|
@ -25,6 +26,7 @@ class Task {
|
|||
title: this.title,
|
||||
description: this.description,
|
||||
error: this.error,
|
||||
showSuccess: this.showSuccess,
|
||||
isFailed: this.isFailed,
|
||||
isFinished: this.isFinished,
|
||||
startedAt: this.startedAt,
|
||||
|
|
@ -32,12 +34,13 @@ class Task {
|
|||
}
|
||||
}
|
||||
|
||||
setData(action, title, description, data = {}) {
|
||||
this.id = getId(action)
|
||||
setData(action, title, description, showSuccess, data = {}) {
|
||||
this.id = uuidv4()
|
||||
this.action = action
|
||||
this.data = { ...data }
|
||||
this.title = title
|
||||
this.description = description
|
||||
this.showSuccess = showSuccess
|
||||
this.startedAt = Date.now()
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +51,10 @@ class Task {
|
|||
this.setFinished()
|
||||
}
|
||||
|
||||
setFinished() {
|
||||
setFinished(newDescription = null) {
|
||||
if (newDescription) {
|
||||
this.description = newDescription
|
||||
}
|
||||
this.isFinished = true
|
||||
this.finishedAt = Date.now()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const Logger = require('../../Logger')
|
||||
const { getId } = require('../../utils/index')
|
||||
const { checkNamesAreEqual } = require('../../utils/parsers/parseNameString')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { checkNamesAreEqual, nameToLastFirst } = require('../../utils/parsers/parseNameString')
|
||||
|
||||
class Author {
|
||||
constructor(author) {
|
||||
|
|
@ -11,6 +11,7 @@ class Author {
|
|||
this.imagePath = null
|
||||
this.addedAt = null
|
||||
this.updatedAt = null
|
||||
this.libraryId = null
|
||||
|
||||
if (author) {
|
||||
this.construct(author)
|
||||
|
|
@ -25,6 +26,12 @@ class Author {
|
|||
this.imagePath = author.imagePath
|
||||
this.addedAt = author.addedAt
|
||||
this.updatedAt = author.updatedAt
|
||||
this.libraryId = author.libraryId
|
||||
}
|
||||
|
||||
get lastFirst() {
|
||||
if (!this.name) return ''
|
||||
return nameToLastFirst(this.name)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
|
@ -35,7 +42,8 @@ class Author {
|
|||
description: this.description,
|
||||
imagePath: this.imagePath,
|
||||
addedAt: this.addedAt,
|
||||
updatedAt: this.updatedAt
|
||||
updatedAt: this.updatedAt,
|
||||
libraryId: this.libraryId
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,14 +60,18 @@ class Author {
|
|||
}
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = getId('aut')
|
||||
this.name = data.name
|
||||
setData(data, libraryId) {
|
||||
this.id = uuidv4()
|
||||
if (!data.name) {
|
||||
Logger.error(`[Author] setData: Setting author data without a name`, data)
|
||||
}
|
||||
this.name = data.name || ''
|
||||
this.description = data.description || null
|
||||
this.asin = data.asin || null
|
||||
this.imagePath = data.imagePath || null
|
||||
this.addedAt = Date.now()
|
||||
this.updatedAt = Date.now()
|
||||
this.libraryId = libraryId
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
const uuidv4 = require("uuid").v4
|
||||
const Path = require('path')
|
||||
const Logger = require('../../Logger')
|
||||
const { getId, cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
|
||||
const { cleanStringForSearch, areEquivalent, copyValue } = require('../../utils/index')
|
||||
const AudioFile = require('../files/AudioFile')
|
||||
const AudioTrack = require('../files/AudioTrack')
|
||||
|
||||
class PodcastEpisode {
|
||||
constructor(episode) {
|
||||
this.libraryItemId = null
|
||||
this.podcastId = null
|
||||
this.id = null
|
||||
this.oldEpisodeId = null
|
||||
this.index = null
|
||||
|
||||
this.season = null
|
||||
|
|
@ -32,7 +35,9 @@ class PodcastEpisode {
|
|||
|
||||
construct(episode) {
|
||||
this.libraryItemId = episode.libraryItemId
|
||||
this.podcastId = episode.podcastId
|
||||
this.id = episode.id
|
||||
this.oldEpisodeId = episode.oldEpisodeId
|
||||
this.index = episode.index
|
||||
this.season = episode.season
|
||||
this.episode = episode.episode
|
||||
|
|
@ -54,7 +59,9 @@ class PodcastEpisode {
|
|||
toJSON() {
|
||||
return {
|
||||
libraryItemId: this.libraryItemId,
|
||||
podcastId: this.podcastId,
|
||||
id: this.id,
|
||||
oldEpisodeId: this.oldEpisodeId,
|
||||
index: this.index,
|
||||
season: this.season,
|
||||
episode: this.episode,
|
||||
|
|
@ -75,7 +82,9 @@ class PodcastEpisode {
|
|||
toJSONExpanded() {
|
||||
return {
|
||||
libraryItemId: this.libraryItemId,
|
||||
podcastId: this.podcastId,
|
||||
id: this.id,
|
||||
oldEpisodeId: this.oldEpisodeId,
|
||||
index: this.index,
|
||||
season: this.season,
|
||||
episode: this.episode,
|
||||
|
|
@ -109,7 +118,7 @@ class PodcastEpisode {
|
|||
}
|
||||
get size() { return this.audioFile.metadata.size }
|
||||
get enclosureUrl() {
|
||||
return this.enclosure ? this.enclosure.url : null
|
||||
return this.enclosure?.url || null
|
||||
}
|
||||
get pubYear() {
|
||||
if (!this.publishedAt) return null
|
||||
|
|
@ -117,7 +126,7 @@ class PodcastEpisode {
|
|||
}
|
||||
|
||||
setData(data, index = 1) {
|
||||
this.id = getId('ep')
|
||||
this.id = uuidv4()
|
||||
this.index = index
|
||||
this.title = data.title
|
||||
this.subtitle = data.subtitle || ''
|
||||
|
|
@ -133,7 +142,7 @@ class PodcastEpisode {
|
|||
}
|
||||
|
||||
setDataFromAudioFile(audioFile, index) {
|
||||
this.id = getId('ep')
|
||||
this.id = uuidv4()
|
||||
this.audioFile = audioFile
|
||||
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
|
||||
this.index = index
|
||||
|
|
@ -148,8 +157,13 @@ class PodcastEpisode {
|
|||
update(payload) {
|
||||
let hasUpdates = false
|
||||
for (const key in this.toJSON()) {
|
||||
if (payload[key] != undefined && !areEquivalent(payload[key], this[key])) {
|
||||
this[key] = copyValue(payload[key])
|
||||
let newValue = payload[key]
|
||||
if (newValue === "") newValue = null
|
||||
let existingValue = this[key]
|
||||
if (existingValue === "") existingValue = null
|
||||
|
||||
if (newValue != undefined && !areEquivalent(newValue, existingValue)) {
|
||||
this[key] = copyValue(newValue)
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { getId } = require('../../utils/index')
|
||||
const uuidv4 = require("uuid").v4
|
||||
const { getTitleIgnorePrefix } = require('../../utils/index')
|
||||
|
||||
class Series {
|
||||
constructor(series) {
|
||||
|
|
@ -7,6 +8,7 @@ class Series {
|
|||
this.description = null
|
||||
this.addedAt = null
|
||||
this.updatedAt = null
|
||||
this.libraryId = null
|
||||
|
||||
if (series) {
|
||||
this.construct(series)
|
||||
|
|
@ -19,6 +21,12 @@ class Series {
|
|||
this.description = series.description || null
|
||||
this.addedAt = series.addedAt
|
||||
this.updatedAt = series.updatedAt
|
||||
this.libraryId = series.libraryId
|
||||
}
|
||||
|
||||
get nameIgnorePrefix() {
|
||||
if (!this.name) return ''
|
||||
return getTitleIgnorePrefix(this.name)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
|
@ -27,7 +35,8 @@ class Series {
|
|||
name: this.name,
|
||||
description: this.description,
|
||||
addedAt: this.addedAt,
|
||||
updatedAt: this.updatedAt
|
||||
updatedAt: this.updatedAt,
|
||||
libraryId: this.libraryId
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -39,12 +48,13 @@ class Series {
|
|||
}
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.id = getId('ser')
|
||||
setData(data, libraryId) {
|
||||
this.id = uuidv4()
|
||||
this.name = data.name
|
||||
this.description = data.description || null
|
||||
this.addedAt = Date.now()
|
||||
this.updatedAt = Date.now()
|
||||
this.libraryId = libraryId
|
||||
}
|
||||
|
||||
update(series) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
const Path = require('path')
|
||||
const { encodeUriPath } = require('../../utils/fileUtils')
|
||||
|
||||
class AudioTrack {
|
||||
constructor() {
|
||||
this.index = null
|
||||
|
|
@ -22,7 +19,7 @@ class AudioTrack {
|
|||
contentUrl: this.contentUrl,
|
||||
mimeType: this.mimeType,
|
||||
codec: this.codec,
|
||||
metadata: this.metadata ? this.metadata.toJSON() : null
|
||||
metadata: this.metadata?.toJSON() || null
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -31,7 +28,8 @@ class AudioTrack {
|
|||
this.startOffset = startOffset
|
||||
this.duration = audioFile.duration
|
||||
this.title = audioFile.metadata.filename || ''
|
||||
this.contentUrl = Path.join(`${global.RouterBasePath}/s/item/${itemId}`, encodeUriPath(audioFile.metadata.relPath))
|
||||
|
||||
this.contentUrl = `${global.RouterBasePath}/api/items/${itemId}/file/${audioFile.ino}`
|
||||
this.mimeType = audioFile.mimeType
|
||||
this.codec = audioFile.codec || null
|
||||
this.metadata = audioFile.metadata.clone()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class LibraryFile {
|
|||
constructor(file) {
|
||||
this.ino = null
|
||||
this.metadata = null
|
||||
this.isSupplementary = null
|
||||
this.addedAt = null
|
||||
this.updatedAt = null
|
||||
|
||||
|
|
@ -18,6 +19,7 @@ class LibraryFile {
|
|||
construct(file) {
|
||||
this.ino = file.ino
|
||||
this.metadata = new FileMetadata(file.metadata)
|
||||
this.isSupplementary = file.isSupplementary === undefined ? null : file.isSupplementary
|
||||
this.addedAt = file.addedAt
|
||||
this.updatedAt = file.updatedAt
|
||||
}
|
||||
|
|
@ -26,6 +28,7 @@ class LibraryFile {
|
|||
return {
|
||||
ino: this.ino,
|
||||
metadata: this.metadata.toJSON(),
|
||||
isSupplementary: this.isSupplementary,
|
||||
addedAt: this.addedAt,
|
||||
updatedAt: this.updatedAt,
|
||||
fileType: this.fileType
|
||||
|
|
@ -50,6 +53,10 @@ class LibraryFile {
|
|||
return this.fileType === 'audio' || this.fileType === 'ebook' || this.fileType === 'video'
|
||||
}
|
||||
|
||||
get isEBookFile() {
|
||||
return this.fileType === 'ebook'
|
||||
}
|
||||
|
||||
get isOPFFile() {
|
||||
return this.metadata.ext === '.opf'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class VideoTrack {
|
|||
this.index = videoFile.index
|
||||
this.duration = videoFile.duration
|
||||
this.title = videoFile.metadata.filename || ''
|
||||
this.contentUrl = Path.join(`${global.RouterBasePath}/s/item/${itemId}`, encodeUriPath(videoFile.metadata.relPath))
|
||||
this.contentUrl = Path.join(`${global.RouterBasePath}/api/items/${itemId}/file/${videoFile.ino}`, encodeUriPath(videoFile.metadata.relPath))
|
||||
this.mimeType = videoFile.mimeType
|
||||
this.codec = videoFile.codec
|
||||
this.metadata = videoFile.metadata.clone()
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ const BookMetadata = require('../metadata/BookMetadata')
|
|||
const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
|
||||
const { parseOpfMetadataXML } = require('../../utils/parsers/parseOpfMetadata')
|
||||
const { parseOverdriveMediaMarkersAsChapters } = require('../../utils/parsers/parseOverdriveMediaMarkers')
|
||||
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||
const abmetadataGenerator = require('../../utils/generators/abmetadataGenerator')
|
||||
const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils')
|
||||
const AudioFile = require('../files/AudioFile')
|
||||
const AudioTrack = require('../files/AudioTrack')
|
||||
|
|
@ -12,6 +12,7 @@ const EBookFile = require('../files/EBookFile')
|
|||
|
||||
class Book {
|
||||
constructor(book) {
|
||||
this.id = null
|
||||
this.libraryItemId = null
|
||||
this.metadata = null
|
||||
|
||||
|
|
@ -32,6 +33,7 @@ class Book {
|
|||
}
|
||||
|
||||
construct(book) {
|
||||
this.id = book.id
|
||||
this.libraryItemId = book.libraryItemId
|
||||
this.metadata = new BookMetadata(book.metadata)
|
||||
this.coverPath = book.coverPath
|
||||
|
|
@ -46,6 +48,7 @@ class Book {
|
|||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSON(),
|
||||
coverPath: this.coverPath,
|
||||
|
|
@ -59,6 +62,7 @@ class Book {
|
|||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
id: this.id,
|
||||
metadata: this.metadata.toJSONMinified(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
|
|
@ -75,6 +79,7 @@ class Book {
|
|||
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSONExpanded(),
|
||||
coverPath: this.coverPath,
|
||||
|
|
@ -142,6 +147,9 @@ class Book {
|
|||
get numTracks() {
|
||||
return this.tracks.length
|
||||
}
|
||||
get isEBookOnly() {
|
||||
return this.ebookFile && !this.numTracks
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
const json = this.toJSON()
|
||||
|
|
@ -195,6 +203,7 @@ class Book {
|
|||
this.coverPath = coverPath
|
||||
return true
|
||||
}
|
||||
|
||||
removeFileWithInode(inode) {
|
||||
if (this.audioFiles.some(af => af.ino === inode)) {
|
||||
this.audioFiles = this.audioFiles.filter(af => af.ino !== inode)
|
||||
|
|
@ -207,8 +216,13 @@ class Book {
|
|||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audio file or ebook file from inode
|
||||
* @param {string} inode
|
||||
* @returns {(AudioFile|EBookFile|null)}
|
||||
*/
|
||||
findFileWithInode(inode) {
|
||||
var audioFile = this.audioFiles.find(af => af.ino === inode)
|
||||
const audioFile = this.audioFiles.find(af => af.ino === inode)
|
||||
if (audioFile) return audioFile
|
||||
if (this.ebookFile && this.ebookFile.ino === inode) return this.ebookFile
|
||||
return null
|
||||
|
|
@ -367,10 +381,20 @@ class Book {
|
|||
return payload
|
||||
}
|
||||
|
||||
setEbookFile(libraryFile) {
|
||||
var ebookFile = new EBookFile()
|
||||
ebookFile.setData(libraryFile)
|
||||
this.ebookFile = ebookFile
|
||||
/**
|
||||
* Set the EBookFile from a LibraryFile
|
||||
* If null then ebookFile will be removed from the book
|
||||
*
|
||||
* @param {LibraryFile} [libraryFile]
|
||||
*/
|
||||
setEbookFile(libraryFile = null) {
|
||||
if (!libraryFile) {
|
||||
this.ebookFile = null
|
||||
} else {
|
||||
const ebookFile = new EBookFile()
|
||||
ebookFile.setData(libraryFile)
|
||||
this.ebookFile = ebookFile
|
||||
}
|
||||
}
|
||||
|
||||
addAudioFile(audioFile) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const Logger = require('../../Logger')
|
|||
const PodcastEpisode = require('../entities/PodcastEpisode')
|
||||
const PodcastMetadata = require('../metadata/PodcastMetadata')
|
||||
const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
|
||||
const abmetadataGenerator = require('../../utils/abmetadataGenerator')
|
||||
const abmetadataGenerator = require('../../utils/generators/abmetadataGenerator')
|
||||
const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils')
|
||||
const { createNewSortInstance } = require('../../libs/fastSort')
|
||||
const naturalSort = createNewSortInstance({
|
||||
|
|
@ -11,6 +11,7 @@ const naturalSort = createNewSortInstance({
|
|||
|
||||
class Podcast {
|
||||
constructor(podcast) {
|
||||
this.id = null
|
||||
this.libraryItemId = null
|
||||
this.metadata = null
|
||||
this.coverPath = null
|
||||
|
|
@ -32,6 +33,7 @@ class Podcast {
|
|||
}
|
||||
|
||||
construct(podcast) {
|
||||
this.id = podcast.id
|
||||
this.libraryItemId = podcast.libraryItemId
|
||||
this.metadata = new PodcastMetadata(podcast.metadata)
|
||||
this.coverPath = podcast.coverPath
|
||||
|
|
@ -50,6 +52,7 @@ class Podcast {
|
|||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSON(),
|
||||
coverPath: this.coverPath,
|
||||
|
|
@ -65,6 +68,7 @@ class Podcast {
|
|||
|
||||
toJSONMinified() {
|
||||
return {
|
||||
id: this.id,
|
||||
metadata: this.metadata.toJSONMinified(),
|
||||
coverPath: this.coverPath,
|
||||
tags: [...this.tags],
|
||||
|
|
@ -80,6 +84,7 @@ class Podcast {
|
|||
|
||||
toJSONExpanded() {
|
||||
return {
|
||||
id: this.id,
|
||||
libraryItemId: this.libraryItemId,
|
||||
metadata: this.metadata.toJSONExpanded(),
|
||||
coverPath: this.coverPath,
|
||||
|
|
@ -280,30 +285,17 @@ class Podcast {
|
|||
|
||||
addPodcastEpisode(podcastEpisode) {
|
||||
this.episodes.push(podcastEpisode)
|
||||
this.reorderEpisodes()
|
||||
}
|
||||
|
||||
addNewEpisodeFromAudioFile(audioFile, index) {
|
||||
var pe = new PodcastEpisode()
|
||||
const pe = new PodcastEpisode()
|
||||
pe.libraryItemId = this.libraryItemId
|
||||
pe.podcastId = this.id
|
||||
audioFile.index = 1 // Only 1 audio file per episode
|
||||
pe.setDataFromAudioFile(audioFile, index)
|
||||
this.episodes.push(pe)
|
||||
}
|
||||
|
||||
reorderEpisodes() {
|
||||
var hasUpdates = false
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
removeEpisode(episodeId) {
|
||||
const episode = this.episodes.find(ep => ep.id === episodeId)
|
||||
if (episode) {
|
||||
|
|
@ -329,6 +321,11 @@ class Podcast {
|
|||
}
|
||||
|
||||
getEpisode(episodeId) {
|
||||
if (!episodeId) return null
|
||||
|
||||
// Support old episode ids for mobile downloads
|
||||
if (episodeId.startsWith('ep_')) return this.episodes.find(ep => ep.oldEpisodeId == episodeId)
|
||||
|
||||
return this.episodes.find(ep => ep.id == episodeId)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ class BookMetadata {
|
|||
|
||||
// Updates author name
|
||||
updateAuthor(updatedAuthor) {
|
||||
var author = this.authors.find(au => au.id === updatedAuthor.id)
|
||||
const author = this.authors.find(au => au.id === updatedAuthor.id)
|
||||
if (!author || author.name == updatedAuthor.name) return false
|
||||
author.name = updatedAuthor.name
|
||||
return true
|
||||
|
|
|
|||
104
server/objects/settings/EmailSettings.js
Normal file
104
server/objects/settings/EmailSettings.js
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
const Logger = require('../../Logger')
|
||||
const { areEquivalent, copyValue, isNullOrNaN } = require('../../utils')
|
||||
|
||||
// REF: https://nodemailer.com/smtp/
|
||||
class EmailSettings {
|
||||
constructor(settings = null) {
|
||||
this.id = 'email-settings'
|
||||
this.host = null
|
||||
this.port = 465
|
||||
this.secure = true
|
||||
this.user = null
|
||||
this.pass = null
|
||||
this.testAddress = null
|
||||
this.fromAddress = null
|
||||
|
||||
// Array of { name:String, email:String }
|
||||
this.ereaderDevices = []
|
||||
|
||||
if (settings) {
|
||||
this.construct(settings)
|
||||
}
|
||||
}
|
||||
|
||||
construct(settings) {
|
||||
this.host = settings.host
|
||||
this.port = settings.port
|
||||
this.secure = !!settings.secure
|
||||
this.user = settings.user
|
||||
this.pass = settings.pass
|
||||
this.testAddress = settings.testAddress
|
||||
this.fromAddress = settings.fromAddress
|
||||
this.ereaderDevices = settings.ereaderDevices?.map(d => ({ ...d })) || []
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
host: this.host,
|
||||
port: this.port,
|
||||
secure: this.secure,
|
||||
user: this.user,
|
||||
pass: this.pass,
|
||||
testAddress: this.testAddress,
|
||||
fromAddress: this.fromAddress,
|
||||
ereaderDevices: this.ereaderDevices.map(d => ({ ...d }))
|
||||
}
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
if (!payload) return false
|
||||
|
||||
if (payload.port !== undefined) {
|
||||
if (isNullOrNaN(payload.port)) payload.port = 465
|
||||
else payload.port = Number(payload.port)
|
||||
}
|
||||
if (payload.secure !== undefined) payload.secure = !!payload.secure
|
||||
|
||||
if (payload.ereaderDevices !== undefined && !Array.isArray(payload.ereaderDevices)) payload.ereaderDevices = undefined
|
||||
|
||||
let hasUpdates = false
|
||||
|
||||
const json = this.toJSON()
|
||||
for (const key in json) {
|
||||
if (key === 'id') continue
|
||||
|
||||
if (payload[key] !== undefined && !areEquivalent(payload[key], json[key])) {
|
||||
this[key] = copyValue(payload[key])
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
getTransportObject() {
|
||||
const payload = {
|
||||
host: this.host,
|
||||
secure: this.secure
|
||||
}
|
||||
if (this.port) payload.port = this.port
|
||||
if (this.user && this.pass !== undefined) {
|
||||
payload.auth = {
|
||||
user: this.user,
|
||||
pass: this.pass
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
getEReaderDevices(user) {
|
||||
// Only accessible to admin or up
|
||||
if (!user.isAdminOrUp) {
|
||||
return []
|
||||
}
|
||||
|
||||
return this.ereaderDevices.map(d => ({ ...d }))
|
||||
}
|
||||
|
||||
getEReaderDevice(deviceName) {
|
||||
return this.ereaderDevices.find(d => d.name === deviceName)
|
||||
}
|
||||
}
|
||||
module.exports = EmailSettings
|
||||
|
|
@ -7,6 +7,8 @@ class LibrarySettings {
|
|||
this.skipMatchingMediaWithAsin = false
|
||||
this.skipMatchingMediaWithIsbn = false
|
||||
this.autoScanCronExpression = null
|
||||
this.audiobooksOnly = false
|
||||
this.hideSingleBookSeries = false // Do not show series that only have 1 book
|
||||
|
||||
if (settings) {
|
||||
this.construct(settings)
|
||||
|
|
@ -19,6 +21,8 @@ class LibrarySettings {
|
|||
this.skipMatchingMediaWithAsin = !!settings.skipMatchingMediaWithAsin
|
||||
this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn
|
||||
this.autoScanCronExpression = settings.autoScanCronExpression || null
|
||||
this.audiobooksOnly = !!settings.audiobooksOnly
|
||||
this.hideSingleBookSeries = !!settings.hideSingleBookSeries
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
|
@ -27,12 +31,14 @@ class LibrarySettings {
|
|||
disableWatcher: this.disableWatcher,
|
||||
skipMatchingMediaWithAsin: this.skipMatchingMediaWithAsin,
|
||||
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn,
|
||||
autoScanCronExpression: this.autoScanCronExpression
|
||||
autoScanCronExpression: this.autoScanCronExpression,
|
||||
audiobooksOnly: this.audiobooksOnly,
|
||||
hideSingleBookSeries: this.hideSingleBookSeries
|
||||
}
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
var hasUpdates = false
|
||||
let hasUpdates = false
|
||||
for (const key in payload) {
|
||||
if (this[key] !== payload[key]) {
|
||||
this[key] = payload[key]
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ class ServerSettings {
|
|||
this.scannerPreferMatchedMetadata = false
|
||||
this.scannerDisableWatcher = false
|
||||
this.scannerPreferOverdriveMediaMarker = false
|
||||
this.scannerUseTone = false
|
||||
|
||||
// Metadata - choose to store inside users library item folder
|
||||
this.storeCoverWithItem = false
|
||||
|
|
@ -30,7 +29,6 @@ class ServerSettings {
|
|||
this.backupSchedule = false // If false then auto-backups are disabled
|
||||
this.backupsToKeep = 2
|
||||
this.maxBackupSize = 1
|
||||
this.backupMetadataCovers = true
|
||||
|
||||
// Logger
|
||||
this.loggerDailyLogsToKeep = 7
|
||||
|
|
@ -49,7 +47,6 @@ class ServerSettings {
|
|||
|
||||
// Misc Flags
|
||||
this.chromecastEnabled = false
|
||||
this.enableEReader = false
|
||||
this.dateFormat = 'MM/dd/yyyy'
|
||||
this.timeFormat = 'HH:mm'
|
||||
this.language = 'en-us'
|
||||
|
|
@ -91,7 +88,6 @@ class ServerSettings {
|
|||
this.scannerPreferMatchedMetadata = !!settings.scannerPreferMatchedMetadata
|
||||
this.scannerDisableWatcher = !!settings.scannerDisableWatcher
|
||||
this.scannerPreferOverdriveMediaMarker = !!settings.scannerPreferOverdriveMediaMarker
|
||||
this.scannerUseTone = !!settings.scannerUseTone
|
||||
|
||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
||||
|
|
@ -103,7 +99,6 @@ class ServerSettings {
|
|||
this.backupSchedule = settings.backupSchedule || false
|
||||
this.backupsToKeep = settings.backupsToKeep || 2
|
||||
this.maxBackupSize = settings.maxBackupSize || 1
|
||||
this.backupMetadataCovers = settings.backupMetadataCovers !== false
|
||||
|
||||
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
||||
this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2
|
||||
|
|
@ -114,7 +109,6 @@ class ServerSettings {
|
|||
this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix
|
||||
this.sortingPrefixes = settings.sortingPrefixes || ['the']
|
||||
this.chromecastEnabled = !!settings.chromecastEnabled
|
||||
this.enableEReader = !!settings.enableEReader
|
||||
this.dateFormat = settings.dateFormat || 'MM/dd/yyyy'
|
||||
this.timeFormat = settings.timeFormat || 'HH:mm'
|
||||
this.language = settings.language || 'en-us'
|
||||
|
|
@ -205,7 +199,6 @@ class ServerSettings {
|
|||
scannerPreferMatchedMetadata: this.scannerPreferMatchedMetadata,
|
||||
scannerDisableWatcher: this.scannerDisableWatcher,
|
||||
scannerPreferOverdriveMediaMarker: this.scannerPreferOverdriveMediaMarker,
|
||||
scannerUseTone: this.scannerUseTone,
|
||||
storeCoverWithItem: this.storeCoverWithItem,
|
||||
storeMetadataWithItem: this.storeMetadataWithItem,
|
||||
metadataFileFormat: this.metadataFileFormat,
|
||||
|
|
@ -214,7 +207,6 @@ class ServerSettings {
|
|||
backupSchedule: this.backupSchedule,
|
||||
backupsToKeep: this.backupsToKeep,
|
||||
maxBackupSize: this.maxBackupSize,
|
||||
backupMetadataCovers: this.backupMetadataCovers,
|
||||
loggerDailyLogsToKeep: this.loggerDailyLogsToKeep,
|
||||
loggerScannerLogsToKeep: this.loggerScannerLogsToKeep,
|
||||
homeBookshelfView: this.homeBookshelfView,
|
||||
|
|
@ -222,7 +214,6 @@ class ServerSettings {
|
|||
sortingIgnorePrefix: this.sortingIgnorePrefix,
|
||||
sortingPrefixes: [...this.sortingPrefixes],
|
||||
chromecastEnabled: this.chromecastEnabled,
|
||||
enableEReader: this.enableEReader,
|
||||
dateFormat: this.dateFormat,
|
||||
timeFormat: this.timeFormat,
|
||||
language: this.language,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
const uuidv4 = require("uuid").v4
|
||||
|
||||
class MediaProgress {
|
||||
constructor(progress) {
|
||||
this.id = null
|
||||
this.userId = null
|
||||
this.libraryItemId = null
|
||||
this.episodeId = null // For podcasts
|
||||
|
||||
this.mediaItemId = null // For use in new data model
|
||||
this.mediaItemType = null // For use in new data model
|
||||
|
||||
this.duration = null
|
||||
this.progress = null // 0 to 1
|
||||
this.currentTime = null // seconds
|
||||
|
|
@ -25,8 +31,11 @@ class MediaProgress {
|
|||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
userId: this.userId,
|
||||
libraryItemId: this.libraryItemId,
|
||||
episodeId: this.episodeId,
|
||||
mediaItemId: this.mediaItemId,
|
||||
mediaItemType: this.mediaItemType,
|
||||
duration: this.duration,
|
||||
progress: this.progress,
|
||||
currentTime: this.currentTime,
|
||||
|
|
@ -42,8 +51,11 @@ class MediaProgress {
|
|||
|
||||
construct(progress) {
|
||||
this.id = progress.id
|
||||
this.userId = progress.userId
|
||||
this.libraryItemId = progress.libraryItemId
|
||||
this.episodeId = progress.episodeId
|
||||
this.mediaItemId = progress.mediaItemId
|
||||
this.mediaItemType = progress.mediaItemType
|
||||
this.duration = progress.duration || 0
|
||||
this.progress = progress.progress
|
||||
this.currentTime = progress.currentTime || 0
|
||||
|
|
@ -57,13 +69,23 @@ class MediaProgress {
|
|||
}
|
||||
|
||||
get inProgress() {
|
||||
return !this.isFinished && (this.progress > 0 || this.ebookLocation != null)
|
||||
return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0))
|
||||
}
|
||||
|
||||
setData(libraryItemId, progress, episodeId = null) {
|
||||
this.id = episodeId ? `${libraryItemId}-${episodeId}` : libraryItemId
|
||||
this.libraryItemId = libraryItemId
|
||||
get notStarted() {
|
||||
return !this.isFinished && this.progress == 0
|
||||
}
|
||||
|
||||
setData(libraryItem, progress, episodeId, userId) {
|
||||
this.id = uuidv4()
|
||||
this.userId = userId
|
||||
this.libraryItemId = libraryItem.id
|
||||
this.episodeId = episodeId
|
||||
|
||||
// PodcastEpisodeId or BookId
|
||||
this.mediaItemId = episodeId || libraryItem.media.id
|
||||
this.mediaItemType = episodeId ? 'podcastEpisode' : 'book'
|
||||
|
||||
this.duration = progress.duration || 0
|
||||
this.progress = Math.min(1, (progress.progress || 0))
|
||||
this.currentTime = progress.currentTime || 0
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const MediaProgress = require('./MediaProgress')
|
|||
class User {
|
||||
constructor(user) {
|
||||
this.id = null
|
||||
this.oldUserId = null // TODO: Temp for keeping old access tokens
|
||||
this.username = null
|
||||
this.pash = null
|
||||
this.type = null
|
||||
|
|
@ -73,6 +74,7 @@ class User {
|
|||
toJSON() {
|
||||
return {
|
||||
id: this.id,
|
||||
oldUserId: this.oldUserId,
|
||||
username: this.username,
|
||||
pash: this.pash,
|
||||
type: this.type,
|
||||
|
|
@ -93,6 +95,7 @@ class User {
|
|||
toJSONForBrowser(hideRootToken = false, minimal = false) {
|
||||
const json = {
|
||||
id: this.id,
|
||||
oldUserId: this.oldUserId,
|
||||
username: this.username,
|
||||
type: this.type,
|
||||
token: (this.type === 'root' && hideRootToken) ? '' : this.token,
|
||||
|
|
@ -126,6 +129,7 @@ class User {
|
|||
}
|
||||
return {
|
||||
id: this.id,
|
||||
oldUserId: this.oldUserId,
|
||||
username: this.username,
|
||||
type: this.type,
|
||||
session,
|
||||
|
|
@ -137,6 +141,7 @@ class User {
|
|||
|
||||
construct(user) {
|
||||
this.id = user.id
|
||||
this.oldUserId = user.oldUserId
|
||||
this.username = user.username
|
||||
this.pash = user.pash
|
||||
this.type = user.type
|
||||
|
|
@ -253,11 +258,15 @@ class User {
|
|||
return hasUpdates
|
||||
}
|
||||
|
||||
getDefaultLibraryId(libraries) {
|
||||
/**
|
||||
* Get first available library id for user
|
||||
*
|
||||
* @param {string[]} libraryIds
|
||||
* @returns {string|null}
|
||||
*/
|
||||
getDefaultLibraryId(libraryIds) {
|
||||
// Libraries should already be in ascending display order, find first accessible
|
||||
var firstAccessibleLibrary = libraries.find(lib => this.checkCanAccessLibrary(lib.id))
|
||||
if (!firstAccessibleLibrary) return null
|
||||
return firstAccessibleLibrary.id
|
||||
return libraryIds.find(lid => this.checkCanAccessLibrary(lid)) || null
|
||||
}
|
||||
|
||||
// Returns most recent media progress w/ `media` object and optionally an `episode` object
|
||||
|
|
@ -320,7 +329,7 @@ class User {
|
|||
if (!itemProgress) {
|
||||
const newItemProgress = new MediaProgress()
|
||||
|
||||
newItemProgress.setData(libraryItem.id, updatePayload, episodeId)
|
||||
newItemProgress.setData(libraryItem, updatePayload, episodeId, this.id)
|
||||
this.mediaProgress.push(newItemProgress)
|
||||
return true
|
||||
}
|
||||
|
|
@ -336,12 +345,6 @@ class User {
|
|||
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
|
||||
}
|
||||
|
||||
checkCanAccessLibrary(libraryId) {
|
||||
if (this.permissions.accessAllLibraries) return true
|
||||
if (!this.librariesAccessible) return false
|
||||
|
|
@ -417,5 +420,23 @@ class User {
|
|||
if (!progress) return false
|
||||
return progress.removeFromContinueListening()
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of podcast episodes not finished for library item
|
||||
* Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance
|
||||
* @param {LibraryItem|object} libraryItem
|
||||
* @returns {number}
|
||||
*/
|
||||
getNumEpisodesIncompleteForPodcast(libraryItem) {
|
||||
if (!libraryItem?.media.episodes) return 0
|
||||
let numEpisodesIncomplete = 0
|
||||
for (const episode of libraryItem.media.episodes) {
|
||||
const mediaProgress = this.getMediaProgress(libraryItem.id, episode.id)
|
||||
if (!mediaProgress?.isFinished) {
|
||||
numEpisodesIncomplete++
|
||||
}
|
||||
}
|
||||
return numEpisodesIncomplete
|
||||
}
|
||||
}
|
||||
module.exports = User
|
||||
Loading…
Add table
Add a link
Reference in a new issue