Skip to content

Commit 8b19fb9

Browse files
committed
Push some old experiments
Signed-off-by: Marcus Crane <[email protected]>
1 parent fa60217 commit 8b19fb9

11 files changed

+556
-0
lines changed

v2/README.md

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# v2
2+
3+
This folder contains some experiments with rewriting October. It may or may not amount to anything but I'd like to untangle various elements of the backend.
4+
5+
It isn't intended to be usable in its current state, I'm approximating the upload process by pointing at files on disc instead of actually using my mounted Kobo.
6+
7+
This version will also be attempting to incorporate better parsing by processing epubs and probably parallelised uploading using Goroutines
8+
9+
## Prerequisites
10+
11+
Quickest way to bootstrap a DB for experimenting:
12+
13+
```bash
14+
git clone [email protected]:marcus-crane/kobodbgen
15+
cd kobodbgen
16+
./initdb.sh
17+
```
18+
19+
This will create a mock Kobo DB for usage although I'm using a dump of my Kobo stored on my desktop at present

v2/go.mod

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/marcus-crane/october/v2
2+
3+
go 1.19
4+
5+
require (
6+
github.com/jmoiron/sqlx v1.3.5
7+
github.com/mattn/go-sqlite3 v1.14.16
8+
github.com/pgaskin/koboutils/v2 v2.1.2-0.20220306004009-a07e72ebae42
9+
github.com/taylorskalyo/goreader v0.0.0-20220528130152-945e7448ceb5
10+
)

v2/go.sum

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
2+
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
3+
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
4+
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
5+
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
6+
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
7+
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
8+
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
9+
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
10+
github.com/pgaskin/koboutils/v2 v2.1.1 h1:Or5y+z8rXlip0Al8tiSj+Fb9NkuLhkcw1UPpzPPvKWY=
11+
github.com/pgaskin/koboutils/v2 v2.1.1/go.mod h1:wTzkDIlsxmUyfwfspGcm0Ap+HOxSUYV0S8kMYrf+0gM=
12+
github.com/pgaskin/koboutils/v2 v2.1.2-0.20220306004009-a07e72ebae42 h1:wwU2E+7+IN4HBv9v5p+TkoaaFSnF5BtguW5bTxopuvk=
13+
github.com/pgaskin/koboutils/v2 v2.1.2-0.20220306004009-a07e72ebae42/go.mod h1:wTzkDIlsxmUyfwfspGcm0Ap+HOxSUYV0S8kMYrf+0gM=
14+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
15+
github.com/taylorskalyo/goreader v0.0.0-20220528130152-945e7448ceb5 h1:dW3HLfusjJuR5/7MCKKcBKzTmRjZnEAEO8AVrHIqqC8=
16+
github.com/taylorskalyo/goreader v0.0.0-20220528130152-945e7448ceb5/go.mod h1:06vTtAxpkyCBMlqDyYuvHgeQec6ne7NWXIEgJNhq2Ks=

v2/main.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"sync"
7+
8+
"github.com/marcus-crane/october/v2/pkg/epub"
9+
"github.com/marcus-crane/october/v2/pkg/kobo"
10+
)
11+
12+
func main() {
13+
root, err := epub.LoadEpub("kobodbgen/epubs/source/james-hogg_the-private-memoirs-and-confessions-of-a-justified-sinner.epub")
14+
if err != nil {
15+
log.Fatalf("Failed to load epub: %+v", err)
16+
}
17+
// Spines are the correct order where manifests may be out of order
18+
// May need to check the manifest for covers
19+
for i, entry := range root.Spine.Itemrefs {
20+
idx := i + 1 // We increment by 1 as we don't trust external systems to not drop zero padding
21+
fmt.Printf("%d - %s\n", idx, entry.HREF)
22+
}
23+
24+
connection := kobo.NewKobo("/Users/marcus/Desktop/Kobo", "/Users/marcus/Desktop/Kobo/.kobo/KoboReader.sqlite")
25+
if err := connection.Connect(); err != nil {
26+
log.Fatalf("Failed to connect to database: %+v", err)
27+
}
28+
defer connection.Disconnect()
29+
numBookmarks, err := kobo.CountBookmarks(&connection)
30+
if err != nil {
31+
log.Fatalf("Failed to count bookmarks")
32+
}
33+
log.Printf("Your device has %d bookmarks", numBookmarks)
34+
numContent, err := kobo.CountContent(&connection)
35+
if err != nil {
36+
log.Fatalf("Failed to count content")
37+
}
38+
log.Printf("Your device has %d non-unique pieces of content", numContent)
39+
volumes, err := kobo.QueryDistinctVolumes(&connection)
40+
if err != nil {
41+
log.Fatalf("Failed to check volumes")
42+
}
43+
log.Printf("Detected %d unique books containing highlights and notes", len(volumes))
44+
// VERY important to keep this buffered (ie; set length) or the goroutine will block
45+
bookmarks := make(chan kobo.Bookmark, len(volumes))
46+
var wg sync.WaitGroup
47+
for _, volume := range volumes {
48+
wg.Add(1)
49+
go func(connection kobo.Kobo, volume string) {
50+
defer wg.Done()
51+
fmt.Printf("Saw %s and tried to ping DB with result %+v\n", volume, connection.Ping())
52+
bookmarks <- kobo.Bookmark{
53+
Text: fmt.Sprintf("Toot from %s", volume),
54+
}
55+
}(connection, volume)
56+
}
57+
wg.Wait()
58+
close(bookmarks)
59+
fmt.Println("Finished waiting")
60+
61+
for bookmark := range bookmarks {
62+
fmt.Printf("Received bookmark with text: %s\n", bookmark.Text)
63+
}
64+
}

