[totem] Fixed linebreaks in iPlayer library



commit f34716d59f11530a9c3a956704abf56fd88d1f33
Author: Philip Withnall <philip tecnocode co uk>
Date:   Sun Apr 26 09:42:32 2009 +0100

    Fixed linebreaks in iPlayer library
---
 src/plugins/iplayer/iplayer2.py | 2046 +++++++++++++++++++-------------------
 1 files changed, 1023 insertions(+), 1023 deletions(-)

diff --git a/src/plugins/iplayer/iplayer2.py b/src/plugins/iplayer/iplayer2.py
index 6555198..14c5f78 100644
--- a/src/plugins/iplayer/iplayer2.py
+++ b/src/plugins/iplayer/iplayer2.py
@@ -1,1027 +1,1027 @@
-#!/usr/bin/python
-
-# Python libs
-import re, time, os, sys
-import urllib2
-import logging
-from pprint import pformat
-from socket import timeout as SocketTimeoutError
-from time import time
+#!/usr/bin/python
+
+# Python libs
+import re, time, os, sys
+import urllib2
+import logging
+from pprint import pformat
+from socket import timeout as SocketTimeoutError
+from time import time
 
 import totem
-
-# external libs
-import httplib2
-import feedparser
-import listparser
-from BeautifulSoup import BeautifulStoneSoup
-
-IMG_DIR = os.path.join(os.getcwd(), 'resources', 'media')
-
-try:
-    logging.basicConfig(
-        filename='iplayer2.log', 
-        filemode='w',
-        format='%(asctime)s %(levelname)4s %(message)s',
-        level=logging.DEBUG
-    )
-except IOError:
-    #print "iplayer2 logging to stdout"
-    logging.basicConfig(
-        stream=sys.stdout,
-        level=logging.DEBUG,
-        format='iplayer2.py: %(asctime)s %(levelname)4s %(message)s',
-    )    
-# me want 2.5!!!
-def any(iterable):
-     for element in iterable:
-         if element:
-             return True
-     return False
-
-
-# http://colinm.org/blog/on-demand-loading-of-flickr-photo-metadata
-# returns immediately for all previously-called functions
-def call_once(fn):
-    called_by = {}
-    def result(self):
-        if self in called_by:
-            return
-        called_by[self] = True
-        fn(self)
-    return result
-
-# runs loader before decorated function
-def loaded_by(loader):
-    def decorator(fn):
-        def result(self, *args, **kwargs):
-            loader(self)
-            return fn(self, *args, **kwargs)
-        return result
-    return decorator
-
-channels_tv_list = [
-    ('bbc_one', 'BBC One'), 
-    ('bbc_two', 'BBC Two'), 
-    ('bbc_three', 'BBC Three'), 
-    ('bbc_four', 'BBC Four'),
-    ('cbbc', 'CBBC'),
-    ('cbeebies', 'CBeebies'),
-    ('bbc_news24', 'BBC News Channel'),
-    ('bbc_parliament', 'BBC Parliament'),
-    ('bbc_one_northern_ireland', 'BBC One Northern Ireland'), 
-    ('bbc_one_scotland', 'BBC One Scotland'),
-    ('bbc_one_wales', 'BBC One Wales'),
-    ('bbc_alba', 'BBC Alba'),
-]
-channels_tv = dict(channels_tv_list)
-channels_national_radio_list = [
-    ('bbc_radio_one', 'Radio 1'), 
-    ('bbc_1xtra', '1 Xtra'),
-    ('bbc_radio_two', 'Radio 2'), 
-    ('bbc_radio_three', 'Radio 3'), 
-    ('bbc_radio_four', 'Radio 4'), 
-    ('bbc_radio_five_live', '5 Live'), 
-    ('bbc_radio_five_live_sports_extra', '5 Live Sports Extra'), 
-    ('bbc_6music', '6 Music'), 
-    ('bbc_7', 'BBC 7'),
-    ('bbc_asian_network', 'Asian Network'),
-    ('bbc_radio_scotland', 'BBC Scotland'),
-    ('bbc_radio_ulster', 'BBC Ulster'),
-    ('bbc_radio_foyle', 'Radio Foyle'),
-    ('bbc_radio_wales', 'BBC Wales'),
-    ('bbc_radio_cymru', 'BBC Cymru'),
-    ('bbc_world_service', 'World Service'),
-    ('bbc_radio_nan_gaidheal', 'BBC nan Gaidheal')
-]
-channels_local_radio_list = [
-    ('bbc_radio_cumbria', 'BBC Cumbria'),
-    ('bbc_radio_newcastle', 'BBC Newcastle'),
-    ('bbc_tees', 'BBC Tees'),
-    ('bbc_radio_lancashire', 'BBC Lancashire'),
-    ('bbc_radio_merseyside', 'BBC Merseyside'),
-    ('bbc_radio_manchester', 'BBC Manchester'),
-    ('bbc_radio_leeds', 'BBC Leeds'),
-    ('bbc_radio_sheffield', 'BBC Sheffield'),
-    ('bbc_radio_york', 'BBC York'),
-    ('bbc_radio_humberside', 'BBC Humberside'),
-    ('bbc_radio_lincolnshire', 'BBC Lincolnshire'),
-    ('bbc_radio_nottingham', 'BBC Nottingham'),
-    ('bbc_radio_leicester', 'BBC Leicester'),
-    ('bbc_radio_derby', 'BBC Derby'),
-    ('bbc_radio_stoke', 'BBC Stoke'),
-    ('bbc_radio_shropshire', 'BBC Shropshire'),
-    ('bbc_wm', 'BBC WM'),
-    ('bbc_radio_coventry_warwickshire', 'BBC Coventry Warwickshire'),
-    ('bbc_radio_hereford_worcester', 'BBC Hereford/Worcester'),
-    ('bbc_radio_northampton', 'BBC Northampton'),
-    ('bbc_three_counties_radio', 'BBC Three Counties Radio'),
-    ('bbc_radio_cambridge', 'BBC Cambridge'),
-    ('bbc_radio_norfolk', 'BBC Norfolk'),
-    ('bbc_radio_suffolk', 'BBC Suffolk'),
-    ('bbc_radio_essex', 'BBC Essex'),
-    ('bbc_london', 'BBC London'),
-    ('bbc_radio_kent', 'BBC Kent'),
-    ('bbc_southern_counties_radio', 'BBC Southern Counties Radio'),
-    ('bbc_radio_oxford', 'BBC Oxford'),
-    ('bbc_radio_berkshire', 'BBC Berkshire'),
-    ('bbc_radio_solent', 'BBC Solent'),
-    ('bbc_radio_gloucestershire', 'BBC Gloucestershire'),
-    ('bbc_radio_swindon', 'BBC Swindon'),
-    ('bbc_radio_wiltshire', 'BBC Wiltshire'),
-    ('bbc_radio_bristol', 'BBC Bristol'),
-    ('bbc_radio_somerset_sound', 'BBC Somerset'),
-    ('bbc_radio_devon', 'BBC Devon'),
-    ('bbc_radio_cornwall', 'BBC Cornwall'),
-    ('bbc_radio_guernsey', 'BBC Guernsey'),
-    ('bbc_radio_jersey', 'BBC Jersey')
-]
-channels_logos = {
-    'bbc_radio_cumbria': 'http://www.bbc.co.uk/englandcms/localradio/images/cumbria.gif',
-    'bbc_radio_newcastle': 'http://www.bbc.co.uk/englandcms/localradio/images/newcastle.gif',
-    'bbc_tees': 'http://www.bbc.co.uk/englandcms/localradio/images/tees.gif',
-    'bbc_radio_lancashire': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_lancs.gif',
-    'bbc_radio_merseyside': 'http://www.bbc.co.uk/englandcms/localradio/images/merseyside.gif',
-    'bbc_radio_manchester': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
-    'bbc_radio_leeds': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_leeds.gif',
-    'bbc_radio_sheffield': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_sheffield.gif',
-    'bbc_radio_york': 'http://www.bbc.co.uk/englandcms/localradio/images/york.gif',
-    'bbc_radio_humberside': 'http://www.bbc.co.uk/radio/images/home/r-home-nation-regions.gif',
-    'bbc_radio_lincolnshire': 'http://www.bbc.co.uk/englandcms/localradio/images/lincs.gif',
-    'bbc_radio_nottingham': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
-    'bbc_radio_leicester': 'http://www.bbc.co.uk/englandcms/localradio/images/leicester.gif',
-    'bbc_radio_derby': 'http://www.bbc.co.uk/englandcms/derby/images/rh_nav170_derby.gif',
-    'bbc_radio_stoke': 'http://www.bbc.co.uk/englandcms/localradio/images/stoke.gif',
-    'bbc_radio_shropshire': 'http://www.bbc.co.uk/englandcms/localradio/images/shropshire.gif',
-    'bbc_wm': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
-    'bbc_radio_coventry_warwickshire': 'http://www.bbc.co.uk/englandcms/localradio/images/cov_warks.gif',
-    'bbc_radio_hereford_worcester': 'http://www.bbc.co.uk/englandcms/localradio/images/hereford_worcester.gif',
-    'bbc_radio_northampton': 'http://www.bbc.co.uk/englandcms/localradio/images/northampton.gif',
-    'bbc_three_counties_radio': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_3counties.gif',
-    'bbc_radio_cambridge': 'http://www.bbc.co.uk/englandcms/localradio/images/cambridgeshire.gif',
-    'bbc_radio_norfolk': 'http://www.bbc.co.uk/englandcms/localradio/images/norfolk.gif',
-    'bbc_radio_suffolk': 'http://www.bbc.co.uk/englandcms/localradio/images/suffolk.gif',
-    'bbc_radio_essex': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_essex.gif',
-    'bbc_london': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
-    'bbc_radio_kent': 'http://www.bbc.co.uk/radio/images/home/r-home-nation-regions.gif',
-    'bbc_southern_counties_radio': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
-    'bbc_radio_oxford': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_oxford.gif',
-    'bbc_radio_berkshire': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_berks.gif',
-    'bbc_radio_solent': 'http://www.bbc.co.uk/englandcms/localradio/images/solent.gif',
-    'bbc_radio_gloucestershire': 'http://www.bbc.co.uk/englandcms/localradio/images/gloucestershire.gif',
-    'bbc_radio_swindon': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
-    'bbc_radio_wiltshire': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
-    'bbc_radio_bristol': 'http://www.bbc.co.uk/englandcms/localradio/images/bristol.gif',
-    'bbc_radio_somerset_sound': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_somerset.gif',
-    'bbc_radio_devon': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_devon.gif',
-    'bbc_radio_cornwall': 'http://www.bbc.co.uk/englandcms/localradio/images/cornwall.gif',
-    'bbc_radio_guernsey': 'http://www.bbc.co.uk/englandcms/localradio/images/guernsey.gif',
-    'bbc_radio_jersey': 'http://www.bbc.co.uk/englandcms/localradio/images/jersey.gif'
-}
-
-
-channels_national_radio = dict(channels_national_radio_list)
-channels_local_radio = dict(channels_local_radio_list)
-channels_radio_list = channels_national_radio_list + channels_local_radio_list
-channels_radio = dict(channels_radio_list)
-
-channels = dict(channels_tv_list + channels_radio_list)
-categories_list = [
-    ('childrens', 'Children\'s'),
-    ('comedy', 'Comedy'),
-    ('drama', 'Drama'),
-    ('entertainment', 'Entertainment'),
-    ('factual', 'Factual'),
-    ('music', 'Music'),
-    ('news', 'News'),
-    ('religion_and_ethics', 'Religion & Ethics'),
-    ('sport', 'Sport'),
-    ('olympics', 'Olympics'),
-    ('wales', 'Wales'),
-    ('signed', 'Sign Zone')
-]
-categories = dict(categories_list)
-
-live_radio_stations = {'Radio 1': 'http://www.bbc.co.uk/radio1/wm_asx/aod/radio1_hi.asx',
-                       '1 Xtra':  'http://www.bbc.co.uk/1xtra/realmedia/1xtra_hi.asx',
-                       'Radio 2': 'http://www.bbc.co.uk/radio2/wm_asx/aod/radio2_hi.asx',
-                       'BBC 3': 'http://www.bbc.co.uk/radio3/wm_asx/aod/radio3_hi.asx',
-                       'BBC 4': 'http://www.bbc.co.uk/radio4/wm_asx/aod/radio4.asx',
-                       '5 Live':  'http://www.bbc.co.uk/fivelive/live/live.asx',
-                       '5 Live Sports Extra': 'http://www.bbc.co.uk/fivelive/live/live_sportsextra.asx',
-                       '6 Music': 'http://www.bbc.co.uk/6music/ram/6music_hi.asx',
-                       'BBC 7':   'http://www.bbc.co.uk/bbc7/realplayer/bbc7_hi.asx',
-                       'Asian Network': 'http://www.bbc.co.uk/asiannetwork/rams/asiannet_hi.asx',
-                       'Radio Scotland': 'http://www.bbc.co.uk/scotland/radioscotland/media/radioscotland.ram',
-                       'World Service': 'http://www.bbc.co.uk/worldservice/meta/tx/nb/live_eneuk_au_nb.asx',
-                       'BBC nan Gaidheal': 'http://www.bbc.co.uk/scotland/alba/media/live/radio_ng.ram',
-                       'BBC London': 'http://www.bbc.co.uk/england/realmedia/live/localradio/london.ram',
-                       'BBC Berkshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/radioberkshire.ram',
-                       'BBC Bristol': 'http://www.bbc.co.uk/england/realmedia/live/localradio/bristol.ram',
-                       'BBC Cambridgeshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/cambridgeshire.ram',
-                       'BBC Cornwall': 'http://www.bbc.co.uk/england/realmedia/live/localradio/cornwall.ram',
-                       'BBC Cumbria': 'http://www.bbc.co.uk/england/realmedia/live/localradio/cumbria.ram',
-                       'BBC Derby': 'http://www.bbc.co.uk/england/realmedia/live/localradio/derby.ram',
-                       'BBC Devon': 'http://www.bbc.co.uk/england/realmedia/live/localradio/devon.ram',
-                       'BBC Essex': 'http://www.bbc.co.uk/england/realmedia/live/localradio/essex.ram',
-                       'BBC Gloucestershire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/gloucestershire.ram',
-                       'BBC Guernsey': 'http://www.bbc.co.uk/england/realmedia/live/localradio/guernsey.ram',
-                       'BBC Hereford/Worcester': 'http://www.bbc.co.uk/england/realmedia/live/localradio/herefordandworcester.ram',
-                       'BBC Humberside': 'http://www.bbc.co.uk/england/realmedia/live/localradio/humberside.ram',
-                       'BBC Jersey': 'http://www.bbc.co.uk/england/realmedia/live/localradio/jersey.ram',
-                       'BBC Kent': 'http://www.bbc.co.uk/england/realmedia/live/localradio/kent.ram',
-                       'BBC Lancashire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/lancashire.ram',
-                       'BBC Leeds': 'http://www.bbc.co.uk/england/realmedia/live/localradio/leeds.ram',
-                       'BBC Leicester': 'http://www.bbc.co.uk/england/realmedia/live/localradio/leicester.ram',
-                       'BBC Lincolnshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/lincolnshire.ram',
-                       'BBC Manchester': 'http://www.bbc.co.uk/england/realmedia/live/localradio/manchester.ram',
-                       'BBC Merseyside': 'http://www.bbc.co.uk/england/realmedia/live/localradio/merseyside.ram',
-                       'BBC Newcastle': 'http://www.bbc.co.uk/england/realmedia/live/localradio/newcastle.ram',
-                       'BBC Norfolk': 'http://www.bbc.co.uk/england/realmedia/live/localradio/norfolk.ram',
-                       'BBC Northampton': 'http://www.bbc.co.uk/england/realmedia/live/localradio/northampton.ram',
-                       'BBC Nottingham': 'http://www.bbc.co.uk/england/realmedia/live/localradio/nottingham.ram',
-                       'BBC Oxford': 'http://www.bbc.co.uk/england/realmedia/live/localradio/radiooxford.ram',
-                       'BBC Sheffield': 'http://www.bbc.co.uk/england/realmedia/live/localradio/sheffield.ram',
-                       'BBC Shropshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/shropshire.ram',
-                       'BBC Solent': 'http://www.bbc.co.uk/england/realmedia/live/localradio/solent.ram',
-                       'BBC Somerset Sound': 'http://www.bbc.co.uk/england/realmedia/live/localradio/somerset.ram',
-                       'BBC Southern Counties Radio': 'http://www.bbc.co.uk/england/realmedia/live/localradio/southerncounties.ram',
-                       'BBC Stoke': 'http://www.bbc.co.uk/england/realmedia/live/localradio/stoke.ram',
-                       'BBC Suffolk': 'http://www.bbc.co.uk/england/realmedia/live/localradio/suffolk.ram',
-                       'BBC Swindon': 'http://www.bbc.co.uk/england/realmedia/live/localradio/swindon.ram',
-                       'BBC Three Counties Radio': 'http://www.bbc.co.uk/england/realmedia/live/localradio/threecounties.ram',
-                       'BBC Wiltshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/wiltshire.ram',
-                       'BBC York': 'http://www.bbc.co.uk/england/realmedia/live/localradio/york.ram',
-                       'BBC WM': 'http://www.bbc.co.uk/england/realmedia/live/localradio/wm.ram',
-                       'BBC Cymru': 'http://www.bbc.co.uk/cymru/live/rc-live.ram',
-                       'Radio Foyle': 'http://www.bbc.co.uk/northernireland/realmedia/rf-live.ram',
-                       'BBC Scotland': 'http://www.bbc.co.uk/scotland/radioscotland/media/radioscotland.ram',
-                       'BBC nan Gaidheal': 'http://www.bbc.co.uk/scotland/alba/media/live/radio_ng.ram',
-                       'BBC Ulster': 'http://www.bbc.co.uk/ni/realmedia/ru-live.ram',
-                       'BBC Wales': 'http://www.bbc.co.uk/wales/live/rwg2.ram',
-                       'BBC Tees': 'http://www.bbc.co.uk/england/realmedia/live/localradio/cleveland.ram',
-                       
-                       }
-live_webcams = {'Radio 1': 'http://www.bbc.co.uk/radio1/webcam/images/live/webcam.jpg',
-                '1 Xtra':  'http://www.bbc.co.uk/1xtra/webcam/live/1xtraa.jpg',
-                'Radio 2': 'http://www.bbc.co.uk/radio2/webcam/live/radio2.jpg',
-                '5 Live':  'http://www.bbc.co.uk/fivelive/inside/webcam/5Lwebcam1.jpg',
-                '6 Music': 'http://www.bbc.co.uk/6music/webcam/live/6music.jpg',
-                'Asian Network': 'http://www.bbc.co.uk/asiannetwork/webcams/birmingham.jpg'}
-
-rss_cache = {}
-
-self_closing_tags = ['alternate', 'mediator']
-
-http = httplib2.Http()
-
-re_selfclose = re.compile('<([a-zA-Z0-9]+)( ?.*)/>', re.M | re.S)
-
-def fix_selfclosing(xml):
-    return re_selfclose.sub('<\\1\\2></\\1>', xml)
-
-def set_http_cache_dir(d):
-    fc = httplib2.FileCache(d)
-    http.cache = fc
-
-def set_http_cache(c):
-    http.cache = c
-
-class NoItemsError(Exception):
-    def __init__(self, reason=None):
-        self.reason = reason
-    
-    def __str__(self):
-        reason = self.reason or '<no reason given>'
-        return "Programme unavailable ('%s')" % (reason)
-    
-
-class memoize(object):
-    def __init__(self, func):
-        self.func = func
-        self._cache = {}
-    def __call__(self, *args, **kwds):
-        key = args
-        if kwds:
-            items = kwds.items()
-            items.sort()
-            key = key + tuple(items)
-        if key in self._cache:
-            return self._cache[key]
-        self._cache[key] = result = self.func(*args, **kwds)
-        return result
-
-def httpretrieve(url, filename):
-    _, data = http.request(url, 'GET')
-    f = open(filename, 'wb')
-    f.write(data)
-    f.close() 
-
-def httpget(url):
-    resp = ''
-    data = ''
-    try:
-        resp, data = http.request(url, 'GET')
-    except:
+
+# external libs
+import httplib2
+import feedparser
+import listparser
+from BeautifulSoup import BeautifulStoneSoup
+
+IMG_DIR = os.path.join(os.getcwd(), 'resources', 'media')
+
+try:
+    logging.basicConfig(
+        filename='iplayer2.log', 
+        filemode='w',
+        format='%(asctime)s %(levelname)4s %(message)s',
+        level=logging.DEBUG
+    )
+except IOError:
+    #print "iplayer2 logging to stdout"
+    logging.basicConfig(
+        stream=sys.stdout,
+        level=logging.DEBUG,
+        format='iplayer2.py: %(asctime)s %(levelname)4s %(message)s',
+    )    
+# me want 2.5!!!
+def any(iterable):
+     for element in iterable:
+         if element:
+             return True
+     return False
+
+
+# http://colinm.org/blog/on-demand-loading-of-flickr-photo-metadata
+# returns immediately for all previously-called functions
+def call_once(fn):
+    called_by = {}
+    def result(self):
+        if self in called_by:
+            return
+        called_by[self] = True
+        fn(self)
+    return result
+
+# runs loader before decorated function
+def loaded_by(loader):
+    def decorator(fn):
+        def result(self, *args, **kwargs):
+            loader(self)
+            return fn(self, *args, **kwargs)
+        return result
+    return decorator
+
+channels_tv_list = [
+    ('bbc_one', 'BBC One'), 
+    ('bbc_two', 'BBC Two'), 
+    ('bbc_three', 'BBC Three'), 
+    ('bbc_four', 'BBC Four'),
+    ('cbbc', 'CBBC'),
+    ('cbeebies', 'CBeebies'),
+    ('bbc_news24', 'BBC News Channel'),
+    ('bbc_parliament', 'BBC Parliament'),
+    ('bbc_one_northern_ireland', 'BBC One Northern Ireland'), 
+    ('bbc_one_scotland', 'BBC One Scotland'),
+    ('bbc_one_wales', 'BBC One Wales'),
+    ('bbc_alba', 'BBC Alba'),
+]
+channels_tv = dict(channels_tv_list)
+channels_national_radio_list = [
+    ('bbc_radio_one', 'Radio 1'), 
+    ('bbc_1xtra', '1 Xtra'),
+    ('bbc_radio_two', 'Radio 2'), 
+    ('bbc_radio_three', 'Radio 3'), 
+    ('bbc_radio_four', 'Radio 4'), 
+    ('bbc_radio_five_live', '5 Live'), 
+    ('bbc_radio_five_live_sports_extra', '5 Live Sports Extra'), 
+    ('bbc_6music', '6 Music'), 
+    ('bbc_7', 'BBC 7'),
+    ('bbc_asian_network', 'Asian Network'),
+    ('bbc_radio_scotland', 'BBC Scotland'),
+    ('bbc_radio_ulster', 'BBC Ulster'),
+    ('bbc_radio_foyle', 'Radio Foyle'),
+    ('bbc_radio_wales', 'BBC Wales'),
+    ('bbc_radio_cymru', 'BBC Cymru'),
+    ('bbc_world_service', 'World Service'),
+    ('bbc_radio_nan_gaidheal', 'BBC nan Gaidheal')
+]
+channels_local_radio_list = [
+    ('bbc_radio_cumbria', 'BBC Cumbria'),
+    ('bbc_radio_newcastle', 'BBC Newcastle'),
+    ('bbc_tees', 'BBC Tees'),
+    ('bbc_radio_lancashire', 'BBC Lancashire'),
+    ('bbc_radio_merseyside', 'BBC Merseyside'),
+    ('bbc_radio_manchester', 'BBC Manchester'),
+    ('bbc_radio_leeds', 'BBC Leeds'),
+    ('bbc_radio_sheffield', 'BBC Sheffield'),
+    ('bbc_radio_york', 'BBC York'),
+    ('bbc_radio_humberside', 'BBC Humberside'),
+    ('bbc_radio_lincolnshire', 'BBC Lincolnshire'),
+    ('bbc_radio_nottingham', 'BBC Nottingham'),
+    ('bbc_radio_leicester', 'BBC Leicester'),
+    ('bbc_radio_derby', 'BBC Derby'),
+    ('bbc_radio_stoke', 'BBC Stoke'),
+    ('bbc_radio_shropshire', 'BBC Shropshire'),
+    ('bbc_wm', 'BBC WM'),
+    ('bbc_radio_coventry_warwickshire', 'BBC Coventry Warwickshire'),
+    ('bbc_radio_hereford_worcester', 'BBC Hereford/Worcester'),
+    ('bbc_radio_northampton', 'BBC Northampton'),
+    ('bbc_three_counties_radio', 'BBC Three Counties Radio'),
+    ('bbc_radio_cambridge', 'BBC Cambridge'),
+    ('bbc_radio_norfolk', 'BBC Norfolk'),
+    ('bbc_radio_suffolk', 'BBC Suffolk'),
+    ('bbc_radio_essex', 'BBC Essex'),
+    ('bbc_london', 'BBC London'),
+    ('bbc_radio_kent', 'BBC Kent'),
+    ('bbc_southern_counties_radio', 'BBC Southern Counties Radio'),
+    ('bbc_radio_oxford', 'BBC Oxford'),
+    ('bbc_radio_berkshire', 'BBC Berkshire'),
+    ('bbc_radio_solent', 'BBC Solent'),
+    ('bbc_radio_gloucestershire', 'BBC Gloucestershire'),
+    ('bbc_radio_swindon', 'BBC Swindon'),
+    ('bbc_radio_wiltshire', 'BBC Wiltshire'),
+    ('bbc_radio_bristol', 'BBC Bristol'),
+    ('bbc_radio_somerset_sound', 'BBC Somerset'),
+    ('bbc_radio_devon', 'BBC Devon'),
+    ('bbc_radio_cornwall', 'BBC Cornwall'),
+    ('bbc_radio_guernsey', 'BBC Guernsey'),
+    ('bbc_radio_jersey', 'BBC Jersey')
+]
+channels_logos = {
+    'bbc_radio_cumbria': 'http://www.bbc.co.uk/englandcms/localradio/images/cumbria.gif',
+    'bbc_radio_newcastle': 'http://www.bbc.co.uk/englandcms/localradio/images/newcastle.gif',
+    'bbc_tees': 'http://www.bbc.co.uk/englandcms/localradio/images/tees.gif',
+    'bbc_radio_lancashire': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_lancs.gif',
+    'bbc_radio_merseyside': 'http://www.bbc.co.uk/englandcms/localradio/images/merseyside.gif',
+    'bbc_radio_manchester': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
+    'bbc_radio_leeds': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_leeds.gif',
+    'bbc_radio_sheffield': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_sheffield.gif',
+    'bbc_radio_york': 'http://www.bbc.co.uk/englandcms/localradio/images/york.gif',
+    'bbc_radio_humberside': 'http://www.bbc.co.uk/radio/images/home/r-home-nation-regions.gif',
+    'bbc_radio_lincolnshire': 'http://www.bbc.co.uk/englandcms/localradio/images/lincs.gif',
+    'bbc_radio_nottingham': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
+    'bbc_radio_leicester': 'http://www.bbc.co.uk/englandcms/localradio/images/leicester.gif',
+    'bbc_radio_derby': 'http://www.bbc.co.uk/englandcms/derby/images/rh_nav170_derby.gif',
+    'bbc_radio_stoke': 'http://www.bbc.co.uk/englandcms/localradio/images/stoke.gif',
+    'bbc_radio_shropshire': 'http://www.bbc.co.uk/englandcms/localradio/images/shropshire.gif',
+    'bbc_wm': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
+    'bbc_radio_coventry_warwickshire': 'http://www.bbc.co.uk/englandcms/localradio/images/cov_warks.gif',
+    'bbc_radio_hereford_worcester': 'http://www.bbc.co.uk/englandcms/localradio/images/hereford_worcester.gif',
+    'bbc_radio_northampton': 'http://www.bbc.co.uk/englandcms/localradio/images/northampton.gif',
+    'bbc_three_counties_radio': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_3counties.gif',
+    'bbc_radio_cambridge': 'http://www.bbc.co.uk/englandcms/localradio/images/cambridgeshire.gif',
+    'bbc_radio_norfolk': 'http://www.bbc.co.uk/englandcms/localradio/images/norfolk.gif',
+    'bbc_radio_suffolk': 'http://www.bbc.co.uk/englandcms/localradio/images/suffolk.gif',
+    'bbc_radio_essex': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_essex.gif',
+    'bbc_london': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
+    'bbc_radio_kent': 'http://www.bbc.co.uk/radio/images/home/r-home-nation-regions.gif',
+    'bbc_southern_counties_radio': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
+    'bbc_radio_oxford': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_oxford.gif',
+    'bbc_radio_berkshire': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_berks.gif',
+    'bbc_radio_solent': 'http://www.bbc.co.uk/englandcms/localradio/images/solent.gif',
+    'bbc_radio_gloucestershire': 'http://www.bbc.co.uk/englandcms/localradio/images/gloucestershire.gif',
+    'bbc_radio_swindon': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
+    'bbc_radio_wiltshire': os.path.join(IMG_DIR, 'bbc_local_radio.png'),
+    'bbc_radio_bristol': 'http://www.bbc.co.uk/englandcms/localradio/images/bristol.gif',
+    'bbc_radio_somerset_sound': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_somerset.gif',
+    'bbc_radio_devon': 'http://www.bbc.co.uk/englandcms/images/rh_nav170_devon.gif',
+    'bbc_radio_cornwall': 'http://www.bbc.co.uk/englandcms/localradio/images/cornwall.gif',
+    'bbc_radio_guernsey': 'http://www.bbc.co.uk/englandcms/localradio/images/guernsey.gif',
+    'bbc_radio_jersey': 'http://www.bbc.co.uk/englandcms/localradio/images/jersey.gif'
+}
+
+
+channels_national_radio = dict(channels_national_radio_list)
+channels_local_radio = dict(channels_local_radio_list)
+channels_radio_list = channels_national_radio_list + channels_local_radio_list
+channels_radio = dict(channels_radio_list)
+
+channels = dict(channels_tv_list + channels_radio_list)
+categories_list = [
+    ('childrens', 'Children\'s'),
+    ('comedy', 'Comedy'),
+    ('drama', 'Drama'),
+    ('entertainment', 'Entertainment'),
+    ('factual', 'Factual'),
+    ('music', 'Music'),
+    ('news', 'News'),
+    ('religion_and_ethics', 'Religion & Ethics'),
+    ('sport', 'Sport'),
+    ('olympics', 'Olympics'),
+    ('wales', 'Wales'),
+    ('signed', 'Sign Zone')
+]
+categories = dict(categories_list)
+
+live_radio_stations = {'Radio 1': 'http://www.bbc.co.uk/radio1/wm_asx/aod/radio1_hi.asx',
+                       '1 Xtra':  'http://www.bbc.co.uk/1xtra/realmedia/1xtra_hi.asx',
+                       'Radio 2': 'http://www.bbc.co.uk/radio2/wm_asx/aod/radio2_hi.asx',
+                       'BBC 3': 'http://www.bbc.co.uk/radio3/wm_asx/aod/radio3_hi.asx',
+                       'BBC 4': 'http://www.bbc.co.uk/radio4/wm_asx/aod/radio4.asx',
+                       '5 Live':  'http://www.bbc.co.uk/fivelive/live/live.asx',
+                       '5 Live Sports Extra': 'http://www.bbc.co.uk/fivelive/live/live_sportsextra.asx',
+                       '6 Music': 'http://www.bbc.co.uk/6music/ram/6music_hi.asx',
+                       'BBC 7':   'http://www.bbc.co.uk/bbc7/realplayer/bbc7_hi.asx',
+                       'Asian Network': 'http://www.bbc.co.uk/asiannetwork/rams/asiannet_hi.asx',
+                       'Radio Scotland': 'http://www.bbc.co.uk/scotland/radioscotland/media/radioscotland.ram',
+                       'World Service': 'http://www.bbc.co.uk/worldservice/meta/tx/nb/live_eneuk_au_nb.asx',
+                       'BBC nan Gaidheal': 'http://www.bbc.co.uk/scotland/alba/media/live/radio_ng.ram',
+                       'BBC London': 'http://www.bbc.co.uk/england/realmedia/live/localradio/london.ram',
+                       'BBC Berkshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/radioberkshire.ram',
+                       'BBC Bristol': 'http://www.bbc.co.uk/england/realmedia/live/localradio/bristol.ram',
+                       'BBC Cambridgeshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/cambridgeshire.ram',
+                       'BBC Cornwall': 'http://www.bbc.co.uk/england/realmedia/live/localradio/cornwall.ram',
+                       'BBC Cumbria': 'http://www.bbc.co.uk/england/realmedia/live/localradio/cumbria.ram',
+                       'BBC Derby': 'http://www.bbc.co.uk/england/realmedia/live/localradio/derby.ram',
+                       'BBC Devon': 'http://www.bbc.co.uk/england/realmedia/live/localradio/devon.ram',
+                       'BBC Essex': 'http://www.bbc.co.uk/england/realmedia/live/localradio/essex.ram',
+                       'BBC Gloucestershire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/gloucestershire.ram',
+                       'BBC Guernsey': 'http://www.bbc.co.uk/england/realmedia/live/localradio/guernsey.ram',
+                       'BBC Hereford/Worcester': 'http://www.bbc.co.uk/england/realmedia/live/localradio/herefordandworcester.ram',
+                       'BBC Humberside': 'http://www.bbc.co.uk/england/realmedia/live/localradio/humberside.ram',
+                       'BBC Jersey': 'http://www.bbc.co.uk/england/realmedia/live/localradio/jersey.ram',
+                       'BBC Kent': 'http://www.bbc.co.uk/england/realmedia/live/localradio/kent.ram',
+                       'BBC Lancashire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/lancashire.ram',
+                       'BBC Leeds': 'http://www.bbc.co.uk/england/realmedia/live/localradio/leeds.ram',
+                       'BBC Leicester': 'http://www.bbc.co.uk/england/realmedia/live/localradio/leicester.ram',
+                       'BBC Lincolnshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/lincolnshire.ram',
+                       'BBC Manchester': 'http://www.bbc.co.uk/england/realmedia/live/localradio/manchester.ram',
+                       'BBC Merseyside': 'http://www.bbc.co.uk/england/realmedia/live/localradio/merseyside.ram',
+                       'BBC Newcastle': 'http://www.bbc.co.uk/england/realmedia/live/localradio/newcastle.ram',
+                       'BBC Norfolk': 'http://www.bbc.co.uk/england/realmedia/live/localradio/norfolk.ram',
+                       'BBC Northampton': 'http://www.bbc.co.uk/england/realmedia/live/localradio/northampton.ram',
+                       'BBC Nottingham': 'http://www.bbc.co.uk/england/realmedia/live/localradio/nottingham.ram',
+                       'BBC Oxford': 'http://www.bbc.co.uk/england/realmedia/live/localradio/radiooxford.ram',
+                       'BBC Sheffield': 'http://www.bbc.co.uk/england/realmedia/live/localradio/sheffield.ram',
+                       'BBC Shropshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/shropshire.ram',
+                       'BBC Solent': 'http://www.bbc.co.uk/england/realmedia/live/localradio/solent.ram',
+                       'BBC Somerset Sound': 'http://www.bbc.co.uk/england/realmedia/live/localradio/somerset.ram',
+                       'BBC Southern Counties Radio': 'http://www.bbc.co.uk/england/realmedia/live/localradio/southerncounties.ram',
+                       'BBC Stoke': 'http://www.bbc.co.uk/england/realmedia/live/localradio/stoke.ram',
+                       'BBC Suffolk': 'http://www.bbc.co.uk/england/realmedia/live/localradio/suffolk.ram',
+                       'BBC Swindon': 'http://www.bbc.co.uk/england/realmedia/live/localradio/swindon.ram',
+                       'BBC Three Counties Radio': 'http://www.bbc.co.uk/england/realmedia/live/localradio/threecounties.ram',
+                       'BBC Wiltshire': 'http://www.bbc.co.uk/england/realmedia/live/localradio/wiltshire.ram',
+                       'BBC York': 'http://www.bbc.co.uk/england/realmedia/live/localradio/york.ram',
+                       'BBC WM': 'http://www.bbc.co.uk/england/realmedia/live/localradio/wm.ram',
+                       'BBC Cymru': 'http://www.bbc.co.uk/cymru/live/rc-live.ram',
+                       'Radio Foyle': 'http://www.bbc.co.uk/northernireland/realmedia/rf-live.ram',
+                       'BBC Scotland': 'http://www.bbc.co.uk/scotland/radioscotland/media/radioscotland.ram',
+                       'BBC nan Gaidheal': 'http://www.bbc.co.uk/scotland/alba/media/live/radio_ng.ram',
+                       'BBC Ulster': 'http://www.bbc.co.uk/ni/realmedia/ru-live.ram',
+                       'BBC Wales': 'http://www.bbc.co.uk/wales/live/rwg2.ram',
+                       'BBC Tees': 'http://www.bbc.co.uk/england/realmedia/live/localradio/cleveland.ram',
+                       
+                       }
+live_webcams = {'Radio 1': 'http://www.bbc.co.uk/radio1/webcam/images/live/webcam.jpg',
+                '1 Xtra':  'http://www.bbc.co.uk/1xtra/webcam/live/1xtraa.jpg',
+                'Radio 2': 'http://www.bbc.co.uk/radio2/webcam/live/radio2.jpg',
+                '5 Live':  'http://www.bbc.co.uk/fivelive/inside/webcam/5Lwebcam1.jpg',
+                '6 Music': 'http://www.bbc.co.uk/6music/webcam/live/6music.jpg',
+                'Asian Network': 'http://www.bbc.co.uk/asiannetwork/webcams/birmingham.jpg'}
+
+rss_cache = {}
+
+self_closing_tags = ['alternate', 'mediator']
+
+http = httplib2.Http()
+
+re_selfclose = re.compile('<([a-zA-Z0-9]+)( ?.*)/>', re.M | re.S)
+
+def fix_selfclosing(xml):
+    return re_selfclose.sub('<\\1\\2></\\1>', xml)
+
+def set_http_cache_dir(d):
+    fc = httplib2.FileCache(d)
+    http.cache = fc
+
+def set_http_cache(c):
+    http.cache = c
+
+class NoItemsError(Exception):
+    def __init__(self, reason=None):
+        self.reason = reason
+    
+    def __str__(self):
+        reason = self.reason or '<no reason given>'
+        return "Programme unavailable ('%s')" % (reason)
+    
+
+class memoize(object):
+    def __init__(self, func):
+        self.func = func
+        self._cache = {}
+    def __call__(self, *args, **kwds):
+        key = args
+        if kwds:
+            items = kwds.items()
+            items.sort()
+            key = key + tuple(items)
+        if key in self._cache:
+            return self._cache[key]
+        self._cache[key] = result = self.func(*args, **kwds)
+        return result
+
+def httpretrieve(url, filename):
+    _, data = http.request(url, 'GET')
+    f = open(filename, 'wb')
+    f.write(data)
+    f.close() 
+
+def httpget(url):
+    resp = ''
+    data = ''
+    try:
+        resp, data = http.request(url, 'GET')
+    except:
         #print "Response for status %s for %s" % (resp.status, data)
