mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-29 10:09:11 +01:00
Migration change metadata folder from /books to /items, podcast data model updates, add podcast routes
This commit is contained in:
parent
43bbfbfee3
commit
f8d0384155
@ -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
|
||||
|
@ -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' })
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 || ''
|
||||
|
62
server/controllers/PodcastController.js
Normal file
62
server/controllers/PodcastController.js
Normal 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()
|
@ -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 || []
|
||||
|
@ -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) {
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user