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

This commit is contained in:
lukeIam 2023-09-10 13:11:35 +00:00
commit f0f03efe17
138 changed files with 11777 additions and 7343 deletions

View file

@ -61,6 +61,10 @@ class FeedMeta {
}
getRSSData() {
const blockTags = [
{ 'itunes:block': 'yes' },
{ 'googleplay:block': 'yes' }
]
return {
title: this.title,
description: this.description || '',
@ -94,8 +98,7 @@ class FeedMeta {
]
},
{ 'itunes:explicit': !!this.explicit },
{ 'itunes:block': this.preventIndexing?"Yes":"No" },
{ 'googleplay:block': this.preventIndexing?"yes":"no" }
...(this.preventIndexing ? blockTags : [])
]
}
}

View file

@ -116,9 +116,9 @@ class Library {
}
update(payload) {
var hasUpdates = false
let hasUpdates = false
var keysToCheck = ['name', 'provider', 'mediaType', 'icon']
const keysToCheck = ['name', 'provider', 'mediaType', 'icon']
keysToCheck.forEach((key) => {
if (payload[key] && payload[key] !== this[key]) {
this[key] = payload[key]
@ -135,18 +135,18 @@ class Library {
hasUpdates = true
}
if (payload.folders) {
var newFolders = payload.folders.filter(f => !f.id)
var removedFolders = this.folders.filter(f => !payload.folders.find(_f => _f.id === f.id))
const newFolders = payload.folders.filter(f => !f.id)
const removedFolders = this.folders.filter(f => !payload.folders.some(_f => _f.id === f.id))
if (removedFolders.length) {
var removedFolderIds = removedFolders.map(f => f.id)
const removedFolderIds = removedFolders.map(f => f.id)
this.folders = this.folders.filter(f => !removedFolderIds.includes(f.id))
}
if (newFolders.length) {
newFolders.forEach((folderData) => {
folderData.libraryId = this.id
var newFolder = new Folder()
const newFolder = new Folder()
newFolder.setData(folderData)
this.folders.push(newFolder)
})

View file

@ -10,7 +10,7 @@ const Podcast = require('./mediaTypes/Podcast')
const Video = require('./mediaTypes/Video')
const Music = require('./mediaTypes/Music')
const { areEquivalent, copyValue, cleanStringForSearch } = require('../utils/index')
const { filePathToPOSIX } = require('../utils/fileUtils')
const { filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
class LibraryItem {
constructor(libraryItem = null) {
@ -40,6 +40,7 @@ class LibraryItem {
this.mediaType = null
this.media = null
/** @type {LibraryFile[]} */
this.libraryFiles = []
if (libraryItem) {
@ -337,183 +338,6 @@ class LibraryItem {
return hasUpdated
}
// Data pulled from scandir during a scan, check it with current data
checkScanData(dataFound) {
let hasUpdated = false
if (this.isMissing) {
// Item no longer missing
this.isMissing = false
hasUpdated = true
}
if (dataFound.isFile !== this.isFile && dataFound.isFile !== undefined) {
Logger.info(`[LibraryItem] Check scan item isFile toggled from ${this.isFile} => ${dataFound.isFile}`)
this.isFile = dataFound.isFile
hasUpdated = true
}
if (dataFound.ino !== this.ino) {
Logger.warn(`[LibraryItem] Check scan item changed inode "${this.ino}" -> "${dataFound.ino}"`)
this.ino = dataFound.ino
hasUpdated = true
}
if (dataFound.folderId !== this.folderId) {
Logger.warn(`[LibraryItem] Check scan item changed folder ${this.folderId} -> ${dataFound.folderId}`)
this.folderId = dataFound.folderId
hasUpdated = true
}
if (dataFound.path !== this.path) {
Logger.warn(`[LibraryItem] Check scan item changed path "${this.path}" -> "${dataFound.path}" (inode ${this.ino})`)
this.path = dataFound.path
this.relPath = dataFound.relPath
hasUpdated = true
}
['mtimeMs', 'ctimeMs', 'birthtimeMs'].forEach((key) => {
if (dataFound[key] != this[key]) {
this[key] = dataFound[key] || 0
hasUpdated = true
}
})
const newLibraryFiles = []
const existingLibraryFiles = []
dataFound.libraryFiles.forEach((lf) => {
const fileFoundCheck = this.checkFileFound(lf, true)
if (fileFoundCheck === null) {
newLibraryFiles.push(lf)
} else if (fileFoundCheck && lf.metadata.format !== 'abs' && lf.metadata.filename !== 'metadata.json') { // Ignore abs file updates
hasUpdated = true
existingLibraryFiles.push(lf)
} else {
existingLibraryFiles.push(lf)
}
})
const filesRemoved = []
// Remove files not found (inodes will all be up to date at this point)
this.libraryFiles = this.libraryFiles.filter(lf => {
if (!dataFound.libraryFiles.find(_lf => _lf.ino === lf.ino)) {
// Check if removing cover path
if (lf.metadata.path === this.media.coverPath) {
Logger.debug(`[LibraryItem] "${this.media.metadata.title}" check scan cover removed`)
this.media.updateCover('')
}
filesRemoved.push(lf.toJSON())
this.media.removeFileWithInode(lf.ino)
return false
}
return true
})
if (filesRemoved.length) {
if (this.media.mediaType === 'book') {
this.media.checkUpdateMissingTracks()
}
hasUpdated = true
}
// Add library files to library item
if (newLibraryFiles.length) {
newLibraryFiles.forEach((lf) => this.libraryFiles.push(lf.clone()))
hasUpdated = true
}
// Check if invalid
this.isInvalid = !this.media.hasMediaEntities
// If cover path is in item folder, make sure libraryFile exists for it
if (this.media.coverPath && this.media.coverPath.startsWith(this.path)) {
const lf = this.libraryFiles.find(lf => lf.metadata.path === this.media.coverPath)
if (!lf) {
Logger.warn(`[LibraryItem] Invalid cover path - library file dne "${this.media.coverPath}"`)
this.media.updateCover('')
hasUpdated = true
}
}
if (hasUpdated) {
this.setLastScan()
}
return {
updated: hasUpdated,
newLibraryFiles,
filesRemoved,
existingLibraryFiles // Existing file data may get re-scanned if forceRescan is set
}
}
// Set metadata from files
async syncFiles(preferOpfMetadata, librarySettings) {
let hasUpdated = false
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
const imageFiles = this.libraryFiles.filter(lf => lf.fileType === 'image')
if (imageFiles.length && !this.media.coverPath) {
// attempt to find a file called cover.<ext> otherwise just fall back to the first image found
const coverMatch = imageFiles.find(iFile => /\/cover\.[^.\/]*$/.test(iFile.metadata.path))
if (coverMatch) {
this.media.coverPath = coverMatch.metadata.path
} else {
this.media.coverPath = imageFiles[0].metadata.path
}
Logger.info('[LibraryItem] Set media cover path', this.media.coverPath)
hasUpdated = true
}
// Parse metadata files
const textMetadataFiles = this.libraryFiles.filter(lf => lf.fileType === 'metadata' || lf.fileType === 'text')
if (textMetadataFiles.length) {
if (await this.media.syncMetadataFiles(textMetadataFiles, preferOpfMetadata)) {
hasUpdated = true
}
}
if (hasUpdated) {
this.updatedAt = Date.now()
}
return hasUpdated
}
searchQuery(query) {
query = cleanStringForSearch(query)
return this.media.searchQuery(query)
@ -525,19 +349,20 @@ class LibraryItem {
/**
* Save metadata.json/metadata.abs file
* @returns {boolean} true if saved
* @returns {Promise<LibraryFile>} null if not saved
*/
async saveMetadata() {
if (this.mediaType === 'video' || this.mediaType === 'music') return
if (this.isSavingMetadata) return null
if (this.isSavingMetadata) return
this.isSavingMetadata = true
let metadataPath = Path.join(global.MetadataPath, 'items', this.id)
if (global.ServerSettings.storeMetadataWithItem && !this.isFile) {
let storeMetadataWithItem = global.ServerSettings.storeMetadataWithItem
if (storeMetadataWithItem && !this.isFile) {
metadataPath = this.path
} else {
// Make sure metadata book dir exists
storeMetadataWithItem = false
await fs.ensureDir(metadataPath)
}
@ -552,20 +377,37 @@ class LibraryItem {
}
return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(async () => {
this.isSavingMetadata = false
// Add metadata.json to libraryFiles array if it is new
if (global.ServerSettings.storeMetadataWithItem && !this.libraryFiles.some(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))) {
const newLibraryFile = new LibraryFile()
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
this.libraryFiles.push(newLibraryFile)
let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
if (storeMetadataWithItem) {
if (!metadataLibraryFile) {
metadataLibraryFile = new LibraryFile()
await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
this.libraryFiles.push(metadataLibraryFile)
} else {
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
if (fileTimestamps) {
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
metadataLibraryFile.metadata.size = fileTimestamps.size
metadataLibraryFile.ino = fileTimestamps.ino
}
}
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
if (libraryItemDirTimestamps) {
this.mtimeMs = libraryItemDirTimestamps.mtimeMs
this.ctimeMs = libraryItemDirTimestamps.ctimeMs
}
}
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
return true
return metadataLibraryFile
}).catch((error) => {
this.isSavingMetadata = false
Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
return false
return null
}).finally(() => {
this.isSavingMetadata = false
})
} else {
// Remove metadata.json if it exists
@ -576,19 +418,37 @@ class LibraryItem {
}
return abmetadataGenerator.generate(this, metadataFilePath).then(async (success) => {
this.isSavingMetadata = false
if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataFilePath}"`)
else {
// Add metadata.abs to libraryFiles array if it is new
if (global.ServerSettings.storeMetadataWithItem && !this.libraryFiles.some(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))) {
const newLibraryFile = new LibraryFile()
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.abs`)
this.libraryFiles.push(newLibraryFile)
}
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
if (!success) {
Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataFilePath}"`)
return null
}
return success
// Add metadata.abs to libraryFiles array if it is new
let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
if (storeMetadataWithItem) {
if (!metadataLibraryFile) {
metadataLibraryFile = new LibraryFile()
await metadataLibraryFile.setDataFromPath(metadataFilePath, `metadata.abs`)
this.libraryFiles.push(metadataLibraryFile)
} else {
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
if (fileTimestamps) {
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
metadataLibraryFile.metadata.size = fileTimestamps.size
metadataLibraryFile.ino = fileTimestamps.ino
}
}
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
if (libraryItemDirTimestamps) {
this.mtimeMs = libraryItemDirTimestamps.mtimeMs
this.ctimeMs = libraryItemDirTimestamps.ctimeMs
}
}
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
return metadataLibraryFile
}).finally(() => {
this.isSavingMetadata = false
})
}
}

View file

@ -78,6 +78,11 @@ class PlaybackSession {
}
}
/**
* Session data to send to clients
* @param {[oldLibraryItem]} libraryItem optional
* @returns {object}
*/
toJSONForClient(libraryItem) {
return {
id: this.id,
@ -105,8 +110,8 @@ class PlaybackSession {
startedAt: this.startedAt,
updatedAt: this.updatedAt,
audioTracks: this.audioTracks.map(at => at.toJSON()),
videoTrack: this.videoTrack ? this.videoTrack.toJSON() : null,
libraryItem: libraryItem.toJSONExpanded()
videoTrack: this.videoTrack?.toJSON() || null,
libraryItem: libraryItem?.toJSONExpanded() || null
}
}

View file

@ -1,5 +1,5 @@
const uuidv4 = require("uuid").v4
const { getTitleIgnorePrefix } = require('../../utils/index')
const { getTitleIgnorePrefix, getTitlePrefixAtEnd } = require('../../utils/index')
class Series {
constructor(series) {
@ -33,6 +33,7 @@ class Series {
return {
id: this.id,
name: this.name,
nameIgnorePrefix: getTitlePrefixAtEnd(this.name),
description: this.description,
addedAt: this.addedAt,
updatedAt: this.updatedAt,

View file

@ -6,6 +6,7 @@ class AudioFile {
constructor(data) {
this.index = null
this.ino = null
/** @type {FileMetadata} */
this.metadata = null
this.addedAt = null
this.updatedAt = null
@ -27,6 +28,7 @@ class AudioFile {
this.embeddedCoverArt = null
// Tags scraped from the audio file
/** @type {AudioMetaTags} */
this.metaTags = null
this.manuallyVerified = false
@ -64,7 +66,7 @@ class AudioFile {
channelLayout: this.channelLayout,
chapters: this.chapters,
embeddedCoverArt: this.embeddedCoverArt,
metaTags: this.metaTags ? this.metaTags.toJSON() : {},
metaTags: this.metaTags?.toJSON() || {},
mimeType: this.mimeType
}
}
@ -114,11 +116,16 @@ class AudioFile {
return !this.invalid && !this.exclude
}
// New scanner creates AudioFile from MediaFileScanner
// New scanner creates AudioFile from AudioFileScanner
setDataFromProbe(libraryFile, probeData) {
this.ino = libraryFile.ino || null
this.metadata = libraryFile.metadata.clone()
if (libraryFile.metadata instanceof FileMetadata) {
this.metadata = libraryFile.metadata.clone()
} else {
this.metadata = new FileMetadata(libraryFile.metadata)
}
this.addedAt = Date.now()
this.updatedAt = Date.now()
@ -163,11 +170,16 @@ class AudioFile {
return new AudioFile(this.toJSON())
}
/**
*
* @param {AudioFile} scannedAudioFile
* @returns {boolean} true if updates were made
*/
updateFromScan(scannedAudioFile) {
let hasUpdated = false
const newjson = scannedAudioFile.toJSON()
const ignoreKeys = ['manuallyVerified', 'exclude', 'addedAt', 'updatedAt']
const ignoreKeys = ['manuallyVerified', 'ctimeMs', 'addedAt', 'updatedAt']
for (const key in newjson) {
if (key === 'metadata') {

View file

@ -4,10 +4,6 @@ const PodcastMetadata = require('../metadata/PodcastMetadata')
const { areEquivalent, copyValue, cleanStringForSearch } = require('../../utils/index')
const abmetadataGenerator = require('../../utils/generators/abmetadataGenerator')
const { readTextFile, filePathToPOSIX } = require('../../utils/fileUtils')
const { createNewSortInstance } = require('../../libs/fastSort')
const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
})
class Podcast {
constructor(podcast) {

View file

@ -330,10 +330,6 @@ class BookMetadata {
{
tag: 'tagASIN',
key: 'asin'
},
{
tag: 'tagOverdriveMediaMarker',
key: 'overdriveMediaMarker'
}
]

View file

@ -43,7 +43,7 @@ class ServerSettings {
// Sorting
this.sortingIgnorePrefix = false
this.sortingPrefixes = ['the']
this.sortingPrefixes = ['the', 'a']
// Misc Flags
this.chromecastEnabled = false

View file

@ -117,23 +117,20 @@ class User {
return json
}
// Data broadcasted
toJSONForPublic(sessions, libraryItems) {
var userSession = sessions ? sessions.find(s => s.userId === this.id) : null
var session = null
if (userSession) {
var libraryItem = libraryItems.find(li => li.id === userSession.libraryItemId)
if (libraryItem) {
session = userSession.toJSONForClient(libraryItem)
}
}
/**
* User data for clients
* @param {[oldPlaybackSession[]]} sessions optional array of open playback sessions
* @returns {object}
*/
toJSONForPublic(sessions) {
const userSession = sessions?.find(s => s.userId === this.id) || null
const session = userSession?.toJSONForClient() || null
return {
id: this.id,
oldUserId: this.oldUserId,
username: this.username,
type: this.type,
session,
mostRecent: this.getMostRecentItemProgress(libraryItems),
lastSeen: this.lastSeen,
createdAt: this.createdAt
}
@ -269,45 +266,6 @@ class User {
return libraryIds.find(lid => this.checkCanAccessLibrary(lid)) || null
}
// Returns most recent media progress w/ `media` object and optionally an `episode` object
getMostRecentItemProgress(libraryItems) {
if (!this.mediaProgress.length) return null
var mediaProgressObjects = this.mediaProgress.map(lip => lip.toJSON())
mediaProgressObjects.sort((a, b) => b.lastUpdate - a.lastUpdate)
var libraryItemMedia = null
var progressEpisode = null
// Find the most recent progress that still has a libraryItem and episode
var mostRecentProgress = mediaProgressObjects.find((progress) => {
const libraryItem = libraryItems.find(li => li.id === progress.libraryItemId)
if (!libraryItem) {
Logger.warn('[User] Library item not found for users progress ' + progress.libraryItemId)
return false
} else if (progress.episodeId) {
const episode = libraryItem.mediaType === 'podcast' ? libraryItem.media.getEpisode(progress.episodeId) : null
if (!episode) {
Logger.warn(`[User] Episode ${progress.episodeId} not found for user media progress, podcast: ${libraryItem.media.metadata.title}`)
return false
} else {
libraryItemMedia = libraryItem.media.toJSONExpanded()
progressEpisode = episode.toJSON()
return true
}
} else {
libraryItemMedia = libraryItem.media.toJSONExpanded()
return true
}
})
if (!mostRecentProgress) return null
return {
...mostRecentProgress,
media: libraryItemMedia,
episode: progressEpisode
}
}
getMediaProgress(libraryItemId, episodeId = null) {
if (!this.mediaProgress) return null
return this.mediaProgress.find(lip => {