diff --git a/components/readers/ComicReader.vue b/components/readers/ComicReader.vue index 62167b9f..f2af14e0 100644 --- a/components/readers/ComicReader.vue +++ b/components/readers/ComicReader.vue @@ -244,12 +244,13 @@ export default { async extract() { this.loading = true - var buff = await this.$axios.$get(this.url, { + const buff = await this.$axios.$get(this.url, { responseType: 'blob', headers: { Authorization: `Bearer ${this.userToken}` } }) + const archive = await Archive.open(buff) const originalFilesObject = await archive.getFilesObject() // to support images in subfolders we need to flatten the object diff --git a/components/readers/EpubReader.vue b/components/readers/EpubReader.vue index 8415d898..64bf5f5f 100644 --- a/components/readers/EpubReader.vue +++ b/components/readers/EpubReader.vue @@ -284,7 +284,7 @@ export default { /** @type {EpubReader} */ const reader = this - + console.log('initEpub', reader.url) /** @type {ePub.Book} */ reader.book = new ePub(reader.url, { width: window.innerWidth, diff --git a/components/readers/Reader.vue b/components/readers/Reader.vue index f72c0681..bec54e11 100644 --- a/components/readers/Reader.vue +++ b/components/readers/Reader.vue @@ -3,11 +3,19 @@
- +
- - - + + +

{{ title }}

@@ -110,6 +118,8 @@ export default { this.comicHasMetadata = false this.registerListeners() this.hideToolbar() + + console.log('showReader for ebookFile', JSON.stringify(this.ebookFile)) } else { this.unregisterListeners() this.$showHideStatusBar(true) @@ -196,7 +206,7 @@ export default { return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr' }, isLocal() { - return !!this.ebookFile?.isLocal + return !!this.ebookFile?.isLocal || !!this.ebookFile?.localFileId }, localContentUrl() { return this.ebookFile?.contentUrl diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 5ba1c536..eed40655 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -27,6 +27,7 @@ 4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B953282EE87C008272D4 /* AbsDownloader.swift */; }; 4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B955282EE951008272D4 /* AbsFileSystem.m */; }; 4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; }; + 4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D91EEC52A40F28D004807ED /* EBookFile.swift */; }; 4DF74912287105C600AC7814 /* DeviceSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF74911287105C600AC7814 /* DeviceSettings.swift */; }; 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; @@ -97,6 +98,7 @@ 4D66B955282EE951008272D4 /* AbsFileSystem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsFileSystem.m; sourceTree = ""; }; 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = ""; }; 4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = ""; }; + 4D91EEC52A40F28D004807ED /* EBookFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EBookFile.swift; sourceTree = ""; }; 4DF74911287105C600AC7814 /* DeviceSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceSettings.swift; sourceTree = ""; }; 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; 504EC3041FED79650016851F /* Audiobookshelf.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Audiobookshelf.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -299,6 +301,7 @@ E9D5505928AC1C4500C746DD /* Folder.swift */, E9D5505B28AC1C6200C746DD /* LibraryFile.swift */, E9D5505D28AC1C8500C746DD /* MediaProgress.swift */, + 4D91EEC52A40F28D004807ED /* EBookFile.swift */, ); path = server; sourceTree = ""; @@ -545,6 +548,7 @@ E9D5506028AC1CA900C746DD /* PlaybackMetadata.swift in Sources */, E9D5504828AC1A7A00C746DD /* MediaType.swift in Sources */, E9D5504E28AC1B0700C746DD /* AudioFile.swift in Sources */, + 4D91EEC62A40F28D004807ED /* EBookFile.swift in Sources */, E9DFCBFB28C28F4A00B36356 /* AudioPlayerSleepTimer.swift in Sources */, E9D5505428AC1B7900C746DD /* AudioTrack.swift in Sources */, E9D5505C28AC1C6200C746DD /* LibraryFile.swift in Sources */, diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index 65495314..8831fabc 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -14,7 +14,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Override point for customization after application launch. let configuration = Realm.Configuration( - schemaVersion: 9, + schemaVersion: 11, migrationBlock: { [weak self] migration, oldSchemaVersion in if (oldSchemaVersion < 1) { self?.logger.log("Realm schema version was \(oldSchemaVersion)") diff --git a/ios/App/App/plugins/AbsDownloader.swift b/ios/App/App/plugins/AbsDownloader.swift index a4c13f45..62d21eb5 100644 --- a/ios/App/App/plugins/AbsDownloader.swift +++ b/ios/App/App/plugins/AbsDownloader.swift @@ -265,7 +265,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { // Handle the different media type downloads switch item.mediaType { case "book": - guard item.media?.tracks.count ?? 0 > 0 else { throw LibraryItemDownloadError.noTracks } + guard item.media?.tracks.count ?? 0 > 0 || item.media?.ebookFile != nil else { throw LibraryItemDownloadError.noTracks } item.media?.tracks.forEach { t in tracks.append(AudioTrack.detachCopy(of: t)!) } case "podcast": guard let episode = episode else { throw LibraryItemDownloadError.podcastEpisodeNotFound } @@ -285,6 +285,12 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { tasks.append(task) } + if (item.media?.ebookFile != nil) { + let task = try startLibraryItemEbookDownload(downloadItemId: downloadItem.id!, item: item, ebookFile: item.media!.ebookFile!) + downloadItem.downloadItemParts.append(task.part) + tasks.append(task) + } + // Also download the cover if item.media?.coverPath != nil && !(item.media?.coverPath!.isEmpty ?? true) { if let task = try? startLibraryItemCoverDownload(downloadItemId: downloadItem.id!, item: item) { @@ -318,7 +324,22 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { let localUrl = "\(itemDirectory)/\(filename)" let task = session.downloadTask(with: serverUrl) - let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode, size: track.metadata?.size ?? 0) + let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: track.title ?? "Unknown", serverPath: Store.serverConfig!.address, audioTrack: track, episode: episode, ebookFile: nil, size: track.metadata?.size ?? 0) + + // Store the id on the task so the download item can be pulled from the database later + task.taskDescription = part.id + + return DownloadItemPartTask(part: part, task: task) + } + + private func startLibraryItemEbookDownload(downloadItemId: String, item: LibraryItem, ebookFile: EBookFile) throws -> DownloadItemPartTask { + let filename = ebookFile.metadata?.filename ?? "ebook.\(ebookFile.ebookFormat)" + let serverPath = "/api/items/\(item.id)/file/\(ebookFile.ino)/download" + let itemDirectory = try createLibraryItemFileDirectory(item: item) + let localUrl = "\(itemDirectory)/\(filename)" + + let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: filename, serverPath: serverPath, audioTrack: nil, episode: nil, ebookFile: ebookFile, size: ebookFile.metadata?.size ?? 0) + let task = session.downloadTask(with: part.downloadURL!) // Store the id on the task so the download item can be pulled from the database later task.taskDescription = part.id @@ -337,7 +358,7 @@ public class AbsDownloader: CAPPlugin, URLSessionDownloadDelegate { $0.metadata?.path == item.media?.coverPath }) - let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil, size: coverLibraryFile?.metadata?.size ?? 0) + let part = DownloadItemPart(downloadItemId: downloadItemId, filename: filename, destination: localUrl, itemTitle: "cover", serverPath: serverPath, audioTrack: nil, episode: nil, ebookFile: nil, size: coverLibraryFile?.metadata?.size ?? 0) let task = session.downloadTask(with: part.downloadURL!) // Store the id on the task so the download item can be pulled from the database later diff --git a/ios/App/Shared/models/download/DownloadItemPart.swift b/ios/App/Shared/models/download/DownloadItemPart.swift index eafda2eb..80a8c434 100644 --- a/ios/App/Shared/models/download/DownloadItemPart.swift +++ b/ios/App/Shared/models/download/DownloadItemPart.swift @@ -17,6 +17,7 @@ class DownloadItemPart: Object, Codable { @Persisted var serverPath: String? @Persisted var audioTrack: AudioTrack? @Persisted var episode: PodcastEpisode? + @Persisted var ebookFile: EBookFile? @Persisted var completed: Bool = false @Persisted var moved: Bool = false @Persisted var failed: Bool = false @@ -63,7 +64,7 @@ class DownloadItemPart: Object, Codable { } extension DownloadItemPart { - convenience init(downloadItemId: String, filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?, size: Double) { + convenience init(downloadItemId: String, filename: String, destination: String, itemTitle: String, serverPath: String, audioTrack: AudioTrack?, episode: PodcastEpisode?, ebookFile: EBookFile?, size: Double) { self.init() self.id = destination.toBase64() @@ -74,6 +75,7 @@ extension DownloadItemPart { self.serverPath = serverPath self.audioTrack = AudioTrack.detachCopy(of: audioTrack) self.episode = PodcastEpisode.detachCopy(of: episode) + self.ebookFile = EBookFile.detachCopy(of: ebookFile) let config = Store.serverConfig! var downloadUrl = "\(config.address)\(serverPath)?token=\(config.token)" @@ -101,6 +103,6 @@ extension DownloadItemPart { } func mimeType() -> String? { - audioTrack?.mimeType ?? episode?.audioTrack?.mimeType + audioTrack?.mimeType ?? episode?.audioTrack?.mimeType ?? ebookFile?.mimeType() } } diff --git a/ios/App/Shared/models/local/LocalLibraryItem.swift b/ios/App/Shared/models/local/LocalLibraryItem.swift index b6c4e858..f99998bf 100644 --- a/ios/App/Shared/models/local/LocalLibraryItem.swift +++ b/ios/App/Shared/models/local/LocalLibraryItem.swift @@ -130,6 +130,12 @@ extension LocalLibraryItem { for i in fromMedia.tracks.indices { _ = fromMedia.tracks[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i) } + if fromMedia.ebookFile != nil { + let ebookLocalFile = files.first(where: { $0.filename == fromMedia.ebookFile?.metadata?.filename ?? "" }) + if ebookLocalFile != nil { + _ = fromMedia.ebookFile?.setLocalInfo(localFile: ebookLocalFile!) + } + } } else if ( self.isPodcast ) { let episodes = List() for episode in fromMedia.episodes { diff --git a/ios/App/Shared/models/server/EBookFile.swift b/ios/App/Shared/models/server/EBookFile.swift new file mode 100644 index 00000000..7280aa45 --- /dev/null +++ b/ios/App/Shared/models/server/EBookFile.swift @@ -0,0 +1,78 @@ +// +// EBookFile.swift +// Audiobookshelf +// +// Created by Advplyr on 6/19/23. +// + +import Foundation +import RealmSwift + +class EBookFile: EmbeddedObject, Codable { + @Persisted var ino: String = "" + @Persisted var metadata: FileMetadata? + @Persisted var ebookFormat: String + @Persisted var contentUrl: String? + @Persisted var localFileId: String? + + private enum CodingKeys : String, CodingKey { + case ino, metadata, ebookFormat, contentUrl, localFileId + } + + override init() { + super.init() + } + + required init(from decoder: Decoder) throws { + super.init() + let values = try decoder.container(keyedBy: CodingKeys.self) + ino = try values.decode(String.self, forKey: .ino) + metadata = try values.decode(FileMetadata.self, forKey: .metadata) + ebookFormat = try values.decode(String.self, forKey: .ebookFormat) + contentUrl = try? values.decode(String.self, forKey: .contentUrl) + localFileId = try? values.decodeIfPresent(String.self, forKey: .localFileId) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(ino, forKey: .ino) + try container.encode(metadata, forKey: .metadata) + try container.encode(ebookFormat, forKey: .ebookFormat) + try container.encode(contentUrl, forKey: .contentUrl) + try container.encode(localFileId, forKey: .localFileId) + } +} + +extension EBookFile { + func setLocalInfo(localFile: LocalFile) -> Bool { + self.localFileId = localFile.id + self.contentUrl = localFile.contentUrl + return false + } + + func getLocalFile() -> LocalFile? { + guard let localFileId = self.localFileId else { return nil } + return Database.shared.getLocalFile(localFileId: localFileId) + } + + func mimeType() -> String? { + var mimeType = "" + switch ebookFormat { + case "epub": + mimeType = "application/epub+zip" + case "pdf": + mimeType = "application/pdf" + case "mobi": + mimeType = "application/x-mobipocket-ebook" + case "azw3": + mimeType = "application/vnd.amazon.mobi8-ebook" + case "cbr": + mimeType = "application/vnd.comicbook-rar" + case "cbz": + mimeType = "application/vnd.comicbook+zip" + default: + mimeType = "application/epub+zip" + } + return mimeType + } +} diff --git a/ios/App/Shared/models/server/MediaProgress.swift b/ios/App/Shared/models/server/MediaProgress.swift index 6572ca01..6512d4ae 100644 --- a/ios/App/Shared/models/server/MediaProgress.swift +++ b/ios/App/Shared/models/server/MediaProgress.swift @@ -16,12 +16,14 @@ class MediaProgress: EmbeddedObject, Codable { @Persisted var progress: Double = 0 @Persisted var currentTime: Double = 0 @Persisted var isFinished: Bool = false + @Persisted var ebookLocation: String? + @Persisted var ebookProgress: Double? @Persisted var lastUpdate: Double = 0 @Persisted var startedAt: Double = 0 @Persisted var finishedAt: Double? private enum CodingKeys : String, CodingKey { - case id, libraryItemId, episodeId, duration, progress, currentTime, isFinished, lastUpdate, startedAt, finishedAt + case id, libraryItemId, episodeId, duration, progress, currentTime, isFinished, ebookLocation, ebookProgress, lastUpdate, startedAt, finishedAt } override init() { @@ -37,6 +39,8 @@ class MediaProgress: EmbeddedObject, Codable { progress = try values.doubleOrStringDecoder(key: .progress) currentTime = try values.doubleOrStringDecoder(key: .currentTime) isFinished = try values.decode(Bool.self, forKey: .isFinished) + ebookLocation = try values.decodeIfPresent(String.self, forKey: .ebookLocation) + ebookProgress = try values.doubleOrStringDecoder(key: .ebookProgress) lastUpdate = try values.doubleOrStringDecoder(key: .lastUpdate) startedAt = try values.doubleOrStringDecoder(key: .startedAt) finishedAt = try? values.doubleOrStringDecoder(key: .finishedAt) @@ -51,6 +55,8 @@ class MediaProgress: EmbeddedObject, Codable { try container.encode(progress, forKey: .progress) try container.encode(currentTime, forKey: .currentTime) try container.encode(isFinished, forKey: .isFinished) + try container.encode(ebookLocation, forKey: .ebookLocation) + try container.encode(ebookProgress, forKey: .ebookProgress) try container.encode(lastUpdate, forKey: .lastUpdate) try container.encode(startedAt, forKey: .startedAt) try container.encode(finishedAt, forKey: .finishedAt) diff --git a/ios/App/Shared/models/server/MediaType.swift b/ios/App/Shared/models/server/MediaType.swift index db7e04d7..bdae5032 100644 --- a/ios/App/Shared/models/server/MediaType.swift +++ b/ios/App/Shared/models/server/MediaType.swift @@ -14,6 +14,7 @@ class MediaType: EmbeddedObject, Codable { @Persisted var coverPath: String? @Persisted var tags = List() @Persisted var audioFiles = List() + @Persisted var ebookFile: EBookFile? @Persisted var chapters = List() @Persisted var tracks = List() @Persisted var size: Int? @@ -22,7 +23,7 @@ class MediaType: EmbeddedObject, Codable { @Persisted var autoDownloadEpisodes: Bool? private enum CodingKeys : String, CodingKey { - case libraryItemId, metadata, coverPath, tags, audioFiles, chapters, tracks, size, duration, episodes, autoDownloadEpisodes + case libraryItemId, metadata, coverPath, tags, audioFiles, ebookFile, chapters, tracks, size, duration, episodes, autoDownloadEpisodes } override init() { @@ -41,6 +42,7 @@ class MediaType: EmbeddedObject, Codable { if let fileList = try? values.decode([AudioFile].self, forKey: .audioFiles) { audioFiles.append(objectsIn: fileList) } + ebookFile = try? values.decode(EBookFile.self, forKey: .ebookFile) if let chapterList = try? values.decode([Chapter].self, forKey: .chapters) { chapters.append(objectsIn: chapterList) } @@ -62,6 +64,7 @@ class MediaType: EmbeddedObject, Codable { try container.encode(coverPath, forKey: .coverPath) try container.encode(Array(tags), forKey: .tags) try container.encode(Array(audioFiles), forKey: .audioFiles) + try container.encode(ebookFile, forKey: .ebookFile) try container.encode(Array(chapters), forKey: .chapters) try container.encode(Array(tracks), forKey: .tracks) try container.encode(size, forKey: .size) diff --git a/pages/item/_id/index.vue b/pages/item/_id/index.vue index ecfd8da4..b50f125d 100644 --- a/pages/item/_id/index.vue +++ b/pages/item/_id/index.vue @@ -63,13 +63,13 @@
-
+
Author
{{ podcastAuthor }}
@@ -79,8 +79,8 @@
Series
@@ -90,16 +90,16 @@
{{ narrators.length === 1 ? 'Narrator' : 'Narrators' }}
{{ genres.length === 1 ? 'Genre' : 'Genres' }}
@@ -359,7 +359,7 @@ export default { }, showDownload() { if (this.isPodcast || this.hasLocal) return false - return this.user && this.userCanDownload && (this.showPlay || (this.showRead && !this.isIos)) + return this.user && this.userCanDownload && (this.showPlay || this.showRead) }, libraryFiles() { return this.libraryItem.libraryFiles || [] diff --git a/plugins/axios.js b/plugins/axios.js index 3eb40c64..73b97238 100644 --- a/plugins/axios.js +++ b/plugins/axios.js @@ -1,7 +1,7 @@ export default function ({ $axios, store }) { $axios.onRequest(config => { console.log('[Axios] Making request to ' + config.url) - if (config.url.startsWith('http:') || config.url.startsWith('https:')) { + if (config.url.startsWith('http:') || config.url.startsWith('https:') || config.url.startsWith('capacitor:')) { return }