def fulfill(self, patron, pin, licensepool, internal_format): # Download the book from the appropriate acquisition link and return its content. # TODO: Implement https://github.com/NYPL-Simplified/Simplified/wiki/BearerTokenPropagation#advertising-bearer-token-propagation # instead. links = licensepool.identifier.links # Find the acquisition link with the right media type. for link in links: media_type = link.resource.representation.media_type if link.rel == Hyperlink.GENERIC_OPDS_ACQUISITION and media_type == internal_format: url = link.resource.representation.url # Obtain a bearer token. _db = Session.object_session(patron) token = self._get_token(_db) headers = dict() auth_header = "Bearer %s" % token headers['Authorization'] = auth_header response = self._request_with_timeout('GET', url, headers=headers) return FulfillmentInfo( licensepool.collection, licensepool.data_source.name, licensepool.identifier.type, licensepool.identifier.identifier, content_link=None, content_type=internal_format, content=response.content, content_expires=None, ) # We couldn't find an acquisition link for this book. raise CannotFulfill()
def fulfill(self, patron, pin, licensepool, internal_format): _db = Session.object_session(patron) loan = _db.query(Loan).filter(Loan.patron == patron).filter( Loan.license_pool_id == licensepool.id) loan = loan.one() doc = self.get_license_status_document(loan) status = doc.get("status") if status not in [self.READY_STATUS, self.ACTIVE_STATUS]: # This loan isn't available for some reason. It's possible # the distributor revoked it or the patron already returned it # through the DRM system, and we didn't get a notification # from the distributor yet. self.update_loan(loan, doc) raise CannotFulfill() expires = doc.get("potential_rights", {}).get("end") expires = datetime.datetime.strptime(expires, self.TIME_FORMAT) content_link = doc.get("links", {}).get("license", {}).get("href") content_type = doc.get("links", {}).get("license", {}).get("type") return FulfillmentInfo( licensepool.collection, licensepool.data_source.name, licensepool.identifier.type, licensepool.identifier.identifier, content_link, content_type, None, expires, )
def fulfill(self, patron, pin, licensepool, internal_format): book_id = licensepool.identifier.identifier response = self.loan_request(patron.authorization_identifier, pin, book_id) if response.status_code != 200: raise CannotFulfill(response.status_code) result = json.loads(response.content)['result'] if not result['success']: message = result['message'] if "There are no available copies" in message: self.log.error("There are no copies of book %s available." % book_id) raise NoAvailableCopies() elif "Login unsuccessful" in message: self.log.error( "User validation against Enki server with %s / %s was unsuccessful." % (patron.authorization_identifier, pin)) raise AuthorizationFailedException() drm_type = self.get_enki_drm_type(book_id) url, item_type, expires = self.parse_fulfill_result(result) if not drm_type and item_type == 'epub': drm_type = self.no_drm return FulfillmentInfo(licensepool.collection, licensepool.data_source.name, licensepool.identifier.type, licensepool.identifier.identifier, content_link=url, content_type=drm_type, content=None, content_expires=expires)
def process_one(self, e, namespaces): """Either turn the given document into a LoanInfo object, or raise an appropriate exception. """ self.raise_exception_on_error(e, namespaces) # If we get to this point it's because the checkout succeeded. expiration_date = self._xpath1(e, '//axis:expirationDate', namespaces) fulfillment_url = self._xpath1(e, '//axis:url', namespaces) if fulfillment_url is not None: fulfillment_url = fulfillment_url.text if expiration_date is not None: expiration_date = expiration_date.text expiration_date = datetime.strptime(expiration_date, self.FULL_DATE_FORMAT) # WTH??? Why is identifier None? Is this ever used? fulfillment = FulfillmentInfo(collection=self.collection, data_source_name=DataSource.AXIS_360, identifier_type=self.id_type, identifier=None, content_link=fulfillment_url, content_type=None, content=None, content_expires=None) loan_start = datetime.utcnow() loan = LoanInfo(collection=self.collection, data_source_name=DataSource.AXIS_360, identifier_type=self.id_type, identifier=None, start_date=loan_start, end_date=expiration_date, fulfillment_info=fulfillment) return loan
def fulfill(self, patron, password, pool, delivery_mechanism): response = self.get_fulfillment_file(patron.authorization_identifier, pool.identifier.identifier) return FulfillmentInfo( pool.identifier.type, pool.identifier.identifier, content_link=None, content_type=response.headers.get('Content-Type'), content=response.content, content_expires=None, )
def process_one(self, e, ns): # Figure out which book we're talking about. axis_identifier = self.text_of_subtag(e, "axis:titleId", ns) availability = self._xpath1(e, 'axis:availability', ns) if availability is None: return None reserved = self._xpath1_boolean(availability, 'axis:isReserved', ns) checked_out = self._xpath1_boolean(availability, 'axis:isCheckedout', ns) on_hold = self._xpath1_boolean(availability, 'axis:isInHoldQueue', ns) info = None if checked_out: start_date = self._xpath1_date(availability, 'axis:checkoutStartDate', ns) end_date = self._xpath1_date(availability, 'axis:checkoutEndDate', ns) download_url = self.text_of_optional_subtag( availability, 'axis:downloadUrl', ns) if download_url: fulfillment = FulfillmentInfo(identifier_type=self.id_type, identifier=axis_identifier, content_link=download_url, content_type=None, content=None, content_expires=None) else: fulfillment = None info = LoanInfo(identifier_type=self.id_type, identifier=axis_identifier, start_date=start_date, end_date=end_date, fulfillment_info=fulfillment) elif reserved: end_date = self._xpath1_date(availability, 'axis:reservedEndDate', ns) info = HoldInfo(identifier_type=self.id_type, identifier=axis_identifier, start_date=None, end_date=end_date, hold_position=0) elif on_hold: position = self.int_of_optional_subtag(availability, 'axis:holdsQueuePosition', ns) info = HoldInfo(identifier_type=self.id_type, identifier=axis_identifier, start_date=None, end_date=None, hold_position=position) return info
def fulfill(self, patron, pin, licensepool, internal_format, **kwargs): """Get the actual resource file to the patron. :param kwargs: A container for arguments to fulfill() which are not relevant to this vendor. :return: a FulfillmentInfo object. """ book_id = licensepool.identifier.identifier enki_library_id = self.enki_library_id(patron.library) response = self.loan_request( patron.authorization_identifier, pin, book_id, enki_library_id ) if response.status_code != 200: raise CannotFulfill(response.status_code) result = json.loads(response.content)['result'] if not result['success']: message = result['message'] if "There are no available copies" in message: self.log.error("There are no copies of book %s available." % book_id) raise NoAvailableCopies() elif "Login unsuccessful" in message: self.log.error("User validation against Enki server with %s / %s was unsuccessful." % (patron.authorization_identifier, pin)) raise AuthorizationFailedException() url, item_type, expires = self.parse_fulfill_result(result) # We don't know for sure which DRM scheme is in use, (that is, # whether the content link points to the actual book or an # ACSM file) but since Enki titles only have a single delivery # mechanism, it's easy to make a guess. drm_type = self.no_drm for lpdm in licensepool.delivery_mechanisms: delivery_mechanism = lpdm.delivery_mechanism if delivery_mechanism: drm_type = delivery_mechanism.drm_scheme break return FulfillmentInfo( licensepool.collection, licensepool.data_source.name, licensepool.identifier.type, licensepool.identifier.identifier, content_link=url, content_type=drm_type, content=None, content_expires=expires )
def fulfill(self, patron, pin, licensepool, internal_format): record_id = licensepool.identifier.identifier content_link, content, content_type = self.get_fulfillment_link(patron, pin, record_id, internal_format) if not content_link and not content: self.log.info("Odilo record_id %s was not available as %s" % (record_id, internal_format)) else: return FulfillmentInfo( licensepool.collection, DataSource.ODILO, Identifier.ODILO_ID, record_id, content_link=content_link, content=content, content_type=content_type, content_expires=None )
def fulfill(self, patron, pin, licensepool, internal_format, **kwargs): """Retrieve a bearer token that can be used to download the book. :param kwargs: A container for arguments to fulfill() which are not relevant to this vendor. :return: a FulfillmentInfo object. """ links = licensepool.identifier.links # Find the acquisition link with the right media type. for link in links: media_type = link.resource.representation.media_type if link.rel == Hyperlink.GENERIC_OPDS_ACQUISITION and media_type == internal_format: url = link.resource.representation.url # Obtain a Credential with the information from our # bearer token. _db = Session.object_session(licensepool) credential = self._get_token(_db) # Build a application/vnd.librarysimplified.bearer-token # document using information from the credential. now = datetime.datetime.utcnow() expiration = int((credential.expires - now).total_seconds()) token_document = dict( token_type="Bearer", access_token=credential.credential, expires_in=expiration, location=url, ) return FulfillmentInfo( licensepool.collection, licensepool.data_source.name, licensepool.identifier.type, licensepool.identifier.identifier, content_link=None, content_type=DeliveryMechanism.BEARER_TOKEN, content=json.dumps(token_document), content_expires=credential.expires, ) # We couldn't find an acquisition link for this book. raise CannotFulfill()
def fulfill(self, patron, pin, licensepool, internal_format): # Download the book from the appropriate acquisition link and return its content. # TODO: Implement https://github.com/NYPL-Simplified/Simplified/wiki/BearerTokenPropagation#advertising-bearer-token-propagation # instead. links = licensepool.identifier.links # Find the acquisition link with the right media type. for link in links: media_type = link.resource.representation.media_type if link.rel == Hyperlink.GENERIC_OPDS_ACQUISITION and media_type == internal_format: url = link.resource.representation.url # Obtain a Credential with the information from our # bearer token. _db = Session.object_session(patron) credential = self._get_token(_db) # Build a application/vnd.librarysimplified.bearer-token # document using information from the credential. now = datetime.datetime.utcnow() expiration = int((credential.expires - now).total_seconds()) token_document = dict( token_type="Bearer", access_token=credential.credential, expires_in=expiration, location=url, ) return FulfillmentInfo( licensepool.collection, licensepool.data_source.name, licensepool.identifier.type, licensepool.identifier.identifier, content_link=None, content_type=DeliveryMechanism.BEARER_TOKEN, content=json.dumps(token_document), content_expires=credential.expires, ) # We couldn't find an acquisition link for this book. raise CannotFulfill()
def fulfill(self, patron, pin, licensepool, internal_format, **kwargs): """Get the actual resource file to the patron. :param kwargs: A container for arguments to fulfill() which are not relevant to this vendor. :return: a FulfillmentInfo object. """ record_id = licensepool.identifier.identifier content_link, content, content_type = self.get_fulfillment_link( patron, pin, record_id, internal_format) if not content_link and not content: self.log.info("Odilo record_id %s was not available as %s" % (record_id, internal_format)) else: return FulfillmentInfo(licensepool.collection, DataSource.ODILO, Identifier.ODILO_ID, record_id, content_link=content_link, content=content, content_type=content_type, content_expires=None)
response = fulfill_method(patron.authorization_identifier, pool.identifier.identifier) content = response.content content_type = None if content_transformation: try: content_type, content = (content_transformation(pool, content)) except Exception, e: self.log.error("Error transforming fulfillment document: %s", response.content, exc_info=e) return FulfillmentInfo( pool.collection, DataSource.BIBLIOTHECA, pool.identifier.type, pool.identifier.identifier, content_link=None, content_type=content_type or response.headers.get('Content-Type'), content=content, content_expires=None, ) def get_fulfillment_file(self, patron_id, bibliotheca_id): args = dict(request_type='ACSMRequest', item_id=bibliotheca_id, patron_id=patron_id) body = self.TEMPLATE % args return self.request('GetItemACSM', body, method="PUT") def get_audio_fulfillment_file(self, patron_id, bibliotheca_id): args = dict(request_type='AudioFulfillmentRequest', item_id=bibliotheca_id,
else: # slightly risky assumption here file_format = Representation.MP3_MEDIA_TYPE # Note: download urls expire 15 minutes after being handed out # in the checkouts call download_url = file.get('downloadUrl', None) # is included in the downloadUrl, actually acs_resource_id = file.get('acsResourceId', None) # TODO: For audio books, the downloads are done by parts, and there are # multiple download urls. Need to have a mechanism for putting lists of # parts into fulfillment objects. fulfillment_info = FulfillmentInfo(Identifier.ONECLICK_ID, identifier, content_link=download_url, content_type=file_format, content=None, content_expires=None) loan = LoanInfo( Identifier.ONECLICK_ID, isbn, start_date=None, end_date=expires, fulfillment_info=fulfillment_info, ) loans.append(loan) return loans
# have an inaccurate delivery mechanism. Try to update the formats, but # reraise the error regardless. self.log.info("Overdrive id %s was not available as %s, getting updated formats" % (licensepool.identifier.identifier, internal_format)) try: self.update_formats(licensepool) except Exception, e2: self.log.error("Could not update formats for Overdrive ID %s" % licensepool.identifier.identifier) raise e return FulfillmentInfo( licensepool.collection, licensepool.data_source.name, licensepool.identifier.type, licensepool.identifier.identifier, content_link=url, content_type=media_type, content=None, content_expires=None ) def get_fulfillment_link(self, patron, pin, overdrive_id, format_type): """Get the link to the ACSM file corresponding to an existing loan. """ loan = self.get_loan(patron, pin, overdrive_id) if not loan: raise NoActiveLoan("Could not find active loan for %s" % overdrive_id) download_link = None if not loan['isFormatLockedIn'] and format_type not in self.STREAMING_FORMATS: # The format is not locked in. Lock it in.
# We need to use self._make_request instead of self.request # because it's a different server that will reject the credentials # we use for the API. access_document = self._make_request(download_url, 'GET', {}) data = json.loads(access_document.content) content_link = data['url'] content_type = data['type'] if content_type == 'application/vnd.adobe': # The manifest spells the media type wrong. Fix it. content_type = DeliveryMechanism.ADOBE_DRM fulfillment_info = FulfillmentInfo( self.collection, DataSource.RB_DIGITAL, Identifier.RB_DIGITAL_ID, identifier, content_link = content_link, content_type = content_type, content = None, content_expires = None ) loan = LoanInfo( self.collection, DataSource.RB_DIGITAL, Identifier.RB_DIGITAL_ID, isbn, start_date=None, end_date=expires, fulfillment_info=fulfillment_info, )
# reraise the error regardless. self.log.info( "Overdrive id %s was not available as %s, getting updated formats" % (licensepool.identifier.identifier, internal_format)) try: self.update_formats(licensepool) except Exception, e2: self.log.error("Could not update formats for Overdrive ID %s" % licensepool.identifier.identifier) raise e return FulfillmentInfo(licensepool.identifier.type, licensepool.identifier.identifier, content_link=url, content_type=media_type, content=None, content_expires=None) def get_fulfillment_link(self, patron, pin, overdrive_id, format_type): """Get the link to the ACSM file corresponding to an existing loan. """ loan = self.get_loan(patron, pin, overdrive_id) if not loan: raise NoActiveLoan("Could not find active loan for %s" % overdrive_id) download_link = None if not loan[ 'isFormatLockedIn'] and format_type not in self.STREAMING_FORMATS: # The format is not locked in. Lock it in. # This will happen the first time someone tries to fulfill
file_format = Representation.MP3_MEDIA_TYPE # Note: download urls expire 15 minutes after being handed out # in the checkouts call download_url = file.get('downloadUrl', None) # is included in the downloadUrl, actually acs_resource_id = file.get('acsResourceId', None) # TODO: For audio books, the downloads are done by parts, and there are # multiple download urls. Need to have a mechanism for putting lists of # parts into fulfillment objects. fulfillment_info = FulfillmentInfo( self.collection, DataSource.ONECLICK, Identifier.ONECLICK_ID, identifier, content_link = download_url, content_type = file_format, content = None, content_expires = None ) loan = LoanInfo( self.collection, DataSource.ONECLICK, Identifier.ONECLICK_ID, isbn, start_date=None, end_date=expires, fulfillment_info=fulfillment_info, )