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 @@
-