diff --git a/server/Database.js b/server/Database.js index 332e0518..d7f91559 100644 --- a/server/Database.js +++ b/server/Database.js @@ -106,7 +106,7 @@ class Database { require('./models/FeedEpisode')(this.sequelize) require('./models/Setting')(this.sequelize) - return this.sequelize.sync({ force }) + return this.sequelize.sync({ force, alter: false }) } async loadData(force = false) { @@ -354,12 +354,13 @@ class Database { this.libraryItems = this.libraryItems.filter(li => li.id !== libraryItemId) } - createFeed(oldFeed) { - // TODO: Implement + async createFeed(oldFeed) { + await this.models.feed.fullCreateFromOld(oldFeed) + this.feeds.push(oldFeed) } updateFeed(oldFeed) { - // TODO: Implement + return this.models.feed.fullUpdateFromOld(oldFeed) } async removeFeed(feedId) { diff --git a/server/managers/RssFeedManager.js b/server/managers/RssFeedManager.js index 485433b2..98f70978 100644 --- a/server/managers/RssFeedManager.js +++ b/server/managers/RssFeedManager.js @@ -29,7 +29,7 @@ class RssFeedManager { } } else if (feedObj.entityType === 'series') { const series = Database.series.find(s => s.id === feedObj.entityId) - const hasSeriesBook = Database.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) + const hasSeriesBook = series ? Database.libraryItems.some(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length) : false if (!hasSeriesBook) { Logger.error(`[RssFeedManager] Removing feed "${feedObj.id}". Series "${feedObj.entityId}" not found or has no audio tracks`) return false @@ -42,16 +42,15 @@ class RssFeedManager { } async init() { - const feedObjects = Database.feeds - if (!feedObjects?.length) return + const feeds = Database.feeds + if (!feeds?.length) return - for (const feedObj of feedObjects) { + for (const feed of feeds) { // Remove invalid feeds - if (!this.validateFeedEntity(feedObj)) { - await Database.removeFeed(feedObj.id) + if (!this.validateFeedEntity(feed)) { + await Database.removeFeed(feed.id) } - const feed = new Feed(feedObj) this.feeds[feed.id] = feed Logger.info(`[RssFeedManager] Opened rss feed ${feed.feedUrl}`) } diff --git a/server/models/Collection.js b/server/models/Collection.js index 8a96cc6d..470f8f35 100644 --- a/server/models/Collection.js +++ b/server/models/Collection.js @@ -80,8 +80,6 @@ module.exports = (sequelize) => { id: oldCollection.id, name: oldCollection.name, description: oldCollection.description, - createdAt: oldCollection.createdAt, - updatedAt: oldCollection.lastUpdate, libraryId: oldCollection.libraryId } } diff --git a/server/models/Feed.js b/server/models/Feed.js index de97271f..5c4f50f8 100644 --- a/server/models/Feed.js +++ b/server/models/Feed.js @@ -1,5 +1,6 @@ const { DataTypes, Model } = require('sequelize') const oldFeed = require('../objects/Feed') +const areEquivalent = require('../utils/areEquivalent') /* * Polymorphic association: https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/ * Feeds can be created from LibraryItem, Collection, Playlist or Series @@ -24,6 +25,7 @@ module.exports = (sequelize) => { userId: feedExpanded.userId, entityType: feedExpanded.entityType, entityId: feedExpanded.entityId, + entityUpdatedAt: feedExpanded.entityUpdatedAt?.valueOf() || null, meta: { title: feedExpanded.title, description: feedExpanded.description, @@ -54,6 +56,92 @@ module.exports = (sequelize) => { }) } + static async fullCreateFromOld(oldFeed) { + const feedObj = this.getFromOld(oldFeed) + const newFeed = await this.create(feedObj) + + if (oldFeed.episodes?.length) { + for (const oldFeedEpisode of oldFeed.episodes) { + const feedEpisode = sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) + feedEpisode.feedId = newFeed.id + await sequelize.models.feedEpisode.create(feedEpisode) + } + } + } + + static async fullUpdateFromOld(oldFeed) { + const oldFeedEpisodes = oldFeed.episodes || [] + const feedObj = this.getFromOld(oldFeed) + + const existingFeed = await this.findByPk(feedObj.id, { + include: sequelize.models.feedEpisode + }) + if (!existingFeed) return false + + let hasUpdates = false + for (const feedEpisode of existingFeed.feedEpisodes) { + const oldFeedEpisode = oldFeedEpisodes.find(ep => ep.id === feedEpisode.id) + // Episode removed + if (!oldFeedEpisode) { + feedEpisode.destroy() + } else { + let episodeHasUpdates = false + const oldFeedEpisodeCleaned = sequelize.models.feedEpisode.getFromOld(oldFeedEpisode) + for (const key in oldFeedEpisodeCleaned) { + if (!areEquivalent(oldFeedEpisodeCleaned[key], feedEpisode[key])) { + episodeHasUpdates = true + } + } + if (episodeHasUpdates) { + await feedEpisode.update(oldFeedEpisodeCleaned) + hasUpdates = true + } + } + } + + let feedHasUpdates = false + for (const key in feedObj) { + let existingValue = existingFeed[key] + if (existingValue instanceof Date) existingValue = existingValue.valueOf() + + if (!areEquivalent(existingValue, feedObj[key])) { + feedHasUpdates = true + } + } + + if (feedHasUpdates) { + await existingFeed.update(feedObj) + hasUpdates = true + } + + return hasUpdates + } + + static getFromOld(oldFeed) { + const oldFeedMeta = oldFeed.meta || {} + return { + id: oldFeed.id, + slug: oldFeed.slug, + entityType: oldFeed.entityType, + entityId: oldFeed.entityId, + entityUpdatedAt: oldFeed.entityUpdatedAt, + serverAddress: oldFeed.serverAddress, + feedURL: oldFeed.feedUrl, + imageURL: oldFeedMeta.imageUrl, + siteURL: oldFeedMeta.link, + title: oldFeedMeta.title, + description: oldFeedMeta.description, + author: oldFeedMeta.author, + podcastType: oldFeedMeta.type || null, + language: oldFeedMeta.language || null, + ownerName: oldFeedMeta.ownerName || null, + ownerEmail: oldFeedMeta.ownerEmail || null, + explicit: !!oldFeedMeta.explicit, + preventIndexing: !!oldFeedMeta.preventIndexing, + userId: oldFeed.userId + } + } + getEntity(options) { if (!this.entityType) return Promise.resolve(null) const mixinMethodName = `get${sequelize.uppercaseFirst(this.entityType)}` diff --git a/server/models/FeedEpisode.js b/server/models/FeedEpisode.js index 2ed6e7fa..405ab211 100644 --- a/server/models/FeedEpisode.js +++ b/server/models/FeedEpisode.js @@ -24,6 +24,26 @@ module.exports = (sequelize) => { fullPath: this.filePath } } + + static getFromOld(oldFeedEpisode) { + return { + id: oldFeedEpisode.id, + title: oldFeedEpisode.title, + author: oldFeedEpisode.author, + description: oldFeedEpisode.description, + siteURL: oldFeedEpisode.link, + enclosureURL: oldFeedEpisode.enclosure?.url || null, + enclosureType: oldFeedEpisode.enclosure?.type || null, + enclosureSize: oldFeedEpisode.enclosure?.size || null, + pubDate: oldFeedEpisode.pubDate, + season: oldFeedEpisode.season || null, + episode: oldFeedEpisode.episode || null, + episodeType: oldFeedEpisode.episodeType || null, + duration: oldFeedEpisode.duration, + filePath: oldFeedEpisode.fullPath, + explicit: !!oldFeedEpisode.explicit + } + } } FeedEpisode.init({ diff --git a/server/models/LibraryItem.js b/server/models/LibraryItem.js index 3cf6d87c..da1f1937 100644 --- a/server/models/LibraryItem.js +++ b/server/models/LibraryItem.js @@ -287,8 +287,6 @@ module.exports = (sequelize) => { birthtime: oldLibraryItem.birthtimeMs, lastScan: oldLibraryItem.lastScan, lastScanVersion: oldLibraryItem.scanVersion, - createdAt: oldLibraryItem.addedAt, - updatedAt: oldLibraryItem.updatedAt, libraryId: oldLibraryItem.libraryId, libraryFolderId: oldLibraryItem.folderId, libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || [] diff --git a/server/models/Playlist.js b/server/models/Playlist.js index b018ee73..54b46277 100644 --- a/server/models/Playlist.js +++ b/server/models/Playlist.js @@ -104,8 +104,6 @@ module.exports = (sequelize) => { id: oldPlaylist.id, name: oldPlaylist.name, description: oldPlaylist.description, - createdAt: oldPlaylist.createdAt, - updatedAt: oldPlaylist.lastUpdate, userId: oldPlaylist.userId, libraryId: oldPlaylist.libraryId } diff --git a/server/models/PodcastEpisode.js b/server/models/PodcastEpisode.js index d289dd50..3614bdbd 100644 --- a/server/models/PodcastEpisode.js +++ b/server/models/PodcastEpisode.js @@ -52,8 +52,6 @@ module.exports = (sequelize) => { enclosureSize: oldEpisode.enclosure?.length || null, enclosureType: oldEpisode.enclosure?.type || null, publishedAt: oldEpisode.publishedAt, - createdAt: oldEpisode.addedAt, - updatedAt: oldEpisode.updatedAt, podcastId: oldEpisode.podcastId, audioFile: oldEpisode.audioFile?.toJSON() || null, chapters: oldEpisode.chapters @@ -88,7 +86,9 @@ module.exports = (sequelize) => { }) const { podcast } = sequelize.models - podcast.hasMany(PodcastEpisode) + podcast.hasMany(PodcastEpisode, { + onDelete: 'CASCADE' + }) PodcastEpisode.belongsTo(podcast) return PodcastEpisode diff --git a/server/objects/FeedEpisode.js b/server/objects/FeedEpisode.js index 3ef9f918..eeef5379 100644 --- a/server/objects/FeedEpisode.js +++ b/server/objects/FeedEpisode.js @@ -1,3 +1,4 @@ +const uuidv4 = require("uuid").v4 const date = require('../libs/dateAndTime') const { secondsToTimestamp } = require('../utils/index') @@ -97,13 +98,11 @@ class FeedEpisode { setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta, additionalOffset = null) { // Example: Fri, 04 Feb 2015 00:00:00 GMT let timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order - let episodeId = String(audioTrack.index) + let episodeId = uuidv4() // Additional offset can be used for collections/series if (additionalOffset !== null && !isNaN(additionalOffset)) { timeOffset += Number(additionalOffset) * 1000 - - episodeId = String(additionalOffset) + '-' + episodeId } // e.g. Track 1 will have a pub date before Track 2 diff --git a/server/objects/entities/PodcastEpisode.js b/server/objects/entities/PodcastEpisode.js index 37e53fba..5c74051e 100644 --- a/server/objects/entities/PodcastEpisode.js +++ b/server/objects/entities/PodcastEpisode.js @@ -153,8 +153,13 @@ class PodcastEpisode { update(payload) { let hasUpdates = false for (const key in this.toJSON()) { - if (payload[key] != undefined && !areEquivalent(payload[key], this[key])) { - this[key] = copyValue(payload[key]) + let newValue = payload[key] + if (newValue === "") newValue = null + let existingValue = this[key] + if (existingValue === "") existingValue = null + + if (newValue != undefined && !areEquivalent(newValue, existingValue)) { + this[key] = copyValue(newValue) hasUpdates = true } } diff --git a/server/utils/areEquivalent.js b/server/utils/areEquivalent.js index 4e3d66a0..924d5310 100644 --- a/server/utils/areEquivalent.js +++ b/server/utils/areEquivalent.js @@ -32,7 +32,7 @@ module.exports = function areEquivalent(value1, value2, numToString = false, sta // Truthy check to handle value1=null, value2=Object if ((value1 && !value2) || (!value1 && value2)) { - console.log('value1/value2 falsy mismatch', value1, value2) + // console.log('value1/value2 falsy mismatch', value1, value2) return false } @@ -40,7 +40,7 @@ module.exports = function areEquivalent(value1, value2, numToString = false, sta // Ensure types match if (type1 !== typeof value2) { - console.log('type diff', type1, typeof value2) + // console.log('type diff', type1, typeof value2) return false } @@ -63,7 +63,7 @@ module.exports = function areEquivalent(value1, value2, numToString = false, sta if (type1 === 'bigint' || type1 === 'boolean' || type1 === 'function' || type1 === 'string' || type1 === 'symbol') { - console.log('no match for values', value1, value2) + // console.log('no match for values', value1, value2) return false } @@ -93,20 +93,17 @@ module.exports = function areEquivalent(value1, value2, numToString = false, sta // Handle arrays if (Array.isArray(value1)) { if (!Array.isArray(value2)) { - console.log('value2 is not array but value1 is', value1, value2) return false } const length = value1.length if (length !== value2.length) { - console.log('array length diff', length) return false } for (let i = 0; i < length; i++) { if (!areEquivalent(value1[i], value2[i], numToString, stack)) { - console.log('2 array items are not equiv', value1[i], value2[i]) return false } } @@ -121,7 +118,6 @@ module.exports = function areEquivalent(value1, value2, numToString = false, sta const numKeys = keys1.length if (keys2.length !== numKeys) { - console.log('Key length is diff', keys2.length, numKeys) return false } @@ -139,7 +135,7 @@ module.exports = function areEquivalent(value1, value2, numToString = false, sta // Ensure perfect match across all keys for (let i = 0; i < numKeys; i++) { if (keys1[i] !== keys2[i]) { - console.log('object key is not equiv', keys1[i], keys2[i]) + // console.log('object key is not equiv', keys1[i], keys2[i]) return false } } @@ -147,7 +143,7 @@ module.exports = function areEquivalent(value1, value2, numToString = false, sta // Ensure perfect match across all values for (let i = 0; i < numKeys; i++) { if (!areEquivalent(value1[keys1[i]], value2[keys1[i]], numToString, stack)) { - console.log('2 subobjects not equiv', keys1[i], value1[keys1[i]], value2[keys1[i]]) + // console.log('2 subobjects not equiv', keys1[i], value1[keys1[i]], value2[keys1[i]]) return false } } diff --git a/server/utils/migrations/dbMigration.js b/server/utils/migrations/dbMigration.js index 28fb5907..0b94dda6 100644 --- a/server/utils/migrations/dbMigration.js +++ b/server/utils/migrations/dbMigration.js @@ -169,6 +169,8 @@ function migratePodcast(oldLibraryItem, LibraryItem) { // const oldEpisodes = oldPodcast.episodes || [] for (const oldEpisode of oldEpisodes) { + oldEpisode.audioFile.index = 1 + const PodcastEpisode = { id: uuidv4(), index: oldEpisode.index,