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
}
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)
this.$toast.error('Failed to get podcast feed')
return null

View File

@ -15,7 +15,7 @@ const Backup = require('./objects/Backup')
class BackupManager {
constructor(db, emitter) {
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.emitter = emitter
@ -115,7 +115,7 @@ class BackupManager {
const zip = new StreamZip.async({ file: backup.fullPath })
await zip.extract('config/', global.ConfigPath)
if (backup.backupMetadataCovers) {
await zip.extract('metadata-books/', this.MetadataBooksPath)
await zip.extract('metadata-items/', this.ItemsMetadataPath)
}
await this.db.reinit()
this.emitter('backup_applied')
@ -154,7 +154,7 @@ class BackupManager {
async runBackup() {
// 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`)
var metadataBooksPath = this.serverSettings.backupMetadataCovers ? this.MetadataBooksPath : null
var metadataItemsPath = this.serverSettings.backupMetadataCovers ? this.ItemsMetadataPath : null
var newBackup = new Backup()
@ -164,7 +164,7 @@ class BackupManager {
}
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}`)
return false
})
@ -204,7 +204,7 @@ class BackupManager {
}
}
zipBackup(metadataBooksPath, backup) {
zipBackup(metadataItemsPath, backup) {
return new Promise((resolve, reject) => {
// create a file to stream archive data to
const output = fs.createWriteStream(backup.fullPath)
@ -274,9 +274,9 @@ class BackupManager {
archive.directory(this.db.AuthorsPath, 'config/authors')
archive.directory(this.db.SeriesPath, 'config/series')
if (metadataBooksPath) {
Logger.debug(`[BackupManager] Backing up Metadata Books "${metadataBooksPath}"`)
archive.directory(metadataBooksPath, 'metadata-books')
if (metadataItemsPath) {
Logger.debug(`[BackupManager] Backing up Metadata Items "${metadataItemsPath}"`)
archive.directory(metadataItemsPath, 'metadata-items')
}
archive.append(backup.detailsString, { name: 'details' })

View File

@ -15,14 +15,14 @@ class CoverController {
this.db = db
this.cacheManager = cacheManager
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
this.ItemMetadataPath = Path.posix.join(global.MetadataPath, 'items')
}
getCoverDirectory(libraryItem) {
if (this.db.serverSettings.storeCoverWithBook) {
return libraryItem.path
} 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)
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
}

View File

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

View File

@ -1,9 +1,7 @@
const Path = require('path')
const fs = require('fs-extra')
const axios = require('axios')
const Logger = require('../Logger')
const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
const { isObject } = require('../utils/index')
//
@ -139,28 +137,6 @@ class MiscController {
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) {
var provider = req.query.provider || 'google'
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 AudioTrack = require('../files/AudioTrack')
@ -5,9 +6,8 @@ class PodcastEpisode {
constructor(episode) {
this.id = null
this.index = null
this.podcastId = null
this.episodeNumber = null
this.episodeNumber = null
this.title = null
this.description = null
this.enclosure = null
@ -25,7 +25,6 @@ class PodcastEpisode {
construct(episode) {
this.id = episode.id
this.index = episode.index
this.podcastId = episode.podcastId
this.episodeNumber = episode.episodeNumber
this.title = episode.title
this.description = episode.description
@ -40,7 +39,6 @@ class PodcastEpisode {
return {
id: this.id,
index: this.index,
podcastId: this.podcastId,
episodeNumber: this.episodeNumber,
title: this.title,
description: this.description,
@ -61,6 +59,18 @@ class PodcastEpisode {
}
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
checkCanDirectPlay(payload) {
var supportedMimeTypes = payload.supportedMimeTypes || []

View File

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

View File

@ -14,6 +14,7 @@ const SeriesController = require('../controllers/SeriesController')
const AuthorController = require('../controllers/AuthorController')
const MediaEntityController = require('../controllers/MediaEntityController')
const SessionController = require('../controllers/SessionController')
const PodcastController = require('../controllers/PodcastController')
const MiscController = require('../controllers/MiscController')
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/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
//
@ -180,7 +187,6 @@ class ApiRouter {
this.router.get('/download/:id', MiscController.download.bind(this))
this.router.patch('/settings', MiscController.updateServerSettings.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.get('/search/covers', MiscController.findCovers.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 {
constructor(db, coverController, emitter) {
this.BookMetadataPath = Path.posix.join(global.MetadataPath, 'books')
this.ScanLogPath = Path.posix.join(global.MetadataPath, 'logs', 'scans')
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) {
var libraryItem = new LibraryItem()
libraryItem.id = getId('li')
@ -184,7 +195,7 @@ function makeLibraryItemFromOldAb(audiobook) {
}
bookEntity.metadata = bookMetadata
bookEntity.coverPath = audiobook.book.coverFullPath
bookEntity.coverPath = cleanOldCoverPath(audiobook.book.coverFullPath)
bookEntity.tags = [...audiobook.tags]
var payload = makeFilesFromOldAb(audiobook)
@ -312,8 +323,6 @@ async function migrateLibraryItems(db) {
seriesToAdd = []
Logger.info(`==== Library Item migration complete ====`)
}
module.exports.migrateLibraryItems = migrateLibraryItems
function cleanUserObject(db, userObj) {
var cleanedUserPayload = {
@ -445,4 +454,24 @@ async function migrateUserData(db) {
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
}