Merge remote-tracking branch 'origin/master' into auth_passportjs

This commit is contained in:
lukeIam 2023-08-12 16:44:44 +02:00
commit dd9a3858d7
249 changed files with 15582 additions and 7835 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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()

View file

@ -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 = [

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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() {

View file

@ -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()
}

View file

@ -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) {

View file

@ -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
}
}

View file

@ -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) {

View file

@ -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()

View file

@ -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'
}

View file

@ -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()

View file

@ -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) {

View file

@ -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)
}

View file

@ -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

View 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

View file

@ -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]

View file

@ -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,

View file

@ -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

View file

@ -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