iTranslated by AI
Extracting Book Information from the New Kindle for Mac (Swift)
Introduction
The previous Kindle app for Mac could retrieve book information from the XML file below, but in the new Kindle app, I couldn't find the XML file, so I looked for where the book information might be stored.
/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/Cache/KindleSyncMetadataCache.xml
Upon searching, I found that book information could likely be retrieved from the following SQLite file.
/Library/Containers/com.amazon.Lassen/Data/Library/Protected/BookData.sqlite
I extracted only manga from here and tried displaying them in a list on an iOS app.
Environment
- macOS 13.5.2
- Kindle 6.85.2
- Xcode 15
- iOS 17
Book Information
There were 13 tables in BookData.sqlite as shown below.
I wasn't sure exactly what each table contained, but it seemed like the ZBOOK table held the book information. The data in the ZBOOK table is as follows.
| cid | name | type |
|---|---|---|
| 0 | Z_PK | INTEGER |
| 1 | Z_ENT | INTEGER |
| 2 | Z_OPT | INTEGER |
| 3 | ZISHIDDENBYUSER | INTEGER |
| 4 | ZRAWAUTOSHELVE | INTEGER |
| 5 | ZRAWBOOKSTATE | INTEGER |
| 6 | ZRAWBOOKTYPE | INTEGER |
| 7 | ZRAWBOOKUPGRADESNEEDED | INTEGER |
| 8 | ZRAWCURRENTPOSITION | INTEGER |
| 9 | ZRAWERL | INTEGER |
| 10 | ZRAWFILESIZE | INTEGER |
| 11 | ZRAWHASCOMPANION | INTEGER |
| 12 | ZRAWHASFIXEDMOPHIGHLIGHTS | INTEGER |
| 13 | ZRAWINBOOKCOVERCHECKED | INTEGER |
| 14 | ZRAWISARCHIVABLE | INTEGER |
| 15 | ZRAWISDICTIONARY | INTEGER |
| 16 | ZRAWISENCRYPTED | INTEGER |
| 17 | ZRAWISHIDDEN | INTEGER |
| 18 | ZRAWISKEPT | INTEGER |
| 19 | ZRAWISMULTIMEDIA | INTEGER |
| 20 | ZRAWISPENDINGVERIFICATION | INTEGER |
| 21 | ZRAWISSMDCREATEDDICTIONARY | INTEGER |
| 22 | ZRAWISTRANSLATIONDICTIONARY | INTEGER |
| 23 | ZRAWISUNREAD | INTEGER |
| 24 | ZRAWLASTACCESSTIME | INTEGER |
| 25 | ZRAWLASTOPENSUCCEEDED | INTEGER |
| 26 | ZRAWLASTVIEW | INTEGER |
| 27 | ZRAWMAXLOCATION | INTEGER |
| 28 | ZRAWMAXPOSITION | INTEGER |
| 29 | ZRAWNCXINFOSTORED | INTEGER |
| 30 | ZRAWPROGRESSDOTCATEGORY | INTEGER |
| 31 | ZRAWPUBLICATIONDATE | INTEGER |
| 32 | ZRAWREADSTATE | INTEGER |
| 33 | ZRAWREADSTATEORIGIN | INTEGER |
| 34 | ZRAWREADINGMODE | INTEGER |
| 35 | ZRAWUSERVISIBLELABELING | INTEGER |
| 36 | ZCOMPANION | INTEGER |
| 37 | ZALTERNATESORTTITLE | VARCHAR |
| 38 | ZBOOKID | VARCHAR |
| 39 | ZBUNDLEPATH | VARCHAR |
| 40 | ZCONTENTTAGS | VARCHAR |
| 41 | ZDICTIONARYLOCALEID | VARCHAR |
| 42 | ZDICTIONARYSHORTTITLE | VARCHAR |
| 43 | ZDICTIONARYSOURCELANG | VARCHAR |
| 44 | ZDISPLAYTITLE | VARCHAR |
| 45 | ZGROUPID | VARCHAR |
| 46 | ZLANGUAGE | VARCHAR |
| 47 | ZLONGCURRENTPOSITION | VARCHAR |
| 48 | ZLONGMAXPOSITION | VARCHAR |
| 49 | ZMIMETYPE | VARCHAR |
| 50 | ZPARENTASIN | VARCHAR |
| 51 | ZPATH | VARCHAR |
| 52 | ZPERASINGUID | VARCHAR |
| 53 | ZRAWPUBLISHER | VARCHAR |
| 54 | ZSHELF | VARCHAR |
| 55 | ZSORTTITLE | VARCHAR |
| 56 | ZWATERMARK | VARCHAR |
| 57 | ZALTERNATESORTAUTHOR | BLOB |
| 58 | ZDISPLAYAUTHOR | BLOB |
| 59 | ZEXTENDEDMETADATA | BLOB |
| 60 | ZORIGINS | BLOB |
| 61 | ZSORTAUTHOR | BLOB |
| 62 | ZSYNCMETADATAATTRIBUTES | BLOB |
| 63 | ZRAWTITLEDETAILSJSON | BLOB |
App Creation
Since it's unclear what each data point represents, I'll extract only the necessary ones. I'll list and extract the following, which seem likely to be relevant:
- Z_PK: ID
- ZRAWPUBLICATIONDATE: Publication Date
- ZBOOKID: ASIN
- ZCONTENTTAGS: Category
- ZDISPLAYTITLE: Title
- ZRAWPUBLISHER: Publisher
- ZSORTTITLE: Title Pronunciation (Kana)
DB Creation
While the current setup works, I'll use SwiftData to create a database for easier handling.
Convert the above data from BookData.sqlite into the following Book class.
import Foundation
import SwiftData
@Model
final class Book {
@Attribute(.unique) var id: Int
var asin: String
var title: String
var titlePronunciation: String
var publicationDate: Date?
var contentTags: String
var publisher: String
init(id: Int,
asin: String,
title: String,
titlePronunciation: String,
publicationDate: Date?,
contentTags: String,
publisher: String) {
self.id = id
self.asin = asin
self.title = title
self.titlePronunciation = titlePronunciation
self.publicationDate = publicationDate
self.contentTags = contentTags
self.publisher = publisher
}
}
Add BookData.sqlite to the project file and load the data using SQLite3 as follows.
import Foundation
import SQLite3
final class Database {
struct MetaData {
let id: Int
let publicationDate: Date?
let asin: String
var contentTags: String
let title: String
var publisher: String
let titlePronunciation: String
init(row: [Any?]) {
self.id = (row[0] as? Int) ?? 0
let time = (row[1] as? Int) ?? 0
self.publicationDate = time == 0 ? nil : Date(timeIntervalSince1970: TimeInterval(time))
self.asin = (row[2] as? String) ?? ""
self.contentTags = (row[3] as? String) ?? ""
self.title = (row[4] as? String) ?? ""
self.publisher = (row[5] as? String) ?? ""
self.titlePronunciation = (row[6] as? String) ?? ""
}
func convertedBook() -> Book {
let asinValue = asin.replacingOccurrences(of: "A:", with: "")
return .init(
id: id,
asin: asinValue.components(separatedBy: "-").first!,
title: title, titlePronunciation: titlePronunciation,
publicationDate: publicationDate, contentTags: contentTags,
publisher: publisher
)
}
}
func fetchBooks() -> [Book] {
var db: OpaquePointer?
defer {
sqlite3_close(db)
}
let filePath = Bundle.main.path(forResource: "BookData", ofType: "sqlite")
guard sqlite3_open(filePath, &db) == SQLITE_OK else {
return []
}
var stmt: OpaquePointer?
defer {
sqlite3_finalize(stmt)
}
let sql = "SELECT Z_PK, ZRAWPUBLICATIONDATE, ZBOOKID, ZCONTENTTAGS, ZDISPLAYTITLE, ZRAWPUBLISHER, ZSORTTITLE FROM ZBOOK;"
guard sqlite3_prepare_v2(db, (sql as NSString).utf8String, -1, &stmt, nil) == SQLITE_OK else {
return []
}
var metaDataList = [MetaData]()
while SQLITE_ROW == sqlite3_step(stmt) {
metaDataList.append(.init(row: [
_getIntValue(0), _getIntValue(1),
_getStringValue(2), _getStringValue(3),
_getStringValue(4), _getStringValue(5),
_getStringValue(6)
]))
}
func _getIntValue(_ column: Int32) -> Int? {
if sqlite3_column_type(stmt, column) == SQLITE_NULL {
return nil
} else {
return Int(sqlite3_column_int(stmt, column))
}
}
func _getStringValue(_ column: Int32) -> String? {
if sqlite3_column_type(stmt, column) == SQLITE_NULL {
return nil
} else {
return String(cString: sqlite3_column_text(stmt, column))
}
}
return metaDataList.map { $0.convertedBook() }
}
}
The key point is the date conversion process. Since the dates in the database are in Unix time, they are converted using Date(timeIntervalSince1970:) as follows:
let time = (row[1] as? Int) ?? 0
self.publicationDate = time == 0 ? nil : Date(timeIntervalSince1970: TimeInterval(time))
Display
All that's left is to extract the manga and display them in a list. Manga are identified by ;MANGA in the ZCONTENTTAGS field, so we will extract only that data.
import SwiftUI
import SwiftData
@main
struct KindleBooksApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Book.self)
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query(filter: #Predicate<Book> { $0.contentTags == ";MANGA" }, sort: \Book.titlePronunciation)
private var books: [Book]
var body: some View {
NavigationStack {
VStack {
if books.isEmpty {
addButton
} else {
List {
ForEach(books, id: \.id) { book in
NavigationLink {
DetailView(book: book)
} label: {
VStack {
Text(book.title)
}
}
}
}
}
}.navigationTitle("List")
}
}
private var addButton: some View {
return Button {
let books = Database().fetchBooks()
books.forEach { context.insert($0) }
do {
try context.save()
} catch let error {
print(error)
}
} label: {
Text("Get Data")
}
}
}
struct DetailView: View {
let book: Book
var dateFormat: DateFormatter = {
let format = DateFormatter()
format.dateFormat = "yyyy/MM/dd"
return format
}()
var body: some View {
VStack(spacing: 8) {
Text("ID: \(book.id)")
Text("ASIN: \(book.asin)")
Text("Title: \(book.title)")
Text("Title Kana: \(book.titlePronunciation)")
if let date = book.publicationDate {
Text("Release Date: \(dateFormat.string(from: date))")
}
Text("Tags: \(book.contentTags)")
Text("Publisher: \(book.publisher)")
}
.padding(16)
.navigationTitle("Details")
}
}
When the "Get Data" button is pressed, the book information is created, and the manga are extracted and sorted in syllabic (Gojuon) order as shown below.
@Query(filter: #Predicate<Book> { $0.contentTags == ";MANGA" }, sort: \Book.titlePronunciation)
With this, it's complete! 🎉
Conclusion
By switching from XML to SQLite, I can now retrieve various information!
However, the data displayed in the Kindle app seems slightly different from what's in the ZBOOK table, so it might be stored elsewhere (it seems the latest books aren't in the ZBOOK table...).
For now, I'm satisfied since I was able to retrieve data that looks plausible.
Discussion