v2/pkg/epub/epub.go

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package epub
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/taylorskalyo/goreader/epub"
8+
)
9+
10+
// NOTE TO SELF: Cover images can be retrieved like so:
11+
// 1) Find meta name="cover" in OPF metadata and check the content
12+
// 2) Find item entry with id set to same value as metadata content
13+
// See https://github.com/kobolabs/epub-spec#cover-images
14+
// Also covers may technically be SVGs so check the types and
15+
// explicitly mention what is supported
16+
17+
// See https://github.com/kobolabs/epub-spec#for-epub3 for a list of nav fallbacks
18+
// if OPF doesn't have the desired information
19+
20+
// LoadEpub takes a relative path to an epub file (aka VolumeID) and returns the content of the epub
21+
// Further parsing is required to actually read files for example but it gives a full book spine
22+
// among other useful metadata that may or may not be present in the database, particular with
23+
// epubs (not kepubs) that have been loaded directly onto the device without using a tool like Calibre
24+
func LoadEpub(path string) (*epub.Rootfile, error) {
25+
if _, err := os.Stat(path); os.IsNotExist(err) {
26+
return nil, err
27+
}
28+
rc, err := epub.OpenReader(path)
29+
if err != nil {
30+
return nil, err
31+
}
32+
if len(rc.Rootfiles) == 0 {
33+
return nil, fmt.Errorf("no root files found in epub")
34+
}
35+
return rc.Rootfiles[0], nil
36+
}

v2/pkg/kobo/bookmark.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package kobo
2+
3+
type Bookmark struct {
4+
BookmarkID string `db:"BookmarkID"`
5+
VolumeID string `db:"VolumeID"`
6+
ContentID string `db:"ContentID"`
7+
StartContainerPath string `db:"StartContainerPath"`
8+
StartContainerChild string `db:"StartContainerChild"`
9+
StartContainerChildIndex int `db:"StartContainerChildIndex"`
10+
StartOffset int `db:"StartOffset"`
11+
EndContainerPath string `db:"EndContainerPath"`
12+
EndContainerChildIndex int `db:"EndContainerChildIndex"`
13+
EndOffset int `db:"EndOffset"`
14+
Text string `db:"Text"`
15+
Annotation string `db:"Annotation"`
16+
ExtraAnnotationData string `db:"ExtraAnnotationData"`
17+
DateCreated string `db:"DateCreated"`
18+
ChapterProgress float64 `db:"ChapterProgress"`
19+
Hidden bool `db:"Hidden"`
20+
Version string `db:"Version"`
21+
DateModified string `db:"DateModified"`
22+
Creator string `db:"Creator"`
23+
UUID string `db:"UUID"`
24+
UserID string `db:"UserID"`
25+
SyncTime string `db:"SyncTime"`
26+
Published bool `db:"Published"`
27+
ContextString string `db:"ContextString"`
28+
Type string `db:"Type"`
29+
}
30+
31+
func CountBookmarks(kobo *Kobo) (int, error) {
32+
var count int
33+
if err := kobo.dbClient.Get(&count, "SELECT count(*) FROM Bookmark"); err != nil {
34+
return count, err
35+
}
36+
return count, nil
37+
}
38+
39+
// QueryDistinctVolumes retrieves all books that contains highlights and reside (or did at one point)
40+
// on the device itself. We exclude Kobo store books for simplicity where the path is a GUID which is
41+
// not useful in the context of retrieving the underlying epub for scanning.
42+
func QueryDistinctVolumes(kobo *Kobo) ([]string, error) {
43+
var contentWithBookmarks []string
44+
if err := kobo.dbClient.Select(
45+
&contentWithBookmarks,
46+
"SELECT DISTINCT VolumeID FROM Bookmark WHERE VolumeID LIKE '%file:///%' ORDER BY VolumeID;",
47+
); err != nil {
48+
return contentWithBookmarks, err
49+
}
50+
return contentWithBookmarks, nil
51+
}

