#!/usr/bin/env python3 # File: libraries/aspen.py # Author: bgstack15 # Startdate: 2024-07-06-7 08:06 # SPDX-License-Identifier: GPL-3.0-only # Title: Library Plugin for Aspen # Project: library_info # Purpose: plugin for aspen-based library websites # History: # Usage: # Reference: # Improve: # Dependencies: # dep-devuan: python3-bs4 from .base import * import requests, json, dateutil, base64, os, sys from bs4 import BeautifulSoup class Library(BaseLibrary): def __init__(self, config_obj = None, alias = None, username = None, password = None, baseurl = None, session = None): if config_obj and "username" in config_obj: self.username = config_obj["username"] else: self.username = username if config_obj and "password" in config_obj: self.password = config_obj["password"] else: self.password = password if config_obj and "baseurl" in config_obj: self.baseurl = config_obj["baseurl"] else: self.baseurl = baseurl if baseurl else "https://aspen.example.org" self.baseurl = self.baseurl.rstrip("/") if session and type(session) == requests.sessions.Session: self.session = session else: self.session = requests.Session() if config_obj and "alias" in config_obj: self.alias = config_obj["alias"] else: self.alias = alias if alias else "Aspen-based library" self.card_expires = None # log in now. Why would we not? self.login() def get_reservations(self, verbose = False): availableReservations = [] unavailableReservations = [] b = self.baseurl s = self.session # step 1: visit "titles on hold" page so it does not complain that I am taking shortcuts headers = { "Referer": f"{b}/MyAccount/CheckedOut?source=all" } s.get(f"{b}/Holds?source=ils",headers=headers) params = { "method": "getHolds", "source": "all" } output = s.get(f"{b}/MyAccount/AJAX",params=params,headers=headers) output = json.loads(output.content)["holds"].replace("\xa0"," ") soup = BeautifulSoup(output, "html.parser") try: availableholds_all = soup.find("label",attrs={"for":"availableHoldSort_all"}).parent.next_sibling.next_sibling except AttributeError: # the label will not exist if there are no availableHolds availableholds_all = None try: unavailableholds_all = soup.find("label",attrs={"for":"unavailableHoldSort_all"}).parent.next_sibling.next_sibling except AttributeError: # the label will not exist if there are no unavailableHolds unavailableholds_all = None if unavailableholds_all: items = unavailableholds_all.find_all("div",class_=["result"]) for i in items: labels = [j.text for j in i.find_all("div","result-label")] values = [j.text for j in i.find_all("div","result-value")] values_dict = dict(map(lambda i,j:(i,j),labels,values)) title_obj = i.find("a",class_="result-title") img_href = i.find("img")["src"] img_b64, img_type = self.get_image(img_href) obj = { "patron": self.alias, "position": values_dict["Position"] if "Position" in values_dict else "", "status": values_dict["Status"], "date_placed": "placed " + values_dict["Date Placed"], "format": values_dict["Format"], "location": values_dict["Pickup Location"], "title": title_obj.text, "img_href": img_href, "img50": img_b64[:50], "img": img_b64, "img_type": img_type, } unavailableReservations.append(obj) if availableholds_all: items = availableholds_all.find_all("div",class_=["result"]) for i in items: labels = [j.text for j in i.find_all("div","result-label")] values = [j.text for j in i.find_all("div","result-value")] values_dict = dict(map(lambda i,j:(i,j),labels,values)) title_obj = i.find("a",class_="result-title") img_href = i.find("img")["src"] img_b64, img_type = self.get_image(img_href) if verbose: print(f"DEBUG available: title {title_obj.text}", file=sys.stderr) print(f"DEBUG available: values_dict {values_dict}", file=sys.stderr) obj = { "patron": self.alias, "position": values_dict["Position"] if "Position" in values_dict else "", "status": "ready", "date_placed": "until " + values_dict["Pickup By"], "format": values_dict["Format"], "location": values_dict["Pickup Location"], "title": title_obj.text, "img_href": img_href, "img50": img_b64[:50], "img": img_b64, "img_type": img_type, } availableReservations.append(obj) # Return a single list of objects return availableReservations + unavailableReservations def get_checkouts(self, verbose = False): checked_out_objects = [] b = self.baseurl s = self.session # step 1: visit the "checked out" web page, so it doesn't freak out that I am taking shortcuts headers = { "Content-Type": "application/x-www-form-urlencoded", "Referer": f"{b}/MyAccount/Home", "Priority": "u=1" } s.get(f"{b}/MyAccount/CheckedOut", headers = headers) # step 2: visit the checkout list which is a cruddy html-inside-json garbage headers = { "Referer": f"{b}/MyAccount/CheckedOut?source=all" } params = { "method": "getCheckouts", "source": "all" } output = s.get(f"{b}/MyAccount/AJAX",params=params,headers=headers) output = json.loads(output.content)["checkouts"].replace("\xa0"," ") soup = BeautifulSoup(output, "html.parser") # goals: get title, format, picture, barcode, due date, possible renewal date, times_renewed, when_checked_out results = soup.find_all("div", class_ = "result row") #results = soup.find_all("span",class_="result-index") #results = [i.parent.parent.parent for i in results] for i in results: title = i.find(class_ = "result-title").contents[0] labels = [j.contents[0] for j in i.find_all("div", class_ = "result-label")] values = [j.contents[0] for j in i.find_all("div", class_ = "result-value")] values_dict = dict(map(lambda i,j:(i,j),labels,values)) if verbose: print(f"DEBUG: Values_dict: {values_dict}",file=sys.stderr) # contains Call number, Format, Barcode, Due img_href = i.find("img", class_="listResultImage")["src"] img_b64, img_type = self.get_image(img_href) # normalize format item_format = "" item_format = "book" if "book" in values_dict["Format"].lower() else "" if not item_format: item_format = values_dict["Format"] obj = { "patron": self.alias, "title": title, "format": item_format, "barcode": values_dict["Barcode"], "due": dateutil.parser.parse(values_dict["Due"]), "img_href": img_href, "img50": img_b64[:50], "img": img_b64, "img_type": img_type, } checked_out_objects.append(obj) return checked_out_objects def get_class_name(self): return os.path.basename(__file__).replace(".py","") def login(self): b = self.baseurl s = self.session # step 1: visit login page s.get(f"{b}/MyAccount/Home") # step 2: log in # curl 'https://aspen.example.org/MyAccount/Home' -X POST -H 'Content-Type: application/x-www-form-urlencoded' -H 'Referer: https://aspen.example.org/MyAccount/Home' -H 'Priority: u=1' --data-raw 'username=987213497234&password=1234&submit=Login' data = { "username": self.username, "password": self.password, "submit": "Login" } headers = { "Content-Type": "application/x-www-form-urlencoded", "Referer": f"{b}/MyAccount/Home", "Priority": "u=1" } s.post(f"{b}/MyAccount/Home", headers = headers, data = data) # step 3: learn card expiration date # curl 'https://aspen.example.org/MyAccount/AJAX?method=getMenuDataIls&activeModule=MyAccount&activeAction=CheckedOut' -H 'Accept: application/json, text/javascript, */*; q=0.01' -H 'Referer: https://aspen.example.org/MyAccount/CheckedOut' -H 'Cookie: aspen_session=bksjhjndqhjcsoplci3b6htl3u' params = { "method": "getMenuDataIls", "activeModule": "MyAccount", "activeAction": "CheckedOut", } headers = { "Referer": f"{b}/MyAccount/CheckedOut" } response = s.get(f"{b}/MyAccount/AJAX",params=params,headers=headers) output = json.loads(response.content) self.card_expires = dateutil.parser.parse(output["summary"]["expires"])