新しいMac版Kindleで書籍情報を取得する(Swift)
はじめに
以前の Mac 版 Kindle アプリは下記の XML ファイルから書籍情報を取得できましたが新しい Kindle アプリでは XML ファイルが見当たらなかったのでどこかに書籍情報がないか探してみました。
/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/Cache/KindleSyncMetadataCache.xml
探してみたところ下記の sqlite ファイルから書籍情報が取得できそうでした。
/Library/Containers/com.amazon.Lassen/Data/Library/Protected/BookData.sqlite
ここから漫画だけ抽出して iOS アプリで一覧表示してみました。
環境
- macOS 13.5.2
- Kindle 6.85.2
- Xcode 15
- iOS 17
書籍情報
BookData.sqlite には下記の 13 このテーブルがありました。
それぞれ何が入ってるのかよくわかりませんでしたが ZBOOK
テーブルに書籍情報がありそうでした。ZBOOK
テーブルのデータは下記です。
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 |
アプリ作成
どのデータがなにかわからないので必要なものだけ抽出します。おそらくそうだろうと思われる下記を抽出して一覧表示してみます。
- Z_PK:ID
- ZRAWPUBLICATIONDATE:発売日
- ZBOOKID:ASIN
- ZCONTENTTAGS:カテゴリー
- ZDISPLAYTITLE:タイトル
- ZRAWPUBLISHER:出版社
- ZSORTTITLE:タイトルカナ
DB作成
そのままでもいいのですが扱いやすいように SwiftData
を使って DB を作成します。
BookData.sqlite から上記データを下記の Book
に変換します。
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
}
}
BookData.sqlite をプロジェクトファイルに追加して下記のように SQLite3
を使ってデータを読み込みます。
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() }
}
}
ポイントは日付の変換処理です。DB 内の日付は Unixtime になっているので下記のように Date(timeIntervalSince1970:)
で変換します。
let time = (row[1] as? Int) ?? 0
self.publicationDate = time == 0 ? nil : Date(timeIntervalSince1970: TimeInterval(time))
表示
あとは漫画を抽出して一覧表示するだけです。漫画は ZCONTENTTAGS
に ;MANGA
と設定されているのでそのデータのみ抽出します。
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("一覧")
}
}
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("データ取得")
}
}
}
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("タイトル: \(book.title)")
Text("タイトルカナ: \(book.titlePronunciation)")
if let date = book.publicationDate {
Text("発売日: \(dateFormat.string(from: date))")
}
Text("タグ: \(book.contentTags)")
Text("出版社: \(book.publisher)")
}
.padding(16)
.navigationTitle("詳細")
}
}
データ取得ボタン押下で書籍情報を作成し下記で漫画を抽出して五十音順にソートしています。
@Query(filter: #Predicate<Book> { $0.contentTags == ";MANGA" }, sort: \Book.titlePronunciation)
これで完成🎉
おわりに
XML から sqlite に変わり色々な情報が取得できるようになりました!
ただ ZBOOK
テーブルと Kindle アプリでは少しだけ表示データが違う気がするので他の場所にあるのかもしれないです(最新のやつは ZBOOK
テーブルになさそうでした。。。)。
とりあえずはそれっぽいデータが取得できたのでよしとします。
Discussion