v2/pkg/kobo/content.go

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package kobo
2+
3+
type Content struct {
4+
ContentID string `db:"ContentID"`
5+
ContentType string `db:"ContentType"`
6+
MimeType string `db:"MimeType"`
7+
BookID string `db:"BookID"`
8+
BookTitle string `db:"BookTitle"`
9+
ImageId string `db:"ImageId"`
10+
Title string `db:"Title"`
11+
Attribution string `db:"Attribution"`
12+
Description string `db:"Description"`
13+
DateCreated string `db:"DateCreated"`
14+
ShortCoverKey string `db:"ShortCoverKey"`
15+
AdobeLocation string `db:"adobe_location"`
16+
Publisher string `db:"Publisher"`
17+
IsEncrypted bool `db:"IsEncrypted"`
18+
DateLastRead string `db:"DateLastRead"`
19+
FirstTimeReading bool `db:"FirstTimeReading"`
20+
ChapterIDBookmarked string `db:"ChapterIDBookmarked"`
21+
ParagraphBookmarked int `db:"ParagraphBookmarked"`
22+
BookmarkWordOffset int `db:"BookmarkWordOffset"`
23+
NumShortcovers int `db:"NumShortcovers"`
24+
VolumeIndex int `db:"VolumeIndex"`
25+
NumPages int `db:"___NumPages"`
26+
ReadStatus int `db:"ReadStatus"`
27+
SyncTime string `db:"___SyncTime"`
28+
UserID string `db:"___UserID"`
29+
PublicationId string `db:"PublicationId"`
30+
FileOffset int `db:"___FileOffset"`
31+
FileSize int `db:"___FileSize"`
32+
PercentRead int `db:"___PercentRead"`
33+
ExpirationStatus int `db:"___ExpirationStatus"`
34+
FavouritesIndex int `db:"FavouritesIndex"`
35+
Accessibility int `db:"Accessibility"`
36+
ContentURL string `db:"ContentURL"`
37+
Language string `db:"Language"`
38+
BookshelfTags string `db:"BookshelfTags"`
39+
IsDownloaded bool `db:"IsDownloaded"`
40+
FeedbackType int `db:"FeedbackType"`
41+
AverageRating int `db:"AverageRating"`
42+
Depth int `db:"Depth"`
43+
PageProgressDirection string `db:"PageProgressDirection"`
44+
InWishlist bool `db:"InWishlist"`
45+
ISBN string `db:"ISBN"`
46+
WishlistedDate string `db:"WishlistedDate"`
47+
FeedbackTypeSynced int `db:"FeedbackTypeSynced"`
48+
IsSocialEnabled bool `db:"IsSocialEnabled"`
49+
EpubType int `db:"EpubType"`
50+
Monetization int `db:"Monetization"`
51+
ExternalId string `db:"ExternalId"`
52+
Series string `db:"Series"`
53+
SeriesNumber string `db:"SeriesNumber"`
54+
Subtitle string `db:"Subtitle"`
55+
WordCount int `db:"WordCount"`
56+
Fallback string `db:"Fallback"`
57+
RestOfBookEstimate int `db:"RestOfBookEstimate"`
58+
CurrentChapterEstimate int `db:"CurrentChapterEstimate"`
59+
CurrentChapterProgress float32 `db:"CurrentChapterProgress"`
60+
PocketStatus int `db:"PocketStatus"`
61+
UnsyncedPocketChanges string `db:"UnsyncedPocketChanges"`
62+
ImageUrl string `db:"ImageUrl"`
63+
DateAdded string `db:"DateAdded"`
64+
WorkId string `db:"WorkId"`
65+
Properties string `db:"Properties"`
66+
RenditionSpread string `db:"RenditionSpread"`
67+
RatingCount int `db:"RatingCount"`
68+
ReviewsSyncDate string `db:"ReviewsSyncDate"`
69+
MediaOverlay string `db:"MediaOverlay"`
70+
MediaOverlayType string `db:"MediaOverlayType"`
71+
RedirectPreviewUrl bool `db:"RedirectPreviewUrl"`
72+
PreviewFileSize int `db:"PreviewFileSize"`
73+
EntitlementId string `db:"EntitlementId"`
74+
CrossRevisionId string `db:"CrossRevisionId"`
75+
DownloadUrl string `db:"DownloadUrl"`
76+
ReadStateSynced bool `db:"ReadStateSynced"`
77+
TimesStartedReading int `db:"TimesStartedReading"`
78+
TimeSpentReading int `db:"TimeSpentReading"`
79+
LastTimeStartedReading string `db:"LastTimeStartedReading"`
80+
LastTimeFinishedReading string `db:"LastTimeFinishedReading"`
81+
ApplicableSubscriptions string `db:"ApplicableSubscriptions"`
82+
ExternalIds string `db:"ExternalIds"`
83+
PurchaseRevisionId string `db:"PurchaseRevisionId"`
84+
SeriesID string `db:"SeriesID"`
85+
SeriesNumberFloat float64 `db:"SeriesNumberFloat"`
86+
AdobeLoanExpiration string `db:"AdobeLoanExpiration"`
87+
HideFromHomePage bool `db:"HideFromHomePage"`
88+
IsInternetArchive bool `db:"IsInternetArchive"`
89+
TitleKana string `db:"titleKana"`
90+
SubtitleKana string `db:"subtitleKana"`
91+
SeriesKana string `db:"seriesKana"`
92+
AttributionKana string `db:"attributionKana"`
93+
PublisherKana string `db:"publisherKana"`
94+
IsPurchaseable bool `db:"IsPurchaseable"`
95+
IsSupported bool `db:"IsSupported"`
96+
AnnotationsSyncToken string `db:"AnnotationsSyncToken"`
97+
DateModified string `db:"DateModified"`
98+
StorePages int `db:"StorePages"`
99+
StoreWordCount int `db:"StoreWordCount"`
100+
StoreTimeToReadLowerEstimate int `db:"StoreTimeToReadLowerEstimate"`
101+
StoreTimeToReadUpperEstimate int `db:"StoreTimeToReadUpperEstimate"`
102+
Duration int `db:"Duration"`
103+
IsAbridged bool `db:"IsAbridged"`
104+
}
105+
106+
func CountContent(kobo *Kobo) (count int, err error) {
107+
if err := kobo.dbClient.Get(
108+
&count,
109+
"SELECT count(*) FROM content WHERE ContentType = ? AND VolumeIndex = ? AND MimeType = ?",
110+
6, -1, "application/x-kobo-epub+zip",
111+
); err != nil {
112+
return count, err
113+
}
114+
return count, nil
115+
}

