Software to email me changes in my overall status

Everything except LibriVox (yes, this is where knitting gets discussed. Now includes non-LV Volunteers Wanted projects)
Post Reply
Posts: 1161
Joined: August 27th, 2019, 5:06 am
Location: Cambridge UK

Post by adrianstephens » January 6th, 2021, 1:15 am

Dear Librivoxers,

As I'm a retired software engineer, I produced the following script, which emails me of significant changes in my overall reader status.
Yes, I know the system generates lots of emails, but the problem is they're not always relevant. And a relevant change notification can get
lost if I ignore a notification that is not relevant.

Sample output (partial):
Project 'Tom Cobb; or Fortune's Toy' new section 1 PL Notes. new section 24 PL OK. new section 29 PL OK. removed section 2. removed section 3. removed section 4.

Title: 'Anthology of Magazine Verse for 1913' (Fully Subscribed). Sections: 9: Assigned, 10: Assigned, 20: PL OK, 23: PL OK, 32: PL OK, 36: PL OK, 44: PL OK
Title: 'Horses of the Hills, And other Verses' (Open). Sections: 2: Assigned, 6: Assigned, 7: Assigned, 9: Assigned, 10: Assigned
Title: 'Poems of James Hebblethwaite' (Open). Sections: 0: Assigned, 1: Assigned, 2: Assigned, 3: Assigned, 4: Assigned, 5: Ready for PL
Title: 'Mediaeval hymns and sequences' (Fully Subscribed). Sections: 17: Ready for PL, 18: Ready for PL, 19: Ready for PL, 20: Ready for PL, 21: Ready for PL
Title: 'Tom Cobb; or Fortune's Toy' (Fully Subscribed). Sections: 1: PL Notes, 24: PL OK, 29: PL OK
Title: 'Count of Monte Cristo (version 4 Dramatic Reading)' (Open). Sections: 792: PL OK, 797: PL OK, 805: PL OK
Title: 'Haworth's' (Fully Subscribed). Sections: 15: PL OK, 16: PL OK

The following python script runs periodically and will email me a summary of significant changes. I run it several times a day.
You will have to update anything with ****s.
I note that the forum page helpfully removed all the leading spaces. How very unpythonesque. If you need a copy with spaces, PM me.
# Written 2020 by Adrian Stephens. I place this into the public domain. Use it as you will.
# This code comes with no warrantee. If your cat explodes when you run it, that's your problem.

# Get reader status
# This python program reads my status from the librivox status page and:
# - parses it
# - displays it
# - records what it was
# - checks for changes
# - displays the changes
# - emails changes to me
# - maintains an audit trail of changes that is summarised on this page

import requests
import bs4
import os
import pickle
import smtplib

# Constants
url = '*****'
fileName = 'readerStatus.pickle'

# This class stores all the data associated with an individual project
class Project:
def __init__(self, name, status): = name
self.status = status
self.sections = {} # key is section number, value is status of that section

def add_section(self, section, status):
self.sections[section] = status

def sort_key(self, value):
# return sortable key
return int(value[0])

def sorted_sections(self):
# return list of tuples sorted by section number
return sorted(self.sections.items(), key=self.sort_key)

def list_sections(self):
# Generate string list of sections: section-number: status,
s = ''
for section in self.sorted_sections():
if len(s) > 0:
s += ", "
s += f'{section[0]}: {section[1]}'
return s

def __repr__(self):
return f"Title: '{}' ({self.status}). " + f'Sections: {self.list_sections()}'

def add_sections(self, secs, status):
# parse secs into section numbers and set each to the specified status in dictionary v
for sec in secs.split(','):
sec = sec.strip()
if len(sec) > 0:
self.add_section(sec, status)

def changed(self, prev):
# return string summarizing self to previous project or None if no change
s = '' # compilation of changes
if self.status != prev.status:
s += f'status changed from {prev.status} to {self.status}. '
# count number of sections with each status
def update(d,s):
if s in d:
d[s] += 1
d[s] = 1

for sec in self.sections:
update(current_counts, self.sections[sec])

for sec in prev.sections:
update(previous_counts, prev.sections[sec])

# Now check if counts the same
count_changed = False
for status in current_counts:
if status not in previous_counts:
count_changed = True
if current_counts[status] != previous_counts[status]:
count_changed = True

if count_changed:
for sec_tuple in self.sorted_sections():
sec = sec_tuple[0] # Section number
if sec not in prev.sections:
s += f'new section {sec} {self.sections[sec]}. '
if self.sections[sec] != prev.sections[sec]:
s += f'section {sec} changed from {prev.sections[sec]} to {self.sections[sec]}. '
for sec_tuple in prev.sorted_sections():
sec = sec_tuple[0] # Section number
if sec not in self.sections:
s += f'removed section {sec}. '

if len(s) > 0:
return f"Project '{}' {s}\n"
return None

def mail(changes):
# Send email to me with changes
# Because we can't talk to directly (macvtap)
# send to our public IP address. Firewall will mirror it back.
import smtplib
from email.mime.text import MIMEText

s=smtplib.SMTP(****, 25) # **** is address of smtp server

# Encode message, as may contain non-ascii characters
msg['Subject'] = 'Reader Status Changes'
msg['From'] = 'Utils <***********>'
msg['To'] = '**************'

def parseHTML(html):
# Parse the html
s=bs4.BeautifulSoup(html, 'lxml')

# find the appropriate html object containing the projects data
table = s.find('table', class_='reader-view').tbody

# build dictionary of projects indexed by name

# For all projects
for row in table.find_all('tr'):

# Find the columns and parse by position
#print (cols)


project.add_sections(readyforpl,'Ready for PL')

project.add_sections(plnotes,'PL Notes')

project.add_sections(spotcheck,'Spot Check')

project.add_sections(pl_ok,'PL OK')
return projects

def getChanges(projects, previousProjects):
# compare current with previous projects
# Identify new projects
c = ''
for p in projects:
if p not in previousProjects:
# New project
c += f"New Project: {projects[p]}\n"

# Identify disappeared projects (published)
for p in previousProjects:
if p not in projects:
# Disappeared project
c += f"Published Project: {previousProjects[p]}\n"

# Identify and report changed projects
for p in projects:
if p in previousProjects:
thisProject = projects[p]
prevProject = previousProjects[p]
if thisProject.changed(prevProject):
c += projects[p].changed(previousProjects[p]) + '\n'
return c

def readPersistent(picklePathName):
# read the previous projects array from persistent storage, if it exists
with open(picklePathName, 'rb') as fd:
previousProjects = pickle.load(fd)
previousProjects = None
return previousProjects

def writePersistent(picklePathName,projects):
# Delete pickle file if it exists
if os.path.exists(picklePathName):

# Write persistent state
with open(picklePathName,'wb') as fd:

def main():
# Values derived from the environment
cwd = os.getcwd()
picklePathName = os.path.join(cwd, fileName)

# Get reader status page
response = requests.get(url)
html = response.text

projects = parseHTML(html)
summary = ''
for name in projects:
summary += projects[name].__repr__() + '\n'

previousProjects = readPersistent(picklePathName)

if previousProjects:
c = getChanges(projects, previousProjects)

# If any changes, print and email them
if len(c) > 0:
c += '\n' + summary
# Otherwise just print the summary

writePersistent(picklePathName, projects)

if __name__ == '__main__':
My Librivox-related YouTube series starts here: Part 0: Introduction.
Part 15: Case Study (Poem)
Part 16: Case Study 2 (Dramatic Reading)

Post Reply