Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in / Register
Toggle navigation
D
douban-api-proxy
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
captainwong
douban-api-proxy
Commits
8e1641da
Commit
8e1641da
authored
Feb 15, 2020
by
Michael Shavit
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add support for syncing Kobo reading state.
parent
57d37ffb
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
278 additions
and
46 deletions
+278
-46
kobo.py
cps/kobo.py
+186
-30
SyncToken.py
cps/services/SyncToken.py
+18
-7
ub.py
cps/ub.py
+68
-6
web.py
cps/web.py
+6
-3
No files found.
cps/kobo.py
View file @
8e1641da
This diff is collapsed.
Click to expand it.
cps/services/SyncToken.py
View file @
8e1641da
...
@@ -42,6 +42,13 @@ def to_epoch_timestamp(datetime_object):
...
@@ -42,6 +42,13 @@ def to_epoch_timestamp(datetime_object):
return
(
datetime_object
-
datetime
(
1970
,
1
,
1
))
.
total_seconds
()
return
(
datetime_object
-
datetime
(
1970
,
1
,
1
))
.
total_seconds
()
def
get_datetime_from_json
(
json_object
,
field_name
):
try
:
return
datetime
.
utcfromtimestamp
(
json_object
[
field_name
])
except
KeyError
:
return
datetime
.
min
class
SyncToken
():
class
SyncToken
():
""" The SyncToken is used to persist state accross requests.
""" The SyncToken is used to persist state accross requests.
When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
When serialized over the response headers, the Kobo device will propagate the token onto following requests to the service.
...
@@ -53,7 +60,8 @@ class SyncToken():
...
@@ -53,7 +60,8 @@ class SyncToken():
"""
"""
SYNC_TOKEN_HEADER
=
"x-kobo-synctoken"
SYNC_TOKEN_HEADER
=
"x-kobo-synctoken"
VERSION
=
"1-0-0"
VERSION
=
"1-1-0"
LAST_MODIFIED_ADDED_VERSION
=
"1-1-0"
MIN_VERSION
=
"1-0-0"
MIN_VERSION
=
"1-0-0"
token_schema
=
{
token_schema
=
{
...
@@ -68,6 +76,7 @@ class SyncToken():
...
@@ -68,6 +76,7 @@ class SyncToken():
"raw_kobo_store_token"
:
{
"type"
:
"string"
},
"raw_kobo_store_token"
:
{
"type"
:
"string"
},
"books_last_modified"
:
{
"type"
:
"string"
},
"books_last_modified"
:
{
"type"
:
"string"
},
"books_last_created"
:
{
"type"
:
"string"
},
"books_last_created"
:
{
"type"
:
"string"
},
"reading_state_last_modified"
:
{
"type"
:
"string"
},
},
},
}
}
...
@@ -76,10 +85,13 @@ class SyncToken():
...
@@ -76,10 +85,13 @@ class SyncToken():
raw_kobo_store_token
=
""
,
raw_kobo_store_token
=
""
,
books_last_created
=
datetime
.
min
,
books_last_created
=
datetime
.
min
,
books_last_modified
=
datetime
.
min
,
books_last_modified
=
datetime
.
min
,
reading_state_last_modified
=
datetime
.
min
,
):
):
self
.
raw_kobo_store_token
=
raw_kobo_store_token
self
.
raw_kobo_store_token
=
raw_kobo_store_token
self
.
books_last_created
=
books_last_created
self
.
books_last_created
=
books_last_created
self
.
books_last_modified
=
books_last_modified
self
.
books_last_modified
=
books_last_modified
self
.
reading_state_last_modified
=
reading_state_last_modified
@
staticmethod
@
staticmethod
def
from_headers
(
headers
):
def
from_headers
(
headers
):
...
@@ -109,12 +121,9 @@ class SyncToken():
...
@@ -109,12 +121,9 @@ class SyncToken():
raw_kobo_store_token
=
data_json
[
"raw_kobo_store_token"
]
raw_kobo_store_token
=
data_json
[
"raw_kobo_store_token"
]
try
:
try
:
books_last_modified
=
datetime
.
utcfromtimestamp
(
books_last_modified
=
get_datetime_from_json
(
data_json
,
"books_last_modified"
)
data_json
[
"books_last_modified"
]
books_last_created
=
get_datetime_from_json
(
data_json
,
"books_last_created"
)
)
reading_state_last_modified
=
get_datetime_from_json
(
data_json
,
"reading_state_last_modified"
)
books_last_created
=
datetime
.
utcfromtimestamp
(
data_json
[
"books_last_created"
]
)
except
TypeError
:
except
TypeError
:
log
.
error
(
"SyncToken timestamps don't parse to a datetime."
)
log
.
error
(
"SyncToken timestamps don't parse to a datetime."
)
return
SyncToken
(
raw_kobo_store_token
=
raw_kobo_store_token
)
return
SyncToken
(
raw_kobo_store_token
=
raw_kobo_store_token
)
...
@@ -123,6 +132,7 @@ class SyncToken():
...
@@ -123,6 +132,7 @@ class SyncToken():
raw_kobo_store_token
=
raw_kobo_store_token
,
raw_kobo_store_token
=
raw_kobo_store_token
,
books_last_created
=
books_last_created
,
books_last_created
=
books_last_created
,
books_last_modified
=
books_last_modified
,
books_last_modified
=
books_last_modified
,
reading_state_last_modified
=
reading_state_last_modified
)
)
def
set_kobo_store_header
(
self
,
store_headers
):
def
set_kobo_store_header
(
self
,
store_headers
):
...
@@ -143,6 +153,7 @@ class SyncToken():
...
@@ -143,6 +153,7 @@ class SyncToken():
"raw_kobo_store_token"
:
self
.
raw_kobo_store_token
,
"raw_kobo_store_token"
:
self
.
raw_kobo_store_token
,
"books_last_modified"
:
to_epoch_timestamp
(
self
.
books_last_modified
),
"books_last_modified"
:
to_epoch_timestamp
(
self
.
books_last_modified
),
"books_last_created"
:
to_epoch_timestamp
(
self
.
books_last_created
),
"books_last_created"
:
to_epoch_timestamp
(
self
.
books_last_created
),
"reading_state_last_modified"
:
to_epoch_timestamp
(
self
.
reading_state_last_modified
)
},
},
}
}
return
b64encode_json
(
token
)
return
b64encode_json
(
token
)
cps/ub.py
View file @
8e1641da
...
@@ -20,6 +20,7 @@
...
@@ -20,6 +20,7 @@
from
__future__
import
division
,
print_function
,
unicode_literals
from
__future__
import
division
,
print_function
,
unicode_literals
import
os
import
os
import
datetime
import
datetime
import
itertools
from
binascii
import
hexlify
from
binascii
import
hexlify
from
flask
import
g
from
flask
import
g
...
@@ -31,10 +32,10 @@ try:
...
@@ -31,10 +32,10 @@ try:
oauth_support
=
True
oauth_support
=
True
except
ImportError
:
except
ImportError
:
oauth_support
=
False
oauth_support
=
False
from
sqlalchemy
import
create_engine
,
exc
,
exists
from
sqlalchemy
import
create_engine
,
exc
,
exists
,
event
from
sqlalchemy
import
Column
,
ForeignKey
from
sqlalchemy
import
Column
,
ForeignKey
from
sqlalchemy
import
String
,
Integer
,
SmallInteger
,
Boolean
,
DateTime
from
sqlalchemy
import
String
,
Integer
,
SmallInteger
,
Boolean
,
DateTime
,
Float
from
sqlalchemy.orm
import
foreign
,
relationship
,
remote
,
sessionmaker
from
sqlalchemy.orm
import
backref
,
foreign
,
relationship
,
remote
,
sessionmaker
,
Session
from
sqlalchemy.ext.declarative
import
declarative_base
from
sqlalchemy.ext.declarative
import
declarative_base
from
sqlalchemy.sql.expression
import
and_
from
sqlalchemy.sql.expression
import
and_
from
werkzeug.security
import
generate_password_hash
from
werkzeug.security
import
generate_password_hash
...
@@ -292,8 +293,12 @@ class ReadBook(Base):
...
@@ -292,8 +293,12 @@ class ReadBook(Base):
id
=
Column
(
Integer
,
primary_key
=
True
)
id
=
Column
(
Integer
,
primary_key
=
True
)
book_id
=
Column
(
Integer
,
unique
=
False
)
book_id
=
Column
(
Integer
,
unique
=
False
)
user_id
=
Column
(
Integer
,
ForeignKey
(
'user.id'
),
unique
=
False
)
user_id
=
Column
(
Integer
,
ForeignKey
(
'user.id'
),
unique
=
False
)
is_read
=
Column
(
Boolean
,
unique
=
False
)
read_status
=
Column
(
Integer
,
unique
=
False
,
default
=
STATUS_UNREAD
,
nullable
=
False
)
read_status
=
Column
(
Integer
,
unique
=
False
,
default
=
STATUS_UNREAD
)
kobo_reading_state
=
relationship
(
"KoboReadingState"
,
uselist
=
False
,
primaryjoin
=
"and_(ReadBook.user_id == foreign(KoboReadingState.user_id), "
"ReadBook.book_id == foreign(KoboReadingState.book_id))"
,
cascade
=
"all"
,
backref
=
backref
(
"book_read_link"
,
uselist
=
False
))
last_modified
=
Column
(
DateTime
,
default
=
datetime
.
datetime
.
utcnow
,
onupdate
=
datetime
.
datetime
.
utcnow
)
last_time_started_reading
=
Column
(
DateTime
,
nullable
=
True
)
times_started_reading
=
Column
(
Integer
,
default
=
0
,
nullable
=
False
)
class
Bookmark
(
Base
):
class
Bookmark
(
Base
):
...
@@ -306,6 +311,54 @@ class Bookmark(Base):
...
@@ -306,6 +311,54 @@ class Bookmark(Base):
bookmark_key
=
Column
(
String
)
bookmark_key
=
Column
(
String
)
# The Kobo ReadingState API keeps track of 4 timestamped entities:
# ReadingState, StatusInfo, Statistics, CurrentBookmark
# Which we map to the following 4 tables:
# KoboReadingState, ReadBook, KoboStatistics and KoboBookmark
class
KoboReadingState
(
Base
):
__tablename__
=
'kobo_reading_state'
id
=
Column
(
Integer
,
primary_key
=
True
,
autoincrement
=
True
)
user_id
=
Column
(
Integer
,
ForeignKey
(
'user.id'
))
book_id
=
Column
(
Integer
)
last_modified
=
Column
(
DateTime
,
default
=
datetime
.
datetime
.
utcnow
,
onupdate
=
datetime
.
datetime
.
utcnow
)
priority_timestamp
=
Column
(
DateTime
,
default
=
datetime
.
datetime
.
utcnow
,
onupdate
=
datetime
.
datetime
.
utcnow
)
current_bookmark
=
relationship
(
"KoboBookmark"
,
uselist
=
False
,
backref
=
"kobo_reading_state"
,
cascade
=
"all"
)
statistics
=
relationship
(
"KoboStatistics"
,
uselist
=
False
,
backref
=
"kobo_reading_state"
,
cascade
=
"all"
)
class
KoboBookmark
(
Base
):
__tablename__
=
'kobo_bookmark'
id
=
Column
(
Integer
,
primary_key
=
True
)
kobo_reading_state_id
=
Column
(
Integer
,
ForeignKey
(
'kobo_reading_state.id'
))
last_modified
=
Column
(
DateTime
,
default
=
datetime
.
datetime
.
utcnow
,
onupdate
=
datetime
.
datetime
.
utcnow
)
location_source
=
Column
(
String
)
location_type
=
Column
(
String
)
location_value
=
Column
(
String
)
progress_percent
=
Column
(
Float
)
content_source_progress_percent
=
Column
(
Float
)
class
KoboStatistics
(
Base
):
__tablename__
=
'kobo_statistics'
id
=
Column
(
Integer
,
primary_key
=
True
)
kobo_reading_state_id
=
Column
(
Integer
,
ForeignKey
(
'kobo_reading_state.id'
))
last_modified
=
Column
(
DateTime
,
default
=
datetime
.
datetime
.
utcnow
,
onupdate
=
datetime
.
datetime
.
utcnow
)
remaining_time_minutes
=
Column
(
Integer
)
spent_reading_minutes
=
Column
(
Integer
)
# Updates the last_modified timestamp in the KoboReadingState table if any of its children tables are modified.
@
event
.
listens_for
(
Session
,
'before_flush'
)
def
receive_before_flush
(
session
,
flush_context
,
instances
):
for
change
in
itertools
.
chain
(
session
.
new
,
session
.
dirty
):
if
isinstance
(
change
,
(
ReadBook
,
KoboStatistics
,
KoboBookmark
)):
if
change
.
kobo_reading_state
:
change
.
kobo_reading_state
.
last_modified
=
datetime
.
datetime
.
utcnow
()
# Baseclass representing Downloads from calibre-web in app.db
# Baseclass representing Downloads from calibre-web in app.db
class
Downloads
(
Base
):
class
Downloads
(
Base
):
__tablename__
=
'downloads'
__tablename__
=
'downloads'
...
@@ -358,6 +411,12 @@ def migrate_Database(session):
...
@@ -358,6 +411,12 @@ def migrate_Database(session):
ReadBook
.
__table__
.
create
(
bind
=
engine
)
ReadBook
.
__table__
.
create
(
bind
=
engine
)
if
not
engine
.
dialect
.
has_table
(
engine
.
connect
(),
"bookmark"
):
if
not
engine
.
dialect
.
has_table
(
engine
.
connect
(),
"bookmark"
):
Bookmark
.
__table__
.
create
(
bind
=
engine
)
Bookmark
.
__table__
.
create
(
bind
=
engine
)
if
not
engine
.
dialect
.
has_table
(
engine
.
connect
(),
"kobo_reading_state"
):
KoboReadingState
.
__table__
.
create
(
bind
=
engine
)
if
not
engine
.
dialect
.
has_table
(
engine
.
connect
(),
"kobo_bookmark"
):
KoboBookmark
.
__table__
.
create
(
bind
=
engine
)
if
not
engine
.
dialect
.
has_table
(
engine
.
connect
(),
"kobo_statistics"
):
KoboStatistics
.
__table__
.
create
(
bind
=
engine
)
if
not
engine
.
dialect
.
has_table
(
engine
.
connect
(),
"registration"
):
if
not
engine
.
dialect
.
has_table
(
engine
.
connect
(),
"registration"
):
ReadBook
.
__table__
.
create
(
bind
=
engine
)
ReadBook
.
__table__
.
create
(
bind
=
engine
)
conn
=
engine
.
connect
()
conn
=
engine
.
connect
()
...
@@ -381,10 +440,13 @@ def migrate_Database(session):
...
@@ -381,10 +440,13 @@ def migrate_Database(session):
session
.
commit
()
session
.
commit
()
try
:
try
:
session
.
query
(
exists
()
.
where
(
ReadBook
.
read_status
))
.
scalar
()
session
.
query
(
exists
()
.
where
(
ReadBook
.
read_status
))
.
scalar
()
except
exc
.
OperationalError
:
# Database is not compatible, some columns are missing
except
exc
.
OperationalError
:
conn
=
engine
.
connect
()
conn
=
engine
.
connect
()
conn
.
execute
(
"ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0"
)
conn
.
execute
(
"ALTER TABLE book_read_link ADD column 'read_status' INTEGER DEFAULT 0"
)
conn
.
execute
(
"UPDATE book_read_link SET 'read_status' = 1 WHERE is_read"
)
conn
.
execute
(
"UPDATE book_read_link SET 'read_status' = 1 WHERE is_read"
)
conn
.
execute
(
"ALTER TABLE book_read_link ADD column 'last_modified' DATETIME"
)
conn
.
execute
(
"ALTER TABLE book_read_link ADD column 'last_time_started_reading' DATETIME"
)
conn
.
execute
(
"ALTER TABLE book_read_link ADD column 'times_started_reading' INTEGER DEFAULT 0"
)
session
.
commit
()
session
.
commit
()
# Handle table exists, but no content
# Handle table exists, but no content
...
...
cps/web.py
View file @
8e1641da
...
@@ -319,11 +319,14 @@ def toggle_read(book_id):
...
@@ -319,11 +319,14 @@ def toggle_read(book_id):
else
:
else
:
book
.
read_status
=
ub
.
ReadBook
.
STATUS_FINISHED
book
.
read_status
=
ub
.
ReadBook
.
STATUS_FINISHED
else
:
else
:
readBook
=
ub
.
ReadBook
()
readBook
=
ub
.
ReadBook
(
user_id
=
current_user
.
id
,
book_id
=
book_id
)
readBook
.
user_id
=
int
(
current_user
.
id
)
readBook
.
book_id
=
book_id
readBook
.
read_status
=
ub
.
ReadBook
.
STATUS_FINISHED
readBook
.
read_status
=
ub
.
ReadBook
.
STATUS_FINISHED
book
=
readBook
book
=
readBook
if
not
book
.
kobo_reading_state
:
kobo_reading_state
=
ub
.
KoboReadingState
(
user_id
=
current_user
.
id
,
book_id
=
book_id
)
kobo_reading_state
.
current_bookmark
=
ub
.
KoboBookmark
()
kobo_reading_state
.
statistics
=
ub
.
KoboStatistics
()
book
.
kobo_reading_state
=
kobo_reading_state
ub
.
session
.
merge
(
book
)
ub
.
session
.
merge
(
book
)
ub
.
session
.
commit
()
ub
.
session
.
commit
()
else
:
else
:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment