mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-27 00:59:14 +01:00
Merge branch 'master' into l10n/lt
This commit is contained in:
commit
b2d45f598b
@ -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 }}
|
||||
|
@ -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) {
|
||||
|
@ -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,14 +177,18 @@ 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)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
await this.models.user.createRootUser(username, pash, auth)
|
||||
this.hasRootUser = true
|
||||
return true
|
||||
}
|
||||
|
||||
updateServerSettings() {
|
||||
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -17,12 +17,11 @@ class FileSystemController {
|
||||
})
|
||||
|
||||
// Do not include existing mapped library paths in response
|
||||
Database.libraries.forEach(lib => {
|
||||
lib.folders.forEach((folder) => {
|
||||
let dir = folder.fullPath
|
||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||
excludedDirs.push(dir)
|
||||
})
|
||||
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({
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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')
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 || '',
|
||||
|
@ -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++)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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: {
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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) => {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user