Merge branch 'master' into l10n/lt

This commit is contained in:
Petras Šukys 2023-07-25 08:03:19 +03:00
commit b2d45f598b
28 changed files with 627 additions and 217 deletions

View File

@ -20,18 +20,14 @@
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
<table v-else class="text-sm tracksTable">
<tr>
<th class="text-left">Sort #</th>
<th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
<th class="text-left">{{ $strings.EpisodeTitle }}</th>
<th class="text-center w-28">{{ $strings.EpisodeDuration }}</th>
<th class="text-center w-28">{{ $strings.EpisodeSize }}</th>
<th class="text-center w-20 min-w-20">{{ $strings.LabelEpisode }}</th>
<th class="text-left">{{ $strings.LabelEpisodeTitle }}</th>
<th class="text-center w-28">{{ $strings.LabelEpisodeDuration }}</th>
<th class="text-center w-28">{{ $strings.LabelEpisodeSize }}</th>
</tr>
<tr v-for="episode in episodes" :key="episode.id">
<td class="text-left">
<p class="px-4">{{ episode.index }}</p>
</td>
<td class="text-left">
<p class="px-4">{{ episode.episode }}</p>
<td class="text-center w-20 min-w-20">
<p>{{ episode.episode }}</p>
</td>
<td>
{{ episode.title }}

View File

