|
|
(One intermediate revision by the same user 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.