Forums

Discuss all things Remember The Milk.

Automating Library Books

nkirsch says:
I use RTM, the API, and a Python script to crawl my local library's RSS feed. I automatically find books that have been checked out and their due date and create RTM tasks for each item (marked with the library location and a tag for book). When I return the book, it automatically marks the task as complete.

I no longer have to worry about keeping track of when books are due - RTM takes care of it all for me!
Posted at 9:20am on June 26, 2010
anna.santos says:
I love this idea. I check out tons of books for my kids so it is always a challenge to keep track of them. I don't understand how you do it though.
Posted 13 years ago
emily (Remember The Milk) says:
Hi nkirsch,

Awesome tip -- just wanted to let you know that you're this week's Tips & Tricks Tuesday winner. We've upgraded your RTM account to have a free year of Pro. :)
Posted 13 years ago
espressodoppio says:
Me too I found the tip useful but don't have an idea how to realize it. Would it be possible to post an Howto?
Posted 13 years ago
(closed account) says:
Wow. Simply amazing. +1 on the request to post a how-to.
Posted 13 years ago
bgarlock says:
Hi, would you mind posting your python script?
Posted 13 years ago
nkirsch says:
Sure - try not to beat me up too much. ;)

I'm not sure the tabs will come through - if there is a better way to post, let me know.

library_script:

#!/usr/bin/python2.5
import user
import feedparser
import datetime
import re
import rtm
import sys

DUE_NMK='URL'
DUE_EK='URL'
RDY_NMK='URL'
DUE_DATE=re.compile('.*Date Due: (.*) ;', re.DOTALL)
RDY_UP=re.compile('.*Ready for pickup.*', re.DOTALL)
TASK='list:Home AND status:incomplete AND location:"Queen Anne Public Library"'

if __name__ == '__main__':
rtm.rtm_auth()

query = TASK
to_return = rtm.rtm_tasks(query)
entries = []

for url in [DUE_NMK, DUE_EK]:
feed = feedparser.parse(url)

# Find and correct entries which are due.
for entry in feed['entries']:
match = DUE_DATE.match(entry['summary'])
if not match:
continue

name = entry['title']
if name[-2:] == " /":
name = name[:-2]

if name[0:3] == "Due":
name = name[name.find(':')+1:]

name = name.strip()
due = match.group(1)

entries.append((name, due))

if len(sys.argv) > 1:
print '%s due %s' % (name, due)

tasks = [task for task in to_return if task['name'].find(name) > -1]

if not tasks:
rtm.rtm_add('Home', 'Return: %s' % (name), 3, due, 'Queen Anne Public Library')
else:
d1 = datetime.datetime.strptime(tasks[0]['task']['due'], "%Y-%m-%dT%H:%M:%SZ")
d2 = datetime.datetime.strptime(due, "%m/%d/%Y")

if d2 - d1:
rtm.rtm_task_update(tasks[0]['task']['id'], tasks[0]['id'], 'Home', 'Return: %s' % (name), 3, due, 'Queen Anne Public Library')

for task in to_return:
book = [entry for entry in entries if task['name'].find(entry[0]) > 0]
if not book:
rtm.rtm_task_complete(task['task']['id'], task['id'], 'Home')

for url in []:
feed = feedparser.parse(url)

for entry in feed['entries']:
print 'TITLE', entry['title']
print 'SUMMARY', entry['summary']

rtm.py:

#!/usr/bin/python
import urllib2
import urllib
import hashlib
import cjson
import getopt
import sys

API_KEY='KEY'
API_SECRET='SECRET'
API_URL='http://api.rememberthemilk.com/services/rest/'
API_AUTH='http://api.rememberthemilk.com/services/auth/'
API_TOKEN=None

def rtm_helper(method, iparams, url, execute):
params = iparams.copy()

params.update({
'api_key':API_KEY,
'format':'json'})

if method:
params['method'] = method

keys = params.keys()
keys.sort()
pairs=reduce(lambda y,x:y+x+params[x], keys, '')

signature=hashlib.md5(API_SECRET + pairs).hexdigest()

params.update({'api_sig':signature})
parameter_string = urllib.urlencode(params.items())

rest_url = '%s?%s' % (url, parameter_string)

