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
d597e05f
Commit
d597e05f
authored
Mar 12, 2020
by
Ozzieisaacs
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'reading_state' into Develop
# Conflicts: # cps/kobo.py
parents
98dc9913
8e1641da
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
300 additions
and
47 deletions
+300
-47
kobo.py
cps/kobo.py
+189
-29
SyncToken.py
cps/services/SyncToken.py
+18
-7
ub.py
cps/ub.py
+79
-4
web.py
cps/web.py
+14
-7
No files found.
cps/kobo.py
View file @
d597e05f
...
...
@@ -17,11 +17,11 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import
datetime
import
sys
import
base64
import
os
import
uuid
from
datetime
import
datetime
from
time
import
gmtime
,
strftime
try
:
from
urllib
import
unquote
...
...
@@ -38,12 +38,13 @@ from flask import (
redirect
,
abort
)
from
flask_login
import
login_required
from
flask_login
import
current_user
,
login_required
from
werkzeug.datastructures
import
Headers
from
sqlalchemy
import
func
from
sqlalchemy.sql.expression
import
and_
import
requests
from
.
import
config
,
logger
,
kobo_auth
,
db
,
helper
from
.
import
config
,
logger
,
kobo_auth
,
db
,
helper
,
ub
from
.services
import
SyncToken
as
SyncToken
from
.web
import
download_required
from
.kobo_auth
import
requires_kobo_auth
...
...
@@ -116,6 +117,9 @@ def redirect_or_proxy_request():
return
make_response
(
jsonify
({}))
def
convert_to_kobo_timestamp_string
(
timestamp
):
return
timestamp
.
strftime
(
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
SZ"
)
@
kobo
.
route
(
"/v1/library/sync"
)
@
requires_kobo_auth
@
download_required
...
...
@@ -130,7 +134,8 @@ def HandleSyncRequest():
new_books_last_modified
=
sync_token
.
books_last_modified
new_books_last_created
=
sync_token
.
books_last_created
entitlements
=
[]
new_reading_state_last_modified
=
sync_token
.
reading_state_last_modified
sync_results
=
[]
# We reload the book database so that the user get's a fresh view of the library
# in case of external changes (e.g: adding a book through Calibre).
...
...
@@ -147,37 +152,63 @@ def HandleSyncRequest():
.
all
()
)
reading_states_in_new_entitlements
=
[]
for
book
in
changed_entries
:
kobo_reading_state
=
get_or_create_reading_state
(
book
.
id
)
entitlement
=
{
"BookEntitlement"
:
create_book_entitlement
(
book
),
"BookMetadata"
:
get_metadata
(
book
),
"ReadingState"
:
reading_state
(
book
),
}
if
kobo_reading_state
.
last_modified
>
sync_token
.
reading_state_last_modified
:
entitlement
[
"ReadingState"
]
=
get_kobo_reading_state_response
(
book
,
kobo_reading_state
)
new_reading_state_last_modified
=
max
(
new_reading_state_last_modified
,
kobo_reading_state
.
last_modified
)
reading_states_in_new_entitlements
.
append
(
book
.
id
)
if
book
.
timestamp
>
sync_token
.
books_last_created
:
entitlemen
ts
.
append
({
"NewEntitlement"
:
entitlement
})
sync_resul
ts
.
append
({
"NewEntitlement"
:
entitlement
})
else
:
entitlemen
ts
.
append
({
"ChangedEntitlement"
:
entitlement
})
sync_resul
ts
.
append
({
"ChangedEntitlement"
:
entitlement
})
new_books_last_modified
=
max
(
book
.
last_modified
,
new_books_last_modified
)
new_books_last_created
=
max
(
book
.
timestamp
,
new_books_last_created
)
changed_reading_states
=
(
ub
.
session
.
query
(
ub
.
KoboReadingState
)
.
filter
(
and_
(
func
.
datetime
(
ub
.
KoboReadingState
.
last_modified
)
>
sync_token
.
reading_state_last_modified
,
ub
.
KoboReadingState
.
user_id
==
current_user
.
id
,
ub
.
KoboReadingState
.
book_id
.
notin_
(
reading_states_in_new_entitlements
))))
for
kobo_reading_state
in
changed_reading_states
.
all
():
book
=
db
.
session
.
query
(
db
.
Books
)
.
filter
(
db
.
Books
.
id
==
kobo_reading_state
.
book_id
)
.
one
()
sync_results
.
append
({
"ChangedReadingState"
:
{
"ReadingState"
:
get_kobo_reading_state_response
(
book
,
kobo_reading_state
)
}
})
new_reading_state_last_modified
=
max
(
new_reading_state_last_modified
,
kobo_reading_state
.
last_modified
)
sync_token
.
books_last_created
=
new_books_last_created
sync_token
.
books_last_modified
=
new_books_last_modified
sync_token
.
reading_state_last_modified
=
new_reading_state_last_modified
return
generate_sync_response
(
request
,
sync_token
,
entitlements
)
if
config
.
config_kobo_proxy
:
return
generate_sync_response
(
request
,
sync_token
,
sync_results
)
return
make_response
(
jsonify
(
sync_results
))
# Missing feature: Detect server-side book deletions.
def
generate_sync_response
(
request
,
sync_token
,
entitlemen
ts
):
def
generate_sync_response
(
request
,
sync_token
,
sync_resul
ts
):
extra_headers
=
{}
if
config
.
config_kobo_proxy
:
# Merge in sync results from the official Kobo store.
try
:
store_response
=
make_request_to_kobo_store
(
sync_token
)
store_
entitlemen
ts
=
store_response
.
json
()
entitlements
+=
store_entitlemen
ts
store_
sync_resul
ts
=
store_response
.
json
()
sync_results
+=
store_sync_resul
ts
sync_token
.
merge_from_store_response
(
store_response
)
extra_headers
[
"x-kobo-sync"
]
=
store_response
.
headers
.
get
(
"x-kobo-sync"
)
extra_headers
[
"x-kobo-sync-mode"
]
=
store_response
.
headers
.
get
(
"x-kobo-sync-mode"
)
...
...
@@ -187,7 +218,7 @@ def generate_sync_response(request, sync_token, entitlements):
log
.
error
(
"Failed to receive or parse response from Kobo's sync endpoint: "
+
str
(
e
))
sync_token
.
to_headers
(
extra_headers
)
response
=
make_response
(
jsonify
(
entitlemen
ts
),
extra_headers
)
response
=
make_response
(
jsonify
(
sync_resul
ts
),
extra_headers
)
return
response
...
...
@@ -229,25 +260,21 @@ def create_book_entitlement(book):
book_uuid
=
book
.
uuid
return
{
"Accessibility"
:
"Full"
,
"ActivePeriod"
:
{
"From"
:
c
urrent_time
(),
},
"Created"
:
book
.
timestamp
.
strftime
(
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
SZ"
),
"ActivePeriod"
:
{
"From"
:
c
onvert_to_kobo_timestamp_string
(
datetime
.
datetime
.
now
())
},
"Created"
:
convert_to_kobo_timestamp_string
(
book
.
timestamp
),
"CrossRevisionId"
:
book_uuid
,
"Id"
:
book_uuid
,
"IsHiddenFromArchive"
:
False
,
"IsLocked"
:
False
,
# Setting this to true removes from the device.
"IsRemoved"
:
False
,
"LastModified"
:
book
.
last_modified
.
strftime
(
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
SZ"
),
"LastModified"
:
convert_to_kobo_timestamp_string
(
book
.
last_modified
),
"OriginCategory"
:
"Imported"
,
"RevisionId"
:
book_uuid
,
"Status"
:
"Active"
,
}
def
current_time
():
return
strftime
(
"
%
Y-
%
m-
%
dT
%
H:
%
M:
%
SZ"
,
gmtime
())
def
get_description
(
book
):
if
not
book
.
comments
:
return
None
...
...
@@ -310,6 +337,8 @@ def get_metadata(book):
"IsSocialEnabled"
:
True
,
"Language"
:
"en"
,
"PhoneticPronunciations"
:
{},
# TODO: Fix book.pubdate to return a datetime object so that we can easily
# convert it to the format Kobo devices expect.
"PublicationDate"
:
book
.
pubdate
,
"Publisher"
:
{
"Imprint"
:
""
,
"Name"
:
get_publisher
(
book
),},
"RevisionId"
:
book_uuid
,
...
...
@@ -333,16 +362,148 @@ def get_metadata(book):
return
metadata
def
reading_state
(
book
):
# TODO: Implement
reading_state
=
{
# "StatusInfo": {
# "LastModified": get_single_cc_value(book, "lastreadtimestamp"),
# "Status": get_single_cc_value(book, "reading_status"),
# }
# TODO: CurrentBookmark, Location
@
kobo
.
route
(
"/v1/library/<book_uuid>/state"
,
methods
=
[
"GET"
,
"PUT"
])
@
login_required
def
HandleStateRequest
(
book_uuid
):
book
=
db
.
session
.
query
(
db
.
Books
)
.
filter
(
db
.
Books
.
uuid
==
book_uuid
)
.
first
()
if
not
book
or
not
book
.
data
:
log
.
info
(
u"Book
%
s not found in database"
,
book_uuid
)
return
redirect_or_proxy_request
()
kobo_reading_state
=
get_or_create_reading_state
(
book
.
id
)
if
request
.
method
==
"GET"
:
return
jsonify
([
get_kobo_reading_state_response
(
book
,
kobo_reading_state
)])
else
:
update_results_response
=
{
"EntitlementId"
:
book_uuid
}
request_data
=
request
.
json
if
"ReadingStates"
not
in
request_data
:
abort
(
400
,
description
=
"Malformed request data is missing 'ReadingStates' key"
)
request_reading_state
=
request_data
[
"ReadingStates"
][
0
]
request_bookmark
=
request_reading_state
.
get
(
"CurrentBookmark"
)
if
request_bookmark
:
current_bookmark
=
kobo_reading_state
.
current_bookmark
current_bookmark
.
progress_percent
=
request_bookmark
.
get
(
"ProgressPercent"
)
current_bookmark
.
content_source_progress_percent
=
request_bookmark
.
get
(
"ContentSourceProgressPercent"
)
location
=
request_bookmark
.
get
(
"Location"
)
if
location
:
current_bookmark
.
location_value
=
location
.
get
(
"Value"
)
current_bookmark
.
location_type
=
location
.
get
(
"Type"
)
current_bookmark
.
location_source
=
location
.
get
(
"Source"
)
update_results_response
[
"CurrentBookmarkResult"
]
=
{
"Result"
:
"Success"
}
request_statistics
=
request_reading_state
.
get
(
"Statistics"
)
if
request_statistics
:
statistics
=
kobo_reading_state
.
statistics
statistics
.
spent_reading_minutes
=
request_statistics
.
get
(
"SpentReadingMinutes"
)
statistics
.
remaining_time_minutes
=
request_statistics
.
get
(
"RemainingTimeMinutes"
)
update_results_response
[
"StatisticsResult"
]
=
{
"Result"
:
"Success"
}
request_status_info
=
request_reading_state
.
get
(
"StatusInfo"
)
if
request_status_info
:
book_read
=
kobo_reading_state
.
book_read_link
new_book_read_status
=
get_ub_read_status
(
request_status_info
.
get
(
"Status"
))
if
new_book_read_status
==
ub
.
ReadBook
.
STATUS_IN_PROGRESS
and
new_book_read_status
!=
book_read
.
read_status
:
book_read
.
times_started_reading
+=
1
book_read
.
last_time_started_reading
=
datetime
.
datetime
.
utcnow
()
book_read
.
read_status
=
new_book_read_status
update_results_response
[
"StatusInfoResult"
]
=
{
"Result"
:
"Success"
}
ub
.
session
.
merge
(
kobo_reading_state
)
ub
.
session
.
commit
()
return
jsonify
({
"RequestResult"
:
"Success"
,
"UpdateResults"
:
[
update_results_response
],
})
def
get_read_status_for_kobo
(
ub_book_read
):
enum_to_string_map
=
{
None
:
"ReadyToRead"
,
ub
.
ReadBook
.
STATUS_UNREAD
:
"ReadyToRead"
,
ub
.
ReadBook
.
STATUS_FINISHED
:
"Finished"
,
ub
.
ReadBook
.
STATUS_IN_PROGRESS
:
"Reading"
,
}
return
reading_state
return
enum_to_string_map
[
ub_book_read
.
read_status
]
def
get_ub_read_status
(
kobo_read_status
):
string_to_enum_map
=
{
None
:
None
,
"ReadyToRead"
:
ub
.
ReadBook
.
STATUS_UNREAD
,
"Finished"
:
ub
.
ReadBook
.
STATUS_FINISHED
,
"Reading"
:
ub
.
ReadBook
.
STATUS_IN_PROGRESS
,
}
return
string_to_enum_map
[
kobo_read_status
]
def
get_or_create_reading_state
(
book_id
):
book_read
=
ub
.
session
.
query
(
ub
.
ReadBook
)
.
filter
(
and_
(
ub
.
ReadBook
.
book_id
==
book_id
,
ub
.
ReadBook
.
user_id
==
current_user
.
id
))
.
one_or_none
()
if
not
book_read
:
book_read
=
ub
.
ReadBook
(
user_id
=
current_user
.
id
,
book_id
=
book_id
)
if
not
book_read
.
kobo_reading_state
:
kobo_reading_state
=
ub
.
KoboReadingState
(
user_id
=
book_read
.
user_id
,
book_id
=
book_id
)
kobo_reading_state
.
current_bookmark
=
ub
.
KoboBookmark
()
kobo_reading_state
.
statistics
=
ub
.
KoboStatistics
()
book_read
.
kobo_reading_state
=
kobo_reading_state
ub
.
session
.
add
(
book_read
)
ub
.
session
.
commit
()
return
book_read
.
kobo_reading_state
def
get_kobo_reading_state_response
(
book
,
kobo_reading_state
):
return
{
"EntitlementId"
:
book
.
uuid
,
"Created"
:
convert_to_kobo_timestamp_string
(
book
.
timestamp
),
"LastModified"
:
convert_to_kobo_timestamp_string
(
kobo_reading_state
.
last_modified
),
# AFAICT PriorityTimestamp is always equal to LastModified.
"PriorityTimestamp"
:
convert_to_kobo_timestamp_string
(
kobo_reading_state
.
priority_timestamp
),
"StatusInfo"
:
get_status_info_response
(
kobo_reading_state
.
book_read_link
),
"Statistics"
:
get_statistics_response
(
kobo_reading_state
.
statistics
),
"CurrentBookmark"
:
get_current_bookmark_response
(
kobo_reading_state
.
current_bookmark
),
}
def
get_status_info_response
(
book_read
):
resp
=
{
"LastModified"
:
convert_to_kobo_timestamp_string
(
book_read
.
last_modified
),
"Status"
:
get_read_status_for_kobo
(
book_read
),
"TimesStartedReading"
:
book_read
.
times_started_reading
,
}
if
book_read
.
last_time_started_reading
:
resp
[
"LastTimeStartedReading"
]
=
convert_to_kobo_timestamp_string
(
book_read
.
last_time_started_reading
)
return
resp
def
get_statistics_response
(
statistics
):
resp
=
{
"LastModified"
:
convert_to_kobo_timestamp_string
(
statistics
.
last_modified
),
}
if
statistics
.
spent_reading_minutes
:
resp
[
"SpentReadingMinutes"
]
=
statistics
.
spent_reading_minutes
if
statistics
.
remaining_time_minutes
:
resp
[
"RemainingTimeMinutes"
]
=
statistics
.
remaining_time_minutes
return
resp
def
get_current_bookmark_response
(
current_bookmark
):
resp
=
{
"LastModified"
:
convert_to_kobo_timestamp_string
(
current_bookmark
.
last_modified
),
}
if
current_bookmark
.
progress_percent
:
resp
[
"ProgressPercent"
]
=
current_bookmark
.
progress_percent
if
current_bookmark
.
content_source_progress_percent
:
resp
[
"ContentSourceProgressPercent"
]
=
current_bookmark
.
content_source_progress_percent
if
current_bookmark
.
location_value
:
resp
[
"Location"
]
=
{
"Value"
:
current_bookmark
.
location_value
,
"Type"
:
current_bookmark
.
location_type
,
"Source"
:
current_bookmark
.
location_source
,
}
return
resp
@
kobo
.
route
(
"/<book_uuid>/image.jpg"
)
...
...
@@ -367,7 +528,6 @@ def TopLevelEndpoint():
# TODO: Implement the following routes
@
kobo
.
route
(
"/v1/library/<dummy>"
,
methods
=
[
"DELETE"
,
"GET"
])
@
kobo
.
route
(
"/v1/library/<book_uuid>/state"
,
methods
=
[
"PUT"
])
@
kobo
.
route
(
"/v1/library/tags"
,
methods
=
[
"POST"
])
@
kobo
.
route
(
"/v1/library/tags/<shelf_name>"
,
methods
=
[
"POST"
])
@
kobo
.
route
(
"/v1/library/tags/<tag_id>"
,
methods
=
[
"DELETE"
])
...
...
cps/services/SyncToken.py
View file @
d597e05f
...
...
@@ -42,6 +42,13 @@ def to_epoch_timestamp(datetime_object):
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
():
""" 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.
...
...
@@ -53,7 +60,8 @@ class 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"
token_schema
=
{
...
...
@@ -68,6 +76,7 @@ class SyncToken():
"raw_kobo_store_token"
:
{
"type"
:
"string"
},
"books_last_modified"
:
{
"type"
:
"string"
},
"books_last_created"
:
{
"type"
:
"string"
},
"reading_state_last_modified"
:
{
"type"
:
"string"
},
},
}
...
...
@@ -76,10 +85,13 @@ class SyncToken():
raw_kobo_store_token
=
""
,
books_last_created
=
datetime
.
min
,
books_last_modified
=
datetime
.
min
,
reading_state_last_modified
=
datetime
.
min
,
):
self
.
raw_kobo_store_token
=
raw_kobo_store_token
self
.
books_last_created
=
books_last_created
self
.
books_last_modified
=
books_last_modified
self
.
reading_state_last_modified
=
reading_state_last_modified
@
staticmethod
def
from_headers
(
headers
):
...
...
@@ -109,12 +121,9 @@ class SyncToken():
raw_kobo_store_token
=
data_json
[
"raw_kobo_store_token"
]
try
:
books_last_modified
=
datetime
.
utcfromtimestamp
(
data_json
[
"books_last_modified"
]
)
books_last_created
=
datetime
.
utcfromtimestamp
(
data_json
[
"books_last_created"
]
)
books_last_modified
=
get_datetime_from_json
(
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"
)
except
TypeError
:
log
.
error
(
"SyncToken timestamps don't parse to a datetime."
)
return
SyncToken
(
raw_kobo_store_token
=
raw_kobo_store_token
)
...
...
@@ -123,6 +132,7 @@ class SyncToken():
raw_kobo_store_token
=
raw_kobo_store_token
,
books_last_created
=
books_last_created
,
books_last_modified
=
books_last_modified
,
reading_state_last_modified
=
reading_state_last_modified
)
def
set_kobo_store_header
(
self
,
store_headers
):
...
...
@@ -143,6 +153,7 @@ class SyncToken():
"raw_kobo_store_token"
:
self
.
raw_kobo_store_token
,
"books_last_modified"
:
to_epoch_timestamp
(
self
.
books_last_modified
),
"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
)
cps/ub.py
View file @
d597e05f
...
...
@@ -20,6 +20,7 @@
from
__future__
import
division
,
print_function
,
unicode_literals
import
os
import
datetime
import
itertools
from
binascii
import
hexlify
from
flask
import
g
...
...
@@ -31,11 +32,12 @@ try:
oauth_support
=
True
except
ImportError
:
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
String
,
Integer
,
SmallInteger
,
Boolean
,
DateTime
from
sqlalchemy.orm
import
relationship
,
sessionmaker
from
sqlalchemy
import
String
,
Integer
,
SmallInteger
,
Boolean
,
DateTime
,
Float
from
sqlalchemy.orm
import
backref
,
foreign
,
relationship
,
remote
,
sessionmaker
,
Session
from
sqlalchemy.ext.declarative
import
declarative_base
from
sqlalchemy.sql.expression
import
and_
from
werkzeug.security
import
generate_password_hash
from
.
import
constants
# , config
...
...
@@ -284,10 +286,19 @@ class BookShelf(Base):
class
ReadBook
(
Base
):
__tablename__
=
'book_read_link'
STATUS_UNREAD
=
0
STATUS_FINISHED
=
1
STATUS_IN_PROGRESS
=
2
id
=
Column
(
Integer
,
primary_key
=
True
)
book_id
=
Column
(
Integer
,
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
)
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
):
...
...
@@ -300,6 +311,54 @@ class Bookmark(Base):
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
class
Downloads
(
Base
):
__tablename__
=
'downloads'
...
...
@@ -352,6 +411,12 @@ def migrate_Database(session):
ReadBook
.
__table__
.
create
(
bind
=
engine
)
if
not
engine
.
dialect
.
has_table
(
engine
.
connect
(),
"bookmark"
):
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"
):
ReadBook
.
__table__
.
create
(
bind
=
engine
)
conn
=
engine
.
connect
()
...
...
@@ -373,6 +438,16 @@ def migrate_Database(session):
conn
.
execute
(
"ALTER TABLE remote_auth_token ADD column 'token_type' INTEGER DEFAULT 0"
)
conn
.
execute
(
"update remote_auth_token set 'token_type' = 0"
)
session
.
commit
()
try
:
session
.
query
(
exists
()
.
where
(
ReadBook
.
read_status
))
.
scalar
()
except
exc
.
OperationalError
:
conn
=
engine
.
connect
()
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
(
"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
()
# Handle table exists, but no content
cnt
=
session
.
query
(
Registration
)
.
count
()
...
...
cps/web.py
View file @
d597e05f
...
...
@@ -314,13 +314,19 @@ def toggle_read(book_id):
book
=
ub
.
session
.
query
(
ub
.
ReadBook
)
.
filter
(
and_
(
ub
.
ReadBook
.
user_id
==
int
(
current_user
.
id
),
ub
.
ReadBook
.
book_id
==
book_id
))
.
first
()
if
book
:
book
.
is_read
=
not
book
.
is_read
if
book
.
read_status
==
ub
.
ReadBook
.
STATUS_FINISHED
:
book
.
read_status
=
ub
.
ReadBook
.
STATUS_UNREAD
else
:
book
.
read_status
=
ub
.
ReadBook
.
STATUS_FINISHED
else
:
readBook
=
ub
.
ReadBook
()
readBook
.
user_id
=
int
(
current_user
.
id
)
readBook
.
book_id
=
book_id
readBook
.
is_read
=
True
readBook
=
ub
.
ReadBook
(
user_id
=
current_user
.
id
,
book_id
=
book_id
)
readBook
.
read_status
=
ub
.
ReadBook
.
STATUS_FINISHED
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
.
commit
()
else
:
...
...
@@ -980,7 +986,7 @@ def render_read_books(page, are_read, as_xml=False, order=None, *args, **kwargs)
order
=
order
or
[]
if
not
config
.
config_read_column
:
readBooks
=
ub
.
session
.
query
(
ub
.
ReadBook
)
.
filter
(
ub
.
ReadBook
.
user_id
==
int
(
current_user
.
id
))
\
.
filter
(
ub
.
ReadBook
.
is_read
==
True
)
.
all
()
.
filter
(
ub
.
ReadBook
.
read_status
==
ub
.
ReadBook
.
STATUS_FINISHED
)
.
all
()
readBookIds
=
[
x
.
book_id
for
x
in
readBooks
]
else
:
try
:
...
...
@@ -1448,7 +1454,8 @@ def show_book(book_id):
if
not
config
.
config_read_column
:
matching_have_read_book
=
ub
.
session
.
query
(
ub
.
ReadBook
)
.
\
filter
(
and_
(
ub
.
ReadBook
.
user_id
==
int
(
current_user
.
id
),
ub
.
ReadBook
.
book_id
==
book_id
))
.
all
()
have_read
=
len
(
matching_have_read_book
)
>
0
and
matching_have_read_book
[
0
]
.
is_read
have_read
=
len
(
matching_have_read_book
)
>
0
and
matching_have_read_book
[
0
]
.
read_status
==
ub
.
ReadBook
.
STATUS_FINISHED
else
:
try
:
matching_have_read_book
=
getattr
(
entries
,
'custom_column_'
+
str
(
config
.
config_read_column
))
...
...
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