|
|
(2 intermediate revisions by 2 users not shown) |
Line 2: |
Line 2: |
| {{Script info | | {{Script info |
| |author=Richard Fearn | | |author=Richard Fearn |
− | |short=mythcal is a script that will synchronise | + | |long=Sync your MythTV recording schedule to a Google calendar |
− | your MythTV recordings to a Google calendar. | |
| |category=Python Scripts | | |category=Python Scripts |
| |name=mythcal.py | | |name=mythcal.py |
| |S23=yes | | |S23=yes |
| |S231=yes | | |S231=yes |
− | |S24=yes}} | + | |S24=yes |
− | | + | |S25=yes |
− | == Introduction ==
| + | |S26=yes |
− | mythcal is a simple script that synchronizes your MythTV recording schedule to
| + | |S27=yes |
− | a Google calendar.
| + | }} |
− | | |
− | Leaving my MythTV server on 24/7 wastes electricity, but I don't want to forget
| |
− | to turn it on to record programmes. I wrote this to make it easy to keep track
| |
− | of upcoming recordings.
| |
− | | |
− | Up to date source can be found here: http://code.google.com/p/mythcal/
| |
− | | |
− | | |
− | == Getting started ==
| |
− | | |
− | 1. Create a calendar in Google Calendar that will hold your MythTV programmes.
| |
− | DO NOT USE YOUR MAIN GOOGLE CALENDAR! YOU MUST CREATE A NEW CALENDAR!
| |
− | mythcal synchronises your programmes by deleting everything from the
| |
− | calendar, then adding an event for each programme.
| |
− | | |
− | 2. Create a new directory somewhere on your MythTV server and copy the script
| |
− | (mythcal) and the template configuration file (mythcal.conf.template) into
| |
− | the directory.
| |
− | | |
− | 3. Install the required packages:
| |
− | | |
− | * MythTV Python bindings ("libmyth-python" for Ubuntu; "python-MythTV"
| |
− | from RPM Fusion free for Fedora).
| |
− | | |
− | * pytz ("python-tz" for Debian and Ubuntu; "pytz" for Fedora).
| |
− | | |
− | * Google Data Python Client Library ("python-gdata" for Debian/Ubuntu/
| |
− | Fedora).
| |
− | | |
− | 4. Copy the template configuration file, mythcal.conf.template, to
| |
− | mythcal.conf, and add the missing settings. The sections are:
| |
− | | |
− | * [mythtv] - details about your MythTV server. "timezone" should be a
| |
− | zoneinfo time zone name such as "Europe/London". To find an appropriate
| |
− | time zone name, use the supplied 'timezones' script (see below).
| |
− | | |
− | * [google] - your Google username and password.
| |
− | | |
− | * [calendar] - details about your Google Calendar. "name" means the
| |
− | name of the calendar. To find the "id", go into Google Calendar Settings,
| |
− | open the "Calendars" tab, click the calendar you want to use, and look
| |
− | in the "Calendar Address" section. The Calendar ID should be displayed:
| |
− | it will look something like "abc123def@group.calendar.google.com". Please
| |
− | remember: USE A SEPARATE CALENDAR FOR MYTHCAL, or your appointments will
| |
− | be deleted! "max_batch_size" is the maximum number of events that will be
| |
− | combined into a single batch request when adding events. 25 should be OK.
| |
− | | |
− | 5. Run mythcal manually with the "--dry-run" or "-n" option. This will tell you
| |
− | what's going to be changed in your Google Calendar. Hopefully it won't tell
| |
− | you that your important appointments are going to be deleted, because you'll
| |
− | be using a *separate* calendar for mythcal!
| |
− | | |
− | 6. If everything looks good, set up a cron job which will execute mythcal as
| |
− | often as you like. The command being run should look something like this:
| |
− | | |
− | cd /path/to/mythcal/directory && ./mythcal
| |
− | | |
− | == Finding an appropriate time zone name ==
| |
− | | |
− | The 'timezones' script can be used to find an appropriate time zone name.
| |
− | Running it with no parameters lists all time zone names. If a parameter is
| |
− | given, the script does a case-insensitive search for time zones containing
| |
− | that text. For example:
| |
− | | |
− | $ ./timezones york
| |
− | America/New_York
| |
− | | |
− | == Troubleshooting ==
| |
− | | |
− | If you receive the error "StopIteration" at line 170 then your calendar is not setup correctly in Google
| |
− | | |
− | == Acknowledgements ==
| |
− | | |
− | Thanks to Michael T. Dean and Raymond Wagner for pointing me in the direction
| |
− | of the MythTV Python bindings.
| |
− | | |
− | {{Code box|mythcal.py|
| |
− | <pre>
| |
− | #! /usr/bin/python -W ignore
| |
− | | |
− | # mythcal
| |
− | | |
− | # Copyright 2009 Richard Fearn
| |
− | #
| |
− | # Licensed under the Apache License, Version 2.0 (the "License");
| |
− | # you may not use this file except in compliance with the License.
| |
− | # You may obtain a copy of the License at
| |
− | #
| |
− | # http://www.apache.org/licenses/LICENSE-2.0
| |
− | #
| |
− | # Unless required by applicable law or agreed to in writing, software
| |
− | # distributed under the License is distributed on an "AS IS" BASIS,
| |
− | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
| |
− | # See the License for the specific language governing permissions and
| |
− | # limitations under the License.
| |
− | | |
− | CONFIG_FILE = "mythcal.conf"
| |
− | CACHE_FILE = "mythcal.cache"
| |
− | | |
− | DATE_FORMAT = "%Y-%m-%d"
| |
− | DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
| |
− | | |
− | import ConfigParser
| |
− | from optparse import Values, OptionParser
| |
− | | |
− | from datetime import datetime
| |
− | import pickle
| |
− | import os
| |
− | import sys
| |
− | | |
− | from MythTV import MythBE
| |
− | import pytz
| |
− | import gdata.calendar.service
| |
− | import atom
| |
− | import time
| |
− | | |
− | config = ConfigParser.RawConfigParser()
| |
− | config.read(CONFIG_FILE)
| |
− | | |
− | settings = Values({
| |
− | "mythtv": Values({
| |
− | "timezone": config.get("mythtv", "timezone")
| |
− | }),
| |
− | "google": Values({
| |
− | "username": config.get("google", "username"),
| |
− | "password": config.get("google", "password")
| |
− | }),
| |
− | "calendar": Values({
| |
− | "name": config.get("calendar", "name"),
| |
− | "id": config.get("calendar", "id"),
| |
− | "max_batch_size" : int(config.get("calendar", "max_batch_size"))
| |
− | })
| |
− | })
| |
− | | |
− | parser = OptionParser()
| |
− | parser.add_option("-n", "--dry-run", action="store_true", dest="dry_run", default=False, help="perform a trial run; don't make any changes")
| |
− | (options, args) = parser.parse_args()
| |
− | | |
− | # get pytz timezone object for local time zone
| |
− | if settings.mythtv.timezone not in pytz.all_timezones:
| |
− | print >>sys.stderr, "mythcal: timezone name '%s' is not recognised" % settings.mythtv.timezone
| |
− | sys.exit(1)
| |
− | time_zone = pytz.timezone(settings.mythtv.timezone)
| |
− | | |
− | def naive_local_time_to_naive_utc_time(naive_local_time):
| |
− | """Convert naive local time to naive UTC time"""
| |
− | aware_local_time = time_zone.localize(naive_local_time)
| |
− | aware_utc_time = aware_local_time.astimezone(pytz.utc)
| |
− | naive_utc_time = aware_utc_time.replace(tzinfo=None)
| |
− | return naive_utc_time
| |
− | | |
− | def convert_program(prog):
| |
− | """Converts a MythTV Program object to a dictionary"""
| |
− | return {
| |
− | "title": prog.title,
| |
− | "subtitle": prog.subtitle,
| |
− | "channel": prog.channame,
| |
− | "start": naive_local_time_to_naive_utc_time(prog.starttime),
| |
− | "end": naive_local_time_to_naive_utc_time(prog.endtime),
| |
− | "description": prog.description
| |
− | }
| |
− | | |
− | def sort_programs_by_start(p1, p2):
| |
− | return cmp(p1["start"], p2["start"])
| |
− | | |
− | def get_recordings_from_backend():
| |
− | """Gets current and upcoming recordings from MythTV"""
| |
− | | |
− | mythtv = MythBE()
| |
− | | |
− | upcoming = mythtv.getUpcomingRecordings()
| |
− | upcoming = map(convert_program, upcoming)
| |
− | upcoming.sort(sort_programs_by_start)
| |
− | | |
− | current = []
| |
− | for recorder in mythtv.getRecorderList():
| |
− | # str(...) required due to MythTV issue #7648
| |
− | # see http://svn.mythtv.org/trac/ticket/7648
| |
− | if mythtv.isRecording(str(recorder)):
| |
− | current.append(mythtv.getCurrentRecording(str(recorder)))
| |
− | current = map(convert_program, current)
| |
− | current.sort(sort_programs_by_start)
| |
− | | |
− | return {"current": current, "future": upcoming}
| |
| | | |
− | # get recordings from MythTV backend
| + | mythcal is a simple script that synchronizes your MythTV recording schedule to a Google calendar. |
− | recordings = get_recordings_from_backend()
| |
| | | |
− | # load recording list from last time
| + | The GitHub project for mythcal can be found here: [https://github.com/richardfearn/mythcal richardfearn/mythcal] |
− | last_recordings = None
| |
− | if os.path.exists(CACHE_FILE):
| |
− | f = open(CACHE_FILE, "r")
| |
− | last_recordings = pickle.load(f)
| |
− | f.close()
| |
| | | |
− | def submit_batch_request(request, url):
| + | Instructions for using mythcal can be found in the [https://github.com/richardfearn/mythcal/blob/master/README.md README]. |
− | response_feed = calendar_service.ExecuteBatch(request, url)
| |
− | # for entry in response_feed.entry:
| |
− | # print "%s; status %s; reason %s" % (entry.batch_id.text, entry.batch_status.code, entry.batch_status.reason)
| |
− | | |
− | def delete_existing_events():
| |
− | """Deletes all events from the calendar"""
| |
− | | |
− | if options.dry_run:
| |
− | print "Deleting existing entries..."
| |
− | event_feed = calendar_service.GetCalendarEventFeed(cal.content.src)
| |
− | while event_feed and len(event_feed.entry):
| |
− | if not options.dry_run:
| |
− | batch_request = gdata.calendar.CalendarEventFeed()
| |
− | for event in event_feed.entry:
| |
− | if options.dry_run:
| |
− | print " deleting \"%s\"" % event.title.text
| |
− | else:
| |
− | event.batch_id = gdata.BatchId(text="delete-request")
| |
− | batch_request.AddDelete(entry=event)
| |
− | if options.dry_run:
| |
− | if event_feed.GetNextLink():
| |
− | event_feed = calendar_service.GetCalendarEventFeed(event_feed.GetNextLink().href)
| |
− | else:
| |
− | event_feed = None
| |
− | else:
| |
− | submit_batch_request(batch_request, batch_url)
| |
− | event_feed = calendar_service.GetCalendarEventFeed(cal.content.src)
| |
− | if options.dry_run:
| |
− | print "Existing entries deleted."
| |
− | | |
− | # update calendar, and output new recording list, if different
| |
− | if recordings != last_recordings:
| |
− | | |
− | # get calendar service and log in
| |
− | calendar_service = gdata.calendar.service.CalendarService()
| |
− | calendar_service.email = settings.google.username
| |
− | calendar_service.password = settings.google.password
| |
− | calendar_service.source = "mythcal"
| |
− | calendar_service.ProgrammaticLogin()
| |
− | | |
− | # get MythTV calendar
| |
− | calendars_feed = calendar_service.GetOwnCalendarsFeed()
| |
− | cal = (c for c in calendars_feed.entry if c.title.text == settings.calendar.name).next()
| |
− | batch_url = "http://www.google.com/calendar/feeds/%s/private/full/batch" % settings.calendar.id
| |
− | | |
− | delete_existing_events()
| |
− | | |
− | def create_event(title, start, end, content=None):
| |
− | event = gdata.calendar.CalendarEventEntry()
| |
− | event.title = atom.Title(text=title)
| |
− | if content:
| |
− | event.content = atom.Content(text=content)
| |
− | event.when.append(gdata.calendar.When(start_time=start, end_time=end))
| |
− | return event
| |
− | | |
− | def create_all_day_event(title, start, end, content=None):
| |
− | event_start = time.strftime(DATE_FORMAT, start)
| |
− | event_end = time.strftime(DATE_FORMAT, end)
| |
− | return create_event(title=title, start=event_start, end=event_end, content=content)
| |
− | | |
− | def create_programme_event(title, subtitle, channel, start, end, content=None):
| |
− | if subtitle:
| |
− | event_title = "%s: %s (%s)" % (title, subtitle, channel)
| |
− | else:
| |
− | event_title = "%s (%s)" % (title, channel)
| |
− | event_start = time.strftime(DATE_TIME_FORMAT, start)
| |
− | event_end = time.strftime(DATE_TIME_FORMAT, end)
| |
− | return create_event(title=event_title, start=event_start, end=event_end, content=content)
| |
− | | |
− | if options.dry_run:
| |
− | print "Adding new entries..."
| |
− | | |
− | # add an event for current/future recordings
| |
− | if not options.dry_run:
| |
− | request_feed = gdata.calendar.CalendarEventFeed()
| |
− | for prog in recordings["current"] + recordings["future"]:
| |
− | if options.dry_run:
| |
− | print " adding \"%s\"" % prog["title"]
| |
− | else:
| |
− | event = create_programme_event(prog["title"], prog["subtitle"], prog["channel"], prog["start"].timetuple(), prog["end"].timetuple(), prog["description"])
| |
− | event.batch_id = gdata.BatchId(text="insert-request")
| |
− | request_feed.AddInsert(entry=event)
| |
− | if len(request_feed.entry) == settings.calendar.max_batch_size:
| |
− | submit_batch_request(request_feed, batch_url)
| |
− | request_feed = gdata.calendar.CalendarEventFeed()
| |
− | | |
− | # add 'last updated' event
| |
− | last_update_text = "MythTV updated %s" % time.strftime("%H:%M", time.localtime())
| |
− | if options.dry_run:
| |
− | print " adding \"%s\"" % last_update_text
| |
− | else:
| |
− | event = create_all_day_event(title=last_update_text, start=time.gmtime(), end=time.gmtime(time.time() + 24*60*60))
| |
− | event.batch_id = gdata.BatchId(text="insert-request")
| |
− | request_feed.AddInsert(entry=event)
| |
− | submit_batch_request(request_feed, batch_url)
| |
− | | |
− | if options.dry_run:
| |
− | print "New entries added."
| |
− | | |
− | # update last recording list
| |
− | if options.dry_run:
| |
− | print "Updating cache..."
| |
− | else:
| |
− | f = open(CACHE_FILE, "w")
| |
− | pickle.dump(recordings, f)
| |
− | f.close()
| |
− | | |
− | if options.dry_run:
| |
− | print "Done."
| |
− | </pre>
| |
− | }}
| |
| | | |
| [[Category:Python_Scripts]] | | [[Category:Python_Scripts]] |
mythcal is a simple script that synchronizes your MythTV recording schedule to a Google calendar.