Migration change metadata folder from /books to /items, podcast data model updates, add podcast routes

This commit is contained in:
advplyr 2022-03-19 10:13:10 -05:00
parent 43bbfbfee3
commit f8d0384155
11 changed files with 153 additions and 89 deletions

View File

@ -82,7 +82,7 @@ export default {
return return
} }
this.processing = true this.processing = true
var podcastfeed = await this.$axios.$post(`/api/getPodcastFeed`, { rssFeed: podcast.feedUrl }).catch((error) => { var podcastfeed = await this.$axios.$post(`/api/podcasts/feed`, { rssFeed: podcast.feedUrl }).catch((error) => {
console.error('Failed to get feed', error) console.error('Failed to get feed', error)
this.$toast.error('Failed to get podcast feed') this.$toast.error('Failed to get podcast feed')
return null return null

View File

@ -15,7 +15,7 @@ const Backup = require('./objects/Backup')
class BackupManager { class BackupManager {
constructor(db, emitter) { constructor(db, emitter) {
this.BackupPath = Path.join(global.MetadataPath, 'backups') this.BackupPath = Path.join(global.MetadataPath, 'backups')
this.MetadataBooksPath = Path.join(global.MetadataPath, 'books') this.ItemsMetadataPath = Path.join(global.MetadataPath, 'items')
this.db = db this.db = db
this.emitter = emitter this.emitter = emitter
@ -115,7 +115,7 @@ class BackupManager {
const zip = new StreamZip.async({ file: backup.fullPath }) const zip = new StreamZip.async({ file: backup.fullPath })
await zip.extract('config/', global.ConfigPath) await zip.extract('config/', global.ConfigPath)
if (backup.backupMetadataCovers) { if (backup.backupMetadataCovers) {
await zip.extract('metadata-books/', this.MetadataBooksPath) await zip.extract('metadata-items/', this.ItemsMetadataPath)
} }
await this.db.reinit() await this.db.reinit()
this.emitter('backup_applied') this.emitter('backup_applied')
@ -154,7 +154,7 @@ class BackupManager {
async runBackup() { async runBackup() {
// Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself) // Check if Metadata Path is inside Config Path (otherwise there will be an infinite loop as the archiver tries to zip itself)
Logger.info(`[BackupManager] Running Backup`) Logger.info(`[BackupManager] Running Backup`)
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? this.MetadataBooksPath : null var metadataItemsPath = this.serverSettings.backupMetadataCovers ? this.ItemsMetadataPath : null
var newBackup = new Backup() var newBackup = new Backup()
@ -164,7 +164,7 @@ class BackupManager {
} }
newBackup.setData(newBackData) newBackup.setData(newBackData)
var zipResult = await this.zipBackup(metadataBooksPath, newBackup).then(() => true).catch((error) => { var zipResult = await this.zipBackup(metadataItemsPath, newBackup).then(() => true).catch((error) => {
Logger.error(`[BackupManager] Backup Failed ${error}`) Logger.error(`[BackupManager] Backup Failed ${error}`)
return false return false
}) })
@ -204,7 +204,7 @@ class BackupManager {
} }
} }
zipBackup(metadataBooksPath, backup) { zipBackup(metadataItemsPath, backup) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// create a file to stream archive data to // create a file to stream archive data to
const output = fs.createWriteStream(backup.fullPath) const output = fs.createWriteStream(backup.fullPath)
@ -274,9 +274,9 @@ class BackupManager {
archive.directory(this.db.AuthorsPath, 'config/authors') archive.directory(this.db.AuthorsPath, 'config/authors')
archive.directory(this.db.SeriesPath, 'config/series') archive.directory(this.db.SeriesPath, 'config/series')
if (metadataBooksPath) { if (metadataItemsPath) {
Logger.debug(`[BackupManager] Backing up Metadata Books "${metadataBooksPath}"`) Logger.debug(`[BackupManager] Backing up Metadata Items "${metadataItemsPath}"`)
archive.directory(metadataBooksPath, 'metadata-books') archive.directory(metadataItemsPath, 'metadata-items')
} }
archive.append(backup.detailsString, { name: 'details' }) archive.append(backup.detailsString, { name: 'details' })

View File

@ -15,14 +15,14 @@ class CoverController {
this.db = db this.db = db
this.cacheManager = cacheManager this.cacheManager = cacheManager
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books') this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items')
} }
getCoverDirectory(libraryItem) { getCoverDirectory(libraryItem) {
if (this.db.serverSettings.storeCoverWithBook) { if (this.db.serverSettings.storeCoverWithBook) {
return libraryItem.path return libraryItem.path
} else { } else {
return Path.posix.join(this.BookMetadataPath, libraryItem.id) return Path.posix.join(this.ItemMetadataPath, libraryItem.id)
} }
} }
@ -237,7 +237,7 @@ class CoverController {
var coverAlreadyExists = await fs.pathExists(coverFilePath) var coverAlreadyExists = await fs.pathExists(coverFilePath)
if (coverAlreadyExists) { if (coverAlreadyExists) {
Logger.warn(`[Audiobook] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`) Logger.warn(`[CoverController] Extract embedded cover art but cover already exists for "${libraryItem.media.metadata.title}" - bail`)
return false return false
} }

View File

@ -111,28 +111,19 @@ class Server {
await this.downloadManager.removeOrphanDownloads() await this.downloadManager.removeOrphanDownloads()
if (version.localeCompare('1.7.3') < 0) { // Old version data model migration if (version.localeCompare('1.7.3') < 0) { // Old version data model migration
await dbMigration.migrateUserData(this.db) // Db not yet loaded await dbMigration.migrate(this.db)
await this.db.init()
await dbMigration.migrateLibraryItems(this.db)
// TODO: Eventually remove audiobooks db when stable
} else { } else {
await this.db.init() await this.db.init()
} }
this.auth.init() this.auth.init()
// TODO: Implement method to remove old user auidobook data and book metadata folders await this.checkUserLibraryItemProgress() // Remove invalid user item progress
// await this.checkUserAudiobookData() await this.purgeMetadata() // Remove metadata folders without library item
// await this.purgeMetadata()
await this.backupManager.init() await this.backupManager.init()
await this.logManager.init() await this.logManager.init()
// If server upgrade and last version was 1.7.0 or earlier - add abmetadata files
// if (this.db.checkPreviousVersionIsBefore('1.7.1')) {
// TODO: wait until stable
// }
if (this.db.serverSettings.scannerDisableWatcher) { if (this.db.serverSettings.scannerDisableWatcher) {
Logger.info(`[Server] Watcher is disabled`) Logger.info(`[Server] Watcher is disabled`)
this.watcher.disabled = true this.watcher.disabled = true
@ -275,18 +266,17 @@ class Server {
socket.emit('save_metadata_complete', response) socket.emit('save_metadata_complete', response)
} }
// Remove unused /metadata/books/{id} folders // Remove unused /metadata/items/{id} folders
async purgeMetadata() { async purgeMetadata() {
var booksMetadata = Path.join(global.MetadataPath, 'books') var itemsMetadata = Path.join(global.MetadataPath, 'items')
var booksMetadataExists = await fs.pathExists(booksMetadata) if (!(await fs.pathExists(itemsMetadata))) return
if (!booksMetadataExists) return var foldersInItemsMetadata = await fs.readdir(itemsMetadata)
var foldersInBooksMetadata = await fs.readdir(booksMetadata)
var purged = 0 var purged = 0
await Promise.all(foldersInBooksMetadata.map(async foldername => { await Promise.all(foldersInItemsMetadata.map(async foldername => {
var hasMatchingAudiobook = this.db.audiobooks.find(ab => ab.id === foldername) var hasMatchingItem = this.db.libraryItems.find(ab => ab.id === foldername)
if (!hasMatchingAudiobook) { if (!hasMatchingItem) {
var folderPath = Path.join(booksMetadata, foldername) var folderPath = Path.join(itemsMetadata, foldername)
Logger.debug(`[Server] Purging unused metadata ${folderPath}`) Logger.debug(`[Server] Purging unused metadata ${folderPath}`)
await fs.remove(folderPath).then(() => { await fs.remove(folderPath).then(() => {
@ -297,24 +287,21 @@ class Server {
} }
})) }))
if (purged > 0) { if (purged > 0) {
Logger.info(`[Server] Purged ${purged} unused audiobook metadata`) Logger.info(`[Server] Purged ${purged} unused library item metadata`)
} }
return purged return purged
} }
// Check user audiobook data has matching audiobook // Remove user library item progress entries that dont have a library item
async checkUserAudiobookData() { async checkUserLibraryItemProgress() {
for (let i = 0; i < this.db.users.length; i++) { for (let i = 0; i < this.db.users.length; i++) {
var _user = this.db.users[i] var _user = this.db.users[i]
if (_user.audiobooks) { if (_user.libraryItemProgress) {
// Find user audiobook data that has no matching audiobook var itemProgressIdsToRemove = _user.libraryItemProgress.map(lip => lip.id).filter(lipId => !this.db.libraryItems.find(_li => _li.id == lipId))
var audiobookIdsToRemove = Object.keys(_user.audiobooks).filter(aid => { if (itemProgressIdsToRemove.length) {
return !this.db.audiobooks.find(ab => ab.id === aid) Logger.debug(`[Server] Found ${itemProgressIdsToRemove.length} library item progress data to remove from user ${_user.username}`)
}) for (const lipId of itemProgressIdsToRemove) {
if (audiobookIdsToRemove.length) { _user.removeLibraryItemProgress(lipId)
Logger.debug(`[Server] Found ${audiobookIdsToRemove.length} audiobook data to remove from user ${_user.username}`)
for (let y = 0; y < audiobookIdsToRemove.length; y++) {
_user.removeLibraryItemProgress(audiobookIdsToRemove[y])
} }
await this.db.updateEntity('user', _user) await this.db.updateEntity('user', _user)
} }

View File

@ -1,9 +1,7 @@
const Path = require('path') const Path = require('path')
const fs = require('fs-extra') const fs = require('fs-extra')
const axios = require('axios')
const Logger = require('../Logger') const Logger = require('../Logger')
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
const { isObject } = require('../utils/index') const { isObject } = require('../utils/index')
// //
@ -139,28 +137,6 @@ class MiscController {
res.sendStatus(200) res.sendStatus(200)
} }
getPodcastFeed(req, res) {
var url = req.body.rssFeed
if (!url) {
return res.status(400).send('Bad request')
}
axios.get(url).then(async (data) => {
if (!data || !data.data) {
Logger.error('Invalid podcast feed request response')
return res.status(500).send('Bad response from feed request')
}
var podcast = await parsePodcastRssFeedXml(data.data)
if (!podcast) {
return res.status(500).send('Invalid podcast RSS feed')
}
res.json(podcast)
}).catch((error) => {
console.error('Failed', error)
res.status(500).send(error)
})
}
async findBooks(req, res) { async findBooks(req, res) {
var provider = req.query.provider || 'google' var provider = req.query.provider || 'google'
var title = req.query.title || '' var title = req.query.title || ''

View File

@ -0,0 +1,62 @@
const axios = require('axios')
const fs = require('fs-extra')
const Logger = require('../Logger')
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
const LibraryItem = require('../objects/LibraryItem')
class PodcastController {
async create(req, res) {
if (!req.user.isRoot) {
Logger.error(`[PodcastController] Non-root user attempted to create podcast`, req.user)
return res.sendStatus(500)
}
const payload = req.body
if (await fs.pathExists(payload.path)) {
Logger.error(`[PodcastController] Attempt to create podcast when folder path already exists "${payload.path}"`)
return res.status(400).send('Path already exists')
}
var success = await fs.ensureDir(payload.path).then(() => true).catch((error) => {
Logger.error(`[PodcastController] Failed to ensure podcast dir "${payload.path}"`, error)
return false
})
if (!success) return res.status(400).send('Invalid podcast path')
if (payload.mediaMetadata.imageUrl) {
// TODO: Download image
}
var libraryItem = new LibraryItem()
libraryItem.setData('podcast', payload)
await this.db.insertLibraryItem(libraryItem)
this.emitter('item_added', libraryItem.toJSONExpanded())
res.json(libraryItem.toJSONExpanded())
}
getPodcastFeed(req, res) {
var url = req.body.rssFeed
if (!url) {
return res.status(400).send('Bad request')
}
axios.get(url).then(async (data) => {
if (!data || !data.data) {
Logger.error('Invalid podcast feed request response')
return res.status(500).send('Bad response from feed request')
}
var podcast = await parsePodcastRssFeedXml(data.data)
if (!podcast) {
return res.status(500).send('Invalid podcast RSS feed')
}
res.json(podcast)
}).catch((error) => {
console.error('Failed', error)
res.status(500).send(error)
})
}
}
module.exports = new PodcastController()

View File

@ -1,3 +1,4 @@
const { getId } = require('../../utils/index')
const AudioFile = require('../files/AudioFile') const AudioFile = require('../files/AudioFile')
const AudioTrack = require('../files/AudioTrack') const AudioTrack = require('../files/AudioTrack')
@ -5,9 +6,8 @@ class PodcastEpisode {
constructor(episode) { constructor(episode) {
this.id = null this.id = null
this.index = null this.index = null
this.podcastId = null
this.episodeNumber = null
this.episodeNumber = null
this.title = null this.title = null
this.description = null this.description = null
this.enclosure = null this.enclosure = null
@ -25,7 +25,6 @@ class PodcastEpisode {
construct(episode) { construct(episode) {
this.id = episode.id this.id = episode.id
this.index = episode.index this.index = episode.index
this.podcastId = episode.podcastId
this.episodeNumber = episode.episodeNumber this.episodeNumber = episode.episodeNumber
this.title = episode.title this.title = episode.title
this.description = episode.description this.description = episode.description
@ -40,7 +39,6 @@ class PodcastEpisode {
return { return {
id: this.id, id: this.id,
index: this.index, index: this.index,
podcastId: this.podcastId,
episodeNumber: this.episodeNumber, episodeNumber: this.episodeNumber,
title: this.title, title: this.title,
description: this.description, description: this.description,
@ -61,6 +59,18 @@ class PodcastEpisode {
} }
get size() { return this.audioFile.metadata.size } get size() { return this.audioFile.metadata.size }
setData(data, index = 1) {
this.id = getId('ep')
this.index = index
this.title = data.title
this.pubDate = data.pubDate || ''
this.description = data.description || ''
this.enclosure = data.enclosure ? { ...data.enclosure } : null
this.episodeNumber = data.episodeNumber || ''
this.addedAt = Date.now()
this.updatedAt = Date.now()
}
// Only checks container format // Only checks container format
checkCanDirectPlay(payload) { checkCanDirectPlay(payload) {
var supportedMimeTypes = payload.supportedMimeTypes || [] var supportedMimeTypes = payload.supportedMimeTypes || []

View File

@ -4,8 +4,6 @@ const { areEquivalent, copyValue } = require('../../utils/index')
class Podcast { class Podcast {
constructor(podcast) { constructor(podcast) {
this.id = null
this.metadata = null this.metadata = null
this.coverPath = null this.coverPath = null
this.tags = [] this.tags = []
@ -22,7 +20,6 @@ class Podcast {
} }
construct(podcast) { construct(podcast) {
this.id = podcast.id
this.metadata = new PodcastMetadata(podcast.metadata) this.metadata = new PodcastMetadata(podcast.metadata)
this.coverPath = podcast.coverPath this.coverPath = podcast.coverPath
this.tags = [...podcast.tags] this.tags = [...podcast.tags]
@ -32,7 +29,6 @@ class Podcast {
toJSON() { toJSON() {
return { return {
id: this.id,
metadata: this.metadata.toJSON(), metadata: this.metadata.toJSON(),
coverPath: this.coverPath, coverPath: this.coverPath,
tags: [...this.tags], tags: [...this.tags],
@ -43,7 +39,6 @@ class Podcast {
toJSONMinified() { toJSONMinified() {
return { return {
id: this.id,
metadata: this.metadata.toJSON(), metadata: this.metadata.toJSON(),
coverPath: this.coverPath, coverPath: this.coverPath,
tags: [...this.tags], tags: [...this.tags],
@ -54,7 +49,6 @@ class Podcast {
toJSONExpanded() { toJSONExpanded() {
return { return {
id: this.id,
metadata: this.metadata.toJSONExpanded(), metadata: this.metadata.toJSONExpanded(),
coverPath: this.coverPath, coverPath: this.coverPath,
tags: [...this.tags], tags: [...this.tags],
@ -124,9 +118,10 @@ class Podcast {
return this.episodes[0] return this.episodes[0]
} }
setData(scanMediaMetadata) { setData(metadata, coverPath = null, autoDownload = false) {
this.metadata = new PodcastMetadata() this.metadata = new PodcastMetadata(metadata)
this.metadata.setData(scanMediaMetadata) this.coverPath = coverPath
this.autoDownloadEpisodes = autoDownload
} }
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) { async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {

View File

@ -14,6 +14,7 @@ const SeriesController = require('../controllers/SeriesController')
const AuthorController = require('../controllers/AuthorController') const AuthorController = require('../controllers/AuthorController')
const MediaEntityController = require('../controllers/MediaEntityController') const MediaEntityController = require('../controllers/MediaEntityController')
const SessionController = require('../controllers/SessionController') const SessionController = require('../controllers/SessionController')
const PodcastController = require('../controllers/PodcastController')
const MiscController = require('../controllers/MiscController') const MiscController = require('../controllers/MiscController')
const BookFinder = require('../finders/BookFinder') const BookFinder = require('../finders/BookFinder')
@ -173,6 +174,12 @@ class ApiRouter {
this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this)) this.router.post('/session/:id/sync', SessionController.middleware.bind(this), SessionController.sync.bind(this))
this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this)) this.router.post('/session/:id/close', SessionController.middleware.bind(this), SessionController.close.bind(this))
//
// Podcast Routes
//
this.router.post('/podcasts', PodcastController.create.bind(this))
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
// //
// Misc Routes // Misc Routes
// //
@ -180,7 +187,6 @@ class ApiRouter {
this.router.get('/download/:id', MiscController.download.bind(this)) this.router.get('/download/:id', MiscController.download.bind(this))
this.router.patch('/settings', MiscController.updateServerSettings.bind(this)) // Root only this.router.patch('/settings', MiscController.updateServerSettings.bind(this)) // Root only
this.router.post('/purgecache', MiscController.purgeCache.bind(this)) // Root only this.router.post('/purgecache', MiscController.purgeCache.bind(this)) // Root only
this.router.post('/getPodcastFeed', MiscController.getPodcastFeed.bind(this))
this.router.post('/authorize', MiscController.authorize.bind(this)) this.router.post('/authorize', MiscController.authorize.bind(this))
this.router.get('/search/covers', MiscController.findCovers.bind(this)) this.router.get('/search/covers', MiscController.findCovers.bind(this))
this.router.get('/search/books', MiscController.findBooks.bind(this)) this.router.get('/search/books', MiscController.findBooks.bind(this))

View File

@ -18,7 +18,6 @@ const Series = require('../objects/entities/Series')
class Scanner { class Scanner {
constructor(db, coverController, emitter) { constructor(db, coverController, emitter) {
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans') this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
this.db = db this.db = db

View File

@ -151,6 +151,17 @@ function makeFilesFromOldAb(audiobook) {
} }
} }
// Metadata path was changed to /metadata/items make sure cover is using new path
function cleanOldCoverPath(coverPath) {
if (!coverPath) return null
var oldMetadataPath = Path.posix.join(global.MetadataPath, 'books')
if (coverPath.startsWith(oldMetadataPath)) {
const newMetadataPath = Path.posix.join(global.MetadataPath, 'items')
return coverPath.replace(oldMetadataPath, newMetadataPath)
}
return coverPath
}
function makeLibraryItemFromOldAb(audiobook) { function makeLibraryItemFromOldAb(audiobook) {
var libraryItem = new LibraryItem() var libraryItem = new LibraryItem()
libraryItem.id = getId('li') libraryItem.id = getId('li')
@ -184,7 +195,7 @@ function makeLibraryItemFromOldAb(audiobook) {
} }
bookEntity.metadata = bookMetadata bookEntity.metadata = bookMetadata
bookEntity.coverPath = audiobook.book.coverFullPath bookEntity.coverPath = cleanOldCoverPath(audiobook.book.coverFullPath)
bookEntity.tags = [...audiobook.tags] bookEntity.tags = [...audiobook.tags]
var payload = makeFilesFromOldAb(audiobook) var payload = makeFilesFromOldAb(audiobook)
@ -312,8 +323,6 @@ async function migrateLibraryItems(db) {
seriesToAdd = [] seriesToAdd = []
Logger.info(`==== Library Item migration complete ====`) Logger.info(`==== Library Item migration complete ====`)
} }
module.exports.migrateLibraryItems = migrateLibraryItems
function cleanUserObject(db, userObj) { function cleanUserObject(db, userObj) {
var cleanedUserPayload = { var cleanedUserPayload = {
@ -445,4 +454,24 @@ async function migrateUserData(db) {
Logger.info(`==== User migration complete (${userCount} Users, ${sessionCount} Sessions) ====`) Logger.info(`==== User migration complete (${userCount} Users, ${sessionCount} Sessions) ====`)
} }
module.exports.migrateUserData = migrateUserData
async function checkUpdateMetadataPath() {
var bookMetadataPath = Path.posix.join(global.MetadataPath, 'books') // OLD
if (!(await fs.pathExists(bookMetadataPath))) {
Logger.debug(`[dbMigration] No need to update books metadata path`)
return
}
var itemsMetadataPath = Path.posix.join(global.MetadataPath, 'items')
await fs.rename(bookMetadataPath, itemsMetadataPath)
Logger.info(`>>> Renamed metadata dir from /metadata/books to /metadata/items`)
}
module.exports.migrate = async (db) => {
await checkUpdateMetadataPath()
// Before DB Load clean data
await migrateUserData(db)
await db.init()
// After DB Load
await migrateLibraryItems(db)
// TODO: Eventually remove audiobooks db when stable
}