const Path = require('path') const uuidv4 = require("uuid").v4 const package = require('../../../package.json') const { AudioMimeType } = require('../constants') const Logger = require('../../Logger') const Database = require('../../Database') const oldDbFiles = require('./oldDbFiles') const dateAndTime = require('../../libs/dateAndTime') const oldDbIdMap = { users: {}, libraries: {}, libraryFolders: {}, libraryItems: {}, tags: {}, // key is tag string genres: {}, // key is genre string people: {}, // key is author id or narrator name clean series: {}, collections: {}, files: {}, // key is fullpath podcastEpisodes: {}, books: {}, // key is library item id devices: {} // key is a json stringify of the old DeviceInfo data } const newRecords = { User: [], UserPermission: [], Library: [], LibraryFolder: [], LibrarySetting: [], FileMetadata: [], Person: [], LibraryItem: [], LibraryFile: [], EBookFile: [], Book: [], BookAuthor: [], BookNarrator: [], BookChapter: [], Tag: [], BookTag: [], Genre: [], BookGenre: [], Series: [], BookSeries: [], Podcast: [], PodcastTag: [], PodcastGenre: [], PodcastEpisode: [], MediaProgress: [], AudioBookmark: [], MediaFile: [], MediaStream: [], AudioTrack: [], Device: [], PlaybackSession: [], PlaybackSessionListenTime: [], Collection: [], CollectionBook: [], Playlist: [], PlaylistMediaItem: [], Feed: [], FeedEpisode: [], Setting: [], Notification: [] } function getDeviceInfoString(deviceInfo, UserId) { if (!deviceInfo) return null const dupe = { ...deviceInfo } dupe.UserId = UserId delete dupe.serverVersion return JSON.stringify(dupe) } function getMimeType(formatName) { const format = formatName.toUpperCase() if (AudioMimeType[format]) { return AudioMimeType[format] } else { return AudioMimeType.MP3 } } function cleanAudioFileMetaTags(metaTags) { if (!metaTags) return {} const cleaned = {} for (const tag in metaTags) { if (tag.startsWith('tag') && metaTags[tag]) { const tagName = tag.substring(3) cleaned[tagName] = metaTags[tag] } } return cleaned } function migrateBook(oldLibraryItem, LibraryItem) { const oldBook = oldLibraryItem.media // // Migrate ImageFile // let ImageFileId = null if (oldBook.coverPath) { ImageFileId = oldDbIdMap.files[oldBook.coverPath] || null if (!ImageFileId) { const FileMetadata = { id: uuidv4(), filename: Path.basename(oldBook.coverPath), ext: Path.extname(oldBook.coverPath), path: oldBook.coverPath, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt } newRecords.FileMetadata.push(FileMetadata) oldDbIdMap.files[oldBook.coverPath] = FileMetadata.id ImageFileId = FileMetadata.id } } // // Migrate EBookFile // let EBookFileId = null if (oldBook.ebookFile) { if (oldDbIdMap.files[oldBook.ebookFile.metadata?.path]) { const EBookFile = { id: uuidv4(), FileMetadataId: oldDbIdMap.files[oldBook.ebookFile.metadata?.path] } newRecords.EBookFile.push(EBookFile) EBookFileId = EBookFile.id } else { Logger.warn(`[dbMigration] migrateBook: `) } } // // Migrate Book // const Book = { id: uuidv4(), title: oldBook.metadata.title, subtitle: oldBook.metadata.subtitle, publishedYear: oldBook.metadata.publishedYear, publishedDate: oldBook.metadata.publishedDate, publisher: oldBook.metadata.publisher, description: oldBook.metadata.description, isbn: oldBook.metadata.isbn, asin: oldBook.metadata.asin, language: oldBook.metadata.language, explicit: !!oldBook.metadata.explicit, lastCoverSearchQuery: oldBook.lastCoverSearchQuery, lastCoverSearch: oldBook.lastCoverSearch, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, ImageFileId, EBookFileId, LibraryItemId: LibraryItem.id, } newRecords.Book.push(Book) oldDbIdMap.books[oldLibraryItem.id] = Book.id // // Migrate AudioTracks // const oldAudioFiles = oldBook.audioFiles let startOffset = 0 for (const oldAudioFile of oldAudioFiles) { const FileMetadataId = oldDbIdMap.files[oldAudioFile.metadata.path] if (!FileMetadataId) { Logger.warn(`[dbMigration] migrateBook: File metadata not found for audio file "${oldAudioFile.metadata.path}"`) continue } const ext = oldAudioFile.metadata.ext || '' const MediaFile = { id: uuidv4(), formatName: ext.slice(1).toLowerCase(), formatNameLong: oldAudioFile.format, duration: oldAudioFile.duration, bitrate: oldAudioFile.bitRate, size: oldAudioFile.metadata.size, tags: cleanAudioFileMetaTags(oldAudioFile.metaTags), createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, FileMetadataId } newRecords.MediaFile.push(MediaFile) const MediaStream = { id: uuidv4(), index: null, codecType: 'audio', codec: oldAudioFile.codec, channels: oldAudioFile.channels, channelLayout: oldAudioFile.channelLayout, bitrate: oldAudioFile.bitRate, timeBase: oldAudioFile.timeBase, duration: oldAudioFile.duration, sampleRate: null, language: oldAudioFile.language, default: true, chapters: oldAudioFile.chapters, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, MediaFileId: MediaFile.id } newRecords.MediaStream.push(MediaStream) if (oldAudioFile.embeddedCoverArt) { const CoverMediaStream = { id: uuidv4(), index: null, codecType: 'video', codec: oldAudioFile.embeddedCoverArt, default: true, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, MediaFileId: MediaFile.id } newRecords.MediaStream.push(CoverMediaStream) } const include = !oldAudioFile.exclude && !oldAudioFile.invalid const AudioTrack = { id: uuidv4(), MediaItemId: Book.id, mediaItemType: 'Book', index: oldAudioFile.index, startOffset: include ? startOffset : null, duration: oldAudioFile.duration, title: oldAudioFile.metadata.filename, mimeType: getMimeType(MediaFile.formatName), codec: oldAudioFile.codec, trackNumber: oldAudioFile.trackNumFromMeta || oldAudioFile.trackNumFromFilename, discNumber: oldAudioFile.discNumFromMeta || oldAudioFile.discNumFromFilename, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, MediaFileId: MediaFile.id } newRecords.AudioTrack.push(AudioTrack) if (include) { startOffset += AudioTrack.duration } } // // Migrate Tags // const oldTags = oldBook.tags || [] for (const oldTag of oldTags) { const oldTagCleaned = oldTag.trim().toLowerCase() let tagId = oldDbIdMap.tags[oldTagCleaned] if (!tagId) { const Tag = { id: uuidv4(), name: oldTag, cleanName: oldTagCleaned, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt } tagId = Tag.id newRecords.Tag.push(Tag) } newRecords.BookTag.push({ id: uuidv4(), BookId: Book.id, TagId: tagId }) } // // Migrate BookChapters // for (const oldChapter of oldBook.chapters) { newRecords.BookChapter.push({ id: uuidv4(), index: oldChapter.id, start: oldChapter.start, end: oldChapter.end, title: oldChapter.title, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, BookId: Book.id }) } // // Migrate Genres // const oldGenres = oldBook.metadata.genres || [] for (const oldGenre of oldGenres) { const oldGenreCleaned = oldGenre.trim().toLowerCase() let genreId = oldDbIdMap.genres[oldGenreCleaned] if (!genreId) { const Genre = { id: uuidv4(), name: oldGenre, cleanName: oldGenreCleaned, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt } genreId = Genre.id newRecords.Genre.push(Genre) } newRecords.BookGenre.push({ id: uuidv4(), BookId: Book.id, GenreId: genreId }) } // // Migrate BookAuthors // for (const oldBookAuthor of oldBook.metadata.authors) { if (oldDbIdMap.people[oldBookAuthor.id]) { newRecords.BookAuthor.push({ id: uuidv4(), PersonId: oldDbIdMap.people[oldBookAuthor.id], BookId: Book.id }) } else { Logger.warn(`[dbMigration] migrateBook: Book author not found "${oldBookAuthor.name}"`) } } // // Migrate BookNarrators // for (const oldBookNarrator of oldBook.metadata.narrators) { let PersonId = oldDbIdMap.people[oldBookNarrator] if (!PersonId) { const Person = { id: uuidv4(), type: 'Narrator', name: oldBookNarrator, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt } newRecords.Person.push(Person) PersonId = Person.id } newRecords.BookNarrator.push({ id: uuidv4(), PersonId, BookId: Book.id }) } // // Migrate BookSeries // for (const oldBookSeries of oldBook.metadata.series) { if (oldDbIdMap.series[oldBookSeries.id]) { const BookSeries = { id: uuidv4(), sequence: oldBookSeries.sequence, SeriesId: oldDbIdMap.series[oldBookSeries.id], BookId: Book.id } newRecords.BookSeries.push(BookSeries) } else { Logger.warn(`[dbMigration] migrateBook: Series not found "${oldBookSeries.name}"`) } } } function migratePodcast(oldLibraryItem, LibraryItem) { const oldPodcast = oldLibraryItem.media const oldPodcastMetadata = oldPodcast.metadata // // Migrate ImageFile // let ImageFileId = null if (oldPodcast.coverPath) { ImageFileId = oldDbIdMap.files[oldPodcast.coverPath] || null if (!ImageFileId) { const FileMetadata = { id: uuidv4(), filename: Path.basename(oldPodcast.coverPath), ext: Path.extname(oldPodcast.coverPath), path: oldPodcast.coverPath, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt } newRecords.FileMetadata.push(FileMetadata) oldDbIdMap.files[oldPodcast.coverPath] = FileMetadata.id ImageFileId = FileMetadata.id } } // // Migrate Podcast // const Podcast = { id: uuidv4(), title: oldPodcastMetadata.title, author: oldPodcastMetadata.author, releaseDate: oldPodcastMetadata.releaseDate, feedURL: oldPodcastMetadata.feedUrl, imageURL: oldPodcastMetadata.imageUrl, description: oldPodcastMetadata.description, itunesPageURL: oldPodcastMetadata.itunesPageUrl, itunesId: oldPodcastMetadata.itunesId, itunesArtistId: oldPodcastMetadata.itunesArtistId, language: oldPodcastMetadata.language, podcastType: oldPodcastMetadata.type, explicit: !!oldPodcastMetadata.explicit, autoDownloadEpisodes: !!oldPodcast.autoDownloadEpisodes, autoDownloadSchedule: oldPodcast.autoDownloadSchedule, lastEpisodeCheck: oldPodcast.lastEpisodeCheck, maxEpisodesToKeep: oldPodcast.maxEpisodesToKeep, maxNewEpisodesToDownload: oldPodcast.maxNewEpisodesToDownload, lastCoverSearchQuery: oldPodcast.lastCoverSearchQuery, lastCoverSearch: oldPodcast.lastCoverSearch, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, ImageFileId, LibraryItemId: LibraryItem.id } newRecords.Podcast.push(Podcast) // // Migrate Tags // const oldTags = oldPodcast.tags || [] for (const oldTag of oldTags) { const oldTagCleaned = oldTag.trim().toLowerCase() let tagId = oldDbIdMap.tags[oldTagCleaned] if (!tagId) { const Tag = { id: uuidv4(), name: oldTag, cleanName: oldTagCleaned, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt } tagId = Tag.id newRecords.Tag.push(Tag) } newRecords.PodcastTag.push({ id: uuidv4(), PodcastId: Podcast.id, TagId: tagId }) } // // Migrate Genres // const oldGenres = oldPodcastMetadata.genres || [] for (const oldGenre of oldGenres) { const oldGenreCleaned = oldGenre.trim().toLowerCase() let genreId = oldDbIdMap.genres[oldGenreCleaned] if (!genreId) { const Genre = { id: uuidv4(), name: oldGenre, cleanName: oldGenreCleaned, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt } genreId = Genre.id newRecords.Genre.push(Genre) } newRecords.PodcastGenre.push({ id: uuidv4(), PodcastId: Podcast.id, GenreId: genreId }) } // // Migrate PodcastEpisodes // const oldEpisodes = oldPodcast.episodes || [] for (const oldEpisode of oldEpisodes) { const oldAudioFile = oldEpisode.audioFile const FileMetadataId = oldDbIdMap.files[oldAudioFile.metadata.path] if (!FileMetadataId) { Logger.warn(`[dbMigration] migratePodcast: File metadata not found for audio file "${oldAudioFile.metadata.path}"`) continue } const PodcastEpisode = { id: uuidv4(), index: oldEpisode.index, season: oldEpisode.season, episode: oldEpisode.episode, episodeType: oldEpisode.episodeType, title: oldEpisode.title, subtitle: oldEpisode.subtitle, description: oldEpisode.description, pubDate: oldEpisode.pubDate, enclosureURL: oldEpisode.enclosure?.url || null, enclosureSize: oldEpisode.enclosure?.length || null, enclosureType: oldEpisode.enclosure?.type || null, publishedAt: oldEpisode.publishedAt, createdAt: oldEpisode.addedAt, updatedAt: oldEpisode.updatedAt, PodcastId: Podcast.id } newRecords.PodcastEpisode.push(PodcastEpisode) oldDbIdMap.podcastEpisodes[oldEpisode.id] = PodcastEpisode.id // // Migrate AudioTrack // const ext = oldAudioFile.metadata.ext || '' const MediaFile = { id: uuidv4(), formatName: ext.slice(1).toLowerCase(), formatNameLong: oldAudioFile.format, duration: oldAudioFile.duration, bitrate: oldAudioFile.bitRate, size: oldAudioFile.metadata.size, tags: cleanAudioFileMetaTags(oldAudioFile.metaTags), createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, FileMetadataId } newRecords.MediaFile.push(MediaFile) const MediaStream = { id: uuidv4(), index: null, codecType: 'audio', codec: oldAudioFile.codec, channels: oldAudioFile.channels, channelLayout: oldAudioFile.channelLayout, bitrate: oldAudioFile.bitRate, timeBase: oldAudioFile.timeBase, duration: oldAudioFile.duration, sampleRate: null, language: oldAudioFile.language, default: true, chapters: oldAudioFile.chapters, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, MediaFileId: MediaFile.id } newRecords.MediaStream.push(MediaStream) if (oldAudioFile.embeddedCoverArt) { const CoverMediaStream = { id: uuidv4(), index: null, codecType: 'video', codec: oldAudioFile.embeddedCoverArt, default: true, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, MediaFileId: MediaFile.id } newRecords.MediaStream.push(CoverMediaStream) } const AudioTrack = { id: uuidv4(), MediaItemId: Podcast.id, mediaItemType: 'Podcast', index: oldAudioFile.index, startOffset: 0, duration: oldAudioFile.duration, title: oldAudioFile.metadata.filename, mimeType: getMimeType(MediaFile.formatName), codec: oldAudioFile.codec, trackNumber: oldAudioFile.trackNumFromMeta || oldAudioFile.trackNumFromFilename, discNumber: oldAudioFile.discNumFromMeta || oldAudioFile.discNumFromFilename, createdAt: LibraryItem.createdAt, updatedAt: LibraryItem.updatedAt, MediaFileId: MediaFile.id } newRecords.AudioTrack.push(AudioTrack) } } function migrateLibraryItems(oldLibraryItems) { for (const oldLibraryItem of oldLibraryItems) { const LibraryId = oldDbIdMap.libraryFolders[oldLibraryItem.folderId] if (!LibraryId) { Logger.error(`[dbMigration] migrateLibraryItems: Old library folder id not found "${oldLibraryItem.folderId}"`) continue } // // Migrate LibraryItem // const LibraryItem = { id: uuidv4(), ino: oldLibraryItem.ino, path: oldLibraryItem.path, relPath: oldLibraryItem.relPath, mediaType: oldLibraryItem.mediaType, isFile: !!oldLibraryItem.isFile, isMissing: !!oldLibraryItem.isMissing, isInvalid: !!oldLibraryItem.isInvalid, mtime: oldLibraryItem.mtimeMs, ctime: oldLibraryItem.ctimeMs, birthtime: oldLibraryItem.birthtimeMs, lastScan: oldLibraryItem.lastScan, lastScanVersion: oldLibraryItem.scanVersion, createdAt: oldLibraryItem.addedAt, updatedAt: oldLibraryItem.updatedAt, LibraryId } oldDbIdMap.libraryItems[oldLibraryItem.id] = LibraryItem.id newRecords.LibraryItem.push(LibraryItem) // // Migrate LibraryFiles // for (const oldLibraryFile of oldLibraryItem.libraryFiles) { const FileMetadata = { id: uuidv4(), ino: oldLibraryFile.ino, filename: oldLibraryFile.metadata.filename, ext: oldLibraryFile.metadata.ext, path: oldLibraryFile.metadata.path, size: oldLibraryFile.metadata.size, mtime: oldLibraryFile.metadata.mtimeMs, ctime: oldLibraryFile.metadata.ctimeMs, birthtime: oldLibraryFile.metadata.birthtimeMs, createdAt: oldLibraryFile.addedAt || Date.now(), updatedAt: oldLibraryFile.updatedAt || Date.now() } newRecords.FileMetadata.push(FileMetadata) oldDbIdMap.files[FileMetadata.path] = FileMetadata.id const LibraryFile = { id: uuidv4(), createdAt: FileMetadata.createdAt, updatedAt: FileMetadata.updatedAt, FileMetadataId: FileMetadata.id, LibraryItemId: LibraryItem.id } newRecords.LibraryFile.push(LibraryFile) } // // Migrate Book/Podcast // if (oldLibraryItem.mediaType === 'book') { migrateBook(oldLibraryItem, LibraryItem) } else if (oldLibraryItem.mediaType === 'podcast') { migratePodcast(oldLibraryItem, LibraryItem) } } } function migrateLibraries(oldLibraries) { for (const oldLibrary of oldLibraries) { // // Migrate Library // const Library = { id: uuidv4(), name: oldLibrary.name, displayOrder: oldLibrary.displayOrder, icon: oldLibrary.icon || null, mediaType: oldLibrary.mediaType || null, provider: oldLibrary.provider, createdAt: oldLibrary.createdAt, updatedAt: oldLibrary.lastUpdate } oldDbIdMap.libraries[oldLibrary.id] = Library.id newRecords.Library.push(Library) // // Migrate LibrarySettings // const oldLibrarySettings = oldLibrary.settings || {} for (const oldSettingsKey in oldLibrarySettings) { newRecords.LibrarySetting.push({ id: uuidv4(), key: oldSettingsKey, value: oldLibrarySettings[oldSettingsKey], createdAt: oldLibrary.createdAt, updatedAt: oldLibrary.lastUpdate, LibraryId: Library.id }) } // // Migrate LibraryFolders // for (const oldFolder of oldLibrary.folders) { const LibraryFolder = { id: uuidv4(), path: oldFolder.fullPath, createdAt: oldFolder.addedAt, updatedAt: oldLibrary.lastUpdate, LibraryId: Library.id } oldDbIdMap.libraryFolders[oldFolder.id] = LibraryFolder.id newRecords.LibraryFolder.push(LibraryFolder) } } } function migrateAuthors(oldAuthors) { for (const oldAuthor of oldAuthors) { let imageFileId = null if (oldAuthor.imagePath) { const FileMetadata = { id: uuidv4(), filename: Path.basename(oldAuthor.imagePath), ext: Path.extname(oldAuthor.imagePath), path: oldAuthor.imagePath, createdAt: oldAuthor.addedAt || Date.now(), updatedAt: oldAuthor.updatedAt || Date.now() } newRecords.FileMetadata.push(FileMetadata) imageFileId = FileMetadata.id } const Person = { id: uuidv4(), type: 'Author', name: oldAuthor.name, asin: oldAuthor.asin || null, description: oldAuthor.description, createdAt: oldAuthor.addedAt || Date.now(), updatedAt: oldAuthor.updatedAt || Date.now(), ImageFileId: imageFileId } oldDbIdMap.people[oldAuthor.id] = Person.id newRecords.Person.push(Person) } } function migrateSeries(oldSerieses) { for (const oldSeries of oldSerieses) { const Series = { id: uuidv4(), name: oldSeries.name, description: oldSeries.description || null, createdAt: oldSeries.addedAt || Date.now(), updatedAt: oldSeries.updatedAt || Date.now() } oldDbIdMap.series[oldSeries.id] = Series.id newRecords.Series.push(Series) } } function migrateUsers(oldUsers) { for (const oldUser of oldUsers) { // // Migrate User // const User = { id: uuidv4(), username: oldUser.username, pash: oldUser.pash || null, type: oldUser.type || null, token: oldUser.token || null, isActive: !!oldUser.isActive, lastSeen: oldUser.lastSeen || null, extraData: { seriesHideFromContinueListening: oldUser.seriesHideFromContinueListening || [] }, createdAt: oldUser.createdAt || Date.now() } oldDbIdMap.users[oldUser.id] = User.id newRecords.User.push(User) // // Migrate UserPermissions // for (const oldUserPermission in oldUser.permissions) { if (!['accessAllLibraries', 'accessAllTags'].includes(oldUserPermission)) { const UserPermission = { id: uuidv4(), key: oldUserPermission, value: !!oldUser.permissions[oldUserPermission], createdAt: User.createdAt, UserId: User.id } newRecords.UserPermission.push(UserPermission) } } if (oldUser.librariesAccessible?.length) { const UserPermission = { id: uuidv4(), key: 'librariesAccessible', value: JSON.stringify(oldUser.librariesAccessible), createdAt: User.createdAt, UserId: User.id } newRecords.UserPermission.push(UserPermission) } if (oldUser.itemTagsAccessible?.length) { const UserPermission = { id: uuidv4(), key: 'itemTagsAccessible', value: JSON.stringify(oldUser.itemTagsAccessible), createdAt: User.createdAt, UserId: User.id } newRecords.UserPermission.push(UserPermission) } // // Migrate MediaProgress // for (const oldMediaProgress of oldUser.mediaProgress) { let mediaItemType = 'Book' let MediaItemId = null if (oldMediaProgress.episodeId) { mediaItemType = 'PodcastEpisode' MediaItemId = oldDbIdMap.podcastEpisodes[oldMediaProgress.episodeId] } else { MediaItemId = oldDbIdMap.books[oldMediaProgress.libraryItemId] } if (!MediaItemId) { Logger.warn(`[dbMigration] migrateUsers: Unable to find media item for media progress "${oldMediaProgress.id}"`) continue } const MediaProgress = { id: uuidv4(), MediaItemId, mediaItemType, duration: oldMediaProgress.duration, currentTime: oldMediaProgress.currentTime, isFinished: !!oldMediaProgress.isFinished, hideFromContinueListening: !!oldMediaProgress.hideFromContinueListening, finishedAt: oldMediaProgress.finishedAt, createdAt: oldMediaProgress.startedAt || oldMediaProgress.lastUpdate, updatedAt: oldMediaProgress.lastUpdate, UserId: User.id } newRecords.MediaProgress.push(MediaProgress) } // // Migrate AudioBookmarks // for (const oldBookmark of oldUser.bookmarks) { const MediaItemId = oldDbIdMap.books[oldBookmark.libraryItemId] if (!MediaItemId) { Logger.warn(`[dbMigration] migrateUsers: Unable to find media item for audio bookmark "${oldBookmark.id}"`) continue } const AudioBookmark = { id: uuidv4(), MediaItemId, mediaItemType: 'Book', title: oldBookmark.title, time: oldBookmark.time, createdAt: oldBookmark.createdAt, updatedAt: oldBookmark.createdAt, UserId: User.id } newRecords.AudioBookmark.push(AudioBookmark) } } } function migrateSessions(oldSessions) { for (const oldSession of oldSessions) { const UserId = oldDbIdMap.users[oldSession.userId] || null // Can be null // // Migrate Device // let DeviceId = null if (oldSession.deviceInfo) { const oldDeviceInfo = oldSession.deviceInfo const deviceInfoStr = getDeviceInfoString(oldDeviceInfo, UserId) DeviceId = oldDbIdMap.devices[deviceInfoStr] if (!DeviceId) { let clientName = 'Unknown' let clientVersion = null let deviceName = null let deviceVersion = oldDeviceInfo.browserVersion || null if (oldDeviceInfo.sdkVersion) { clientName = 'Abs Android' clientVersion = oldDeviceInfo.clientVersion || null deviceName = `${oldDeviceInfo.manufacturer} ${oldDeviceInfo.model}` deviceVersion = oldDeviceInfo.sdkVersion } else if (oldDeviceInfo.model) { clientName = 'Abs iOS' clientVersion = oldDeviceInfo.clientVersion || null deviceName = `${oldDeviceInfo.manufacturer} ${oldDeviceInfo.model}` } else if (oldDeviceInfo.osName && oldDeviceInfo.browserName) { clientName = 'Abs Web' clientVersion = oldDeviceInfo.serverVersion || null deviceName = `${oldDeviceInfo.osName} ${oldDeviceInfo.osVersion || 'N/A'} ${oldDeviceInfo.browserName}` } const id = uuidv4() const Device = { id, identifier: id, // Temp for migration clientName, clientVersion, ipAddress: oldDeviceInfo.ipAddress, deviceName, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3 deviceVersion, UserId } newRecords.Device.push(Device) oldDbIdMap.devices[deviceInfoStr] = Device.id } } // // Migrate PlaybackSession // let MediaItemId = null let mediaItemType = 'Book' if (oldSession.mediaType === 'podcast') { MediaItemId = oldDbIdMap.podcastEpisodes[oldSession.episodeId] || null mediaItemType = 'PodcastEpisode' } else { MediaItemId = oldDbIdMap.books[oldSession.libraryItemId] || null } const PlaybackSession = { id: uuidv4(), MediaItemId, // Can be null mediaItemType, displayTitle: oldSession.displayTitle, displayAuthor: oldSession.displayAuthor, duration: oldSession.duration, playMethod: oldSession.playMethod, mediaPlayer: oldSession.mediaPlayer, startTime: oldSession.startTime, currentTime: oldSession.currentTime, serverVersion: oldSession.deviceInfo?.serverVersion || null, createdAt: oldSession.startedAt, updatedAt: oldSession.updatedAt, UserId, // Can be null DeviceId } newRecords.PlaybackSession.push(PlaybackSession) if (oldSession.timeListening) { const PlaybackSessionListenTime = { id: uuidv4(), time: Math.min(Math.round(oldSession.timeListening), 86400), // Max time will be 24 hours, date: oldSession.date || dateAndTime.format(new Date(PlaybackSession.createdAt), 'YYYY-MM-DD'), createdAt: PlaybackSession.createdAt, updatedAt: PlaybackSession.updatedAt, PlaybackSessionId: PlaybackSession.id } newRecords.PlaybackSessionListenTime.push(PlaybackSessionListenTime) } } } function migrateCollections(oldCollections) { for (const oldCollection of oldCollections) { const LibraryId = oldDbIdMap.libraries[oldCollection.libraryId] if (!LibraryId) { Logger.warn(`[dbMigration] migrateCollections: Library not found for collection "${oldCollection.name}" (id:${oldCollection.libraryId})`) continue } const BookIds = oldCollection.books.map(lid => oldDbIdMap.books[lid]).filter(bid => bid) if (!BookIds.length) { Logger.warn(`[dbMigration] migrateCollections: Collection "${oldCollection.name}" has no books`) continue } const Collection = { id: uuidv4(), name: oldCollection.name, description: oldCollection.description, createdAt: oldCollection.createdAt, updatedAt: oldCollection.lastUpdate, LibraryId } oldDbIdMap.collections[oldCollection.id] = Collection.id newRecords.Collection.push(Collection) BookIds.forEach((BookId) => { const CollectionBook = { id: uuidv4(), createdAt: Collection.createdAt, BookId, CollectionId: Collection.id } newRecords.CollectionBook.push(CollectionBook) }) } } function migratePlaylists(oldPlaylists) { for (const oldPlaylist of oldPlaylists) { const LibraryId = oldDbIdMap.libraries[oldPlaylist.libraryId] if (!LibraryId) { Logger.warn(`[dbMigration] migratePlaylists: Library not found for playlist "${oldPlaylist.name}" (id:${oldPlaylist.libraryId})`) continue } const UserId = oldDbIdMap.users[oldPlaylist.userId] if (!UserId) { Logger.warn(`[dbMigration] migratePlaylists: User not found for playlist "${oldPlaylist.name}" (id:${oldPlaylist.userId})`) continue } let mediaItemType = 'Book' let MediaItemIds = [] oldPlaylist.items.forEach((itemObj) => { if (itemObj.episodeId) { mediaItemType = 'PodcastEpisode' if (oldDbIdMap.podcastEpisodes[itemObj.episodeId]) { MediaItemIds.push(oldDbIdMap.podcastEpisodes[itemObj.episodeId]) } } else if (oldDbIdMap.books[itemObj.libraryItemId]) { MediaItemIds.push(oldDbIdMap.books[itemObj.libraryItemId]) } }) if (!MediaItemIds.length) { Logger.warn(`[dbMigration] migratePlaylists: Playlist "${oldPlaylist.name}" has no items`) continue } const Playlist = { id: uuidv4(), name: oldPlaylist.name, description: oldPlaylist.description, createdAt: oldPlaylist.createdAt, updatedAt: oldPlaylist.lastUpdate, UserId, LibraryId } newRecords.Playlist.push(Playlist) MediaItemIds.forEach((MediaItemId) => { const PlaylistMediaItem = { id: uuidv4(), MediaItemId, mediaItemType, createdAt: Playlist.createdAt, PlaylistId: Playlist.id } newRecords.PlaylistMediaItem.push(PlaylistMediaItem) }) } } function migrateFeeds(oldFeeds) { for (const oldFeed of oldFeeds) { if (!oldFeed.episodes?.length) { continue } let entityType = null let EntityId = null if (oldFeed.entityType === 'collection') { entityType = 'Collection' EntityId = oldDbIdMap.collections[oldFeed.entityId] } else if (oldFeed.entityType === 'libraryItem') { entityType = 'LibraryItem' EntityId = oldDbIdMap.libraryItems[oldFeed.entityId] } else if (oldFeed.entityType === 'series') { entityType = 'Series' EntityId = oldDbIdMap.series[oldFeed.entityId] } if (!EntityId) { Logger.warn(`[dbMigration] migrateFeeds: Entity not found for feed "${entityType}" (id:${oldFeed.entityId})`) continue } const UserId = oldDbIdMap.users[oldFeed.userId] if (!UserId) { Logger.warn(`[dbMigration] migrateFeeds: User not found for feed (id:${oldFeed.userId})`) continue } const oldFeedMeta = oldFeed.meta const Feed = { id: uuidv4(), slug: oldFeed.slug, entityType, 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, createdAt: oldFeed.createdAt, updatedAt: oldFeed.updatedAt, UserId } newRecords.Feed.push(Feed) // // Migrate FeedEpisodes // for (const oldFeedEpisode of oldFeed.episodes) { const FeedEpisode = { id: uuidv4(), 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, createdAt: oldFeed.createdAt, updatedAt: oldFeed.updatedAt, FeedId: Feed.id } newRecords.FeedEpisode.push(FeedEpisode) } } } function migrateSettings(oldSettings) { const serverSettings = oldSettings.find(s => s.id === 'server-settings') const notificationSettings = oldSettings.find(s => s.id === 'notification-settings') if (serverSettings) { for (const serverSettingsKey in serverSettings) { if (serverSettingsKey === 'id') continue let value = serverSettings[serverSettingsKey] if (value === undefined) value = null else if (serverSettingsKey === 'sortingPrefixes') value = JSON.stringify(value) newRecords.Setting.push({ key: serverSettingsKey, value, type: 0 }) } } if (notificationSettings) { const cleanedCopy = { appriseApiUrl: notificationSettings.appriseApiUrl || null, notificationMaxFailedAttempts: notificationSettings.maxFailedAttempts ?? 5, notificationMaxQueue: notificationSettings.maxNotificationQueue ?? 20, notificationDelay: notificationSettings.notificationDelay ?? 1000 // ms delay between firing notifications } for (const notificationSettingKey in cleanedCopy) { newRecords.Setting.push({ key: notificationSettingKey, value: cleanedCopy[notificationSettingKey], type: 1 }) } // // Migrate Notifications // if (notificationSettings.notifications?.length) { for (const oldNotification of notificationSettings.notifications) { const Notification = { id: uuidv4(), eventName: oldNotification.eventName, urls: JSON.stringify(oldNotification.urls), // JSON array of urls titleTemplate: oldNotification.titleTemplate, bodyTemplate: oldNotification.bodyTemplate, type: oldNotification.type, lastFiredAt: oldNotification.lastFiredAt, lastAttemptFailed: oldNotification.lastAttemptFailed, numConsecutiveFailedAttempts: oldNotification.numConsecutiveFailedAttempts, numTimesFired: oldNotification.numTimesFired, enabled: !!oldNotification.enabled, createdAt: oldNotification.createdAt, updatedAt: oldNotification.createdAt } newRecords.Notification.push(Notification) } } } } module.exports.migrate = async () => { Logger.info(`[dbMigration] Starting migration`) const data = await oldDbFiles.init() const start = Date.now() migrateAuthors(data.authors) migrateSeries(data.series) migrateLibraries(data.libraries) migrateLibraryItems(data.libraryItems) migrateUsers(data.users) migrateSessions(data.sessions) migrateCollections(data.collections) migratePlaylists(data.playlists) migrateFeeds(data.feeds) migrateSettings(data.settings) let totalRecords = 0 for (const model in newRecords) { Logger.info(`[dbMigration] Inserting ${newRecords[model].length} ${model} rows`) if (newRecords[model].length) { await Database.models[model].bulkCreate(newRecords[model]) totalRecords += newRecords[model].length } } const elapsed = Date.now() - start Logger.info(`[dbMigration] Migration complete. ${totalRecords} rows. Elapsed ${(elapsed / 1000).toFixed(2)}s`) }