home *** CD-ROM | disk | FTP | other *** search
/ Windows News 2005 November / WNnov2005.iso / Windows / Equipement / Media-portal / Setup.msi / _6F5CFA107520147A76D0EEE5A1A7F29D / _1EE7E06543CB417684249FDC75CDC7EE < prev    next >
Text File  |  2004-04-22  |  31KB  |  968 lines

  1. #!/usr/bin/env python
  2.  
  3. # $Id: tv_grab_nl.py,v 1.2 2004/04/22 12:12:53 yamp Exp $
  4.  
  5. # $Log: tv_grab_nl.py,v $
  6. # Revision 1.2  2004/04/22 12:12:53  yamp
  7. #   22-04-2004: tvguide     : fixed : Mediaportal didnt use the timezone settings specified in tvguide.xml
  8. #
  9. # Revision 1.1  2004/04/21 14:56:30  yamp
  10. # no message
  11. #
  12. # Revision 1.1  2004/04/21 05:47:14  yamp
  13. # no message
  14. #
  15. # Revision 1.16  2004/03/04 12:48:15  paul
  16. # Added check for programming with equal begin and end time. Sesamstraat
  17. # now works :-)
  18. #
  19. # Revision 1.15  2003/12/28 10:03:56  paul
  20. # Added --slowdays option. Grabs --slowdays with full info and the rest
  21. # with fast info.
  22. #
  23. # Revision 1.14  2003/11/06 07:12:36  paul
  24. # Python barfed on a regexp with a recursion limit error. Reworked the
  25. # regular expression.
  26. #
  27. # Revision 1.13  2003/11/02 07:54:45  paul
  28. # Fix: loop continues after exception.
  29. #
  30. # Revision 1.12  2003/11/01 21:53:06  paul
  31. # Blah.
  32. #
  33. # Revision 1.11  2003/09/30 11:27:03  paul
  34. # 1) FIXED BUG: two programs with identical end times and close start times could
  35. #    remove programming for the rest of the day.
  36. # 2) FIXED BUG: Removing items from a list did not check for double entries. This
  37. #    could cause the removal of a single program.
  38. # 3) FIXED BUG: the cache counters are now updated correctly. Before, a
  39. #    cached entry would not be counted anymore, now the cleaning and
  40. #    thresholding is more functional. Caching still needs a rethinking.
  41. # 4) MINOR: changed the output while fetching a little bit.
  42. #
  43. # Revision 1.10  2003/09/10 07:16:56  paul
  44. # Long overdue indentation tab->space conversion
  45. # Fixed bug in ampersand handling: &...; tags were allowed to include
  46. # whitespace.
  47. #
  48. # Revision 1.9  2003/08/01 06:50:56  paul
  49. # Config file quick fix
  50. #
  51. # Revision 1.8  2003/07/28 11:26:29  paul
  52. # Small fixes for pages that cannot be downloaded.
  53. #
  54. # Revision 1.7  2003/07/23 11:19:05  paul
  55. # Removed the 250 character limit because it sometimes broke by breaking
  56. # at &
  57. #
  58. # Revision 1.6  2003/07/15 07:08:46  paul
  59. # Removed some of the debugging output.
  60. #
  61. # Revision 1.5  2003/07/14 22:33:46  paul
  62. # More or less perfectly working. Have to clean up some of the debugging
  63. # code.
  64. #
  65. # Revision 1.4  2003/07/13 17:37:15  paul
  66. # Added most of the caching stuff.
  67. #
  68. # Revision 1.3  2003/07/10 06:47:25  paul
  69. # Randomized the fetching order for detail information.
  70. #
  71. # Revision 1.2  2003/07/09 21:08:44  paul
  72. # Fixed the Rembo&Rembo error. Added a little bit of documentation.
  73. #
  74. # Revision 1.1.1.1  2003/07/09 16:26:43  paul
  75. # First working version
  76. #
  77.  
  78. import re
  79. import urllib
  80. import getopt, sys
  81. from string import replace, split, strip
  82. import time
  83. import random
  84. import htmlentitydefs
  85. import os.path
  86. import pickle
  87.  
  88. # do extra debug stuff
  89. debug = 1
  90.  
  91. try:
  92.     import redirect
  93. except:
  94.     debug = 0
  95.     pass
  96.  
  97. """
  98. This is yet another XMLTV grabber based on tvgids.nl. It relies heavily
  99. on parsing the website and if anything changes on the site this script
  100. will probably fail.
  101.  
  102. Note:
  103. All this is created without any serious knowledge of python or how
  104. to create xmltv stuff. If it works, great! If not, well, send me a
  105. patch.
  106.  
  107. Some minor adaptation of category stuff is made to please mythtv.
  108. """ 
  109.  
  110.  
  111. # globals
  112.  
  113. tvgids = 'http://www.tvgids.nl/'
  114. uitgebreid_zoeken = tvgids + 'uitgebreidzoeken.html'
  115.  
  116.  
  117. # Wait a random number of seconds between each page fetch.
  118. # We want to be nice to tvgids.nl
  119. # Also, it appears tvguid.nl throttles its output.
  120. # So there.
  121. nice_time = [1, 3]
  122.  
  123. # Create a category translation dictionary
  124. # Look in mythtv/themes/blue/ui.xml for all categories
  125. # The keys are the categories used by tvgids.nl
  126. cattrans = { 'Film'             : 'Movies',
  127.              'Amusement'        : 'Talk',
  128.              'Nieuws/actualiteiten' : 'News',
  129.              'Informatief'      : 'Educational',
  130.              'Comedy'           : 'Comedy',
  131.              'Serie/soap'       : 'SERIE',
  132.              'Misdaad'          : 'Crime/Mystery',
  133.              'Sport'            : 'Sports',
  134.              'Muziek'           : 'Music',
  135.              'Documentaire'     : 'Documentary',
  136.              'Erotiek'          : 'Adult',
  137.              'Kunst/Cultuur'    : 'Educational',
  138.              'Wetenschap'       : 'Science/Nature',
  139.              'Jeugd'            : 'Kids',
  140.              'Natuur'           : 'Nature',
  141.              'Animatie'         : 'Kids',
  142.              'Religieus'        : 'Religion'}
  143.               
  144.  
  145. # build inverse htmldefs dict
  146. #html_to_latin1 = htmlentitydefs.entitydefs
  147. #latin1_to_html = {}
  148. #for key in html_to_latin1.keys():
  149. #   latin1_to_html[html_to_latin1[key]] = key
  150.  
  151. # Work in progress, the idea is to cache program categories and
  152. # descriptions to eliminate a lot of page fetches from tvgids.nl
  153. # for programs that do not have interesting/changing descriptions
  154.  
  155. class ProgramCache:
  156.     """
  157.     A cache to hold program name and category info
  158.     """
  159.     def __init__(self, filename=None, threshold=3):
  160.         """
  161.         Nothing much to do
  162.         """
  163.         # if a program has been queried at threshold times with
  164.         # the same information return the cached version
  165.         self.threshold = threshold
  166.         self.filename  = filename
  167.  
  168.         if filename == None:
  169.             self.pdict = {}
  170.         else:
  171.             if os.path.isfile(filename):
  172.                 self.load(filename)
  173.             else:
  174.                 self.pdict = {}
  175.  
  176.  
  177.     def load(self, filename):
  178.         """
  179.         Loads a pickled cache dict from file
  180.         """
  181.         self.pdict = pickle.load(open(filename,'r'))
  182.  
  183.     def dump(self, filename):
  184.         """
  185.         Dumps a pickled cache
  186.         """
  187.         pickle.dump(self.pdict, open(filename, 'w'))
  188.  
  189.     
  190.     def query(self, program_name):
  191.         """
  192.         Updates/gets/whatever.
  193.         """
  194.  
  195.         retval = None
  196.  
  197.         for key in self.pdict.keys():
  198.             item = self.pdict[key]
  199.             if program_name == item[0] and item[3] >= self.threshold:
  200.                 retval = item
  201.                 break
  202.         return retval
  203.  
  204.     def add(self, program_name, category, description):
  205.         """
  206.         Adds a program_name, category, description tuple
  207.         """
  208.         key = (program_name, category, description)
  209.         if self.pdict.has_key(key):
  210.             self.pdict[key][3] += 1
  211.         else:
  212.             self.pdict[key] = [program_name, category, description, 1]
  213.  
  214.     def clean(self):
  215.         """
  216.         Removes all programming under the current threshold from the cache
  217.         """
  218.         for key in self.pdict.keys():
  219.             item = self.pdict[key]
  220.             if item[3]<self.threshold:
  221.                 del self.pdict[key]
  222.  
  223.     def info(self):
  224.         """
  225.         Prints some info on the cache
  226.         """
  227.         n = len(self.pdict.keys())
  228.         t = 0.0
  229.  
  230.         for key in self.pdict.keys():
  231.             kaas = self.pdict[key]
  232.             t += self.pdict[key][3]
  233.             print "%s %s" % (kaas[3], kaas[0])
  234.             
  235.         print "Average threshold %s %s %s" % (t, n, t/n)
  236.  
  237.  
  238.  
  239.  
  240. def usage():
  241.     print 'Yet another grabber for Dutch television'
  242.     print 'Usage:'
  243.     print '--help, -h    = print this info'
  244.     print '--days        = # number of days to grab'
  245.     print '--slow        = also grab descriptions of programming'
  246.     print '--slowdays    = grab slowdays initial days and the rest in fast mode'
  247.     print '--offset      = # day offset from where to grab (0 is today)'
  248.     print '--configure   = create configfile (overwrites existing file)'
  249.     print '--output      = file where to put the output'
  250.     print '--cache       = cache descriptions and use the file to store'
  251.     print '--threshold # = number above which an item is considered to be cached'
  252.     print '--clean_cache = clean the cache file and exit'
  253.     print '--cache_info  = print some statistics about the cache and exit'
  254.  
  255.  
  256. def filter_line(s):
  257.     """
  258.     Removes unwanted stuff in strings (copied from tv_grab_be)
  259.     """
  260.  
  261.     # do the latin1 stuff
  262.     tag = re.compile('(&\S*?;)')
  263.     m   = tag.finditer(s)
  264.     for match in m:
  265.         s = replace(s,match.group(1), htmlentitydefs.entitydefs[match.group(1)])
  266.  
  267.     s = replace(s,' ',' ')
  268.     s = replace(s,'\r',' ')
  269.     x = re.compile('(<.*?>)')
  270.     s = x.sub('', s)
  271.  
  272.     # A couple of characters which are not legal in Latin-1, we have
  273.     # to guess what they are.
  274.     #
  275.     s = replace(s, '~Q', "'")
  276.     s = replace(s, '~R', "'")
  277.  
  278.     # Hmm, not sure if I understand this. Without it, mythfilldatabase barfs
  279.     # on program names like "Steinbrecher &..."
  280.     s = replace(s,'&','&')
  281.  
  282.     return s
  283.  
  284. def delete_from_list(list, items):
  285.     """
  286.     Removes the indices in items from the list
  287.  
  288.     ['a', 'b', 'c', 'd' ] [2,1] -> ['a', 'd']
  289.     """
  290.  
  291.     # unique-ify, sort, and reverse the range so that we start removing from the
  292.     # back of the list
  293.     u = {}
  294.     for item in items:
  295.         u[item] = 1;
  296.     
  297.     keys = u.keys()
  298.     keys.sort()
  299.     keys.reverse()
  300.     for i in keys:
  301.         del list[i]
  302.     
  303.  
  304. def time_kludge(s):
  305.     """
  306.     Given a string of "HH:MM" returns a list with hh and mm
  307.  
  308.     "23:10" -> [23, 10]
  309.     """
  310.     hh = int(s[0:2])
  311.     mm = int(s[3:5])
  312.     return [hh, mm]
  313.     
  314. def duration(h1,m1,h2,m2):
  315.     """
  316.     Calculates the duration of a program (24h times)
  317.     in minutes. [h2,m2] can be on the next day.
  318.  
  319.     duration(23,10,23,15) -> 5
  320.     duration(23,10,0,20)  -> 70
  321.     """
  322.     if h2<h1:
  323.         hd = 23-h1 + h2
  324.         md = 60-m1 + m2
  325.     else:
  326.         hd = h2-h1
  327.         md = m2-m1
  328.         if md<0:
  329.             md = 60+md
  330.             hd = hd - 1
  331.     return hd*60+md
  332.  
  333. def get_page(url):
  334.     """
  335.     Retrieves the url and returns a string with the contents
  336.     """
  337.     try:
  338.         fp = urllib.urlopen(url)
  339.         lines = fp.readlines()
  340.         page = "".join(lines)
  341.         return page
  342.     except:
  343.         sys.stderr.write('Cannot open url: %s\n' % url)
  344.         sys.exit(10);
  345.         return None
  346.  
  347.  
  348. def get_channels(file):
  349.     """
  350.     Get a list of all available channels and store these
  351.     in a file.
  352.     """
  353.     # store channels in a dict
  354.     channels = {}
  355.  
  356.     # tvgids stores several instances of channels, we want to
  357.     # find all the possibile channels
  358.     channel_get = re.compile('<select.*?name="station">(.*?)</select>', \
  359.                              re.DOTALL)
  360.  
  361.     # this is how we will find a (number, channel) instance
  362.     channel_re  = re.compile('<option value="([0-9]+)">(.*?)</option>', \
  363.                              re.DOTALL)
  364.  
  365.     # this is where we will try to find our channel list
  366.     total = get_page(uitgebreid_zoeken)
  367.     if total == None:
  368.         return
  369.  
  370.     # get a list of match objects of all the <select blah station>
  371.     stations = channel_get.finditer(total)
  372.  
  373.     # and create a dict of number, channel_name pairs
  374.     # we do this this way because several instances of the 
  375.     # channel list are stored in the url and not all of the 
  376.     # instances have all the channels, this way we get them all.
  377.     for station in stations:
  378.         m = channel_re.finditer(station.group(0))           
  379.         for p in m:
  380.             a = int(p.group(1))
  381.             b = p.group(2)
  382.             # this is not naughty, tvgids starts with language selections
  383.             # from 100 onwards, channel 0 does not exist
  384.             if a>0 and a<100:
  385.                 channels[a] = b
  386.  
  387.     # sort on channel number (arbitrary but who cares)
  388.     keys = channels.keys()
  389.     keys.sort()
  390.  
  391.     # and create a file with the channels
  392.     f = open(file,'w')
  393.     for k in keys:
  394.         f.write("%s %s\n" % (k, channels[k]))
  395.     f.close()
  396.  
  397.  
  398. def get_channel_day(channel, offset):
  399.     """
  400.     Get a day of programming for channel number, with
  401.     an offset, where offset is one of (tvgids contains 
  402.     no info beyond a certain day, 5 seems a reasonable upper limit
  403.     0 = today 
  404.     1 = tomorrow 
  405.     2 = day after tomorrow etc
  406.  
  407.     The output is a list of programming in order where each row
  408.     contains:
  409.     [ start_time, stop_time, detail_url, program_name ]
  410.     """
  411.  
  412.  
  413.     channel_url = 'http://www.tvgids.nl/zoekprogramma.php?'+\
  414.                   'trefwoord=Titel+of+trefwoord&'+\
  415.                   'station=%s&genre=alle&interval=%s×lot=0&periode=0&order=0' % (channel, offset)
  416.  
  417.  
  418.     # get the raw programming for a single day
  419.     total = get_page(channel_url)
  420.     if total == None:
  421.         return
  422.  
  423.     # check for following pages on the url and fetch those too
  424.  
  425.     # get the url given for the next page on this page
  426.     following_page = re.compile('<td\s+class="lijst_zender".*?<[aA].*?[hH][rR][Ee][fF]="(.*?)">Volgende</[Aa]>\s*</td>')
  427.  
  428.     all_pages_done = 0
  429.     extra_page = total
  430.  
  431.     while not all_pages_done:
  432.         # check for following page
  433.         follow = following_page.search(extra_page)
  434.         if follow != None:
  435.             try:
  436.                 zoek = follow.group(1).strip()
  437.                 new_url = tvgids + zoek
  438.                 extra_page = get_page(new_url)
  439.                 total = total + extra_page
  440.             except:
  441.                 all_pages_done = 1
  442.                 pass
  443.         else:
  444.             all_pages_done = 1
  445.                 
  446.     # Setup a number of regexps
  447.  
  448.     # match  <tr>.*</tr> 
  449.     getrow = re.compile('<[tT][Rr]>(.*?)</[tT][rR]>',re.DOTALL)
  450.  
  451.     # match the required program info
  452.     # 1 = channel name
  453.     # 2 = times
  454.     # 3 = program name
  455.     # 4 = url for details
  456.     parserow = re.compile('class="lijst_zender">(.*?)</td>.*?'  +\
  457.                           'class="lijst_tijd">(.*?)</td>.*?'    +\
  458.                           'class="lijst_programma">.*?<[aA]\s+' +\
  459.                           '[hH][rR][eE][fF]="(/.*?)".*?'        +\
  460.                           'class="details_programma">(.*?)</[aA]', re.DOTALL)
  461.  
  462.     # tvgids.nl uses the following system:
  463.     # 
  464.     # 1) normal begin and end times
  465.     times = re.compile('([0-9]+:[0-9]+)-([0-9]+:[0-9]+)')
  466.  
  467.     # 2) a program that starts at the end of another program
  468.     #    (i.e. no start time for second program)
  469.     #    for example:
  470.     #    SBS 6  22:15-00:00    Hart van Nederland
  471.     #    SBS 6 -22:40          Trekking Lotto
  472.     #    meaning that "Trekking Lotto" starts after "Hart van Nederland" 
  473.     times_follow = re.compile('-([0-9]+:[0-9]+)')
  474.  
  475.     # 3) nightly progamming (this is even worse)
  476.     #    SBS 6  02:15-00:00  Nachtprogrammering
  477.     #    SBS 6 -00:00  Mobile Nights
  478.     #    SBS 6 -00:00  Stem van Nederland
  479.     #    SBS 6 -  Hart van Nederland
  480.     #    not handled
  481.  
  482.     # and find relevant programming info
  483.     allrows = getrow.finditer(total)
  484.  
  485.     programs = []
  486.  
  487.     for r in allrows:
  488.  
  489.         detail = parserow.search(r.group(1))
  490.  
  491.         if detail != None: 
  492.  
  493.             # default times
  494.             start_time = None
  495.             stop_time  = None
  496.  
  497.             # parse for begin and end times
  498.             t  = times.search(detail.group(2))
  499.             tf = times_follow.search(detail.group(2))
  500.  
  501.             if t != None:
  502.                 start_time = t.group(1)
  503.                 stop_time  = t.group(2)
  504.             elif tf != None:
  505.                 stop_time = tf.group(1)
  506.             else:
  507.                 # Well, here we reach wonderful
  508.                 # programming that is so important that
  509.                 # begin and end times are not given.
  510.                 # Skip.
  511.                 pass
  512.  
  513.             program_url  = tvgids + detail.group(3)
  514.             program_name = detail.group(4)
  515.             program_name = program_name.strip()
  516.  
  517.             # store time, name and detail url in a list 
  518.             programs.append([start_time, stop_time, program_url, program_name])
  519.  
  520.     #if debug:
  521.         #sys.stderr.write('get_channel output----------\n')
  522.         #for program in programs:
  523.             #sys.stderr.write('%s\n' % program)
  524.  
  525.     # done
  526.     return programs
  527.  
  528. def parse_programs(programs, offset):
  529.     """
  530.     Parse a list of programs as generated by get_channel_day()  and
  531.     convert begin and end times to xmltv compatible times.  
  532.  
  533.     Programs is a list where each row contains:
  534.     [ start_time, stop_time, detail_url, program_name ]
  535.     
  536.     """
  537.  
  538.     # good programs
  539.     good_programs = []
  540.  
  541.  
  542.     for i in range(len(programs)):
  543.  
  544.         # The common case: start and end times are present and are not
  545.     # equal to each other (yes, this can happen)
  546.         if programs[i][0] != None and programs[i][1] != None and programs[i][0] != programs[i][1]:
  547.             good_programs.append(programs[i])
  548.  
  549.  
  550.         # Check for clumped programming
  551.         elif programs[i][0] == None and programs[i][1] != None:
  552.             # This is programming that follows directly
  553.             # after the previous programming. 
  554.             # As far as I can see MythTV does not cater for clumpidx, here
  555.             # we concatenate the names, create one program and hope for the best
  556.             try:
  557.                 # double check
  558.                 if good_programs[-1][1] == '00:00':
  559.                     # set end time of previous program to
  560.                     # end time of this program
  561.                     good_programs[-1][1] = programs[i][1]
  562.                     # and adjust program name
  563.                     good_programs[-1][3] += ' [ + %s]' % programs[i][3]
  564.             except:
  565.                 # good_programs was empty. Should
  566.                 # not happen. Oh well.
  567.                 pass
  568.         # hmm both times are none, skip
  569.         else:
  570.             pass
  571.  
  572.     # adjust last program, this also has the ``benefit'' that it
  573.     # skips allnight sex/mobile phne programming.
  574.     try:
  575.         if good_programs[-1][1] == '00:00' and good_programs[-1][0][0] == '0':
  576.             del(good_programs[-1])
  577.     except:
  578.         pass
  579.  
  580.     # So, good programming contains a list of all correct
  581.     # programming with filled in start and end times.
  582.     # All we have to do now is to correct the times for xmltv and to
  583.     # account for a day switch.
  584.  
  585.     ##if debug:
  586.         #sys.stderr.write('parse_program: after first test\n')
  587.         #for program in good_programs:
  588.             #sys.stderr.write('%s\n' % program)
  589.  
  590.     
  591.     # Check for `motherprograms' i.e. a name given to a grouping of
  592.     # programs, which is not a program itself.
  593.     # Two checks are performed: if begin times match then the first
  594.     # is removed.
  595.     # If the time difference between the start times + the duration
  596.     # of the second program is smaller than the duration of the
  597.     # first program then the first program is removed.
  598.     to_remove = []
  599.     for c in range(0, len(good_programs)-1):
  600.         first = good_programs[c]
  601.         next  = good_programs[c+1]
  602.  
  603.         # also check for duration
  604.         start1 = time_kludge(first[0])
  605.         stop1  = time_kludge(first[1])
  606.         start2 = time_kludge(next[0])
  607.         stop2  = time_kludge(next[1])
  608.         duration1 = duration(start1[0], start1[1], stop1[0], stop1[1])
  609.         duration2 = duration(start2[0], start2[1], stop2[0], stop2[1])
  610.         duration3 = duration(start1[0], start1[1], start2[0], start2[1])
  611.         
  612.         # if begin times match, remove the first program
  613.         if first[0] == next[0]: 
  614.             to_remove.append(c)
  615.  
  616.         # e.g.:
  617.         # 09:00-12:00 Z@ppelin
  618.         # 09:03-09:10 Pingu
  619.         # 09:10-09:15 Whatever
  620.         # the first is a mother program, but the start times do
  621.         # not match
  622.         elif (duration3+duration2)<duration1:
  623.             to_remove.append(c)
  624.  
  625.     # if end times match, remove first program
  626.         elif first[1] == next[1]:
  627.         to_remove.append(c)
  628.  
  629.     delete_from_list(good_programs, to_remove)
  630.  
  631.     # done with checks, now correct the times/dates
  632.  
  633.     # store enough dates so that an offset is an index into dates
  634.     dates = [time.strftime('%Y%m%d', time.gmtime(time.time()+x*86400)) for x in range(0,offset+2)]
  635.  
  636.     # and finally, modify the start/end times to xmltv format
  637.     add = 0
  638.     prevstart = 0000
  639.     prevstop  = 0000
  640.     for c in range(len(good_programs)):
  641.         start = good_programs[c][0].replace(':','')
  642.         stop  = good_programs[c][1].replace(':','')
  643.  
  644.         # assign the new times
  645.         good_programs[c][0] = dates[offset+add]+start+'00'
  646.         good_programs[c][1] = dates[offset+add]+stop +'00'
  647.  
  648.         # check for day switch between programs
  649.         if start<prevstop:
  650.             add = 1
  651.             good_programs[c][0] = dates[offset+add]+start+'00'
  652.             good_programs[c][1] = dates[offset+add]+stop +'00'
  653.             
  654.         # check for day switch in program
  655.         if int(stop) < int(start):
  656.             add = 1
  657.             good_programs[c][1] = dates[offset+add]+stop +'00'
  658.  
  659.         prevstart = start
  660.         prevstop  = stop
  661.     
  662.     #if debug:
  663.         #sys.stderr.write('parse done-------------\n')
  664.         #for program in good_programs:
  665.             #sys.stderr.write('%s\n' % program)
  666.         
  667.     # done, nothing to see here, please move on 
  668.     return good_programs
  669.  
  670. def get_descriptions(programs, program_cache=None):
  671.     """
  672.     Given a programs list (from get_channel) 
  673.     retrieve program information
  674.     """
  675.  
  676.     detail = re.compile('<div class="detailDeel">.*?' +\
  677.                         '<div class="detailLabel2">(.*?)</div>.*?'+\
  678.                         '<div class="detailContent2">(.*?)</div>',\
  679.                         re.DOTALL)
  680.     
  681.     num_descriptions = 0
  682.     sys.stderr.write('Descriptions[%s]: ' % len(programs))
  683.  
  684.     # randomize detail requests
  685.     fetch_order = range(0,len(programs))
  686.     random.shuffle(fetch_order)
  687.  
  688.     #for i in range(0,len(programs)):
  689.     for i in fetch_order:
  690.         
  691.         sys.stderr.write('\n%s: %s ' % (i, programs[i][3]))
  692.  
  693.         # add a dictionary to hold program details
  694.         programs[i].append({})
  695.  
  696.         # if we have a cache use it
  697.         if program_cache != None:
  698.             cached_program = program_cache.query(programs[i][3])
  699.             if cached_program != None:
  700.                 programs[i][-1]['Genre:']        = cached_program[1]
  701.                 programs[i][-1]['Beschrijving:'] = cached_program[2]
  702.         # this adds 1 to the cache usage counter
  703.         # silly structure, need to rethink the caching.
  704.                 program_cache.add(programs[i][3], cached_program[1], cached_program[2])
  705.         sys.stderr.write('cached(%s) ' % cached_program[3]);
  706.                 #sys.stderr.write('Using cache(%s) ' %i)
  707.                 continue
  708.                     
  709.         # be nice to tvgids.nl
  710.         time.sleep(random.randint(nice_time[0], nice_time[1]))
  711.  
  712.         # get the details page, and get all the detail nodes
  713.         try:
  714.             total = get_page(programs[i][2])
  715.             descriptions = detail.finditer(total)
  716.         except:
  717.             # if we cannot find the description page, 
  718.         # go to next in the loop
  719.             sys.stderr.write('Oh, thingy\n')
  720.         continue
  721.             
  722.  
  723.         # now parse the details
  724.         for description in descriptions:
  725.             title   = description.groups()[0].strip()
  726.             content = description.groups()[1].strip()
  727.             content = filter_line(content)
  728.             if content == '':
  729.                 continue
  730.  
  731.             elif title == '':
  732.                 programs[i][-1]['Beschrijving:'] = content
  733.                 #if len(content)<250:
  734.                     #programs[i][-1]['Beschrijving:'] = content
  735.                 #else:
  736.                     #programs[i][-1]['Beschrijving:'] = content[0:250]+"..."
  737.  
  738.             elif title == 'Genre:':
  739.                 try:
  740.                     programs[i][-1][title] = cattrans[content]
  741.                 except:
  742.                     programs[i][-1][title] = content
  743.             
  744.  
  745.             elif title in ['Inhoud:', 'Titel aflevering:']:
  746.                 programs[i][-1][title] = content
  747.                 #if len(content)<250:
  748.                     #programs[i][-1][title] = content
  749.                 #else:
  750.                     #programs[i][-1][title] = content[0:250]+"..."
  751.  
  752.         # update the cache if necessary
  753.         if program_cache != None:
  754.             try:
  755.                 category = programs[i][-1]['Genre:']
  756.             except:
  757.                 category = ''
  758.             try:
  759.                 description = programs[i][-1]['Beschrijving:']
  760.             except:
  761.                 description = ''
  762.             program_cache.add(programs[i][3], category, description)
  763.                     
  764.  
  765.     sys.stderr.write('done...\n')
  766.                     
  767.     # done
  768.  
  769.       
  770.  
  771. def xmlefy_programs(programs, channel):
  772.     """
  773.     Given a list of programming (from get_channels())
  774.     returns a string with the xml equivalent
  775.     """
  776.     output = []
  777.     for program in programs:
  778.         output.append('  <programme start="%s +0100" stop="%s +0100" channel="%s">\n' % (program[0], program[1], channel))
  779.         output.append('    <title lang="nl">%s</title>\n' % filter_line(program[3]))
  780.         if len(program)==5:
  781.             try:
  782.                 output.append('    <sub-title lang="nl">%s</sub-title>\n' % program[4]['Titel aflevering:'])
  783.             except:
  784.                 pass
  785.             if program[4].has_key('Inhoud:') and program[4].has_key('Beschrijving:'):
  786.                 try:
  787.                     output.append('    <desc lang="nl">%s\n%s</desc>\n' % (program[4]['Inhoud:'], program[4]['Beschrijving:']))
  788.                 except:
  789.                     pass
  790.             else:
  791.                 try:
  792.                     output.append('    <desc lang="nl">%s</desc>\n' % program[4]['Inhoud:'])
  793.                 except:
  794.                     pass
  795.                 try:
  796.                     output.append('    <desc lang="nl">%s</desc>\n' % program[4]['Beschrijving:'])
  797.                 except:
  798.                     pass
  799.             try:
  800.                 output.append('    <category lang="nl">%s</category>\n' % program[4]['Genre:'])
  801.             except:
  802.                 pass
  803.                 
  804.         output.append('  </programme>\n')
  805.     
  806.     return "".join(output)
  807.  
  808. def main():
  809.  
  810.     # Parse command line options
  811.     try:
  812.         opts, args = getopt.getopt(sys.argv[1:], "h", ["help", "output=", 
  813.                                                        "offset=", "days=", 
  814.                                    "configure", "slow", 
  815.                                    "cache=", "threshold=", 
  816.                                    "clean_cache", "cache_info"])
  817.     except getopt.GetoptError:
  818.         usage()
  819.         sys.exit(2)
  820.  
  821.     output      = None
  822.     output_file = None
  823.     offset      = 0
  824.     days        = 7
  825.     slow        = 0
  826.     slowdays    = 2
  827.     program_cache = None
  828.     program_cache_file = 'program_cache'
  829.     config_file = 'tv_grab_nl_pdb.conf'
  830.     threshold   = 3
  831.     clean_cache = 0
  832.     cache_info  = 0
  833.  
  834.     # seed the random generator
  835.     random.seed()
  836.  
  837.  
  838.     for o, a in opts:
  839.         if o in ("-h", "--help"):
  840.             usage()
  841.             sys.exit(1)
  842.         if o == "--configure":
  843.             sys.stderr.write('Creating config file: %s\n' % config_file)
  844.             get_channels(config_file)
  845.             sys.exit(2)
  846.         if o == "--days":
  847.             days = int(a)
  848.         if o == "--offset":
  849.             offset = int(a)
  850.         if o == "--slow":
  851.             slow = 1
  852.         if o == "--slowdays":
  853.         slowdays = int(a)
  854.             # slowdays implies slow == 0
  855.         slow = 0
  856.         if o == "--clean_cache":
  857.             clean_cache = 1
  858.         if o == "--threshold":
  859.             threshold = int(a)
  860.         if o == "--cache":
  861.             program_cache_file = a
  862.         if o == "--cache_info":
  863.             cache_info = 1
  864.         if o == "--output":
  865.             output_file = a
  866.             try:
  867.                 output = open(output_file,'w')
  868.                 # and redirect output
  869.                 if debug:
  870.                     debug_file = open('/tmp/kaas.xml','w')
  871.                     blah = redirect.Tee(output, debug_file) 
  872.                     sys.stdout = blah
  873.                 else:
  874.                     sys.stdout = output
  875.             except:
  876.                 sys.stderr.write('Cannot write to outputfile: %s\n' % output_file)
  877.                 sys.exit(10);
  878.  
  879.  
  880.  
  881.     # get configfile if available
  882.     try:
  883.         f = open(config_file,'r')
  884.     except:
  885.         sys.stderr.write('Config file not found.\n')
  886.         sys.stderr.write('Re-run me with the --configure flag.\n')
  887.         sys.exit(1)
  888.  
  889.     #check for cache
  890.     program_cache = ProgramCache(program_cache_file, threshold)
  891.     if clean_cache != 0:
  892.         print "Cleaning the cache using threshold = %s\n" % threshold
  893.         program_cache.clean()
  894.         program_cache.dump(program_cache_file)
  895.         sys.exit(0)
  896.     elif cache_info != 0:
  897.         print "Cache info"
  898.         program_cache.info()
  899.         sys.exit(0)
  900.  
  901.  
  902.     # Go!
  903.     channels = {}
  904.  
  905.     # Read the channel stuff
  906.     for blah in f.readlines():
  907.         blah = blah.lstrip()
  908.         blah = blah.replace('\n','')
  909.         if blah[0] != '#':
  910.             channel = blah.split()
  911.             channels[channel[0]] = " ".join(channel[1:])
  912.  
  913.     # channels are now in channels dict keyed on channel id
  914.  
  915.     # print header stuff
  916.     print '<?xml version="1.0" encoding="ISO-8859-1"?>'
  917.     print '<!DOCTYPE tv SYSTEM "xmltv.dtd">'
  918.     print '<tv generator-info-name="Icky Pooh">'
  919.  
  920.     # first do the channel info
  921.     for key in channels.keys():
  922.         print '  <channel id="%s">' % key
  923.         print '    <display-name lang="nl">%s</display-name>' % channels[key]
  924.         print '  </channel>'
  925.  
  926.     num_chans = len(channels.keys())
  927.     cur_day = -1
  928.  
  929.     # now loop over the days and get the programming
  930.     for x in range(0, days):
  931.         cur_day += 1
  932.         sys.stderr.write('Day %s of %s\n' % (cur_day, days))
  933.         cur_chan  = -1
  934.         fluffy = channels.keys()
  935.         random.shuffle(fluffy)
  936.     
  937.         for id in fluffy:
  938.             cur_chan += 1
  939.             sys.stderr.write('Channel %s of %s\n' % (cur_chan, num_chans))
  940.         try:
  941.                 info = get_channel_day(id, offset+x)
  942.                 blah = parse_programs(info, offset+x)
  943.                 if slow or (slowdays>0 and x<slowdays):
  944.                     get_descriptions(blah, program_cache)
  945.                 print xmlefy_programs(blah, id)
  946.             except:
  947.             sys.stderr.write('Could not get channel %s\n' % cur_chan)
  948.             sys.exit(10);
  949.             # be nice to tvgids.nl
  950.             time.sleep(random.randint(2,5))
  951.  
  952.     # print footer stuff
  953.     print "</tv>"
  954.  
  955.     # close the outputfile if necessary
  956.     if output != None:
  957.         output.close()
  958.  
  959.     # save the cache if necessary
  960.     if program_cache != None:
  961.         program_cache.dump(program_cache_file)
  962.  
  963. # allow this to be a module
  964. if __name__ == '__main__':
  965.     main()
  966.  
  967. # vim:tw=0:et:sw=4
  968.