if execute:
json = urllib2.urlopen(rest_url).read()
return cjson.decode(json)['rsp']

return rest_url

def rtm_auth():
global API_TOKEN

if API_TOKEN:
return
try:
API_TOKEN = open('rtm.auth').read().strip()
except:
frob = rtm_method('rtm.auth.getFrob')['frob']
print rtm_helper(None, {'perms':'delete', 'frob':frob}, API_AUTH, False)
raw_input("Copy URL to web browser, authorize, and hit enter to continue.")
resp = rtm_method('rtm.auth.getToken', {'frob':frob})
auth = resp['auth']
open('rtm.auth', 'w').write(auth['token'])

API_TOKEN = auth['token']

def rtm_method(method, iparams = {}):
global API_TOKEN

if API_TOKEN:
iparams.update({'auth_token':API_TOKEN})

return rtm_helper(method, iparams, API_URL, True)

def rtm_location(name):
locations = rtm_method('rtm.locations.getList')['locations']['location']
for location in locations:
if location['name'] == name:
return location['id']

raise 'Location cannot be found.'

def rtm_lists(name):
tasklists = rtm_method('rtm.lists.getList')['lists']['list']
for tasklist in tasklists:
if tasklist['name'] == name:
return tasklist['id']

raise 'List cannot be found.'

def rtm_add(listname, name, priority, due, location):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)
locationid = rtm_location(location)

task = rtm_method('rtm.tasks.add',
{'timeline':timeline, 'name':name, 'list_id': listid})

taskseries = task['list']['taskseries']['id']
taskid = task['list']['taskseries']['task']['id']

task = rtm_method('rtm.tasks.setLocation',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'location_id':locationid})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setDueDate',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'due': due})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setPriority',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'priority': str(priority)})
assert(task['stat'] == 'ok'), task

def rtm_task_complete(taskid, taskseries, listname):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)

task = rtm_method('rtm.tasks.complete',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid})
assert(task['stat'] == 'ok'), task

def rtm_task_update(taskid, taskseries, listname, name, priority, due, location):
timeline = rtm_method('rtm.timelines.create')['timeline']
listid = rtm_lists(listname)
locationid = rtm_location(location)

task = rtm_method('rtm.tasks.setLocation',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'location_id':locationid})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setDueDate',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'due': due})
assert(task['stat'] == 'ok'), task
task = rtm_method('rtm.tasks.setPriority',
{'timeline':timeline, 'list_id': listid, 'taskseries_id':taskseries,
'task_id':taskid, 'priority': str(priority)})
assert(task['stat'] == 'ok'), task

def rtm_tasks(task_filter):
tasks = []
result = rtm_method('rtm.tasks.getList', {'filter':task_filter})['tasks']
if not result:
return tasks
if not result.has_key('list'):
return tasks

revision = result['rev']
if not isinstance(result['list'], list):
result['list'] = [result['list']]

for tlist in result['list']:
list_id = tlist['id']
if not isinstance(tlist['taskseries'], list):
tlist['taskseries'] = [tlist['taskseries']]

for tseries in tlist['taskseries']:
taskseries_id = tseries['id']

task = tseries.copy()
task['list_id'] = list_id
task['taskseries_id'] = taskseries_id

tasks.append(task)
return tasks
Posted 13 years ago
nkirsch says:
I'm happy to help others with this - if you send me a private note (is that possible?) with a link to your library, I will see if they have an RSS feed that could be used.

I could potentially turn this into a free web app if other libraries work in a similar fashion.
Posted 13 years ago
pixl.dave says:
@nhirsch I would recommend to also post the two snippets to a code sharing site like http://dpaste.com
This is a django paste, so you have proper indentation of code and colored syntax for the language... make sure you click the hold check box otherwise these snippets will be deleted automatically after 7 days.

If that is not of convenience to you, than maybe you can archive the two python scripts and share them on a free sharing site.

Either way thanks for this experiment of yours :)
Posted 13 years ago
voyagerfan5761 says:
This sounds like an awesome project. How about putting it up on GitHub?
Posted 13 years ago
hibbers says:
I would LOVE to know how to implement this - haven't used the API before - if you have time to write a how-to that would be awesome :-)
Posted 13 years ago
Log in to post a reply.