v2/pkg/kobo/detection.go

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package kobo
2+
3+
import (
4+
"github.com/pgaskin/koboutils/v2/kobo"
5+
)
6+
7+
// FindMountDevices runs a variety of detection methods for different operating systems (Linux, macOS, Windows)
8+
// returning any results it can find. Those results are then checked for the existence of a .kobo
9+
// folder which is used to determine whether a device is valid or not.
10+
func FindMountedDevices() ([]string, error) {
11+
confirmedLocations := []string{}
12+
locations, err := kobo.Find()
13+
if err != nil {
14+
return nil, err
15+
}
16+
for _, location := range locations {
17+
if kobo.IsKobo(location) {
18+
confirmedLocations = append(confirmedLocations, location)
19+
}
20+
}
21+
return confirmedLocations, nil
22+
}
23+
24+
// GetDeviceMetadata looks up the connected Kobo by checking the version string located at <kobo>/.kobo/version
25+
// An uninitialised KoboConnection will be returned populated with identifiers like device name, storage etc
26+
// If a device is unknown (ie; released before we support it), a minimally populated KoboConnection will be
27+
// returned which should still contain enough information for October to still connect regardless.
28+
func GetDeviceMetadata(path string) (Kobo, error) {
29+
serial, version, deviceId, err := kobo.ParseKoboVersion(path)
30+
if err != nil {
31+
return Kobo{}, err
32+
}
33+
device, known := kobo.DeviceByID(deviceId)
34+
if !known {
35+
// We can handle unsupported Kobos that release in future, before support is added to koboutils
36+
// but we should tell the user that no support is 100% guaranteed if there are DB changes etc
37+
// We only read data anyway so there is very little risk of just trying out best
38+
return Kobo{
39+
Name: "Unknown Device",
40+
MountPath: path,
41+
DbPath: formatUsualDbPath(path),
42+
}, nil
43+
}
44+
return Kobo{
45+
Name: device.Name(),
46+
Storage: device.StorageGB(),
47+
DisplayPPI: device.DisplayPPI(),
48+
MountPath: path,
49+
DbPath: formatUsualDbPath(path),
50+
Serial: serial,
51+
Version: version,
52+
DeviceId: deviceId,
53+
}, nil
54+
}

0 commit comments

Comments
 (0)