mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-03-14 00:21:31 +01:00
Merge branch 'master' into l10n/lt
This commit is contained in:
commit
b2d45f598b
client/components/modals/item/tabs
server
@ -20,18 +20,14 @@
|
|||||||
<div v-if="!episodes.length" class="flex my-4 text-center justify-center text-xl">{{ $strings.MessageNoEpisodes }}</div>
|
<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">
|
<table v-else class="text-sm tracksTable">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left">Sort #</th>
|
<th class="text-center w-20 min-w-20">{{ $strings.LabelEpisode }}</th>
|
||||||
<th class="text-left whitespace-nowrap">{{ $strings.LabelEpisode }}</th>
|
<th class="text-left">{{ $strings.LabelEpisodeTitle }}</th>
|
||||||
<th class="text-left">{{ $strings.EpisodeTitle }}</th>
|
<th class="text-center w-28">{{ $strings.LabelEpisodeDuration }}</th>
|
||||||
<th class="text-center w-28">{{ $strings.EpisodeDuration }}</th>
|
<th class="text-center w-28">{{ $strings.LabelEpisodeSize }}</th>
|
||||||
<th class="text-center w-28">{{ $strings.EpisodeSize }}</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="episode in episodes" :key="episode.id">
|
<tr v-for="episode in episodes" :key="episode.id">
|
||||||
<td class="text-left">
|
<td class="text-center w-20 min-w-20">
|
||||||
<p class="px-4">{{ episode.index }}</p>
|
<p>{{ episode.episode }}</p>
|
||||||
</td>
|
|
||||||
<td class="text-left">
|
|
||||||
<p class="px-4">{{ episode.episode }}</p>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ episode.title }}
|
{{ episode.title }}
|
||||||
|
@ -32,12 +32,13 @@ class Auth {
|
|||||||
await Database.updateServerSettings()
|
await Database.updateServerSettings()
|
||||||
|
|
||||||
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
// New token secret creation added in v2.1.0 so generate new API tokens for each user
|
||||||
if (Database.users.length) {
|
const users = await Database.models.user.getOldUsers()
|
||||||
for (const user of Database.users) {
|
if (users.length) {
|
||||||
|
for (const user of users) {
|
||||||
user.token = await this.generateAccessToken({ userId: user.id, username: user.username })
|
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`)
|
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) {
|
verifyToken(token) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
jwt.verify(token, Database.serverSettings.tokenSecret, (err, payload) => {
|
jwt.verify(token, Database.serverSettings.tokenSecret, async (err, payload) => {
|
||||||
if (!payload || err) {
|
if (!payload || err) {
|
||||||
Logger.error('JWT Verify Token Failed', err)
|
Logger.error('JWT Verify Token Failed', err)
|
||||||
return resolve(null)
|
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 {
|
return {
|
||||||
user: user.toJSONForBrowser(),
|
user: user.toJSONForBrowser(),
|
||||||
userDefaultLibraryId: user.getDefaultLibraryId(Database.libraries),
|
userDefaultLibraryId: user.getDefaultLibraryId(libraryIds),
|
||||||
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
serverSettings: Database.serverSettings.toJSONForBrowser(),
|
||||||
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
ereaderDevices: Database.emailSettings.getEReaderDevices(user),
|
||||||
Source: global.Source
|
Source: global.Source
|
||||||
@ -119,7 +131,7 @@ class Auth {
|
|||||||
const username = (req.body.username || '').toLowerCase()
|
const username = (req.body.username || '').toLowerCase()
|
||||||
const password = req.body.password || ''
|
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) {
|
if (!user?.isActive) {
|
||||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
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)')
|
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||||
} else {
|
} else {
|
||||||
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
|
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)
|
const compare = await bcrypt.compare(password, user.pash)
|
||||||
if (compare) {
|
if (compare) {
|
||||||
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
|
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
|
||||||
res.json(this.getUserLoginResponsePayload(user))
|
const userLoginResponsePayload = await this.getUserLoginResponsePayload(user)
|
||||||
|
res.json(userLoginResponsePayload)
|
||||||
} else {
|
} else {
|
||||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||||
if (req.rateLimit.remaining <= 2) {
|
if (req.rateLimit.remaining <= 2) {
|
||||||
@ -164,7 +178,7 @@ class Auth {
|
|||||||
async userChangePassword(req, res) {
|
async userChangePassword(req, res) {
|
||||||
var { password, newPassword } = req.body
|
var { password, newPassword } = req.body
|
||||||
newPassword = newPassword || ''
|
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
|
// Only root can have an empty password
|
||||||
if (matchingUser.type !== 'root' && !newPassword) {
|
if (matchingUser.type !== 'root' && !newPassword) {
|
||||||
|
@ -6,21 +6,19 @@ const fs = require('./libs/fsExtra')
|
|||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
const dbMigration = require('./utils/migrations/dbMigration')
|
const dbMigration = require('./utils/migrations/dbMigration')
|
||||||
|
const Auth = require('./Auth')
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sequelize = null
|
this.sequelize = null
|
||||||
this.dbPath = null
|
this.dbPath = null
|
||||||
this.isNew = false // New absdatabase.sqlite created
|
this.isNew = false // New absdatabase.sqlite created
|
||||||
|
this.hasRootUser = false // Used to show initialization page in web ui
|
||||||
|
|
||||||
// Temporarily using format of old DB
|
// Temporarily using format of old DB
|
||||||
// TODO: below data should be loaded from the DB as needed
|
// TODO: below data should be loaded from the DB as needed
|
||||||
this.libraryItems = []
|
this.libraryItems = []
|
||||||
this.users = []
|
|
||||||
this.libraries = []
|
|
||||||
this.settings = []
|
this.settings = []
|
||||||
this.collections = []
|
|
||||||
this.playlists = []
|
|
||||||
this.authors = []
|
this.authors = []
|
||||||
this.series = []
|
this.series = []
|
||||||
|
|
||||||
@ -33,10 +31,6 @@ class Database {
|
|||||||
return this.sequelize?.models || {}
|
return this.sequelize?.models || {}
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasRootUser() {
|
|
||||||
return this.users.some(u => u.type === 'root')
|
|
||||||
}
|
|
||||||
|
|
||||||
async checkHasDb() {
|
async checkHasDb() {
|
||||||
if (!await fs.pathExists(this.dbPath)) {
|
if (!await fs.pathExists(this.dbPath)) {
|
||||||
Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`)
|
Logger.info(`[Database] absdatabase.sqlite not found at ${this.dbPath}`)
|
||||||
@ -66,7 +60,8 @@ class Database {
|
|||||||
this.sequelize = new Sequelize({
|
this.sequelize = new Sequelize({
|
||||||
dialect: 'sqlite',
|
dialect: 'sqlite',
|
||||||
storage: this.dbPath,
|
storage: this.dbPath,
|
||||||
logging: false
|
logging: false,
|
||||||
|
transactionType: 'IMMEDIATE'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper function
|
// Helper function
|
||||||
@ -164,24 +159,15 @@ class Database {
|
|||||||
this.libraryItems = await this.models.libraryItem.loadAllLibraryItems()
|
this.libraryItems = await this.models.libraryItem.loadAllLibraryItems()
|
||||||
Logger.info(`[Database] Loaded ${this.libraryItems.length} library items`)
|
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()
|
this.authors = await this.models.author.getOldAuthors()
|
||||||
Logger.info(`[Database] Loaded ${this.authors.length} authors`)
|
Logger.info(`[Database] Loaded ${this.authors.length} authors`)
|
||||||
|
|
||||||
this.series = await this.models.series.getAllOldSeries()
|
this.series = await this.models.series.getAllOldSeries()
|
||||||
Logger.info(`[Database] Loaded ${this.series.length} series`)
|
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`)
|
Logger.info(`[Database] Db data loaded in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
|
||||||
|
|
||||||
if (packageJson.version !== this.serverSettings.version) {
|
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
|
if (!this.sequelize) return false
|
||||||
const newUser = await this.models.user.createRootUser(username, pash, token)
|
await this.models.user.createRootUser(username, pash, auth)
|
||||||
if (newUser) {
|
this.hasRootUser = true
|
||||||
this.users.push(newUser)
|
return true
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateServerSettings() {
|
updateServerSettings() {
|
||||||
@ -215,7 +205,6 @@ class Database {
|
|||||||
async createUser(oldUser) {
|
async createUser(oldUser) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.user.createFromOld(oldUser)
|
await this.models.user.createFromOld(oldUser)
|
||||||
this.users.push(oldUser)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,7 +221,6 @@ class Database {
|
|||||||
async removeUser(userId) {
|
async removeUser(userId) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.user.removeById(userId)
|
await this.models.user.removeById(userId)
|
||||||
this.users = this.users.filter(u => u.id !== userId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
upsertMediaProgress(oldMediaProgress) {
|
upsertMediaProgress(oldMediaProgress) {
|
||||||
@ -253,7 +241,6 @@ class Database {
|
|||||||
async createLibrary(oldLibrary) {
|
async createLibrary(oldLibrary) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.library.createFromOld(oldLibrary)
|
await this.models.library.createFromOld(oldLibrary)
|
||||||
this.libraries.push(oldLibrary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLibrary(oldLibrary) {
|
updateLibrary(oldLibrary) {
|
||||||
@ -264,7 +251,6 @@ class Database {
|
|||||||
async removeLibrary(libraryId) {
|
async removeLibrary(libraryId) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.library.removeById(libraryId)
|
await this.models.library.removeById(libraryId)
|
||||||
this.libraries = this.libraries.filter(lib => lib.id !== libraryId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCollection(oldCollection) {
|
async createCollection(oldCollection) {
|
||||||
@ -286,7 +272,6 @@ class Database {
|
|||||||
await this.createBulkCollectionBooks(collectionBooks)
|
await this.createBulkCollectionBooks(collectionBooks)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.collections.push(oldCollection)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCollection(oldCollection) {
|
updateCollection(oldCollection) {
|
||||||
@ -308,7 +293,6 @@ class Database {
|
|||||||
async removeCollection(collectionId) {
|
async removeCollection(collectionId) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.collection.removeById(collectionId)
|
await this.models.collection.removeById(collectionId)
|
||||||
this.collections = this.collections.filter(c => c.id !== collectionId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createCollectionBook(collectionBook) {
|
createCollectionBook(collectionBook) {
|
||||||
@ -353,7 +337,6 @@ class Database {
|
|||||||
await this.createBulkPlaylistMediaItems(playlistMediaItems)
|
await this.createBulkPlaylistMediaItems(playlistMediaItems)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.playlists.push(oldPlaylist)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlaylist(oldPlaylist) {
|
updatePlaylist(oldPlaylist) {
|
||||||
@ -376,7 +359,6 @@ class Database {
|
|||||||
async removePlaylist(playlistId) {
|
async removePlaylist(playlistId) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
await this.models.playlist.removeById(playlistId)
|
await this.models.playlist.removeById(playlistId)
|
||||||
this.playlists = this.playlists.filter(p => p.id !== playlistId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createPlaylistMediaItem(playlistMediaItem) {
|
createPlaylistMediaItem(playlistMediaItem) {
|
||||||
@ -405,12 +387,14 @@ class Database {
|
|||||||
|
|
||||||
async createLibraryItem(oldLibraryItem) {
|
async createLibraryItem(oldLibraryItem) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
|
await oldLibraryItem.saveMetadata()
|
||||||
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
||||||
this.libraryItems.push(oldLibraryItem)
|
this.libraryItems.push(oldLibraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLibraryItem(oldLibraryItem) {
|
async updateLibraryItem(oldLibraryItem) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
|
await oldLibraryItem.saveMetadata()
|
||||||
return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
return this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,8 +402,11 @@ class Database {
|
|||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
let updatesMade = 0
|
let updatesMade = 0
|
||||||
for (const oldLibraryItem of oldLibraryItems) {
|
for (const oldLibraryItem of oldLibraryItems) {
|
||||||
|
await oldLibraryItem.saveMetadata()
|
||||||
const hasUpdates = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
const hasUpdates = await this.models.libraryItem.fullUpdateFromOld(oldLibraryItem)
|
||||||
if (hasUpdates) updatesMade++
|
if (hasUpdates) {
|
||||||
|
updatesMade++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return updatesMade
|
return updatesMade
|
||||||
}
|
}
|
||||||
@ -427,6 +414,7 @@ class Database {
|
|||||||
async createBulkLibraryItems(oldLibraryItems) {
|
async createBulkLibraryItems(oldLibraryItems) {
|
||||||
if (!this.sequelize) return false
|
if (!this.sequelize) return false
|
||||||
for (const oldLibraryItem of oldLibraryItems) {
|
for (const oldLibraryItem of oldLibraryItems) {
|
||||||
|
await oldLibraryItem.saveMetadata()
|
||||||
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
await this.models.libraryItem.fullCreateFromOld(oldLibraryItem)
|
||||||
this.libraryItems.push(oldLibraryItem)
|
this.libraryItems.push(oldLibraryItem)
|
||||||
}
|
}
|
||||||
|
@ -93,6 +93,10 @@ class Server {
|
|||||||
this.auth.authMiddleware(req, res, next)
|
this.auth.authMiddleware(req, res, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database, backups, logs, rss feeds, cron jobs & watcher
|
||||||
|
* Cleanup stale/invalid data
|
||||||
|
*/
|
||||||
async init() {
|
async init() {
|
||||||
Logger.info('[Server] Init v' + version)
|
Logger.info('[Server] Init v' + version)
|
||||||
await this.playbackSessionManager.removeOrphanStreams()
|
await this.playbackSessionManager.removeOrphanStreams()
|
||||||
@ -105,20 +109,21 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.cleanUserData() // Remove invalid user item progress
|
await this.cleanUserData() // Remove invalid user item progress
|
||||||
await this.purgeMetadata() // Remove metadata folders without library item
|
|
||||||
await this.cacheManager.ensureCachePaths()
|
await this.cacheManager.ensureCachePaths()
|
||||||
|
|
||||||
await this.backupManager.init()
|
await this.backupManager.init()
|
||||||
await this.logManager.init()
|
await this.logManager.init()
|
||||||
await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
|
await this.apiRouter.checkRemoveEmptySeries(Database.series) // Remove empty series
|
||||||
await this.rssFeedManager.init()
|
await this.rssFeedManager.init()
|
||||||
this.cronManager.init()
|
|
||||||
|
const libraries = await Database.models.library.getAllOldLibraries()
|
||||||
|
this.cronManager.init(libraries)
|
||||||
|
|
||||||
if (Database.serverSettings.scannerDisableWatcher) {
|
if (Database.serverSettings.scannerDisableWatcher) {
|
||||||
Logger.info(`[Server] Watcher is disabled`)
|
Logger.info(`[Server] Watcher is disabled`)
|
||||||
this.watcher.disabled = true
|
this.watcher.disabled = true
|
||||||
} else {
|
} else {
|
||||||
this.watcher.initWatcher(Database.libraries)
|
this.watcher.initWatcher(libraries)
|
||||||
this.watcher.on('files', this.filesChanged.bind(this))
|
this.watcher.on('files', this.filesChanged.bind(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -243,39 +248,10 @@ class Server {
|
|||||||
await this.scanner.scanFilesChanged(fileUpdates)
|
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
|
// Remove user media progress with items that no longer exist & remove seriesHideFrom that no longer exist
|
||||||
async cleanUserData() {
|
async cleanUserData() {
|
||||||
for (const _user of Database.users) {
|
const users = await Database.models.user.getOldUsers()
|
||||||
|
for (const _user of users) {
|
||||||
if (_user.mediaProgress.length) {
|
if (_user.mediaProgress.length) {
|
||||||
for (const mediaProgress of _user.mediaProgress) {
|
for (const mediaProgress of _user.mediaProgress) {
|
||||||
const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId)
|
const libraryItem = Database.libraryItems.find(li => li.id === mediaProgress.libraryItemId)
|
||||||
|
@ -20,9 +20,10 @@ class CollectionController {
|
|||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(req, res) {
|
async findAll(req, res) {
|
||||||
|
const collections = await Database.models.collection.getOldCollections()
|
||||||
res.json({
|
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))
|
res.json(collection.toJSONExpanded(Database.libraryItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
async middleware(req, res, next) {
|
||||||
if (req.params.id) {
|
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) {
|
if (!collection) {
|
||||||
return res.status(404).send('Collection not found')
|
return res.status(404).send('Collection not found')
|
||||||
}
|
}
|
||||||
|
@ -17,12 +17,11 @@ class FileSystemController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Do not include existing mapped library paths in response
|
// Do not include existing mapped library paths in response
|
||||||
Database.libraries.forEach(lib => {
|
const libraryFoldersPaths = await Database.models.libraryFolder.getAllLibraryFolderPaths()
|
||||||
lib.folders.forEach((folder) => {
|
libraryFoldersPaths.forEach((path) => {
|
||||||
let dir = folder.fullPath
|
let dir = path || ''
|
||||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||||
excludedDirs.push(dir)
|
excludedDirs.push(dir)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -44,7 +44,9 @@ class LibraryController {
|
|||||||
|
|
||||||
const library = new Library()
|
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)
|
library.setData(newLibraryPayload)
|
||||||
await Database.createLibrary(library)
|
await Database.createLibrary(library)
|
||||||
|
|
||||||
@ -60,17 +62,18 @@ class LibraryController {
|
|||||||
res.json(library)
|
res.json(library)
|
||||||
}
|
}
|
||||||
|
|
||||||
findAll(req, res) {
|
async findAll(req, res) {
|
||||||
|
const libraries = await Database.models.library.getAllOldLibraries()
|
||||||
|
|
||||||
const librariesAccessible = req.user.librariesAccessible || []
|
const librariesAccessible = req.user.librariesAccessible || []
|
||||||
if (librariesAccessible.length) {
|
if (librariesAccessible.length) {
|
||||||
return res.json({
|
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({
|
res.json({
|
||||||
libraries: Database.libraries.map(lib => lib.toJSON())
|
libraries: libraries.map(lib => lib.toJSON())
|
||||||
// libraries: Database.libraries.map(lib => lib.toJSON())
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +83,7 @@ class LibraryController {
|
|||||||
return res.json({
|
return res.json({
|
||||||
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
filterdata: libraryHelpers.getDistinctFilterDataNew(req.libraryItems),
|
||||||
issues: req.libraryItems.filter(li => li.hasIssues).length,
|
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
|
library: req.library
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -151,6 +154,12 @@ class LibraryController {
|
|||||||
return res.json(library.toJSON())
|
return res.json(library.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE: /api/libraries/:id
|
||||||
|
* Delete a library
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
|
*/
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
const library = req.library
|
const library = req.library
|
||||||
|
|
||||||
@ -158,10 +167,9 @@ class LibraryController {
|
|||||||
this.watcher.removeLibrary(library)
|
this.watcher.removeLibrary(library)
|
||||||
|
|
||||||
// Remove collections for library
|
// Remove collections for library
|
||||||
const collections = Database.collections.filter(c => c.libraryId === library.id)
|
const numCollectionsRemoved = await Database.models.collection.removeAllForLibrary(library.id)
|
||||||
for (const collection of collections) {
|
if (numCollectionsRemoved) {
|
||||||
Logger.info(`[Server] deleting collection "${collection.name}" for library "${library.name}"`)
|
Logger.info(`[Server] Removed ${numCollectionsRemoved} collections for library "${library.name}"`)
|
||||||
await Database.removeCollection(collection.id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove items in this library
|
// Remove items in this library
|
||||||
@ -173,6 +181,10 @@ class LibraryController {
|
|||||||
|
|
||||||
const libraryJson = library.toJSON()
|
const libraryJson = library.toJSON()
|
||||||
await Database.removeLibrary(library.id)
|
await Database.removeLibrary(library.id)
|
||||||
|
|
||||||
|
// Re-order libraries
|
||||||
|
await Database.models.library.resetDisplayOrder()
|
||||||
|
|
||||||
SocketAuthority.emitter('library_removed', libraryJson)
|
SocketAuthority.emitter('library_removed', libraryJson)
|
||||||
return res.json(libraryJson)
|
return res.json(libraryJson)
|
||||||
}
|
}
|
||||||
@ -514,7 +526,9 @@ class LibraryController {
|
|||||||
include: include.join(',')
|
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)
|
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
||||||
|
|
||||||
// If all books restricted to user in this collection then hide this collection
|
// If all books restricted to user in this collection then hide this collection
|
||||||
@ -543,7 +557,8 @@ class LibraryController {
|
|||||||
|
|
||||||
// api/libraries/:id/playlists
|
// api/libraries/:id/playlists
|
||||||
async getUserPlaylistsForLibrary(req, res) {
|
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 = {
|
const payload = {
|
||||||
results: [],
|
results: [],
|
||||||
@ -601,17 +616,23 @@ class LibraryController {
|
|||||||
res.json(categories)
|
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) {
|
async reorder(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
|
Logger.error('[LibraryController] ReorderLibraries invalid user', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
const libraries = await Database.models.library.getAllOldLibraries()
|
||||||
|
|
||||||
var orderdata = req.body
|
const orderdata = req.body
|
||||||
var hasUpdates = false
|
let hasUpdates = false
|
||||||
for (let i = 0; i < orderdata.length; i++) {
|
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) {
|
if (!library) {
|
||||||
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
|
Logger.error(`[LibraryController] Invalid library not found in reorder ${orderdata[i].id}`)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
@ -623,14 +644,14 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdates) {
|
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`)
|
Logger.debug(`[LibraryController] Updated library display orders`)
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[LibraryController] Library orders were up to date`)
|
Logger.debug(`[LibraryController] Library orders were up to date`)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
libraries: Database.libraries.map(lib => lib.toJSON())
|
libraries: libraries.map(lib => lib.toJSON())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -902,13 +923,13 @@ class LibraryController {
|
|||||||
res.send(opmlText)
|
res.send(opmlText)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
async middleware(req, res, next) {
|
||||||
if (!req.user.checkCanAccessLibrary(req.params.id)) {
|
if (!req.user.checkCanAccessLibrary(req.params.id)) {
|
||||||
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
|
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
|
||||||
return res.sendStatus(403)
|
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) {
|
if (!library) {
|
||||||
return res.status(404).send('Library not found')
|
return res.status(404).send('Library not found')
|
||||||
}
|
}
|
||||||
|
@ -24,18 +24,18 @@ class MiscController {
|
|||||||
Logger.error('Invalid request, no files')
|
Logger.error('Invalid request, no files')
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
var files = Object.values(req.files)
|
const files = Object.values(req.files)
|
||||||
var title = req.body.title
|
const title = req.body.title
|
||||||
var author = req.body.author
|
const author = req.body.author
|
||||||
var series = req.body.series
|
const series = req.body.series
|
||||||
var libraryId = req.body.library
|
const libraryId = req.body.library
|
||||||
var folderId = req.body.folder
|
const folderId = req.body.folder
|
||||||
|
|
||||||
var library = Database.libraries.find(lib => lib.id === libraryId)
|
const library = await Database.models.library.getOldById(libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return res.status(404).send(`Library not found with id ${libraryId}`)
|
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) {
|
if (!folder) {
|
||||||
return res.status(404).send(`Folder not found with id ${folderId} in library ${library.name}`)
|
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
|
// For setting permissions recursively
|
||||||
var outputDirectory = ''
|
let outputDirectory = ''
|
||||||
var firstDirPath = ''
|
let firstDirPath = ''
|
||||||
|
|
||||||
if (library.isPodcast) { // Podcasts only in 1 folder
|
if (library.isPodcast) { // Podcasts only in 1 folder
|
||||||
outputDirectory = Path.join(folder.fullPath, title)
|
outputDirectory = Path.join(folder.fullPath, title)
|
||||||
@ -62,8 +62,7 @@ class MiscController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var exists = await fs.pathExists(outputDirectory)
|
if (await fs.pathExists(outputDirectory)) {
|
||||||
if (exists) {
|
|
||||||
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
||||||
return res.status(500).send(`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) {
|
if (!req.user) {
|
||||||
Logger.error('Invalid user in authorize')
|
Logger.error('Invalid user in authorize')
|
||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
const userResponse = this.auth.getUserLoginResponsePayload(req.user)
|
const userResponse = await this.auth.getUserLoginResponsePayload(req.user)
|
||||||
res.json(userResponse)
|
res.json(userResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,9 +22,10 @@ class PlaylistController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GET: api/playlists
|
// GET: api/playlists
|
||||||
findAllForUser(req, res) {
|
async findAllForUser(req, res) {
|
||||||
|
const playlistsForUser = await Database.models.playlist.getPlaylistsForUserAndLibrary(req.user.id)
|
||||||
res.json({
|
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
|
// POST: api/playlists/collection/:collectionId
|
||||||
async createFromCollection(req, res) {
|
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) {
|
if (!collection) {
|
||||||
return res.status(404).send('Collection not found')
|
return res.status(404).send('Collection not found')
|
||||||
}
|
}
|
||||||
@ -231,9 +232,9 @@ class PlaylistController {
|
|||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
async middleware(req, res, next) {
|
||||||
if (req.params.id) {
|
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) {
|
if (!playlist) {
|
||||||
return res.status(404).send('Playlist not found')
|
return res.status(404).send('Playlist not found')
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class PodcastController {
|
|||||||
}
|
}
|
||||||
const payload = req.body
|
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) {
|
if (!library) {
|
||||||
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
Logger.error(`[PodcastController] Create: Library not found "${payload.libraryId}"`)
|
||||||
return res.status(404).send('Library not found')
|
return res.status(404).send('Library not found')
|
||||||
@ -241,18 +241,18 @@ class PodcastController {
|
|||||||
|
|
||||||
// DELETE: api/podcasts/:id/episode/:episodeId
|
// DELETE: api/podcasts/:id/episode/:episodeId
|
||||||
async removeEpisode(req, res) {
|
async removeEpisode(req, res) {
|
||||||
var episodeId = req.params.episodeId
|
const episodeId = req.params.episodeId
|
||||||
var libraryItem = req.libraryItem
|
const libraryItem = req.libraryItem
|
||||||
var hardDelete = req.query.hard === '1'
|
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) {
|
if (!episode) {
|
||||||
Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
Logger.error(`[PodcastController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hardDelete) {
|
if (hardDelete) {
|
||||||
var audioFile = episode.audioFile
|
const audioFile = episode.audioFile
|
||||||
// TODO: this will trigger the watcher. should maybe handle this gracefully
|
// TODO: this will trigger the watcher. should maybe handle this gracefully
|
||||||
await fs.remove(audioFile.metadata.path).then(() => {
|
await fs.remove(audioFile.metadata.path).then(() => {
|
||||||
Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`)
|
Logger.info(`[PodcastController] Hard deleted episode file at "${audioFile.metadata.path}"`)
|
||||||
@ -267,6 +267,22 @@ class PodcastController {
|
|||||||
libraryItem.removeLibraryFile(episodeRemoved.audioFile.ino)
|
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)
|
await Database.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
res.json(libraryItem.toJSON())
|
res.json(libraryItem.toJSON())
|
||||||
|
@ -45,7 +45,7 @@ class RSSFeedController {
|
|||||||
async openRSSFeedForCollection(req, res) {
|
async openRSSFeedForCollection(req, res) {
|
||||||
const options = req.body || {}
|
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)
|
if (!collection) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check request body options exist
|
// Check request body options exist
|
||||||
|
@ -43,17 +43,17 @@ class SessionController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
getOpenSessions(req, res) {
|
async getOpenSessions(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`)
|
Logger.error(`[SessionController] getOpenSessions: Non-admin user requested open session data ${req.user.id}/"${req.user.username}"`)
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
|
||||||
const openSessions = this.playbackSessionManager.sessions.map(se => {
|
const openSessions = this.playbackSessionManager.sessions.map(se => {
|
||||||
const user = Database.users.find(u => u.id === se.userId) || null
|
|
||||||
return {
|
return {
|
||||||
...se.toJSON(),
|
...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())
|
const includes = (req.query.include || '').split(',').map(i => i.trim())
|
||||||
|
|
||||||
// Minimal toJSONForBrowser does not include mediaProgress and bookmarks
|
// 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')) {
|
if (includes.includes('latestSession')) {
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
@ -31,25 +32,20 @@ class UserController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(req, res) {
|
async findOne(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error('User other than admin attempting to get user', req.user)
|
Logger.error('User other than admin attempting to get user', req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = Database.users.find(u => u.id === req.params.id)
|
res.json(this.userJsonWithItemProgressDetails(req.reqUser, !req.user.isRoot))
|
||||||
if (!user) {
|
|
||||||
return res.sendStatus(404)
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(this.userJsonWithItemProgressDetails(user, !req.user.isRoot))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
var account = req.body
|
const account = req.body
|
||||||
|
const username = account.username
|
||||||
|
|
||||||
var username = account.username
|
const usernameExists = await Database.models.user.getUserByUsername(username)
|
||||||
var usernameExists = Database.users.find(u => u.username.toLowerCase() === username.toLowerCase())
|
|
||||||
if (usernameExists) {
|
if (usernameExists) {
|
||||||
return res.status(500).send('Username already taken')
|
return res.status(500).send('Username already taken')
|
||||||
}
|
}
|
||||||
@ -73,7 +69,7 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
var user = req.reqUser
|
const user = req.reqUser
|
||||||
|
|
||||||
if (user.type === 'root' && !req.user.isRoot) {
|
if (user.type === 'root' && !req.user.isRoot) {
|
||||||
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
|
Logger.error(`[UserController] Admin user attempted to update root user`, req.user.username)
|
||||||
@ -84,7 +80,7 @@ class UserController {
|
|||||||
var shouldUpdateToken = false
|
var shouldUpdateToken = false
|
||||||
|
|
||||||
if (account.username !== undefined && account.username !== user.username) {
|
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) {
|
if (usernameExists) {
|
||||||
return res.status(500).send('Username already taken')
|
return res.status(500).send('Username already taken')
|
||||||
}
|
}
|
||||||
@ -126,7 +122,7 @@ class UserController {
|
|||||||
// Todo: check if user is logged in and cancel streams
|
// Todo: check if user is logged in and cancel streams
|
||||||
|
|
||||||
// Remove user playlists
|
// 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) {
|
for (const playlist of userPlaylists) {
|
||||||
await Database.removePlaylist(playlist.id)
|
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) {
|
if (!req.user.isAdminOrUp && req.user.id !== req.params.id) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) {
|
} else if ((req.method == 'PATCH' || req.method == 'POST' || req.method == 'DELETE') && !req.user.isAdminOrUp) {
|
||||||
@ -186,7 +182,7 @@ class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (req.params.id) {
|
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) {
|
if (!req.reqUser) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,21 @@ class CronManager {
|
|||||||
this.podcastCronExpressionsExecuting = []
|
this.podcastCronExpressionsExecuting = []
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
/**
|
||||||
this.initLibraryScanCrons()
|
* Initialize library scan crons & podcast download crons
|
||||||
|
* @param {oldLibrary[]} libraries
|
||||||
|
*/
|
||||||
|
init(libraries) {
|
||||||
|
this.initLibraryScanCrons(libraries)
|
||||||
this.initPodcastCrons()
|
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) {
|
if (library.settings.autoScanCronExpression) {
|
||||||
this.startCronForLibrary(library)
|
this.startCronForLibrary(library)
|
||||||
}
|
}
|
||||||
|
@ -14,15 +14,15 @@ class NotificationManager {
|
|||||||
return notificationData
|
return notificationData
|
||||||
}
|
}
|
||||||
|
|
||||||
onPodcastEpisodeDownloaded(libraryItem, episode) {
|
async onPodcastEpisodeDownloaded(libraryItem, episode) {
|
||||||
if (!Database.notificationSettings.isUseable) return
|
if (!Database.notificationSettings.isUseable) return
|
||||||
|
|
||||||
Logger.debug(`[NotificationManager] onPodcastEpisodeDownloaded: Episode "${episode.title}" for podcast ${libraryItem.media.metadata.title}`)
|
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 = {
|
const eventData = {
|
||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
libraryId: libraryItem.libraryId,
|
libraryId: libraryItem.libraryId,
|
||||||
libraryName: library ? library.name : 'Unknown',
|
libraryName: library?.name || 'Unknown',
|
||||||
mediaTags: (libraryItem.media.tags || []).join(', '),
|
mediaTags: (libraryItem.media.tags || []).join(', '),
|
||||||
podcastTitle: libraryItem.media.metadata.title,
|
podcastTitle: libraryItem.media.metadata.title,
|
||||||
podcastAuthor: libraryItem.media.metadata.author || '',
|
podcastAuthor: libraryItem.media.metadata.author || '',
|
||||||
|
@ -50,7 +50,7 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async downloadPodcastEpisodes(libraryItem, episodesToDownload, isAutoDownload) {
|
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) {
|
for (const ep of episodesToDownload) {
|
||||||
const newPe = new PodcastEpisode()
|
const newPe = new PodcastEpisode()
|
||||||
newPe.setData(ep, index++)
|
newPe.setData(ep, index++)
|
||||||
|
@ -10,9 +10,10 @@ const Feed = require('../objects/Feed')
|
|||||||
class RssFeedManager {
|
class RssFeedManager {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
validateFeedEntity(feedObj) {
|
async validateFeedEntity(feedObj) {
|
||||||
if (feedObj.entityType === 'collection') {
|
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`)
|
Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Collection "${feedObj.entityId}" not found`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -42,7 +43,7 @@ class RssFeedManager {
|
|||||||
const feeds = await Database.models.feed.getOldFeeds()
|
const feeds = await Database.models.feed.getOldFeeds()
|
||||||
for (const feed of feeds) {
|
for (const feed of feeds) {
|
||||||
// Remove invalid feeds
|
// Remove invalid feeds
|
||||||
if (!this.validateFeedEntity(feed)) {
|
if (!await this.validateFeedEntity(feed)) {
|
||||||
await Database.removeFeed(feed.id)
|
await Database.removeFeed(feed.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -101,7 +102,7 @@ class RssFeedManager {
|
|||||||
await Database.updateFeed(feed)
|
await Database.updateFeed(feed)
|
||||||
}
|
}
|
||||||
} else if (feed.entityType === 'collection') {
|
} 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) {
|
if (collection) {
|
||||||
const collectionExpanded = collection.toJSONExpanded(Database.libraryItems)
|
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({
|
Collection.init({
|
||||||
|
@ -4,6 +4,10 @@ const oldLibrary = require('../objects/Library')
|
|||||||
|
|
||||||
module.exports = (sequelize) => {
|
module.exports = (sequelize) => {
|
||||||
class Library extends Model {
|
class Library extends Model {
|
||||||
|
/**
|
||||||
|
* Get all old libraries
|
||||||
|
* @returns {Promise<oldLibrary[]>}
|
||||||
|
*/
|
||||||
static async getAllOldLibraries() {
|
static async getAllOldLibraries() {
|
||||||
const libraries = await this.findAll({
|
const libraries = await this.findAll({
|
||||||
include: sequelize.models.libraryFolder,
|
include: sequelize.models.libraryFolder,
|
||||||
@ -12,6 +16,11 @@ module.exports = (sequelize) => {
|
|||||||
return libraries.map(lib => this.getOldLibrary(lib))
|
return libraries.map(lib => this.getOldLibrary(lib))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert expanded Library to oldLibrary
|
||||||
|
* @param {Library} libraryExpanded
|
||||||
|
* @returns {Promise<oldLibrary>}
|
||||||
|
*/
|
||||||
static getOldLibrary(libraryExpanded) {
|
static getOldLibrary(libraryExpanded) {
|
||||||
const folders = libraryExpanded.libraryFolders.map(folder => {
|
const folders = libraryExpanded.libraryFolders.map(folder => {
|
||||||
return {
|
return {
|
||||||
@ -58,6 +67,11 @@ module.exports = (sequelize) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update library and library folders
|
||||||
|
* @param {object} oldLibrary
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
static async updateFromOld(oldLibrary) {
|
static async updateFromOld(oldLibrary) {
|
||||||
const existingLibrary = await this.findByPk(oldLibrary.id, {
|
const existingLibrary = await this.findByPk(oldLibrary.id, {
|
||||||
include: sequelize.models.libraryFolder
|
include: sequelize.models.libraryFolder
|
||||||
@ -112,6 +126,11 @@ module.exports = (sequelize) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroy library by id
|
||||||
|
* @param {string} libraryId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
static removeById(libraryId) {
|
static removeById(libraryId) {
|
||||||
return this.destroy({
|
return this.destroy({
|
||||||
where: {
|
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({
|
Library.init({
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model } = require('sequelize')
|
||||||
|
|
||||||
module.exports = (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({
|
LibraryFolder.init({
|
||||||
id: {
|
id: {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model, Op } = require('sequelize')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
const oldPlaylist = require('../objects/Playlist')
|
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({
|
Playlist.init({
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
const uuidv4 = require("uuid").v4
|
const uuidv4 = require("uuid").v4
|
||||||
const { DataTypes, Model } = require('sequelize')
|
const { DataTypes, Model, Op } = require('sequelize')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const oldUser = require('../objects/user/User')
|
const oldUser = require('../objects/user/User')
|
||||||
|
|
||||||
module.exports = (sequelize) => {
|
module.exports = (sequelize) => {
|
||||||
class User extends Model {
|
class User extends Model {
|
||||||
|
/**
|
||||||
|
* Get all oldUsers
|
||||||
|
* @returns {Promise<oldUser>}
|
||||||
|
*/
|
||||||
static async getOldUsers() {
|
static async getOldUsers() {
|
||||||
const users = await this.findAll({
|
const users = await this.findAll({
|
||||||
include: sequelize.models.mediaProgress
|
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) {
|
static async createRootUser(username, pash, auth) {
|
||||||
const userId = uuidv4()
|
const userId = uuidv4()
|
||||||
|
|
||||||
@ -106,6 +117,95 @@ module.exports = (sequelize) => {
|
|||||||
await this.createFromOld(newRoot)
|
await this.createFromOld(newRoot)
|
||||||
return 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({
|
User.init({
|
||||||
|
@ -523,7 +523,10 @@ class LibraryItem {
|
|||||||
return this.media.getDirectPlayTracklist(episodeId)
|
return this.media.getDirectPlayTracklist(episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Saves metadata.abs file
|
/**
|
||||||
|
* Save metadata.json/metadata.abs file
|
||||||
|
* @returns {boolean} true if saved
|
||||||
|
*/
|
||||||
async saveMetadata() {
|
async saveMetadata() {
|
||||||
if (this.mediaType === 'video' || this.mediaType === 'music') return
|
if (this.mediaType === 'video' || this.mediaType === 'music') return
|
||||||
|
|
||||||
@ -556,6 +559,7 @@ class LibraryItem {
|
|||||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||||
this.libraryFiles.push(newLibraryFile)
|
this.libraryFiles.push(newLibraryFile)
|
||||||
}
|
}
|
||||||
|
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
@ -285,7 +285,6 @@ class Podcast {
|
|||||||
|
|
||||||
addPodcastEpisode(podcastEpisode) {
|
addPodcastEpisode(podcastEpisode) {
|
||||||
this.episodes.push(podcastEpisode)
|
this.episodes.push(podcastEpisode)
|
||||||
this.reorderEpisodes()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addNewEpisodeFromAudioFile(audioFile, index) {
|
addNewEpisodeFromAudioFile(audioFile, index) {
|
||||||
@ -297,19 +296,6 @@ class Podcast {
|
|||||||
this.episodes.push(pe)
|
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) {
|
removeEpisode(episodeId) {
|
||||||
const episode = this.episodes.find(ep => ep.id === episodeId)
|
const episode = this.episodes.find(ep => ep.id === episodeId)
|
||||||
if (episode) {
|
if (episode) {
|
||||||
|
@ -258,11 +258,15 @@ class User {
|
|||||||
return hasUpdates
|
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
|
// Libraries should already be in ascending display order, find first accessible
|
||||||
var firstAccessibleLibrary = libraries.find(lib => this.checkCanAccessLibrary(lib.id))
|
return libraryIds.find(lid => this.checkCanAccessLibrary(lid)) || null
|
||||||
if (!firstAccessibleLibrary) return null
|
|
||||||
return firstAccessibleLibrary.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns most recent media progress w/ `media` object and optionally an `episode` object
|
// Returns most recent media progress w/ `media` object and optionally an `episode` object
|
||||||
|
@ -381,19 +381,19 @@ class ApiRouter {
|
|||||||
|
|
||||||
async handleDeleteLibraryItem(libraryItem) {
|
async handleDeleteLibraryItem(libraryItem) {
|
||||||
// Remove media progress for this library item from all users
|
// 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)) {
|
for (const mediaProgress of user.getAllMediaProgressForLibraryItem(libraryItem.id)) {
|
||||||
await Database.removeMediaProgress(mediaProgress.id)
|
await Database.removeMediaProgress(mediaProgress.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove open sessions for library item
|
// TODO: Remove open sessions for library item
|
||||||
|
let mediaItemIds = []
|
||||||
if (libraryItem.isBook) {
|
if (libraryItem.isBook) {
|
||||||
// remove book from collections
|
// remove book from collections
|
||||||
const collectionsWithBook = Database.collections.filter(c => c.books.includes(libraryItem.id))
|
const collectionsWithBook = await Database.models.collection.getAllForBook(libraryItem.media.id)
|
||||||
for (let i = 0; i < collectionsWithBook.length; i++) {
|
for (const collection of collectionsWithBook) {
|
||||||
const collection = collectionsWithBook[i]
|
|
||||||
collection.removeBook(libraryItem.id)
|
collection.removeBook(libraryItem.id)
|
||||||
await Database.removeCollectionBook(collection.id, libraryItem.media.id)
|
await Database.removeCollectionBook(collection.id, libraryItem.media.id)
|
||||||
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
SocketAuthority.emitter('collection_updated', collection.toJSONExpanded(Database.libraryItems))
|
||||||
@ -401,12 +401,15 @@ class ApiRouter {
|
|||||||
|
|
||||||
// Check remove empty series
|
// Check remove empty series
|
||||||
await this.checkRemoveEmptySeries(libraryItem.media.metadata.series, libraryItem.id)
|
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
|
// remove item from playlists
|
||||||
const playlistsWithItem = Database.playlists.filter(p => p.hasItemsForLibraryItem(libraryItem.id))
|
const playlistsWithItem = await Database.models.playlist.getPlaylistsForMediaItemIds(mediaItemIds)
|
||||||
for (let i = 0; i < playlistsWithItem.length; i++) {
|
for (const playlist of playlistsWithItem) {
|
||||||
const playlist = playlistsWithItem[i]
|
|
||||||
playlist.removeItemsForLibraryItem(libraryItem.id)
|
playlist.removeItemsForLibraryItem(libraryItem.id)
|
||||||
|
|
||||||
// If playlist is now empty then remove it
|
// If playlist is now empty then remove it
|
||||||
@ -462,11 +465,11 @@ class ApiRouter {
|
|||||||
async getAllSessionsWithUserData() {
|
async getAllSessionsWithUserData() {
|
||||||
const sessions = await Database.getPlaybackSessions()
|
const sessions = await Database.getPlaybackSessions()
|
||||||
sessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
sessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
|
const minifiedUserObjects = await Database.models.user.getMinifiedUserObjects()
|
||||||
return sessions.map(se => {
|
return sessions.map(se => {
|
||||||
const user = Database.users.find(u => u.id === se.userId)
|
|
||||||
return {
|
return {
|
||||||
...se,
|
...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))
|
const existingAudioFiles = mediaScanResult.audioFiles.filter(af => libraryItem.media.findFileWithInode(af.ino))
|
||||||
|
|
||||||
if (newAudioFiles.length) {
|
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) => {
|
newAudioFiles.forEach((newAudioFile) => {
|
||||||
libraryItem.media.addNewEpisodeFromAudioFile(newAudioFile, newIndex++)
|
libraryItem.media.addNewEpisodeFromAudioFile(newAudioFile, newIndex++)
|
||||||
})
|
})
|
||||||
libraryItem.media.reorderEpisodes()
|
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update audio file metadata for audio files already there
|
// Update audio file metadata for audio files already there
|
||||||
existingAudioFiles.forEach((af) => {
|
existingAudioFiles.forEach((af) => {
|
||||||
const podcastEpisode = libraryItem.media.findEpisodeWithInode(af.ino)
|
const podcastEpisode = libraryItem.media.findEpisodeWithInode(af.ino)
|
||||||
|
af.index = 1
|
||||||
if (podcastEpisode?.audioFile.updateFromScan(af)) {
|
if (podcastEpisode?.audioFile.updateFromScan(af)) {
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async scanLibraryItemByRequest(libraryItem) {
|
async scanLibraryItemByRequest(libraryItem) {
|
||||||
const library = Database.libraries.find(lib => lib.id === libraryItem.libraryId)
|
const library = await Database.models.library.getOldById(libraryItem.libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
@ -552,7 +552,7 @@ class Scanner {
|
|||||||
|
|
||||||
for (const folderId in folderGroups) {
|
for (const folderId in folderGroups) {
|
||||||
const libraryId = folderGroups[folderId].libraryId
|
const libraryId = folderGroups[folderId].libraryId
|
||||||
const library = Database.libraries.find(lib => lib.id === libraryId)
|
const library = await Database.models.library.getOldById(libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
|
Logger.error(`[Scanner] Library not found in files changed ${libraryId}`)
|
||||||
continue;
|
continue;
|
||||||
|
Loading…
Reference in New Issue
Block a user