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
adrianstephens
Posts: 1440
Joined: August 27th, 2019, 5:06 am
Location: Cambridge UK
Contact:

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 = 'https://librivox.org/sections/readers/*****'
fileName = 'readerStatus.pickle'

# This class stores all the data associated with an individual project
class Project:
def __init__(self, name, status):
self.name = 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.name}' ({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
else:
d[s] = 1

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

previous_counts={}
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
else:
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]}. '
else:
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 '{self.name}' {s}\n"
else:
return None


def mail(changes):
# Send email to me with changes
# Because we can't talk to 10.1.1.50 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=MIMEText(changes)
msg['Subject'] = 'Reader Status Changes'
msg['From'] = 'Utils <***********>'
msg['To'] = '**************'
s.send_message(msg)
s.quit()


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
projects={}

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

# Find the columns and parse by position
cols=row.find_all('td')
#print (cols)
title=cols[0].a.text
status=cols[1].text
project=Project(title,status)

assigned=cols[2].text
project.add_sections(assigned,'Assigned')

readyforpl=cols[3].text
project.add_sections(readyforpl,'Ready for PL')

plnotes=cols[4].text
project.add_sections(plnotes,'PL Notes')

spotcheck=cols[5].text
project.add_sections(spotcheck,'Spot Check')

pl_ok=cols[6].text
project.add_sections(pl_ok,'PL OK')
projects[title]=project
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
try:
with open(picklePathName, 'rb') as fd:
previousProjects = pickle.load(fd)
except:
previousProjects = None
return previousProjects

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

# Write persistent state
with open(picklePathName,'wb') as fd:
pickle.dump(projects,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
print(c)
mail(c)
# Otherwise just print the summary
else:
print(summary)

writePersistent(picklePathName, projects)
return

if __name__ == '__main__':
main()
My Librivox-related YouTube series starts here: Part 0: Introduction. https://youtu.be/pMHYycgA5VU
...
Part 15: Case Study (Poem) https://youtu.be/41sr_VC1Qxo
Part 16: Case Study 2 (Dramatic Reading) https://youtu.be/GBIAd469vnM

Post Reply