-        totem.action_error ('Network Error', 'Failed to fetch URL %s' % url)
-        raise
-    
-    return data
-
-def parse_entry_id(entry_id):
-    # tag:bbc.co.uk,2008:PIPS:b00808sc
-    r = re.compile('PIPS:([0-9a-z]{8})')
-    matches = r.findall(entry_id)
-    if not matches: return None
-    return matches[0]
-
-class media(object):
-    def __init__(self, item, media_node):
-        self.item = item
-        self.href = None
-        self.kind = None
-        self.method = None
-        self.width, self.height = None, None
-        self.read_media_node(media_node)
-    
-    @property
-    def url(self):
-        if self.connection_method == 'resolve':
-            logging.info("Resolving URL %s", self.connection_href)
-            page = urllib2.urlopen(self.connection_href)
-            page.close()
-            url = page.geturl()
-            logging.info("URL resolved to %s", url)
-            return page.geturl()
-        else:
-            return self.connection_href
-        
-    @property
-    def application(self):
-        """
-        The type of stream represented as a string.
-        i.e. 'captions', 'flashhigh', 'flashmed', 'flashwii', 'mobile', 'mp3' or 'real'
-        """
-        tep = {}
-        tep['captions', 'application/ttaf+xml', None, 'http'] = 'captions'
-        tep['video', 'video/mp4', 'h264', 'rtmp'] = 'flashhigh'
-        tep['video', 'video/x-flv', 'vp6', 'rtmp'] = 'flashmed'
-        tep['video', 'video/x-flv', 'spark', 'rtmp'] = 'flashwii'
-        tep['video', 'video/mpeg', 'h264', 'http'] = 'mobile'
-        tep['audio', 'audio/mpeg', 'mp3', 'rtmp'] = 'mp3'
-        tep['audio', 'audio/real', 'real', 'http'] = 'real'
-        me = (self.kind, self.mimetype, self.encoding, self.connection_protocol)
-        return tep.get(me, None)
-
-    def read_media_node(self, media, resolve=False):
-        """
-        Reads media info from a media XML node
-        media: media node from BeautifulStoneSoup
-        """
-        self.kind = media.get('kind')
-        self.mimetype = media.get('type')
-        self.encoding = media.get('encoding')
-        self.width, self.height = media.get('width'), media.get('height')
-        self.live = media.get('live') == 'true'
-        
-        conn = media.find('connection')
-        self.connection_kind = conn.get('kind')
-        self.connection_live = conn.get('live') == 'true'
-        self.connection_protocol = None
-        self.connection_href = None
-        self.connection_method = None
-          
-        if self.connection_kind in ['http', 'sis']: # http
-            self.connection_href = conn.get('href')
-            self.connection_protocol = 'http'
-            if self.mimetype == 'video/mp4' and self.encoding == 'h264':
-                # iPhone, don't redirect or it goes to license failure page
-                self.connection_method = 'iphone'
-            elif self.kind == 'captions':
-                self.connection_method = None                
-            else:
-                self.connection_method = 'resolve'
-        elif self.connection_kind in ['level3', 'akamai']: #rtmp
-            self.connection_protocol = 'rtmp'
-            server = conn.get('server')
-            identifier = conn.get('identifier')
-
-            if self.connection_live:
-                logging.error("No support for live streams!")                
-            else:
-                auth = conn.get('authstring')
-                params = dict(ip=server, server=server, auth=auth, identifier=identifier)
-                self.connection_href = "rtmp://%(ip)s:1935/ondemand?_fcs_vhost=%(server)s&auth=%(auth)s&aifp=v001&slist=%(identifier)s" % params
-        else:
-            logging.error("connectionkind %s unknown", self.connection_kind)
-        
-        if self.connection_protocol:
-            logging.info("conn protocol: %s - conn kind: %s - media type: %s - media encoding: %s" % 
-                         (self.connection_protocol, self.connection_kind, self.mimetype, self.encoding))
-            logging.info("conn href: %s", self.connection_href)
-
-    
-    @property 
-    def programme(self):
-        return self.item.programme
-
-class item(object):
-    """
-    Represents an iPlayer programme item. Most programmes consist of 2 such items,
-    (1) the ident, and (2) the actual programme. The item specifies the properties 
-    of the media available, such as whether it's a radio/TV programme, if it's live,
-    signed, etc.
-    """
-    
-    def __init__(self, programme, item_node):
-        """
-        programme: a programme object that represents the 'parent' of this item.
-        item_node: an XML &lt;item&gt; node representing this item.
-        """
-        self.programme = programme
-        self.identifier = None
-        self.service = None
-        self.guidance = None
-        self.masterbrand = None
-        self.alternate = None
-        self.duration = ''
-        self.medias = None
-        self.read_item_node(item_node)
-            
-    def read_item_node(self, node):
-        """
-        Reads the specified XML &lt;item&gt; node and sets this instance's
-        properties.
-        """
-        self.kind = node.get('kind')
-        self.identifier = node.get('identifier')
-        logging.info('Found item: %s, %s', self.kind, self.identifier)
-        if self.kind in ['programme', 'radioProgramme']:
-            self.live = node.get('live') == 'true'
-            #self.title = node.get('title')
-            self.group = node.get('group')
-            self.duration = node.get('duration')
-            #self.broadcast = node.broadcast
-            self.service = node.service and node.service.get('id')
-            self.masterbrand = node.masterbrand and node.masterbrand.get('id')
-            self.alternate = node.alternate and node.alternate.get('id')
-            self.guidance = node.guidance 
-        
-    @property
-    def is_radio(self):
-        """ True if this stream is a radio programme. """
-        return self.kind == 'radioProgramme'
-
-    @property
-    def is_tv(self):
-        """ True if this stream is a TV programme. """
-        return self.kind == 'programme'
-
-    @property
-    def is_ident(self):
-        """ True if this stream is an ident. """
-        return self.kind == 'ident'
-
-    @property
-    def is_programme(self):
-        """ True if this stream is a programme (TV or Radio). """
-        return self.is_radio or self.is_tv
-
-    @property
-    def is_live(self):
-        """ True if this stream is being broadcast live. """
-        return self.live
-
-    @property
-    def is_signed(self):
-        """ True if this stream is 'signed' for the hard-of-hearing. """
-        return self.alternate == 'signed'
-    
-    @property
-    def mediaselector_url(self):        
-        return "http://www.bbc.co.uk/mediaselector/4/mtis/stream/%s"; % self.identifier    
-        
-    @property
-    def media(self):
-        """
-        Returns a list of all the media available for this item.
-        """
-        if self.medias: return self.medias
-        url = self.mediaselector_url
-        logging.info("Stream XML URL: %s", str(url))
-        _, xml = http.request(url)
-        soup = BeautifulStoneSoup(xml)
-        medias = [media(self, m) for m in soup('media')]
-        #logging.info('Found media: %s', pformat(medias, indent=8))
-        self.medias = medias
-        return medias
-    
-    def get_media_for(self, application):
-        """
-        Returns a media object for the given application type.
-        """
-        medias = [m for m in self.media if m.application == application]
-        if not medias:
-            return None
-        return medias[0]
-
-    def get_medias_for(self, applications):
-        """
-        Returns a dictionary of media objects for the given application types.
-        """
-        medias = [m for m in self.media if m.application in applications]
-        d = {}.fromkeys(applications)
-        for m in medias:
-            d[m.application] = m
-        return d
-
-class programme(object):
-    """
-    Represents an individual iPlayer programme, as identified by an 8-letter PID,
-    and contains the programme title, subtitle, broadcast time and list of playlist
-    items (e.g. ident and then the actual programme.)
-    """
-    
-    def __init__(self, pid):
-        self.pid = pid
-        self.meta = {}
-        self._items = []
-        self._related = []
-
-    @call_once
-    def read_playlist(self):
-        logging.info('Read playlist for %s...', self.pid)
-        self.parse_playlist(self.playlist)
-    
-    def get_playlist_xml(self):
-        """ Downloads and returns the XML for a PID from the iPlayer site. """
-        try:
-            url = self.playlist_url
-            logging.info("Getting XML playlist at URL: %s", url)
-            r, xml = http.request(url, 'GET')
-            return xml
-        except SocketTimeoutError:
-            logging.error("Timed out trying to download programme XML")
-            raise
-
-    def parse_playlist(self, xml):
-        #logging.info('Parsing playlist XML... %s', xml)
-        #xml.replace('<summary/>', '<summary></summary>')
-        #xml = fix_selfclosing(xml)
-        
-        soup = BeautifulStoneSoup(xml, selfClosingTags=self_closing_tags)
-        
-        self.meta = {}
-        self._items = []
-        self._related = []
-
-        logging.info('  Found programme: %s', soup.playlist.title.string)
-        self.meta['title'] = soup.playlist.title.string
-        self.meta['summary'] = soup.playlist.summary.string
-        self.meta['updated'] = soup.playlist.updated.string
-        
-        if soup.playlist.noitems:
-            logging.info('No playlist items: %s', soup.playlist.noitems.get('reason'))
-            self.meta['reason'] = soup.playlist.noitems.get('reason')
-                        
-        self._items = [item(self, i) for i in soup('item')]
-        for i in self._items:
-            print i, i.alternate , " ",
-        print
-
-        rId = re.compile('concept_pid:([a-z0-9]{8})')
-        for link in soup('relatedlink'):
-            i = {}
-            i['title'] = link.title.string
-            #i['summary'] = item.summary # FIXME looks like a bug in BSS
-            i['pid'] = (rId.findall(link.id.string) or [None])[0]
-            i['programme'] = programme(i['pid'])
-            self._related.append(i)
-        
-    def get_thumbnail(self, size='large', tvradio='tv'):
-        """
-        Returns the URL of a thumbnail.
-        size: '640x360'/'biggest'/'largest' or '512x288'/'big'/'large' or None
-        """
-        if size in ['640x360', '640x', 'x360', 'biggest', 'largest']:
-            return "http://www.bbc.co.uk/iplayer/images/episode/%s_640_360.jpg"; % (self.pid)
-        elif size in ['512x288', '512x', 'x288', 'big', 'large']:
-            return "http://www.bbc.co.uk/iplayer/images/episode/%s_512_288.jpg"; % (self.pid)
-        elif size in ['178x100', '178x', 'x100', 'small']:
-            return "http://www.bbc.co.uk/iplayer/images/episode/%s_178_100.jpg"; % (self.pid)
-        elif size in ['150x84', '150x', 'x84', 'smallest']:
-            return "http://www.bbc.co.uk/iplayer/images/episode/%s_150_84.jpg"; % (self.pid)
-        else:
-            return os.path.join(IMG_DIR, '%s.png' % tvradio)
-       
-
-    def get_url(self):
-        """
-        Returns the programmes episode page.
-        """
-        return "http://www.bbc.co.uk/iplayer/episode/%s"; % (self.pid)
-    
-    @property
-    def playlist_url(self):
-        return "http://www.bbc.co.uk/iplayer/playlist/%s"; % self.pid
-
-    @property
-    def playlist(self):
-        return self.get_playlist_xml()
-
-    def get_updated(self):
-        return self.meta['updated']
-    
-    @loaded_by(read_playlist)
-    def get_title(self):
-        return self.meta['title']
-    
-    @loaded_by(read_playlist)
-    def get_summary(self):
-        return self.meta['summary']
-
-    @loaded_by(read_playlist)
-    def get_related(self):
-        return self._related
-
-    @loaded_by(read_playlist)
-    def get_items(self):
-        if not self._items:
-            raise NoItemsError(self.meta['reason'])
-        return self._items
-
-    @property
-    def programme(self):
-        for i in self.items:
-            if i.is_programme:
-                return i
-        return None
-
-    title = property(get_title)
-    summary = property(get_summary)
-    updated = property(get_updated)
-    thumbnail = property(get_thumbnail)    
-    related = property(get_related)
-    items = property(get_items)
-
-#programme = memoize(programme)
-
-
-class programme_simple(object):
-    """
-    Represents an individual iPlayer programme, as identified by an 8-letter PID,
-    and contains the programme pid, title, subtitle etc
-    """
-    
-    def __init__(self, pid, entry):
-        self.pid = pid
-        self.meta = {}
-        self.meta['title'] = entry.title
-        self.meta['summary'] = entry.summary
-        self.meta['updated'] = entry.updated
-        self.categories = [] 
-        for c in entry.categories:
-            if c != 'TV':
-                self.categories.append(c.rstrip())
-        self._items = []
-        self._related = []
-
-    @call_once
-    def read_playlist(self):
-        pass
-        
-    def get_playlist_xml(self):
-        pass
-
-    def parse_playlist(self, xml):
-        pass
-        
-    def get_thumbnail(self, size='large', tvradio='tv'):
-        """
-        Returns the URL of a thumbnail.
-        size: '640x360'/'biggest'/'largest' or '512x288'/'big'/'large' or None
-        """
-        
-        if size in ['640x360', '640x', 'x360', 'biggest', 'largest']:
-            return "http://www.bbc.co.uk/iplayer/images/episode/%s_640_360.jpg"; % (self.pid)
-        elif size in ['512x288', '512x', 'x288', 'big', 'large']:
-            return "http://www.bbc.co.uk/iplayer/images/episode/%s_512_288.jpg"; % (self.pid)
-        elif size in ['178x100', '178x', 'x100', 'small']:
-            return "http://www.bbc.co.uk/iplayer/images/episode/%s_178_100.jpg"; % (self.pid)
-        elif size in ['150x84', '150x', 'x84', 'smallest']:
-            return "http://www.bbc.co.uk/iplayer/images/episode/%s_150_84.jpg"; % (self.pid)
-        else:
-            return os.path.join(IMG_DIR, '%s.png' % tvradio)
-
-
-    def get_url(self):
-        """
-        Returns the programmes episode page.
-        """
-        return "http://www.bbc.co.uk/iplayer/episode/%s"; % (self.pid)
-    
-    @property
-    def playlist_url(self):
-        return "http://www.bbc.co.uk/iplayer/playlist/%s"; % self.pid
-
-    @property
-    def playlist(self):
-        return self.get_playlist_xml()
-
-    def get_updated(self):
-        return self.meta['updated']
-    
-    @loaded_by(read_playlist)
-    def get_title(self):
-        return self.meta['title']
-    
-    @loaded_by(read_playlist)
-    def get_summary(self):
-        return self.meta['summary']
-
-    @loaded_by(read_playlist)
-    def get_related(self):
-        return self._related
-
-    @loaded_by(read_playlist)
-    def get_items(self):
-        if not self._items:
-            raise NoItemsError(self.meta['reason'])
-        return self._items
-
-    @property
-    def programme(self):
-        for i in self.items:
-            if i.is_programme:
-                return i
-        return None
-
-    title = property(get_title)
-    summary = property(get_summary)
-    updated = property(get_updated)
-    thumbnail = property(get_thumbnail)    
-    related = property(get_related)
-    items = property(get_items)
-
-
-class feed(object):
-    def __init__(self, tvradio=None, channel=None, category=None, subcategory=None, atoz=None, searchterm=None):
-        """
-        Creates a feed for the specified channel/category/whatever.
-        tvradio: type of channel - 'tv' or 'radio'. If a known channel is specified, use 'auto'.
-        channel: name of channel, e.g. 'bbc_one'
-        category: category name, e.g. 'drama'
-        subcategory: subcategory name, e.g. 'period_drama'
-        atoz: A-Z listing for the specified letter
-        """
-        if tvradio == 'auto':
-            if not channel and not searchterm:
-                raise Exception, "Must specify channel or searchterm when using 'auto'"
-            elif channel in channels_tv:
-                self.tvradio = 'tv'
-            elif channel in channels_radio:
-                self.tvradio = 'radio'
-            else:
-                raise Exception, "TV channel '%s' not recognised." % self.channel
-                                
-        elif tvradio in ['tv', 'radio']:
-            self.tvradio = tvradio
-        else:
-            self.tvradio = None
-        self.channel = channel
-        self.category = category            
-        self.subcategory = subcategory
-        self.atoz = atoz
-        self.searchterm = searchterm      
-        
-    def create_url(self, listing):
-        """
-        <channel>/['list'|'popular'|'highlights']
-        'categories'/<category>(/<subcategory>)(/['tv'/'radio'])/['list'|'popular'|'highlights']
-        """
-        assert listing in ['list', 'popular', 'highlights'], "Unknown listing type"
-        if self.searchterm:
-            path = ['search']
-            if self.tvradio:
-                path += [self.tvradio]
-            path += ['?q=%s' % self.searchterm]
-        elif self.channel: 
-            path = [self.channel]
-            if self.atoz:
-                path += ['atoz', self.atoz]
-            path += [listing]
-        elif self.atoz:
-            path = ['atoz', self.atoz, listing]
-            if self.tvradio:
-                path += [self.tvradio]
-        else:
-            assert listing != 'list', "Can't list at tv/radio level'"
-            path = [listing, self.tvradio]
-          
-        return "http://feeds.bbc.co.uk/iplayer/"; + '/'.join(path)
-
-       
-    def get_name(self, separator=' '):
-        """
-        A readable title for this feed, e.g. 'BBC One' or 'TV Drama' or 'BBC One Drama'
-        separator: string to separate name parts with, defaults to ' '. Use None to return a list (e.g. ['TV', 'Drama']).
-        """
-        path = []
-        
-        # if got a channel, don't need tv/radio distinction
-        if self.channel:
-            assert self.channel in channels_tv or self.channel in channels_radio, 'Unknown channel'
-            #print self.tvradio
-            if self.tvradio == 'tv':
-                path.append(channels_tv.get(self.channel, '(TV)'))
-            else:
-                path.append(channels_radio.get(self.channel, '(Radio)'))
-        elif self.tvradio: 
-            # no channel
-            medium = 'TV'
-            if self.tvradio == 'radio': medium = 'Radio'
-            path.append(medium)
-                    
-        if self.searchterm:
-            path += ['Search results for %s' % self.searchterm]
-                    
-        if self.category:
-            assert self.category in categories, 'Unknown category'
-            path.append(categories.get(self.category, '(Category)'))
-            
-        if self.atoz:
-            path.append("beginning with %s" % self.atoz.upper())
-        
-        if separator != None:
-            return separator.join(path)
-        else:
-            return path
-    
-    def channels(self):
-        """
-        Return a list of available channels.
-        """
-        if self.channel: return None
-        if self.tvradio == 'tv': return channels_tv
-        if self.tvradio == 'radio': return channels_radio
-        return None    
-    
-    def channels_feed(self):
-        """
-        Return a list of available channels as a list of feeds.
-        """
-        if self.channel:
-            logging.warning("%s doesn\'t have any channels!", self.channel)
-            return None
-        if self.tvradio == 'tv': 
-            return [feed('tv', channel=ch) for (ch, title) in channels_tv_list]
-        if self.tvradio == 'radio': 
-            return [feed('radio', channel=ch) for (ch, title) in channels_radio_list]
-        return None
-
-        
-    def subcategories(self):
-        raise NotImplementedError('Sub-categories not yet supported')
-    
-    @classmethod
-    def is_atoz(self, letter):
-        """
-        Return False if specified letter is not a valid 'A to Z' directory entry.
-        Otherwise returns the directory name.
-        
-        >>> feed.is_atoz('a'), feed.is_atoz('z')
-        ('a', 'z')
-        >>> feed.is_atoz('0'), feed.is_atoz('9')
-        ('0-9', '0-9')
-        >>> feed.is_atoz('123'), feed.is_atoz('abc')
-        (False, False)
-        >>> feed.is_atoz('big british castle'), feed.is_atoz('')
-        (False, False)
-        """
-        l = letter.lower()
-        if len(l) != 1 and l != '0-9': 
-            return False
-        if l in '0123456789': l = "0-9"
-        if l not in 'abcdefghijklmnopqrstuvwxyz0-9':
-            return False
-        return l
-    
-    def sub(self, *args, **kwargs):
-        """
-        Clones this feed, altering the specified parameters.
-        
-        >>> feed('tv').sub(channel='bbc_one').channel
-        'bbc_one'
-        >>> feed('tv', channel='bbc_one').sub(channel='bbc_two').channel
-        'bbc_two'
-        >>> feed('tv', channel='bbc_one').sub(category='drama').category
-        'drama'
-        >>> feed('tv', channel='bbc_one').sub(channel=None).channel
-        >>> 
-        """
-        d = self.__dict__.copy()
-        d.update(kwargs)
-        return feed(**d)
-    
-    def get(self, subfeed):
-        """
-        Returns a child/subfeed of this feed.
-        child: can be channel/cat/subcat/letter, e.g. 'bbc_one'
-        """
-        if self.channel and subfeed in categories: 
-            # no children: channel feeds don't support categories
-            return None
-        elif self.category:
-            # no children: TODO support subcategories
-            return None
-        elif subfeed in categories:
-            return self.sub(category=subfeed)
-        elif self.is_atoz(subfeed):
-            return self.sub(atoz=self.is_atoz(subfeed))
-        else:
-            if subfeed in channels_tv: return feed('tv', channel=subfeed)
-            if subfeed in channels_radio: return feed('radio', channel=subfeed)
-        # TODO handle properly oh pants
-        return None
-
-    @classmethod
-    def read_rss(self, url):
-        logging.info('Read RSS: %s', url)
-        if url not in rss_cache:
-            logging.info('Feed URL not in cache, requesting...')
-            xml = httpget(url)
-            progs = listparser.parse(xml)
-            if not progs: return []
-            d = []
-            for entry in progs.entries: 
-                pid = parse_entry_id(entry.id)
-                p = programme_simple(pid, entry)
-                d.append(p)        
-            logging.info('Found %d entries', len(d))
-            rss_cache[url] = d
-        else:
-            logging.info('RSS found in cache')
-        return rss_cache[url]
-    
-    def popular(self):
-        return self.read_rss(self.create_url('popular'))
-
-    def highlights(self):
-        return self.read_rss(self.create_url('highlights'))
-        
-    def list(self):
-        return self.read_rss(self.create_url('list'))
-    
-    def categories(self):
-        # quick and dirty category extraction and count
-        xmlURL = self.create_url('list')
-        xml    = httpget(xmlURL)
-        cat    = re.findall( "<category .*term=\"(.*?)\"", xml )
-        categories = {}
-        for c in cat:
-            if c != 'TV':
-                if not categories.has_key(c): categories[c] = 0
-                categories[c] += 1
-        alist=[]
-        k = categories.keys()
-        k.sort()
-        for c in k:
-            n = categories[c]
-            c = c.replace('&amp;', '&')
-            c = c.replace('&gt;', '>')
-            c = c.replace('&lt;', '<')
-            alist.append((c, n))
-        return alist    
-
-    @property
-    def is_radio(self):
-        """ True if this feed is for radio. """
-        return self.tvradio == 'radio'
-
-    @property
-    def is_tv(self):
-        """ True if this feed is for tv. """
-        return self.tvradio == 'tv'
-
-    name = property(get_name)
-
-
-tv = feed('tv')
-radio = feed('radio')
-
-def test():
-    tv = feed('tv')
-    print tv.popular()
-    print tv.channels()
-    print tv.get('bbc_one')
-    print tv.get('bbc_one').list()
-    for c in tv.get('bbc_one').categories():
-        print c
-    #print tv.get('bbc_one').channels()
-    #print tv.categories()
-    #print tv.get('drama').list()
-    #print tv.get('drama').get_subcategory('period').list()
-
-if __name__ == '__main__':
+        totem.action_error ('Network Error', 'Failed to fetch URL %s' % url)
+        raise
+    
+    return data
+
+def parse_entry_id(entry_id):
+    # tag:bbc.co.uk,2008:PIPS:b00808sc
+    r = re.compile('PIPS:([0-9a-z]{8})')
+    matches = r.findall(entry_id)
+    if not matches: return None
+    return matches[0]
+
+class media(object):
+    def __init__(self, item, media_node):
+        self.item = item
+        self.href = None
+        self.kind = None
+        self.method = None
+        self.width, self.height = None, None
+        self.read_media_node(media_node)
+    
+    @property
+    def url(self):
+        if self.connection_method == 'resolve':
+            logging.info("Resolving URL %s", self.connection_href)
+            page = urllib2.urlopen(self.connection_href)
+            page.close()
+            url = page.geturl()
+            logging.info("URL resolved to %s", url)
+            return page.geturl()
+        else:
+            return self.connection_href
+        
+    @property
+    def application(self):
+        """
+        The type of stream represented as a string.
+        i.e. 'captions', 'flashhigh', 'flashmed', 'flashwii', 'mobile', 'mp3' or 'real'
+        """
+        tep = {}
+        tep['captions', 'application/ttaf+xml', None, 'http'] = 'captions'
+        tep['video', 'video/mp4', 'h264', 'rtmp'] = 'flashhigh'
+        tep['video', 'video/x-flv', 'vp6', 'rtmp'] = 'flashmed'
+        tep['video', 'video/x-flv', 'spark', 'rtmp'] = 'flashwii'
+        tep['video', 'video/mpeg', 'h264', 'http'] = 'mobile'
+        tep['audio', 'audio/mpeg', 'mp3', 'rtmp'] = 'mp3'
+        tep['audio', 'audio/real', 'real', 'http'] = 'real'
+        me = (self.kind, self.mimetype, self.encoding, self.connection_protocol)
+        return tep.get(me, None)
+
+    def read_media_node(self, media, resolve=False):
+        """
+        Reads media info from a media XML node
+        media: media node from BeautifulStoneSoup
+        """
+        self.kind = media.get('kind')
+        self.mimetype = media.get('type')
+        self.encoding = media.get('encoding')
+        self.width, self.height = media.get('width'), media.get('height')
+        self.live = media.get('live') == 'true'
+        
+        conn = media.find('connection')
+        self.connection_kind = conn.get('kind')
+        self.connection_live = conn.get('live') == 'true'
+        self.connection_protocol = None
+        self.connection_href = None
+        self.connection_method = None
+          
+        if self.connection_kind in ['http', 'sis']: # http
+            self.connection_href = conn.get('href')
+            self.connection_protocol = 'http'
+            if self.mimetype == 'video/mp4' and self.encoding == 'h264':
+                # iPhone, don't redirect or it goes to license failure page
+                self.connection_method = 'iphone'
+            elif self.kind == 'captions':
+                self.connection_method = None                
+            else:
+                self.connection_method = 'resolve'
+        elif self.connection_kind in ['level3', 'akamai']: #rtmp
+            self.connection_protocol = 'rtmp'
+            server = conn.get('server')
+            identifier = conn.get('identifier')
+
+            if self.connection_live:
+                logging.error("No support for live streams!")                
+            else:
+                auth = conn.get('authstring')
+                params = dict(ip=server, server=server, auth=auth, identifier=identifier)
+                self.connection_href = "rtmp://%(ip)s:1935/ondemand?_fcs_vhost=%(server)s&auth=%(auth)s&aifp=v001&slist=%(identifier)s" % params
+        else:
+            logging.error("connectionkind %s unknown", self.connection_kind)
+        
+        if self.connection_protocol:
+            logging.info("conn protocol: %s - conn kind: %s - media type: %s - media encoding: %s" % 
+                         (self.connection_protocol, self.connection_kind, self.mimetype, self.encoding))
+            logging.info("conn href: %s", self.connection_href)
+
+    
+    @property 
+    def programme(self):
+        return self.item.programme
+
+class item(object):
+    """
+    Represents an iPlayer programme item. Most programmes consist of 2 such items,
+    (1) the ident, and (2) the actual programme. The item specifies the properties 
+    of the media available, such as whether it's a radio/TV programme, if it's live,
+    signed, etc.
+    """
+    
+    def __init__(self, programme, item_node):
+        """
+        programme: a programme object that represents the 'parent' of this item.
+        item_node: an XML &lt;item&gt; node representing this item.
+        """
+        self.programme = programme
+        self.identifier = None
+        self.service = None
+        self.guidance = None
+        self.masterbrand = None
+        self.alternate = None
+        self.duration = ''
+        self.medias = None
+        self.read_item_node(item_node)
+            
+    def read_item_node(self, node):
+        """
+        Reads the specified XML &lt;item&gt; node and sets this instance's
+        properties.
+        """
+        self.kind = node.get('kind')
+        self.identifier = node.get('identifier')
+        logging.info('Found item: %s, %s', self.kind, self.identifier)
+        if self.kind in ['programme', 'radioProgramme']:
+            self.live = node.get('live') == 'true'
+            #self.title = node.get('title')
+            self.group = node.get('group')
+            self.duration = node.get('duration')
+            #self.broadcast = node.broadcast
+            self.service = node.service and node.service.get('id')
+            self.masterbrand = node.masterbrand and node.masterbrand.get('id')
+            self.alternate = node.alternate and node.alternate.get('id')
+            self.guidance = node.guidance 
+        
+    @property
+    def is_radio(self):
+        """ True if this stream is a radio programme. """
+        return self.kind == 'radioProgramme'
+
+    @property
+    def is_tv(self):
+        """ True if this stream is a TV programme. """
+        return self.kind == 'programme'
+
+    @property
+    def is_ident(self):
+        """ True if this stream is an ident. """
+        return self.kind == 'ident'
+
+    @property
+    def is_programme(self):
+        """ True if this stream is a programme (TV or Radio). """
+        return self.is_radio or self.is_tv
+
+    @property
+    def is_live(self):
+        """ True if this stream is being broadcast live. """
+        return self.live
+
+    @property
+    def is_signed(self):
+        """ True if this stream is 'signed' for the hard-of-hearing. """
+        return self.alternate == 'signed'
+    
+    @property
+    def mediaselector_url(self):        
+        return "http://www.bbc.co.uk/mediaselector/4/mtis/stream/%s"; % self.identifier    
+        
+    @property
+    def media(self):
+        """
+        Returns a list of all the media available for this item.
+        """
+        if self.medias: return self.medias
+        url = self.mediaselector_url
+        logging.info("Stream XML URL: %s", str(url))
+        _, xml = http.request(url)
+        soup = BeautifulStoneSoup(xml)
+        medias = [media(self, m) for m in soup('media')]
+        #logging.info('Found media: %s', pformat(medias, indent=8))
+        self.medias = medias
+        return medias
+    
+    def get_media_for(self, application):
+        """
+        Returns a media object for the given application type.
+        """
+        medias = [m for m in self.media if m.application == application]
+        if not medias:
+            return None
+        return medias[0]
+
+    def get_medias_for(self, applications):
+        """
+        Returns a dictionary of media objects for the given application types.
+        """
+        medias = [m for m in self.media if m.application in applications]
+        d = {}.fromkeys(applications)
+        for m in medias:
+            d[m.application] = m
+        return d
+
+class programme(object):
+    """
+    Represents an individual iPlayer programme, as identified by an 8-letter PID,
+    and contains the programme title, subtitle, broadcast time and list of playlist
+    items (e.g. ident and then the actual programme.)
+    """
+    
+    def __init__(self, pid):
+        self.pid = pid
+        self.meta = {}
+        self._items = []
+        self._related = []
+
+    @call_once
+    def read_playlist(self):
+        logging.info('Read playlist for %s...', self.pid)
+        self.parse_playlist(self.playlist)
+    
+    def get_playlist_xml(self):
+        """ Downloads and returns the XML for a PID from the iPlayer site. """
+        try:
+            url = self.playlist_url
+            logging.info("Getting XML playlist at URL: %s", url)
+            r, xml = http.request(url, 'GET')
+            return xml
+        except SocketTimeoutError:
+            logging.error("Timed out trying to download programme XML")
+            raise
+
+    def parse_playlist(self, xml):
+        #logging.info('Parsing playlist XML... %s', xml)
+        #xml.replace('<summary/>', '<summary></summary>')
+        #xml = fix_selfclosing(xml)
+        
+        soup = BeautifulStoneSoup(xml, selfClosingTags=self_closing_tags)
+        
+        self.meta = {}
+        self._items = []
+        self._related = []
+
+        logging.info('  Found programme: %s', soup.playlist.title.string)
+        self.meta['title'] = soup.playlist.title.string
+        self.meta['summary'] = soup.playlist.summary.string
+        self.meta['updated'] = soup.playlist.updated.string
+        
+        if soup.playlist.noitems:
+            logging.info('No playlist items: %s', soup.playlist.noitems.get('reason'))
+            self.meta['reason'] = soup.playlist.noitems.get('reason')
+                        
+        self._items = [item(self, i) for i in soup('item')]
+        for i in self._items:
+            print i, i.alternate , " ",
+        print
+
+        rId = re.compile('concept_pid:([a-z0-9]{8})')
+        for link in soup('relatedlink'):
+            i = {}
+            i['title'] = link.title.string
+            #i['summary'] = item.summary # FIXME looks like a bug in BSS
+            i['pid'] = (rId.findall(link.id.string) or [None])[0]
+            i['programme'] = programme(i['pid'])
+            self._related.append(i)
+        
+    def get_thumbnail(self, size='large', tvradio='tv'):
+        """
+        Returns the URL of a thumbnail.
+        size: '640x360'/'biggest'/'largest' or '512x288'/'big'/'large' or None
+        """
+        if size in ['640x360', '640x', 'x360', 'biggest', 'largest']:
+            return "http://www.bbc.co.uk/iplayer/images/episode/%s_640_360.jpg"; % (self.pid)
+        elif size in ['512x288', '512x', 'x288', 'big', 'large']:
+            return "http://www.bbc.co.uk/iplayer/images/episode/%s_512_288.jpg"; % (self.pid)
+        elif size in ['178x100', '178x', 'x100', 'small']:
+            return "http://www.bbc.co.uk/iplayer/images/episode/%s_178_100.jpg"; % (self.pid)
+        elif size in ['150x84', '150x', 'x84', 'smallest']:
+            return "http://www.bbc.co.uk/iplayer/images/episode/%s_150_84.jpg"; % (self.pid)
+        else:
+            return os.path.join(IMG_DIR, '%s.png' % tvradio)
+       
+
+    def get_url(self):
+        """
+        Returns the programmes episode page.
+        """
+        return "http://www.bbc.co.uk/iplayer/episode/%s"; % (self.pid)
+    
+    @property
+    def playlist_url(self):
+        return "http://www.bbc.co.uk/iplayer/playlist/%s"; % self.pid
+
+    @property
+    def playlist(self):
+        return self.get_playlist_xml()
+
+    def get_updated(self):
+        return self.meta['updated']
+    
+    @loaded_by(read_playlist)
+    def get_title(self):
+        return self.meta['title']
+    
+    @loaded_by(read_playlist)
+    def get_summary(self):
+        return self.meta['summary']
+
+    @loaded_by(read_playlist)
+    def get_related(self):
+        return self._related
+
+    @loaded_by(read_playlist)
+    def get_items(self):
+        if not self._items:
+            raise NoItemsError(self.meta['reason'])
+        return self._items
+
+    @property
+    def programme(self):
+        for i in self.items:
+            if i.is_programme:
+                return i
+        return None
+
+    title = property(get_title)
+    summary = property(get_summary)
+    updated = property(get_updated)
+    thumbnail = property(get_thumbnail)    
+    related = property(get_related)
+    items = property(get_items)
+
+#programme = memoize(programme)
+
+
+class programme_simple(object):
+    """
+    Represents an individual iPlayer programme, as identified by an 8-letter PID,
+    and contains the programme pid, title, subtitle etc
+    """
+    
+    def __init__(self, pid, entry):
+        self.pid = pid
+        self.meta = {}
+        self.meta['title'] = entry.title
+        self.meta['summary'] = entry.summary
+        self.meta['updated'] = entry.updated
+        self.categories = [] 
+        for c in entry.categories:
+            if c != 'TV':
+                self.categories.append(c.rstrip())
+        self._items = []
+        self._related = []
+
+    @call_once
+    def read_playlist(self):
+        pass
+        
+    def get_playlist_xml(self):
+        pass
+
+    def parse_playlist(self, xml):
+        pass
+        
+    def get_thumbnail(self, size='large', tvradio='tv'):
+        """
+        Returns the URL of a thumbnail.
+        size: '640x360'/'biggest'/'largest' or '512x288'/'big'/'large' or None
+        """
+        
+        if size in ['640x360', '640x', 'x360', 'biggest', 'largest']:
+            return "http://www.bbc.co.uk/iplayer/images/episode/%s_640_360.jpg"; % (self.pid)
+        elif size in ['512x288', '512x', 'x288', 'big', 'large']:
+            return "http://www.bbc.co.uk/iplayer/images/episode/%s_512_288.jpg"; % (self.pid)
+        elif size in ['178x100', '178x', 'x100', 'small']:
+            return "http://www.bbc.co.uk/iplayer/images/episode/%s_178_100.jpg"; % (self.pid)
+        elif size in ['150x84', '150x', 'x84', 'smallest']:
+            return "http://www.bbc.co.uk/iplayer/images/episode/%s_150_84.jpg"; % (self.pid)
+        else:
+            return os.path.join(IMG_DIR, '%s.png' % tvradio)
+
+
+    def get_url(self):
+        """
+        Returns the programmes episode page.
+        """
+        return "http://www.bbc.co.uk/iplayer/episode/%s"; % (self.pid)
+    
+    @property
+    def playlist_url(self):
+        return "http://www.bbc.co.uk/iplayer/playlist/%s"; % self.pid
+
+    @property
+    def playlist(self):
+        return self.get_playlist_xml()
+
+    def get_updated(self):
+        return self.meta['updated']
+    
+    @loaded_by(read_playlist)
+    def get_title(self):
+        return self.meta['title']
+    
+    @loaded_by(read_playlist)
+    def get_summary(self):
+        return self.meta['summary']
+
+    @loaded_by(read_playlist)
+    def get_related(self):
+        return self._related
+
+    @loaded_by(read_playlist)
+    def get_items(self):
+        if not self._items:
+            raise NoItemsError(self.meta['reason'])
+        return self._items
+
+    @property
+    def programme(self):
+        for i in self.items:
+            if i.is_programme:
+                return i
+        return None
+
+    title = property(get_title)
+    summary = property(get_summary)
+    updated = property(get_updated)
+    thumbnail = property(get_thumbnail)    
+    related = property(get_related)
+    items = property(get_items)
+
+
+class feed(object):
+    def __init__(self, tvradio=None, channel=None, category=None, subcategory=None, atoz=None, searchterm=None):
+        """
+        Creates a feed for the specified channel/category/whatever.
+        tvradio: type of channel - 'tv' or 'radio'. If a known channel is specified, use 'auto'.
+        channel: name of channel, e.g. 'bbc_one'
+        category: category name, e.g. 'drama'
+        subcategory: subcategory name, e.g. 'period_drama'
+        atoz: A-Z listing for the specified letter
+        """
+        if tvradio == 'auto':
+            if not channel and not searchterm:
+                raise Exception, "Must specify channel or searchterm when using 'auto'"
+            elif channel in channels_tv:
+                self.tvradio = 'tv'
+            elif channel in channels_radio:
+                self.tvradio = 'radio'
+            else:
+                raise Exception, "TV channel '%s' not recognised." % self.channel
+                                
+        elif tvradio in ['tv', 'radio']:
+            self.tvradio = tvradio
+        else:
+            self.tvradio = None
+        self.channel = channel
+        self.category = category            
+        self.subcategory = subcategory
+        self.atoz = atoz
+        self.searchterm = searchterm      
+        
+    def create_url(self, listing):
+        """
+        <channel>/['list'|'popular'|'highlights']
+        'categories'/<category>(/<subcategory>)(/['tv'/'radio'])/['list'|'popular'|'highlights']
+        """
+        assert listing in ['list', 'popular', 'highlights'], "Unknown listing type"
+        if self.searchterm:
+            path = ['search']
+            if self.tvradio:
+                path += [self.tvradio]
+            path += ['?q=%s' % self.searchterm]
+        elif self.channel: 
+            path = [self.channel]
+            if self.atoz:
+                path += ['atoz', self.atoz]
+            path += [listing]
+        elif self.atoz:
+            path = ['atoz', self.atoz, listing]
+            if self.tvradio:
+                path += [self.tvradio]
+        else:
+            assert listing != 'list', "Can't list at tv/radio level'"
+            path = [listing, self.tvradio]
+          
+        return "http://feeds.bbc.co.uk/iplayer/"; + '/'.join(path)
+
+       
+    def get_name(self, separator=' '):
+        """
+        A readable title for this feed, e.g. 'BBC One' or 'TV Drama' or 'BBC One Drama'
+        separator: string to separate name parts with, defaults to ' '. Use None to return a list (e.g. ['TV', 'Drama']).
+        """
+        path = []
+        
+        # if got a channel, don't need tv/radio distinction
+        if self.channel:
+            assert self.channel in channels_tv or self.channel in channels_radio, 'Unknown channel'
+            #print self.tvradio
+            if self.tvradio == 'tv':
+                path.append(channels_tv.get(self.channel, '(TV)'))
+            else:
+                path.append(channels_radio.get(self.channel, '(Radio)'))
+        elif self.tvradio: 
+            # no channel
+            medium = 'TV'
+            if self.tvradio == 'radio': medium = 'Radio'
+            path.append(medium)
+                    
+        if self.searchterm:
+            path += ['Search results for %s' % self.searchterm]
+                    
+        if self.category:
+            assert self.category in categories, 'Unknown category'
+            path.append(categories.get(self.category, '(Category)'))
+            
+        if self.atoz:
+            path.append("beginning with %s" % self.atoz.upper())
+        
+        if separator != None:
+            return separator.join(path)
+        else:
+            return path
+    
+    def channels(self):
+        """
+        Return a list of available channels.
+        """
+        if self.channel: return None
+        if self.tvradio == 'tv': return channels_tv
+        if self.tvradio == 'radio': return channels_radio
+        return None    
+    
+    def channels_feed(self):
+        """
+        Return a list of available channels as a list of feeds.
+        """
+        if self.channel:
+            logging.warning("%s doesn\'t have any channels!", self.channel)
+            return None
+        if self.tvradio == 'tv': 
+            return [feed('tv', channel=ch) for (ch, title) in channels_tv_list]
+        if self.tvradio == 'radio': 
+            return [feed('radio', channel=ch) for (ch, title) in channels_radio_list]
+        return None
+
+        
+    def subcategories(self):
+        raise NotImplementedError('Sub-categories not yet supported')
+    
+    @classmethod
+    def is_atoz(self, letter):
+        """
+        Return False if specified letter is not a valid 'A to Z' directory entry.
+        Otherwise returns the directory name.
+        
+        >>> feed.is_atoz('a'), feed.is_atoz('z')
+        ('a', 'z')
+        >>> feed.is_atoz('0'), feed.is_atoz('9')
+        ('0-9', '0-9')
+        >>> feed.is_atoz('123'), feed.is_atoz('abc')
+        (False, False)
+        >>> feed.is_atoz('big british castle'), feed.is_atoz('')
+        (False, False)
+        """
+        l = letter.lower()
+        if len(l) != 1 and l != '0-9': 
+            return False
+        if l in '0123456789': l = "0-9"
+        if l not in 'abcdefghijklmnopqrstuvwxyz0-9':
+            return False
+        return l
+    
+    def sub(self, *args, **kwargs):
+        """
+        Clones this feed, altering the specified parameters.
+        
+        >>> feed('tv').sub(channel='bbc_one').channel
+        'bbc_one'
+        >>> feed('tv', channel='bbc_one').sub(channel='bbc_two').channel
+        'bbc_two'
+        >>> feed('tv', channel='bbc_one').sub(category='drama').category
+        'drama'
+        >>> feed('tv', channel='bbc_one').sub(channel=None).channel
+        >>> 
+        """
+        d = self.__dict__.copy()
+        d.update(kwargs)
+        return feed(**d)
+    
+    def get(self, subfeed):
+        """
+        Returns a child/subfeed of this feed.
+        child: can be channel/cat/subcat/letter, e.g. 'bbc_one'
+        """
+        if self.channel and subfeed in categories: 
+            # no children: channel feeds don't support categories
+            return None
+        elif self.category:
+            # no children: TODO support subcategories
+            return None
+        elif subfeed in categories:
+            return self.sub(category=subfeed)
+        elif self.is_atoz(subfeed):
+            return self.sub(atoz=self.is_atoz(subfeed))
+        else:
+            if subfeed in channels_tv: return feed('tv', channel=subfeed)
+            if subfeed in channels_radio: return feed('radio', channel=subfeed)
+        # TODO handle properly oh pants
+        return None
+
+    @classmethod
+    def read_rss(self, url):
+        logging.info('Read RSS: %s', url)
+        if url not in rss_cache:
+            logging.info('Feed URL not in cache, requesting...')
+            xml = httpget(url)
+            progs = listparser.parse(xml)
+            if not progs: return []
+            d = []
+            for entry in progs.entries: 
+                pid = parse_entry_id(entry.id)
+                p = programme_simple(pid, entry)
+                d.append(p)        
+            logging.info('Found %d entries', len(d))
+            rss_cache[url] = d
+        else:
+            logging.info('RSS found in cache')
+        return rss_cache[url]
+    
+    def popular(self):
+        return self.read_rss(self.create_url('popular'))
+
+    def highlights(self):
+        return self.read_rss(self.create_url('highlights'))
+        
+    def list(self):
+        return self.read_rss(self.create_url('list'))
+    
+    def categories(self):
+        # quick and dirty category extraction and count
+        xmlURL = self.create_url('list')
+        xml    = httpget(xmlURL)
+        cat    = re.findall( "<category .*term=\"(.*?)\"", xml )
+        categories = {}
+        for c in cat:
+            if c != 'TV':
+                if not categories.has_key(c): categories[c] = 0
+                categories[c] += 1
+        alist=[]
+        k = categories.keys()
+        k.sort()
+        for c in k:
+            n = categories[c]
+            c = c.replace('&amp;', '&')
+            c = c.replace('&gt;', '>')
+            c = c.replace('&lt;', '<')
+            alist.append((c, n))
+        return alist    
+
+    @property
+    def is_radio(self):
+        """ True if this feed is for radio. """
+        return self.tvradio == 'radio'
+
+    @property
+    def is_tv(self):
+        """ True if this feed is for tv. """
+        return self.tvradio == 'tv'
+
+    name = property(get_name)
+
+
+tv = feed('tv')
+radio = feed('radio')
+
+def test():
+    tv = feed('tv')
+    print tv.popular()
+    print tv.channels()
+    print tv.get('bbc_one')
+    print tv.get('bbc_one').list()
+    for c in tv.get('bbc_one').categories():
+        print c
+    #print tv.get('bbc_one').channels()
+    #print tv.categories()
+    #print tv.get('drama').list()
+    #print tv.get('drama').get_subcategory('period').list()
+
+if __name__ == '__main__':
     test()



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]