@ -32,12 +32,13 @@ class Auth {
await Database.updateServerSettings()
// New token secret creation added in v2.1.0 so generate new API tokens for each user
if (Database.users.length) {
for (const user of Database.users) {
const users = await Database.models.user.getOldUsers()
if (users.length) {
for (const user of users) {
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
Logger.warn(`[Auth] User ${user.username} api token has been updated using new token secret`)
}
await Database.updateBulkUsers(Database.users)
await Database.updateBulkUsers(users)
}
}
@ -93,21 +94,32 @@ class Auth {
verifyToken(token) {
return new Promise((resolve) => {
jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => {
jwt.verify(token, Database.serverSettings.tokenSecret, async (err, payload) => {
if (!payload || err) {
Logger.error('JWT Verify Token Failed', err)
return resolve(null)
}
const user = Database.users.find(u => (u.id === payload.userId || u.oldUserId === payload.userId) && u.username === payload.username)
resolve(user || null)
const user = await Database.models.user.getUserByIdOrOldId(payload.userId)
if (user && user.username === payload.username) {
resolve(user)
} else {
resolve(null)
}
})
})
}
getUserLoginResponsePayload(user) {
/**
* Payload returned to a user after successful login
* @param {oldUser} user
* @returns {object}
*/
async getUserLoginResponsePayload(user) {
const libraryIds = await Database.models.library.getAllLibraryIds()
return {
user: user.toJSONForBrowser(),
userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries),
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
serverSettings: Database.serverSettings.toJSONForBrowser(),
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
Source: global.Source
@ -119,7 +131,7 @@ class Auth {
const username = (req.body.username || '').toLowerCase()
const password = req.body.password || ''
const user = Database.users.find(u => u.username.toLowerCase() === username)
const user = await Database.models.user.getUserByUsername(username)
if (!user?.isActive) {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
@ -136,7 +148,8 @@ class Auth {
return res.status(401).send('Invalid root password (hint: there is none)')
} else {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
return res.json(this.getUserLoginResponsePayload(user))
const userLoginResponsePayload = await this.getUserLoginResponsePayload(user)
return res.json(userLoginResponsePayload)
}
}
@ -144,7 +157,8 @@ class Auth {
const compare = await bcrypt.compare(password, user.pash)
if (compare) {
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
res.json(this.getUserLoginResponsePayload(user))
const userLoginResponsePayload = await this.getUserLoginResponsePayload(user)
res.json(userLoginResponsePayload)
} else {
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
if (req.rateLimit.remaining <= 2) {
@ -164,7 +178,7 @@ class Auth {
async userChangePassword(req, res) {
var { password, newPassword } = req.body
newPassword = newPassword || ''
const matchingUser = Database.users.find(u => u.id === req.user.id)
const matchingUser = await Database.models.user.getUserById(req.user.id)
// Only root can have an empty password
if (matchingUser.type !== 'root' && !newPassword) {

View File

@ -6,21 +6,19 @@ const fs = require('./libs/fsExtra')
const Logger = require('./Logger')
const dbMigration = require('./utils/migrations/dbMigration')
const Auth = require('./Auth')
class Database {
constructor() {
this.sequelize = null
this.dbPath = null
this.isNew = false // New absdatabase.sqlite created
this.hasRootUser = false // Used to show initialization page in web ui
// Temporarily using format of old DB
// TODO: below data should be loaded from the DB as needed
this.libraryItems = []
this.users = []
this.libraries = []
this.settings = []
this.collections = []
this.playlists = []
this.authors = []
this.series = []
@ -33,10 +31,6 @@ class Database {
return this.sequelize?.models || {}
}
get hasRootUser() {
return this.users.some(u => u.type === 'root')
}
async checkHasDb() {
if (!await fs.pathExists(this.dbPath)) {
Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`)
@ -66,7 +60,8 @@ class Database {
this.sequelize = new Sequelize({
dialect: 'sqlite',
storage: this.dbPath,
logging: false
logging: false,
transactionType: 'IMMEDIATE'
})
// Helper function
@ -164,24 +159,15 @@ class Database {
this.libraryItems = await this.models.libraryItem.loadAllLibraryItems()
Logger.info(`[Database] Loaded ${this.libraryItems.length} library items`)
this.users = await this.models.user.getOldUsers()
Logger.info(`[Database] Loaded ${this.users.length} users`)
this.libraries = await this.models.library.getAllOldLibraries()
Logger.info(`[Database] Loaded ${this.libraries.length} libraries`)
this.collections = await this.models.collection.getOldCollections()
Logger.info(`[Database] Loaded ${this.collections.length} collections`)
this.playlists = await this.models.playlist.getOldPlaylists()
Logger.info(`[Database] Loaded ${this.playlists.length} playlists`)
this.authors = await this.models.author.getOldAuthors()
Logger.info(`[Database] Loaded ${this.authors.length} authors`)
this.series = await this.models.series.getAllOldSeries()
Logger.info(`[Database] Loaded ${this.series.length} series`)
// Set if root user has been created
this.hasRootUser = await this.models.user.getHasRootUser()
Logger.info(`[Database] Db data loaded in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
if (packageJson.version !== this.serverSettings.version) {
@ -191,15 +177,19 @@ class Database {
}
}
async createRootUser(username, pash, token) {
/**
* Create root user
* @param {string} username
* @param {string} pash
* @param {Auth} auth
* @returns {boolean} true if created
*/
async createRootUser(username, pash, auth) {
if (!this.sequelize) return false
const newUser = await this.models.user.createRootUser(username, pash, token)
if (newUser) {
this.users.push(newUser)
await this.models.user.createRootUser(username, pash, auth)
this.hasRootUser = true
return true
}
return false
}
updateServerSettings() {
if (!this.sequelize) return false
@ -215,7 +205,6 @@ class Database {
async createUser(oldUser) {
if (!this.sequelize) return false
await this.models.user.createFromOld(oldUser)
this.users.push(oldUser)
return true
}
@ -232,7 +221,6 @@ class Database {
async removeUser(userId) {
if (!this.sequelize) return false
await this.models.user.removeById(userId)
this.users = this.users.filter(u => u.id !== userId)
}
upsertMediaProgress(oldMediaProgress) {
@ -253,7 +241,6 @@ class Database {
async createLibrary(oldLibrary) {
if (!this.sequelize) return false
await this.models.library.createFromOld(oldLibrary)
this.libraries.push(oldLibrary)
}
updateLibrary(oldLibrary) {
@ -264,7 +251,6 @@ class Database {
async removeLibrary(libraryId) {
if (!this.sequelize) return false
await this.models.library.removeById(libraryId)
this.libraries = this.libraries.filter(lib => lib.id !== libraryId)
}
async createCollection(oldCollection) {
@ -286,7 +272,6 @@ class Database {
await this.createBulkCollectionBooks(collectionBooks)
}
}
this.collections.push(oldCollection)
}
updateCollection(oldCollection) {
@ -308,7 +293,6 @@ class Database {
async removeCollection(collectionId) {
if (!this.sequelize) return false
await this.models.collection.removeById(collectionId)
this.collections = this.collections.filter(c => c.id !== collectionId)
}
createCollectionBook(collectionBook) {
@ -353,7 +337,6 @@ class Database {
await this.createBulkPlaylistMediaItems(playlistMediaItems)
}
}
this.playlists.push(oldPlaylist)
}
updatePlaylist(oldPlaylist) {
@ -376,7 +359,6 @@ class Database {
async removePlaylist(playlistId) {
if (!this.sequelize) return false
await this.models.playlist.removeById(playlistId)
this.playlists = this.playlists.filter(p => p.id !== playlistId)
}
createPlaylistMediaItem(playlistMediaItem) {
@ -405,12 +387,14 @@ class Database {
async createLibraryItem(oldLibraryItem) {
if (!this.sequelize) return false
await oldLibraryItem.saveMetadata()
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
this.libraryItems.push(oldLibraryItem)
}
updateLibraryItem(oldLibraryItem) {
async updateLibraryItem(oldLibraryItem) {
if (!this.sequelize) return false
await oldLibraryItem.saveMetadata()
return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
}
@ -418,8 +402,11 @@ class Database {
if (!this.sequelize) return false
let updatesMade = 0
for (const oldLibraryItem of oldLibraryItems) {
await oldLibraryItem.saveMetadata()
const hasUpdates = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
if (hasUpdates) updatesMade++
if (hasUpdates) {
updatesMade++
}
}
return updatesMade
}
@ -427,6 +414,7 @@ class Database {
async createBulkLibraryItems(oldLibraryItems) {
if (!this.sequelize) return false
for (const oldLibraryItem of oldLibraryItems) {
await oldLibraryItem.saveMetadata()
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
this.libraryItems.push(oldLibraryItem)
}

View File

@ -93,6 +93,10 @@ class Server {
this.auth.authMiddleware(req, res, next)
}
/**
* Initialize database, backups, logs, rss feeds, cron jobs & watcher
* Cleanup stale/invalid data
*/
async init() {
Logger.info('[Server] Init v' + version)
await this.playbackSessionManager.removeOrphanStreams()
@ -105,20 +109,21 @@ class Server {
}
await this.cleanUserData() // Remove invalid user item progress
await this.purgeMetadata() // Remove metadata folders without library item
await this.cacheManager.ensureCachePaths()
await this.backupManager.init()
await this.logManager.init()
await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
await this.rssFeedManager.init()
this.cronManager.init()
const libraries = await Database.models.library.getAllOldLibraries()
this.cronManager.init(libraries)
if (Database.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`)
this.watcher.disabled = true
} else {
this.watcher.initWatcher(Database.libraries)
this.watcher.initWatcher(libraries)
this.watcher.on('files', this.filesChanged.bind(this))
}
}
@ -243,39 +248,10 @@ class Server {
await this.scanner.scanFilesChanged(fileUpdates)
}
// Remove unused /metadata/items/{id} folders
async purgeMetadata() {
const itemsMetadata = Path.join(global.MetadataPath, 'items')
if (!(await fs.pathExists(itemsMetadata))) return
const foldersInItemsMetadata = await fs.readdir(itemsMetadata)
let purged = 0
await Promise.all(foldersInItemsMetadata.map(async foldername => {
const itemFullPath = fileUtils.filePathToPOSIX(Path.join(itemsMetadata, foldername))
const hasMatchingItem = Database.libraryItems.find(li => {
if (!li.media.coverPath) return false
return itemFullPath === fileUtils.filePathToPOSIX(Path.dirname(li.media.coverPath))
})
if (!hasMatchingItem) {
Logger.debug(`[Server] Purging unused metadata ${itemFullPath}`)
await fs.remove(itemFullPath).then(() => {
purged++
}).catch((err) => {
Logger.error(`[Server] Failed to delete folder path ${itemFullPath}`, err)
})
}
}))
if (purged > 0) {
Logger.info(`[Server] Purged ${purged} unused library item metadata`)
}
return purged
}
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
async cleanUserData() {
for (const _user of Database.users) {
const users = await Database.models.user.getOldUsers()
for (const _user of users) {
if (_user.mediaProgress.length) {
for (const mediaProgress of _user.mediaProgress) {
const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId)

View File

@ -20,9 +20,10 @@ class CollectionController {
res.json(jsonExpanded)
}
findAll(req, res) {
async findAll(req, res) {
const collections = await Database.models.collection.getOldCollections()
res.json({
collections: Database.collections.map(c => c.toJSONExpanded(Database.libraryItems))
collections: collections.map(c => c.toJSONExpanded(Database.libraryItems))
})
}
@ -160,9 +161,9 @@ class CollectionController {
res.json(collection.toJSONExpanded(Database.libraryItems))
}
middleware(req, res, next) {
async middleware(req, res, next) {
if (req.params.id) {
const collection = Database.collections.find(c => c.id === req.params.id)
const collection = await Database.models.collection.getById(req.params.id)
if (!collection) {
return res.status(404).send('Collection not found')
}

View File

@ -17,13 +17,12 @@ class FileSystemController {
})
// Do not include existing mapped library paths in response
Database.libraries.forEach(lib => {
lib.folders.forEach((folder) => {
let dir = folder.fullPath
const libraryFoldersPaths = await Database.models.libraryFolder.getAllLibraryFolderPaths()
libraryFoldersPaths.forEach((path) => {
let dir = path || ''
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
excludedDirs.push(dir)
})
})
res.json({
directories: await this.getDirectories(global.appRoot, '/', excludedDirs)

View File

@ -44,7 +44,9 @@ class LibraryController {
const library = new Library()
newLibraryPayload.displayOrder = Database.libraries.map(li => li.displayOrder).sort((a, b) => a - b).pop() + 1
let currentLargestDisplayOrder = await Database.models.library.getMaxDisplayOrder()
if (isNaN(currentLargestDisplayOrder)) currentLargestDisplayOrder = 0
newLibraryPayload.displayOrder = currentLargestDisplayOrder + 1
library.setData(newLibraryPayload)
await Database.createLibrary(library)
@ -60,17 +62,18 @@ class LibraryController {
res.json(library)
}
findAll(req, res) {
async findAll(req, res) {
const libraries = await Database.models.library.getAllOldLibraries()
const librariesAccessible = req.user.librariesAccessible || []
if (librariesAccessible.length) {
return res.json({
libraries: Database.libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
libraries: libraries.filter(lib => librariesAccessible.includes(lib.id)).map(lib => lib.toJSON())
})
}
res.json({
libraries: Database.libraries.map(lib => lib.toJSON())
// libraries: Database.libraries.map(lib => lib.toJSON())
libraries: libraries.map(lib => lib.toJSON())
})
}
@ -80,7 +83,7 @@ class LibraryController {
return res.json({
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
issues: req.libraryItems.filter(li => li.hasIssues).length,
numUserPlaylists: Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).length,
numUserPlaylists: await Database.models.playlist.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
library: req.library
})
}
@ -151,6 +154,12 @@ class LibraryController {
return res.json(library.toJSON())
}
/**
* DELETE: /api/libraries/:id
* Delete a library
* @param {*} req
* @param {*} res
*/
async delete(req, res) {
const library = req.library
@ -158,10 +167,9 @@ class LibraryController {
this.watcher.removeLibrary(library)
// Remove collections for library
const collections = Database.collections.filter(c => c.libraryId === library.id)
for (const collection of collections) {
Logger.info(`[Server] deleting collection "${collection.name}" for library "${library.name}"`)
await Database.removeCollection(collection.id)
const numCollectionsRemoved = await Database.models.collection.removeAllForLibrary(library.id)
if (numCollectionsRemoved) {
Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`)
}
// Remove items in this library
@ -173,6 +181,10 @@ class LibraryController {
const libraryJson = library.toJSON()
await Database.removeLibrary(library.id)
// Re-order libraries
await Database.models.library.resetDisplayOrder()
SocketAuthority.emitter('library_removed', libraryJson)
return res.json(libraryJson)
}
@ -514,7 +526,9 @@ class LibraryController {
include: include.join(',')
}
let collections = await Promise.all(Database.collections.filter(c => c.libraryId === req.library.id).map(async c => {
const collectionsForLibrary = await Database.models.collection.getAllForLibrary(req.library.id)
let collections = await Promise.all(collectionsForLibrary.map(async c => {
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
// If all books restricted to user in this collection then hide this collection
@ -543,7 +557,8 @@ class LibraryController {
// api/libraries/:id/playlists
async getUserPlaylistsForLibrary(req, res) {
let playlistsForUser = Database.playlists.filter(p => p.userId === req.user.id && p.libraryId === req.library.id).map(p => p.toJSONExpanded(Database.libraryItems))
let playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id, req.library.id)
playlistsForUser = playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems))
const payload = {
results: [],
@ -601,17 +616,23 @@ class LibraryController {
res.json(categories)
}
// PATCH: Change the order of libraries
/**
* POST: /api/libraries/order
* Change the display order of libraries
* @param {*} req
* @param {*} res
*/
async reorder(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
return res.sendStatus(403)
}
const libraries = await Database.models.library.getAllOldLibraries()
var orderdata = req.body
var hasUpdates = false
const orderdata = req.body
let hasUpdates = false
for (let i = 0; i < orderdata.length; i++) {
var library = Database.libraries.find(lib => lib.id === orderdata[i].id)
const library = libraries.find(lib => lib.id === orderdata[i].id)
if (!library) {
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
return res.sendStatus(500)
@ -623,14 +644,14 @@ class LibraryController {
}
if (hasUpdates) {
Database.libraries.sort((a, b) => a.displayOrder - b.displayOrder)
libraries.sort((a, b) => a.displayOrder - b.displayOrder)
Logger.debug(`[LibraryController] Updated library display orders`)
} else {
Logger.debug(`[LibraryController] Library orders were up to date`)
}
res.json({
libraries: Database.libraries.map(lib => lib.toJSON())
libraries: libraries.map(lib => lib.toJSON())
})
}
@ -902,13 +923,13 @@ class LibraryController {
res.send(opmlText)
}
middleware(req, res, next) {
async middleware(req, res, next) {
if (!req.user.checkCanAccessLibrary(req.params.id)) {
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
return res.sendStatus(403)
}
const library = Database.libraries.find(lib => lib.id === req.params.id)
const library = await Database.models.library.getOldById(req.params.id)
if (!library) {
return res.status(404).send('Library not found')
}

View File

@ -24,18 +24,18 @@ class MiscController {
Logger.error('Invalid request, no files')
return res.sendStatus(400)
}
var files = Object.values(req.files)
var title = req.body.title
var author = req.body.author
var series = req.body.series
var libraryId = req.body.library
var folderId = req.body.folder
const files = Object.values(req.files)
const title = req.body.title
const author = req.body.author
const series = req.body.series
const libraryId = req.body.library
const folderId = req.body.folder
var library = Database.libraries.find(lib => lib.id === libraryId)
const library = await Database.models.library.getOldById(libraryId)
if (!library) {
return res.status(404).send(`Library not found with id ${libraryId}`)
}
var folder = library.folders.find(fold => fold.id === folderId)
const folder = library.folders.find(fold => fold.id === folderId)
if (!folder) {
return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`)
}
@ -45,8 +45,8 @@ class MiscController {
}
// For setting permissions recursively
var outputDirectory = ''
var firstDirPath = ''
let outputDirectory = ''
let firstDirPath = ''
if (library.isPodcast) { // Podcasts only in 1 folder
outputDirectory = Path.join(folder.fullPath, title)
@ -62,8 +62,7 @@ class MiscController {
}
}
var exists = await fs.pathExists(outputDirectory)
if (exists) {
if (await fs.pathExists(outputDirectory)) {
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
return res.status(500).send(`Directory "${outputDirectory}" already exists`)
}
@ -132,12 +131,19 @@ class MiscController {
})
}
authorize(req, res) {
/**
* POST: /api/authorize
* Used to authorize an API token
*
* @param {*} req
* @param {*} res
*/
async authorize(req, res) {
if (!req.user) {
Logger.error('Invalid user in authorize')
return res.sendStatus(401)
}
const userResponse = this.auth.getUserLoginResponsePayload(req.user)
const userResponse = await this.auth.getUserLoginResponsePayload(req.user)
res.json(userResponse)
}

View File

@ -22,9 +22,10 @@ class PlaylistController {
}
// GET: api/playlists
findAllForUser(req, res) {
async findAllForUser(req, res) {
const playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id)
res.json({
playlists: Database.playlists.filter(p => p.userId === req.user.id).map(p => p.toJSONExpanded(Database.libraryItems))
playlists: playlistsForUser.map(p => p.toJSONExpanded(Database.libraryItems))
})
}
@ -200,7 +201,7 @@ class PlaylistController {
// POST: api/playlists/collection/:collectionId
async createFromCollection(req, res) {
let collection = Database.collections.find(c => c.id === req.params.collectionId)
let collection = await Database.models.collection.getById(req.params.collectionId)
if (!collection) {
return res.status(404).send('Collection not found')
}
@ -231,9 +232,9 @@ class PlaylistController {
res.json(jsonExpanded)
}
middleware(req, res, next) {
async middleware(req, res, next) {
if (req.params.id) {
const playlist = Database.playlists.find(p => p.id === req.params.id)
const playlist = await Database.models.playlist.getById(req.params.id)
if (!playlist) {
return res.status(404).send('Playlist not found')
}

View File

@ -19,7 +19,7 @@ class PodcastController {
}
const payload = req.body
const library = Database.libraries.find(lib => lib.id === payload.libraryId)
const library = await Database.models.library.getOldById(payload.libraryId)
if (!library) {
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
return res.status(404).send('Library not found')
@ -241,18 +241,18 @@ class PodcastController {
// DELETE: api/podcasts/:id/episode/:episodeId
async removeEpisode(req, res) {
var episodeId = req.params.episodeId
var libraryItem = req.libraryItem
var hardDelete = req.query.hard === '1'
const episodeId = req.params.episodeId
const libraryItem = req.libraryItem
const hardDelete = req.query.hard === '1'
var episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
const episode = libraryItem.media.episodes.find(ep => ep.id === episodeId)
if (!episode) {
Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
return res.sendStatus(404)
}
if (hardDelete) {
var audioFile = episode.audioFile
const audioFile = episode.audioFile
// TODO: this will trigger the watcher. should maybe handle this gracefully
await fs.remove(audioFile.metadata.path).then(() => {
Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`)
@ -267,6 +267,22 @@ class PodcastController {
libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
}
// Update/remove playlists that had this podcast episode
const playlistsWithEpisode = await Database.models.playlist.getPlaylistsForMediaItemIds([episodeId])
for (const playlist of playlistsWithEpisode) {
playlist.removeItem(libraryItem.id, episodeId)
// If playlist is now empty then remove it
if (!playlist.items.length) {
Logger.info(`[PodcastController] Playlist "${playlist.name}" has no more items - removing it`)
await Database.removePlaylist(playlist.id)
SocketAuthority.clientEmitter(playlist.userId, 'playlist_removed', playlist.toJSONExpanded(Database.libraryItems))
} else {
await Database.updatePlaylist(playlist)
SocketAuthority.clientEmitter(playlist.userId, 'playlist_updated', playlist.toJSONExpanded(Database.libraryItems))
}
}
await Database.updateLibraryItem(libraryItem)
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
res.json(libraryItem.toJSON())

View File

@ -45,7 +45,7 @@ class RSSFeedController {
async openRSSFeedForCollection(req, res) {
const options = req.body || {}
const collection = Database.collections.find(li => li.id === req.params.collectionId)
const collection = await Database.models.collection.getById(req.params.collectionId)
if (!collection) return res.sendStatus(404)
// Check request body options exist

View File

@ -43,17 +43,17 @@ class SessionController {
res.json(payload)
}
getOpenSessions(req, res) {
async getOpenSessions(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`)
return res.sendStatus(404)
}
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
const openSessions = this.playbackSessionManager.sessions.map(se => {
const user = Database.users.find(u => u.id === se.userId) || null
return {
...se.toJSON(),
user: user ? { id: user.id, username: user.username } : null
user: minifiedUserObjects.find(u => u.id === se.userId) || null
}
})

View File

@ -17,7 +17,8 @@ class UserController {
const includes = (req.query.include || '').split(',').map(i => i.trim())
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
const users = Database.users.map(u => u.toJSONForBrowser(hideRootToken, true))
const allUsers = await Database.models.user.getOldUsers()
const users = allUsers.map(u => u.toJSONForBrowser(hideRootToken, true))
if (includes.includes('latestSession')) {
for (const user of users) {
@ -31,25 +32,20 @@ class UserController {
})
}
findOne(req, res) {
async findOne(req, res) {
if (!req.user.isAdminOrUp) {
Logger.error('User other than admin attempting to get user', req.user)
return res.sendStatus(403)
}
const user = Database.users.find(u => u.id === req.params.id)
if (!user) {
return res.sendStatus(404)
}
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot))
}
async create(req, res) {
var account = req.body
const account = req.body
const username = account.username
var username = account.username
var usernameExists = Database.users.find(u => u.username.toLowerCase() === username.toLowerCase())
const usernameExists = await Database.models.user.getUserByUsername(username)
if (usernameExists) {
return res.status(500).send('Username already taken')
}
@ -73,7 +69,7 @@ class UserController {
}
async update(req, res) {
var user = req.reqUser
const user = req.reqUser
if (user.type === 'root' && !req.user.isRoot) {
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
@ -84,7 +80,7 @@ class UserController {
var shouldUpdateToken = false
if (account.username !== undefined && account.username !== user.username) {
var usernameExists = Database.users.find(u => u.username.toLowerCase() === account.username.toLowerCase())
const usernameExists = await Database.models.user.getUserByUsername(account.username)
if (usernameExists) {
return res.status(500).send('Username already taken')
}
@ -126,7 +122,7 @@ class UserController {
// Todo: check if user is logged in and cancel streams
// Remove user playlists
const userPlaylists = Database.playlists.filter(p => p.userId === user.id)
const userPlaylists = await Database.models.playlist.getPlaylistsForUserAndLibrary(user.id)
for (const playlist of userPlaylists) {
await Database.removePlaylist(playlist.id)
}
@ -178,7 +174,7 @@ class UserController {
})
}
middleware(req, res, next) {
async middleware(req, res, next) {
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
return res.sendStatus(403)
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) {
@ -186,7 +182,7 @@ class UserController {
}
if (req.params.id) {
req.reqUser = Database.users.find(u => u.id === req.params.id)
req.reqUser = await Database.models.user.getUserById(req.params.id)
if (!req.reqUser) {
return res.sendStatus(404)
}

View File

@ -13,13 +13,21 @@ class CronManager {
this.podcastCronExpressionsExecuting = []
}
init() {
this.initLibraryScanCrons()
/**
* Initialize library scan crons & podcast download crons
* @param {oldLibrary[]} libraries
*/
init(libraries) {
this.initLibraryScanCrons(libraries)
this.initPodcastCrons()
}
initLibraryScanCrons() {
for (const library of Database.libraries) {
/**
* Initialize library scan crons
* @param {oldLibrary[]} libraries
*/
initLibraryScanCrons(libraries) {
for (const library of libraries) {
if (library.settings.autoScanCronExpression) {
this.startCronForLibrary(library)
}

View File

@ -14,15 +14,15 @@ class NotificationManager {
return notificationData
}
onPodcastEpisodeDownloaded(libraryItem, episode) {
async onPodcastEpisodeDownloaded(libraryItem, episode) {
if (!Database.notificationSettings.isUseable) return
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId)
const library = await Database.models.library.getOldById(libraryItem.libraryId)
const eventData = {
libraryItemId: libraryItem.id,
libraryId: libraryItem.libraryId,
libraryName: library ? library.name : 'Unknown',
libraryName: library?.name || 'Unknown',
mediaTags: (libraryItem.media.tags || []).join(', '),
podcastTitle: libraryItem.media.metadata.title,
podcastAuthor: libraryItem.media.metadata.author || '',

View File

@ -50,7 +50,7 @@ class PodcastManager {
}
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
let index = libraryItem.media.episodes.length + 1
let index = Math.max(...libraryItem.media.episodes.filter(ep => ep.index == null || isNaN(ep.index)).map(ep => Number(ep.index))) + 1
for (const ep of episodesToDownload) {
const newPe = new PodcastEpisode()
newPe.setData(ep, index++)

View File

@ -10,9 +10,10 @@ const Feed = require('../objects/Feed')
class RssFeedManager {
constructor() { }
validateFeedEntity(feedObj) {
async validateFeedEntity(feedObj) {
if (feedObj.entityType === 'collection') {
if (!Database.collections.some(li => li.id === feedObj.entityId)) {
const collection = await Database.models.collection.getById(feedObj.entityId)
if (!collection) {
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
return false
}
@ -42,7 +43,7 @@ class RssFeedManager {
const feeds = await Database.models.feed.getOldFeeds()
for (const feed of feeds) {
// Remove invalid feeds
if (!this.validateFeedEntity(feed)) {
if (!await this.validateFeedEntity(feed)) {
await Database.removeFeed(feed.id)
}
}
@ -101,7 +102,7 @@ class RssFeedManager {
await Database.updateFeed(feed)
}
} else if (feed.entityType === 'collection') {
const collection = Database.collections.find(c => c.id === feed.entityId)
const collection = await Database.models.collection.getById(feed.entityId)
if (collection) {
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)

View File

@ -92,6 +92,73 @@ module.exports = (sequelize) => {
}
})
}
/**
* Get collection by id
* @param {string} collectionId
* @returns {Promise<oldCollection|null>} returns null if not found
*/
static async getById(collectionId) {
if (!collectionId) return null
const collection = await this.findByPk(collectionId, {
include: {
model: sequelize.models.book,
include: sequelize.models.libraryItem
},
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']]
})
if (!collection) return null
return this.getOldCollection(collection)
}
/**
* Remove all collections belonging to library
* @param {string} libraryId
* @returns {Promise<number>} number of collections destroyed
*/
static async removeAllForLibrary(libraryId) {
if (!libraryId) return 0
return this.destroy({
where: {
libraryId
}
})
}
/**
* Get all collections for a library
* @param {string} libraryId
* @returns {Promise<oldCollection[]>}
*/
static async getAllForLibrary(libraryId) {
if (!libraryId) return []
const collections = await this.findAll({
where: {
libraryId
},
include: {
model: sequelize.models.book,
include: sequelize.models.libraryItem
},
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']]
})
return collections.map(c => this.getOldCollection(c))
}
static async getAllForBook(bookId) {
const collections = await this.findAll({
include: {
model: sequelize.models.book,
where: {
id: bookId
},
required: true,
include: sequelize.models.libraryItem
},
order: [[sequelize.models.book, sequelize.models.collectionBook, 'order', 'ASC']]
})
return collections.map(c => this.getOldCollection(c))
}
}
Collection.init({

View File

@ -4,6 +4,10 @@ const oldLibrary = require('../objects/Library')
module.exports = (sequelize) => {
class Library extends Model {
/**
* Get all old libraries
* @returns {Promise<oldLibrary[]>}
*/
static async getAllOldLibraries() {
const libraries = await this.findAll({
include: sequelize.models.libraryFolder,
@ -12,6 +16,11 @@ module.exports = (sequelize) => {
return libraries.map(lib => this.getOldLibrary(lib))
}
/**
* Convert expanded Library to oldLibrary
* @param {Library} libraryExpanded
* @returns {Promise<oldLibrary>}
*/
static getOldLibrary(libraryExpanded) {
const folders = libraryExpanded.libraryFolders.map(folder => {
return {
@ -58,6 +67,11 @@ module.exports = (sequelize) => {
})
}
/**
* Update library and library folders
* @param {object} oldLibrary
* @returns
*/
static async updateFromOld(oldLibrary) {
const existingLibrary = await this.findByPk(oldLibrary.id, {
include: sequelize.models.libraryFolder
@ -112,6 +126,11 @@ module.exports = (sequelize) => {
}
}
/**
* Destroy library by id
* @param {string} libraryId
* @returns
*/
static removeById(libraryId) {
return this.destroy({
where: {
@ -119,6 +138,59 @@ module.exports = (sequelize) => {
}
})
}
/**
* Get all library ids
* @returns {Promise<string[]>} array of library ids
*/
static async getAllLibraryIds() {
const libraries = await this.findAll({
attributes: ['id']
})
return libraries.map(l => l.id)
}
/**
* Find Library by primary key & return oldLibrary
* @param {string} libraryId
* @returns {Promise<oldLibrary|null>} Returns null if not found
*/
static async getOldById(libraryId) {
if (!libraryId) return null
const library = await this.findByPk(libraryId, {
include: sequelize.models.libraryFolder
})
if (!library) return null
return this.getOldLibrary(library)
}
/**
* Get the largest value in the displayOrder column
* Used for setting a new libraries display order
* @returns {Promise<number>}
*/
static getMaxDisplayOrder() {
return this.max('displayOrder') || 0
}
/**
* Updates displayOrder to be sequential
* Used after removing a library
*/
static async resetDisplayOrder() {
const libraries = await this.findAll({
order: [['displayOrder', 'ASC']]
})
for (let i = 0; i < libraries.length; i++) {
const library = libraries[i]
if (library.displayOrder !== i + 1) {
Logger.dev(`[Library] Updating display order of library from ${library.displayOrder} to ${i + 1}`)
await library.update({ displayOrder: i + 1 }).catch((error) => {
Logger.error(`[Library] Failed to update library display order to ${i + 1}`, error)
})
}
}
}
}
Library.init({

View File

@ -1,7 +1,18 @@
const { DataTypes, Model } = require('sequelize')
module.exports = (sequelize) => {
class LibraryFolder extends Model { }
class LibraryFolder extends Model {
/**
* Gets all library folder path strings
* @returns {Promise<string[]>} array of library folder paths
*/
static async getAllLibraryFolderPaths() {
const libraryFolders = await this.findAll({
attributes: ['path']
})
return libraryFolders.map(l => l.path)
}
}
LibraryFolder.init({
id: {

View File

@ -1,4 +1,4 @@
const { DataTypes, Model } = require('sequelize')
const { DataTypes, Model, Op } = require('sequelize')
const Logger = require('../Logger')
const oldPlaylist = require('../objects/Playlist')
@ -119,6 +119,146 @@ module.exports = (sequelize) => {
}
})
}
/**
* Get playlist by id
* @param {string} playlistId
* @returns {Promise<oldPlaylist|null>} returns null if not found
*/
static async getById(playlistId) {
if (!playlistId) return null
const playlist = await this.findByPk(playlistId, {
include: {
model: sequelize.models.playlistMediaItem,
include: [
{
model: sequelize.models.book,
include: sequelize.models.libraryItem
},
{
model: sequelize.models.podcastEpisode,
include: {
model: sequelize.models.podcast,
include: sequelize.models.libraryItem
}
}
]
},
order: [['playlistMediaItems', 'order', 'ASC']]
})
if (!playlist) return null
return this.getOldPlaylist(playlist)
}
/**
* Get playlists for user and optionally for library
* @param {string} userId
* @param {[string]} libraryId optional
* @returns {Promise<oldPlaylist[]>}
*/
static async getPlaylistsForUserAndLibrary(userId, libraryId = null) {
if (!userId && !libraryId) return []
const whereQuery = {}
if (userId) {
whereQuery.userId = userId
}
if (libraryId) {
whereQuery.libraryId = libraryId
}
const playlists = await this.findAll({
where: whereQuery,
include: {
model: sequelize.models.playlistMediaItem,
include: [
{
model: sequelize.models.book,
include: sequelize.models.libraryItem
},
{
model: sequelize.models.podcastEpisode,
include: {
model: sequelize.models.podcast,
include: sequelize.models.libraryItem
}
}
]
},
order: [['playlistMediaItems', 'order', 'ASC']]
})
return playlists.map(p => this.getOldPlaylist(p))
}
/**
* Get number of playlists for a user and library
* @param {string} userId
* @param {string} libraryId
* @returns
*/
static async getNumPlaylistsForUserAndLibrary(userId, libraryId) {
return this.count({
where: {
userId,
libraryId
}
})
}
/**
* Get all playlists for mediaItemIds
* @param {string[]} mediaItemIds
* @returns {Promise<oldPlaylist[]>}
*/
static async getPlaylistsForMediaItemIds(mediaItemIds) {
if (!mediaItemIds?.length) return []
const playlistMediaItemsExpanded = await sequelize.models.playlistMediaItem.findAll({
where: {
mediaItemId: {
[Op.in]: mediaItemIds
}
},
include: [
{
model: sequelize.models.playlist,
include: {
model: sequelize.models.playlistMediaItem,
include: [
{
model: sequelize.models.book,
include: sequelize.models.libraryItem
},
{
model: sequelize.models.podcastEpisode,
include: {
model: sequelize.models.podcast,
include: sequelize.models.libraryItem
}
}
]
}
}
],
order: [['playlist', 'playlistMediaItems', 'order', 'ASC']]
})
return playlistMediaItemsExpanded.map(pmie => {
pmie.playlist.playlistMediaItems = pmie.playlist.playlistMediaItems.map(pmi => {
if (pmi.mediaItemType === 'book' && pmi.book !== undefined) {
pmi.mediaItem = pmi.book
pmi.dataValues.mediaItem = pmi.dataValues.book
} else if (pmi.mediaItemType === 'podcastEpisode' && pmi.podcastEpisode !== undefined) {
pmi.mediaItem = pmi.podcastEpisode
pmi.dataValues.mediaItem = pmi.dataValues.podcastEpisode
}
delete pmi.book
delete pmi.dataValues.book
delete pmi.podcastEpisode
delete pmi.dataValues.podcastEpisode
return pmi
})
return this.getOldPlaylist(pmie.playlist)
})
}
}
Playlist.init({

View File

@ -1,10 +1,14 @@
const uuidv4 = require("uuid").v4
const { DataTypes, Model } = require('sequelize')
const { DataTypes, Model, Op } = require('sequelize')
const Logger = require('../Logger')
const oldUser = require('../objects/user/User')
module.exports = (sequelize) => {
class User extends Model {
/**
* Get all oldUsers
* @returns {Promise<oldUser>}
*/
static async getOldUsers() {
const users = await this.findAll({
include: sequelize.models.mediaProgress
@ -89,6 +93,13 @@ module.exports = (sequelize) => {
})
}
/**
* Create root user
* @param {string} username
* @param {string} pash
* @param {Auth} auth
* @returns {oldUser}
*/
static async createRootUser(username, pash, auth) {
const userId = uuidv4()
@ -106,6 +117,95 @@ module.exports = (sequelize) => {
await this.createFromOld(newRoot)
return newRoot
}
/**
* Get a user by id or by the old database id
* @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id
* @param {string} userId
* @returns {Promise<oldUser|null>} null if not found
*/
static async getUserByIdOrOldId(userId) {
if (!userId) return null
const user = await this.findOne({
where: {
[Op.or]: [
{
id: userId
},
{
extraData: {
[Op.substring]: userId
}
}
]
},
include: sequelize.models.mediaProgress
})
if (!user) return null
return this.getOldUser(user)
}
/**
* Get user by username case insensitive
* @param {string} username
* @returns {Promise<oldUser|null>} returns null if not found
*/
static async getUserByUsername(username) {
if (!username) return null
const user = await this.findOne({
where: {
username: {
[Op.like]: username
}
},
include: sequelize.models.mediaProgress
})
if (!user) return null
return this.getOldUser(user)
}
/**
* Get user by id
* @param {string} userId
* @returns {Promise<oldUser|null>} returns null if not found
*/
static async getUserById(userId) {
if (!userId) return null
const user = await this.findByPk(userId, {
include: sequelize.models.mediaProgress
})
if (!user) return null
return this.getOldUser(user)
}
/**
* Get array of user id and username
* @returns {object[]} { id, username }
*/
static async getMinifiedUserObjects() {
const users = await this.findAll({
attributes: ['id', 'username']
})
return users.map(u => {
return {
id: u.id,
username: u.username
}
})
}
/**
* Return true if root user exists
* @returns {boolean}
*/
static async getHasRootUser() {
const count = await this.count({
where: {
type: 'root'
}
})
return count > 0
}
}
User.init({

View File

@ -523,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
@ -556,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) => {

View File

@ -285,7 +285,6 @@ class Podcast {
addPodcastEpisode(podcastEpisode) {
this.episodes.push(podcastEpisode)
this.reorderEpisodes()
}
addNewEpisodeFromAudioFile(audioFile, index) {
@ -297,19 +296,6 @@ class Podcast {
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) {

View File

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

View File

@ -381,19 +381,19 @@ class ApiRouter {
async handleDeleteLibraryItem(libraryItem) {
// Remove media progress for this library item from all users
for (const user of Database.users) {
const users = await Database.models.user.getOldUsers()
for (const user of users) {
for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) {
await Database.removeMediaProgress(mediaProgress.id)
}
}
// TODO: Remove open sessions for library item
let mediaItemIds = []
if (libraryItem.isBook) {
// remove book from collections
const collectionsWithBook = Database.collections.filter(c => c.books.includes(libraryItem.id))
for (let i = 0; i < collectionsWithBook.length; i++) {
const collection = collectionsWithBook[i]
const collectionsWithBook = await Database.models.collection.getAllForBook(libraryItem.media.id)
for (const collection of collectionsWithBook) {
collection.removeBook(libraryItem.id)
await Database.removeCollectionBook(collection.id, libraryItem.media.id)
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
@ -401,12 +401,15 @@ class ApiRouter {
// Check remove empty series
await this.checkRemoveEmptySeries(libraryItem.media.metadata.series, libraryItem.id)
mediaItemIds.push(libraryItem.media.id)
} else if (libraryItem.isPodcast) {
mediaItemIds.push(...libraryItem.media.episodes.map(ep => ep.id))
}
// remove item from playlists
const playlistsWithItem = Database.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id))
for (let i = 0; i < playlistsWithItem.length; i++) {
const playlist = playlistsWithItem[i]
const playlistsWithItem = await Database.models.playlist.getPlaylistsForMediaItemIds(mediaItemIds)
for (const playlist of playlistsWithItem) {
playlist.removeItemsForLibraryItem(libraryItem.id)
// If playlist is now empty then remove it
@ -462,11 +465,11 @@ class ApiRouter {
async getAllSessionsWithUserData() {
const sessions = await Database.getPlaybackSessions()
sessions.sort((a, b) => b.updatedAt - a.updatedAt)
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
return sessions.map(se => {
const user = Database.users.find(u => u.id === se.userId)
return {
...se,
user: user ? { id: user.id, username: user.username } : null
user: minifiedUserObjects.find(u => u.id === se.userId) || null
}
})
}

View File

@ -278,17 +278,17 @@ class MediaFileScanner {
const existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
if (newAudioFiles.length) {
let newIndex = libraryItem.media.episodes.length + 1
let newIndex = Math.max(...libraryItem.media.episodes.filter(ep => ep.index == null || isNaN(ep.index)).map(ep => Number(ep.index))) + 1
newAudioFiles.forEach((newAudioFile) => {
libraryItem.media.addNewEpisodeFromAudioFile(newAudioFile, newIndex++)
})
libraryItem.media.reorderEpisodes()
hasUpdated = true
}
// Update audio file metadata for audio files already there
existingAudioFiles.forEach((af) => {
const podcastEpisode = libraryItem.media.findEpisodeWithInode(af.ino)
af.index = 1
if (podcastEpisode?.audioFile.updateFromScan(af)) {
hasUpdated = true

View File

@ -66,7 +66,7 @@ class Scanner {
}
async scanLibraryItemByRequest(libraryItem) {
const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId)
const library = await Database.models.library.getOldById(libraryItem.libraryId)
if (!library) {
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
return ScanResult.NOTHING
@ -552,7 +552,7 @@ class Scanner {
for (const folderId in folderGroups) {
const libraryId = folderGroups[folderId].libraryId
const library = Database.libraries.find(lib => lib.id === libraryId)
const library = await Database.models.library.getOldById(libraryId)
if (!library) {
Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
continue;