Include loanToken and licenseServices when authorizing a tethered eReader (fixes loaned books on Kobo) - #165
Conversation
|
Interesting finding, thanks for the pull request. I haven't really used this "authorize through USB" much, so I didn't know that. So you are saying that each time a new book is loaned from a library, that loan data needs to be refreshed on the Reader? Or just the first time any book is loaned from a particular library? Did you test if that's the same for ADE? What happens if you authorize your reader with ADE, disconnect the reader, get a book from a library, fulfill it through ADE, completely close/shutdown ADE, then plug in the reader and copy over the EPUB/PDF file through a file explorer? Would that also fail? Also, your code seems to just take whatever loanToken has been returned from the fulfillment request, and stores that inside the loantoken.xml. What happens if you have different loans from multiple different operators? I don't see any code that merges that data so right now that would result in only some loans working on the eReader, or am I missing something? |
|
Thanks for the careful review — you've found real gaps. Multiple operators: you're right, that's a bug. How often it needs refreshing: honestly, my testing was thin — two PDF loans, one operator, one device (Kobo Libra Colour). I inferred the "re-authorize after each new loan" behavior rather than testing that exact sequence, and I stated it too firmly in the note. That was an overreach on my part; I'll verify it properly instead of leaving an assumption in the PR. ADE comparison: haven't tested it — I'll run the test you described. My working hypothesis is that ADE refreshes the loanToken during its device-transfer step (the plugin doesn't hook calibre's send-to-device, so I did it at authorize-time instead), but that's a guess until I check. I'm without the reader for a bit, so the rework + retesting will take some time. I'll move this to a draft so it's clearly not merge-ready, and come back with actual results. Thanks again. |
|
Also, please confirm if you've used a LLM to write this fix, and if so, if it's actually been tested by a human. Particularly your last comment sounds an awful lot like LLM-generated text. |
|
I believe I am running into this same problem - also with a Kobo device. I'm happy to act as a human tester for this fix. |
I don't have a working ADE at the moment, but in my past experience, yes that's the case: Library books need to be loaded onto the eReader via Digital Editions, not just copied as a file. |
|
I've just tested and can confirm this fix makes it possible to check out a library book from Overdrive, fulfill the ACSM using Calibre, transfer the book and read it on a Kobo Clara 2E running Kobo software version |
Nice! Thanks for your feedback. P.S. You are right, @Leseratte10. It's written by an LLM but backed by human supervision and testing. I'll make sure to stay transparent on the test I made myself and what has been tested by the machine, haha. |
|
@Alexandre-Lessard let me know if you want me to test two ebooks checked out from two different libraries. I assume that because they are both using the Overdrive platform authenticated with the same Adobe ID that they will share the same loan tokens and not clobber one another, but worth double checking. I know with ADE I have had books out from different libraries at the same time. IMHO the bigger pain point is this...
... it will quickly get irritating having to re-authorize the reader every time you borrow a new book. I don't know anything about Calibre plugin development but if we can hook into the "Send to device..." operation and push a fresh loan token along with the .epub, that seems like a much more natural workflow. |
| # "Your eReader is not authorized to open this book") even though the device is authorized. | ||
| # The loanToken returned by the operator during fulfillment covers all of the user's | ||
| # currently-active loans; fulfill() stores it as "loantoken.xml" next to the account data. | ||
| def _embed_adept(node): |
There was a problem hiding this comment.
Nitpick but instead of doing raw string manipulation with regex, if we need to remove this adobe adept namespace (are we sure we do? why?) I think it would be cleaner to make the changes as XML and keep working with the data as XML.
AI suggested this but I haven't verified it and it might be overkill:
def _embed_adept(node):
ADEPT_NS = "http://ns.adobe.com/adept"
# Find all elements with the Adobe namespace
for elem in node.xpath(f'.//*[namespace-uri()="{ADEPT_NS}"]'):
# Replace the tag with its local name
elem.tag = etree.QName(elem).localname
# Clean up namespace declarations for the Adobe namespace
for elem in node.iter():
if elem.nsmap:
# Remove the Adobe namespace from the mapping
elem.nsmap = {k: v for k, v in elem.nsmap.items() if v != ADEPT_NS}
return node
bjmc
left a comment
There was a problem hiding this comment.
Very happy to see this fix included in some form. 👍
…ader A device authorization (activationToken) on its own is enough to open *purchased* ADEPT books on a tethered eReader, but a library LOAN also requires a signed loanToken (bound to user+loan, not to a device) and the operator's licenseServices to be present in the device's activation.xml. Without them the reader refuses loaned content even though the device is correctly authorized -- e.g. a Kobo shows "Your eReader is not authorized to open this book". exportProxyAuth() previously wrote only the activationServiceInfo, credentials and activationToken, so loaned books never opened on a device authorized through "Authorize eReader over USB". - fulfill(): persist the loanToken returned by the operator (it covers all of the user's currently-active loans) to "loantoken.xml" next to the account data, instead of discarding it. - exportProxyAuth(): embed that loanToken and the account's licenseServices into the eReader's activation.xml. Tested end-to-end on a factory-reset Kobo Libra Colour (fresh device fingerprint): authorizing through the plugin now lets loaned library PDFs open on the device, with no Adobe Digital Editions involved. Only that model was verified; other current-generation Kobo models are expected to behave the same (standard Adobe RMSDK), older RMSDK 9.x generations unverified.
Each operator signs its own loanToken, and that token only covers the loans that operator issued. Keeping just the most recent one meant a fulfillment from a second operator silently dropped the first operator's token, so the next "Authorize eReader over USB" wrote an activation that no longer covered the earlier loans - books that had been working stopped opening. This is not an exotic case: a single library platform can route to different operators depending on the book's distributor, so it happens with one library card on one website. Loan tokens now live in the account's activation.xml, next to operatorURLList and licenseServices, which are already per-operator collections maintained the same way. They are keyed by operatorURL, and because a loanToken is a signed blob it is stored verbatim and never edited: a newer token from an operator replaces that operator's entry and leaves the others alone. There is no expiry field in a loanToken, so superseding by operator is the only workable strategy. Also adds test coverage for the tethered-device authorization, which had none: one test checks that the authorization written to the device carries the loan tokens and licenseServices a loaned book needs (and that an account with no loans still produces a valid authorization), and one checks that storing a second operator's token doesn't evict the first.
4b6cf17 to
dc0c806
Compare
|
Thanks @Leseratte10 and @bjmc. I have updated the patch and PR description. @Leseratte10, you were right about the multi-operator bug. The patch now keeps I also rebased onto the current @bjmc, thanks for the independent Clara 2E test. I reproduced the multi-operator With the bug fixed and the remaining gaps documented, I am taking the PR out LLM disclosure: The code and this comment were written with LLM assistance. I |
Problem
Authorize eReader over USB creates a valid device authorization, but
library loans can remain locked. On a Kobo, the error is:
Cause and fix
Adding the operator's signed
loanTokenandlicenseServicesto the reader'sactivation.xmlmade the tested loans open; removing them made the same booksstop opening.
exportProxyAuth()previously wrote neither element.The plugin already receives the token during
fulfill(), but discarded it.The patch now:
operatorURLin the account'sactivation.xml;licenseServicesto the reader.The license files suggest that loan permissions require this signed token, but
that explanation is inferred; the reader's internal behavior was not measured.
Multiple operators
The first version kept only one token. Testing confirmed that adding a second
operator could make earlier loans stop opening after re-authorization.
This can happen on one library platform: two books borrowed through the same
prêtnumérique.ca account used
depot2.sogides.comandacs4.edenlivres.fr. The revised version therefore keeps both tokens.Testing
tests/main.pynow passes 24 tests (up from 22):test_tetheredDeviceAuthorizationIncludesLoanDatachecks that the exportedauthorization contains
loanTokenandlicenseServices, and remains validfor an account with no loans.
test_loanTokenStorageIsPerOperatorchecks that adding a second operatordoes not evict the first.
Both fail on the pre-PR code, with
Loan tokens missing from the device authorizationand
Second operator's loan token evicted the first operator'srespectively.They require no reader, Adobe account, or network access.
I also ran the hardware checks below manually through the calibre 9.12 GUI on
a Kobo Libra Colour. No authorization files were hand-edited and no scripts
transferred books to the reader. The test set contained five active PDF loans
from two operators.
Each operator's token is cumulative, so one refresh after importing several
loans should cover them all. That exact batch sequence was not tested.
@bjmc also confirmed the core fix independently on a Kobo Clara 2E with an
OverDrive loan.
Not tested
Follow-up
Newly imported loans still require a manual reader refresh before they open.
A lighter refresh during Send to device would be a better workflow, but
belongs in a separate PR after this storage design is reviewed.