home *** CD-ROM | disk | FTP | other *** search
/ PC World 2007 December (DVD) / PCWorld_2007-12_DVD.iso / multimedia / miro / Miro_Installer.exe / xulrunner / python / feed.py < prev    next >
Encoding:
Python Source  |  2007-10-31  |  83.6 KB  |  2,325 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. # FIXME import * is really bad practice..  At the very least, lest keep it at
  19. # the top, so it cant overwrite other symbols.
  20. from item import *
  21.  
  22. from HTMLParser import HTMLParser,HTMLParseError
  23. from cStringIO import StringIO
  24. from copy import copy
  25. from datetime import datetime, timedelta
  26. from gtcache import gettext as _
  27. from inspect import isfunction
  28. from new import instancemethod
  29. from urlparse import urlparse, urljoin
  30. from xhtmltools import unescape,xhtmlify,fixXMLHeader, fixHTMLHeader, urlencode, urldecode
  31. import os
  32. import string
  33. import re
  34. import traceback
  35. import xml
  36.  
  37. from database import defaultDatabase, DatabaseConstraintError
  38. from httpclient import grabURL, NetworkError
  39. from iconcache import iconCacheUpdater, IconCache
  40. from templatehelper import quoteattr, escape, toUni
  41. from string import Template
  42. import app
  43. import config
  44. import dialogs
  45. import eventloop
  46. import folder
  47. import menu
  48. import prefs
  49. import resources
  50. import downloader
  51. from util import returnsUnicode, unicodify, chatter, checkU, checkF, quoteUnicodeURL, miro_listdir
  52. from platformutils import filenameToUnicode, makeURLSafe, unmakeURLSafe, osFilenameToFilenameType, FilenameType
  53. import filetypes
  54. import views
  55. import indexes
  56. import searchengines
  57. import sorts
  58. import logging
  59. import shutil
  60. from clock import clock
  61.  
  62. whitespacePattern = re.compile(r"^[ \t\r\n]*$")
  63.  
  64. @returnsUnicode
  65. def defaultFeedIconURL():
  66.     return resources.url(u"images/feedicon.png")
  67.  
  68. @returnsUnicode
  69. def defaultFeedIconURLTablist():
  70.     return resources.url(u"images/feedicon-tablist.png")
  71.  
  72. # Notes on character set encoding of feeds:
  73. #
  74. # The parsing libraries built into Python mostly use byte strings
  75. # instead of unicode strings.  However, sometimes they get "smart" and
  76. # try to convert the byte stream to a unicode stream automatically.
  77. #
  78. # What does what when isn't clearly documented
  79. #
  80. # We use the function toUni() to fix those smart conversions
  81. #
  82. # If you run into Unicode crashes, adding that function in the
  83. # appropriate place should fix it.
  84.  
  85. # Universal Feed Parser http://feedparser.org/
  86. # Licensed under Python license
  87. import feedparser
  88.  
  89. # Pass in a connection to the frontend
  90. def setDelegate(newDelegate):
  91.     global delegate
  92.     delegate = newDelegate
  93.  
  94. # Pass in a feed sorting function 
  95. def setSortFunc(newFunc):
  96.     global sortFunc
  97.     sortFunc = newFunc
  98.  
  99. #
  100. # Adds a new feed using USM
  101. def addFeedFromFile(file):
  102.     checkF(file)
  103.     d = feedparser.parse(file)
  104.     if d.feed.has_key('links'):
  105.         for link in d.feed['links']:
  106.             if link['rel'] == 'start' or link['rel'] == 'self':
  107.                 Feed(link['href'])
  108.                 return
  109.     if d.feed.has_key('link'):
  110.         addFeedFromWebPage(d.feed.link)
  111.  
  112. #
  113. # Adds a new feed based on a link tag in a web page
  114. def addFeedFromWebPage(url):
  115.     checkU(url)
  116.     def callback(info):
  117.         url = HTMLFeedURLParser().getLink(info['updated-url'],info['body'])
  118.         if url:
  119.             Feed(url)
  120.     def errback(error):
  121.         logging.warning ("unhandled error in addFeedFromWebPage: %s", error)
  122.     grabURL(url, callback, errback)
  123.  
  124. # URL validitation and normalization
  125. def validateFeedURL(url):
  126.     checkU(url)
  127.     for c in url.encode('utf8'):
  128.         if ord(c) > 127:
  129.             return False
  130.     if re.match(r"^(http|https)://[^/ ]+/[^ ]*$", url) is not None:
  131.         return True
  132.     if re.match(r"^file://.", url) is not None:
  133.         return True
  134.     match = re.match(r"^dtv:searchTerm:(.*)\?(.*)$", url)
  135.     if match is not None and validateFeedURL(urldecode(match.group(1))):
  136.         return True
  137.     return False
  138.  
  139. def normalizeFeedURL(url):
  140.     checkU(url)
  141.     # Valid URL are returned as-is
  142.     if validateFeedURL(url):
  143.         return url
  144.  
  145.     searchTerm = None
  146.     m = re.match(r"^dtv:searchTerm:(.*)\?([^?]+)$", url)
  147.     if m is not None:
  148.         searchTerm = urldecode(m.group(2))
  149.         url = urldecode(m.group(1))
  150.  
  151.     originalURL = url
  152.     url = url.strip()
  153.     
  154.     # Check valid schemes with invalid separator
  155.     match = re.match(r"^(http|https):/*(.*)$", url)
  156.     if match is not None:
  157.         url = "%s://%s" % match.group(1,2)
  158.  
  159.     # Replace invalid schemes by http
  160.     match = re.match(r"^(([A-Za-z]*):/*)*(.*)$", url)
  161.     if match and match.group(2) in ['feed', 'podcast', 'fireant', None]:
  162.         url = "http://%s" % match.group(3)
  163.     elif match and match.group(1) == 'feeds':
  164.         url = "https://%s" % match.group(3)
  165.  
  166.     # Make sure there is a leading / character in the path
  167.     match = re.match(r"^(http|https)://[^/]*$", url)
  168.     if match is not None:
  169.         url = url + "/"
  170.  
  171.     if searchTerm is not None:
  172.         url = "dtv:searchTerm:%s?%s" % (urlencode(url), urlencode(searchTerm))
  173.     else:
  174.         url = quoteUnicodeURL(url)
  175.  
  176.     if not validateFeedURL(url):
  177.         logging.info ("unable to normalize URL %s", originalURL)
  178.         return originalURL
  179.     else:
  180.         return url
  181.  
  182.  
  183. ##
  184. # Handle configuration changes so we can update feed update frequencies
  185.  
  186. def configDidChange(key, value):
  187.     if key is prefs.CHECK_CHANNELS_EVERY_X_MN.key:
  188.         for feed in views.feeds:
  189.             updateFreq = 0
  190.             try:
  191.                 updateFreq = feed.parsed["feed"]["ttl"]
  192.             except:
  193.                 pass
  194.             feed.setUpdateFrequency(updateFreq)
  195.  
  196. config.addChangeCallback(configDidChange)
  197.  
  198. ##
  199. # Actual implementation of a basic feed.
  200. class FeedImpl:
  201.     def __init__(self, url, ufeed, title = None, visible = True):
  202.         checkU(url)
  203.         if title:
  204.             checkU(title)
  205.         self.url = url
  206.         self.ufeed = ufeed
  207.         self.calc_item_list()
  208.         if title == None:
  209.             self.title = url
  210.         else:
  211.             self.title = title
  212.         self.created = datetime.now()
  213.         self.visible = visible
  214.         self.updating = False
  215.         self.lastViewed = datetime.min
  216.         self.thumbURL = defaultFeedIconURL()
  217.         self.initialUpdate = True
  218.         self.updateFreq = config.get(prefs.CHECK_CHANNELS_EVERY_X_MN)*60
  219.  
  220.     def calc_item_list(self):
  221.         self.items = views.toplevelItems.filterWithIndex(indexes.itemsByFeed, self.ufeed.id)
  222.         self.availableItems = self.items.filter(lambda x: x.getState() == 'new')
  223.         self.unwatchedItems = self.items.filter(lambda x: x.getState() == 'newly-downloaded')
  224.         self.availableItems.addAddCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  225.         self.availableItems.addRemoveCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  226.         self.unwatchedItems.addAddCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  227.         self.unwatchedItems.addRemoveCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  228.         
  229.     def signalChange(self):
  230.         self.ufeed.signalChange()
  231.  
  232.     @returnsUnicode
  233.     def getBaseHref(self):
  234.         """Get a URL to use in the <base> tag for this channel.  This is used
  235.         for relative links in this channel's items.
  236.         """
  237.         return escape(self.url)
  238.  
  239.     # Sets the update frequency (in minutes). 
  240.     # - A frequency of -1 means that auto-update is disabled.
  241.     def setUpdateFrequency(self, frequency):
  242.         try:
  243.             frequency = int(frequency)
  244.         except ValueError:
  245.             frequency = -1
  246.  
  247.         if frequency < 0:
  248.             self.cancelUpdateEvents()
  249.             self.updateFreq = -1
  250.         else:
  251.             newFreq = max(config.get(prefs.CHECK_CHANNELS_EVERY_X_MN),
  252.                           frequency)*60
  253.             if newFreq != self.updateFreq:
  254.                 self.updateFreq = newFreq
  255.                 self.scheduleUpdateEvents(-1)
  256.         self.ufeed.signalChange()
  257.  
  258.     def scheduleUpdateEvents(self, firstTriggerDelay):
  259.         self.cancelUpdateEvents()
  260.         if firstTriggerDelay >= 0:
  261.             self.scheduler = eventloop.addTimeout(firstTriggerDelay, self.update, "Feed update (%s)" % self.getTitle())
  262.         else:
  263.             if self.updateFreq > 0:
  264.                 self.scheduler = eventloop.addTimeout(self.updateFreq, self.update, "Feed update (%s)" % self.getTitle())
  265.  
  266.     def cancelUpdateEvents(self):
  267.         if hasattr(self, 'scheduler') and self.scheduler is not None:
  268.             self.scheduler.cancel()
  269.             self.scheduler = None
  270.  
  271.     # Subclasses should override this
  272.     def update(self):
  273.         self.scheduleUpdateEvents(-1)
  274.  
  275.     # Returns true iff this feed has been looked at
  276.     def getViewed(self):
  277.         return self.lastViewed != datetime.min
  278.  
  279.     # Returns the ID of the actual feed, never that of the UniversalFeed wrapper
  280.     def getFeedID(self):
  281.         return self.getID()
  282.  
  283.     def getID(self):
  284.         try:
  285.             return self.ufeed.getID()
  286.         except:
  287.             logging.info ("%s has no ufeed", self)
  288.  
  289.     # Returns string with number of unwatched videos in feed
  290.     def numUnwatched(self):
  291.         return len(self.unwatchedItems)
  292.  
  293.     # Returns string with number of available videos in feed
  294.     def numAvailable(self):
  295.         return len(self.availableItems)
  296.  
  297.     # Returns true iff both unwatched and available numbers should be shown
  298.     def showBothUAndA(self):
  299.         return self.showU() and self.showA()
  300.  
  301.     # Returns true iff unwatched should be shown 
  302.     def showU(self):
  303.         return len(self.unwatchedItems) > 0
  304.  
  305.     # Returns true iff available should be shown
  306.     def showA(self):
  307.         return len(self.availableItems) > 0 and not self.isAutoDownloadable()
  308.  
  309.     ##
  310.     # Sets the last time the feed was viewed to now
  311.     def markAsViewed(self):
  312.         self.lastViewed = datetime.now() 
  313.         for item in self.items:
  314.             if item.getState() == "new":
  315.                 item.signalChange(needsSave=False)
  316.  
  317.         self.ufeed.signalChange()
  318.  
  319.     ##
  320.     # Returns true iff the feed is loading. Only makes sense in the
  321.     # context of UniversalFeeds
  322.     def isLoading(self):
  323.         return False
  324.  
  325.     ##
  326.     # Returns true iff this feed has a library
  327.     def hasLibrary(self):
  328.         return False
  329.  
  330.     def startManualDownload(self):
  331.         next = None
  332.         for item in self.items:
  333.             if item.isPendingManualDownload():
  334.                 if next is None:
  335.                     next = item
  336.                 elif item.getPubDateParsed() > next.getPubDateParsed():
  337.                     next = item
  338.         if next is not None:
  339.             next.download(autodl = False)
  340.  
  341.     def startAutoDownload(self):
  342.         next = None
  343.         for item in self.items:
  344.             if item.isEligibleForAutoDownload():
  345.                 if next is None:
  346.                     next = item
  347.                 elif item.getPubDateParsed() > next.getPubDateParsed():
  348.                     next = item
  349.         if next is not None:
  350.             next.download(autodl = True)
  351.  
  352.     ##
  353.     # Returns marks expired items as expired
  354.     def expireItems(self):
  355.         for item in self.items:
  356.             expireTime = item.getExpirationTime()
  357.             if (item.getState() == 'expiring' and expireTime is not None and 
  358.                     expireTime < datetime.now()):
  359.                 item.executeExpire()
  360.  
  361.     ##
  362.     # Returns true iff feed should be visible
  363.     def isVisible(self):
  364.         self.ufeed.confirmDBThread()
  365.         return self.visible
  366.  
  367.     def signalItems (self):
  368.         for item in self.items:
  369.             item.signalChange(needsSave=False)
  370.  
  371.     ##
  372.     # Return the 'system' expiration delay, in days (can be < 1.0)
  373.     def getDefaultExpiration(self):
  374.         return float(config.get(prefs.EXPIRE_AFTER_X_DAYS))
  375.  
  376.     ##
  377.     # Returns the 'system' expiration delay as a formatted string
  378.     @returnsUnicode
  379.     def getFormattedDefaultExpiration(self):
  380.         expiration = self.getDefaultExpiration()
  381.         formattedExpiration = u''
  382.         if expiration < 0:
  383.             formattedExpiration = _('never')
  384.         elif expiration < 1.0:
  385.             formattedExpiration = _('%d hours') % int(expiration * 24.0)
  386.         elif expiration == 1:
  387.             formattedExpiration = _('%d day') % int(expiration)
  388.         elif expiration > 1 and expiration < 30:
  389.             formattedExpiration = _('%d days') % int(expiration)
  390.         elif expiration >= 30:
  391.             formattedExpiration = _('%d months') % int(expiration / 30)
  392.         return formattedExpiration
  393.  
  394.     ##
  395.     # Returns "feed," "system," or "never"
  396.     @returnsUnicode
  397.     def getExpirationType(self):
  398.         self.ufeed.confirmDBThread()
  399.         return self.ufeed.expire
  400.  
  401.     ##
  402.     # Returns"unlimited" or the maximum number of items this feed can fall behind
  403.     def getMaxFallBehind(self):
  404.         self.ufeed.confirmDBThread()
  405.         if self.ufeed.fallBehind < 0:
  406.             return u"unlimited"
  407.         else:
  408.             return self.ufeed.fallBehind
  409.  
  410.     ##
  411.     # Returns "unlimited" or the maximum number of items this feed wants
  412.     def getMaxNew(self):
  413.         self.ufeed.confirmDBThread()
  414.         if self.ufeed.maxNew < 0:
  415.             return u"unlimited"
  416.         else:
  417.             return self.ufeed.maxNew
  418.  
  419.     ##
  420.     # Returns the total absolute expiration time in hours.
  421.     # WARNING: 'system' and 'never' expiration types return 0
  422.     def getExpirationTime(self):
  423.         delta = None
  424.         self.ufeed.confirmDBThread()
  425.         expireAfterSetting = config.get(prefs.EXPIRE_AFTER_X_DAYS)
  426.         if (self.ufeed.expireTime is None or self.ufeed.expire == 'never' or 
  427.             (self.ufeed.expire == 'system' and expireAfterSetting <= 0)):
  428.             return 0
  429.         else:
  430.             return (self.ufeed.expireTime.days * 24 + 
  431.                     self.ufeed.expireTime.seconds / 3600)
  432.  
  433.     ##
  434.     # Returns the number of days until a video expires
  435.     def getExpireDays(self):
  436.         ret = 0
  437.         self.ufeed.confirmDBThread()
  438.         try:
  439.             return self.ufeed.expireTime.days
  440.         except:
  441.             return timedelta(days=config.get(prefs.EXPIRE_AFTER_X_DAYS)).days
  442.  
  443.     ##
  444.     # Returns the number of hours until a video expires
  445.     def getExpireHours(self):
  446.         ret = 0
  447.         self.ufeed.confirmDBThread()
  448.         try:
  449.             return int(self.ufeed.expireTime.seconds/3600)
  450.         except:
  451.             return int(timedelta(days=config.get(prefs.EXPIRE_AFTER_X_DAYS)).seconds/3600)
  452.  
  453.     def getExpires (self):
  454.         expireAfterSetting = config.get(prefs.EXPIRE_AFTER_X_DAYS)
  455.         return (self.ufeed.expireTime is None or self.ufeed.expire == 'never' or 
  456.                 (self.ufeed.expire == 'system' and expireAfterSetting <= 0))
  457.  
  458.     ##
  459.     # Returns true iff item is autodownloadable
  460.     def isAutoDownloadable(self):
  461.         self.ufeed.confirmDBThread()
  462.         return self.ufeed.autoDownloadable
  463.  
  464.     def autoDownloadStatus(self):
  465.         status = self.isAutoDownloadable()
  466.         if status:
  467.             return u"ON"
  468.         else:
  469.             return u"OFF"
  470.  
  471.     ##
  472.     # Returns the title of the feed
  473.     @returnsUnicode
  474.     def getTitle(self):
  475.         try:
  476.             title = self.title
  477.             if whitespacePattern.match(title):
  478.                 title = self.url
  479.             return title
  480.         except:
  481.             return u""
  482.  
  483.     ##
  484.     # Returns the URL of the feed
  485.     @returnsUnicode
  486.     def getURL(self):
  487.         try:
  488.             if self.ufeed.searchTerm is None:
  489.                 return self.url
  490.             else:
  491.                 return u"dtv:searchTerm:%s?%s" % (urlencode(self.url), urlencode(self.ufeed.searchTerm))
  492.         except:
  493.             return u""
  494.  
  495.     ##
  496.     # Returns the URL of the feed
  497.     @returnsUnicode
  498.     def getBaseURL(self):
  499.         try:
  500.             return self.url
  501.         except:
  502.             return u""
  503.  
  504.     ##
  505.     # Returns the description of the feed
  506.     @returnsUnicode
  507.     def getDescription(self):
  508.         return u"<span />"
  509.  
  510.     ##
  511.     # Returns a link to a webpage associated with the feed
  512.     @returnsUnicode
  513.     def getLink(self):
  514.         return self.ufeed.getBaseHref()
  515.  
  516.     ##
  517.     # Returns the URL of the library associated with the feed
  518.     @returnsUnicode
  519.     def getLibraryLink(self):
  520.         return u""
  521.  
  522.     ##
  523.     # Returns the URL of a thumbnail associated with the feed
  524.     @returnsUnicode
  525.     def getThumbnailURL(self):
  526.         return self.thumbURL
  527.  
  528.     ##
  529.     # Returns URL of license assocaited with the feed
  530.     @returnsUnicode
  531.     def getLicense(self):
  532.         return u""
  533.  
  534.     ##
  535.     # Returns the number of new items with the feed
  536.     def getNewItems(self):
  537.         self.ufeed.confirmDBThread()
  538.         count = 0
  539.         for item in self.items:
  540.             try:
  541.                 if item.getState() == u'newly-downloaded':
  542.                     count += 1
  543.             except:
  544.                 pass
  545.         return count
  546.  
  547.     def onRestore(self):        
  548.         self.updating = False
  549.         self.calc_item_list()
  550.  
  551.     def onRemove(self):
  552.         """Called when the feed uses this FeedImpl is removed from the DB.
  553.         subclasses can perform cleanup here."""
  554.         pass
  555.  
  556.     def __str__(self):
  557.         return "FeedImpl - %s" % self.getTitle()
  558.  
  559. ##
  560. # This class is a magic class that can become any type of feed it wants
  561. #
  562. # It works by passing on attributes to the actual feed.
  563. class Feed(DDBObject):
  564.     ICON_CACHE_SIZES = [
  565.         (20, 20),
  566.         (76, 76),
  567.     ] + Item.ICON_CACHE_SIZES
  568.  
  569.     def __init__(self,url, initiallyAutoDownloadable=True):
  570.         DDBObject.__init__(self, add=False)
  571.         checkU(url)
  572.         self.autoDownloadable = initiallyAutoDownloadable
  573.         self.getEverything = False
  574.         self.maxNew = 3
  575.         self.expire = u"system"
  576.         self.expireTime = None
  577.         self.fallBehind = -1
  578.  
  579.         self.origURL = url
  580.         self.errorState = False
  581.         self.loading = True
  582.         self.actualFeed = FeedImpl(url,self)
  583.         self.iconCache = IconCache(self, is_vital = True)
  584.         self.informOnError = True
  585.         self.folder_id = None
  586.         self.searchTerm = None
  587.         self.userTitle = None
  588.         self._initRestore()
  589.         self.dd.addAfterCursor(self)
  590.         self.generateFeed(True)
  591.  
  592.     def signalChange (self, needsSave=True, needsSignalFolder=False):
  593.         if needsSignalFolder:
  594.             folder = self.getFolder()
  595.             if folder:
  596.                 folder.signalChange(needsSave=False)
  597.         DDBObject.signalChange (self, needsSave=needsSave)
  598.  
  599.     def _initRestore(self):
  600.         self.download = None
  601.         self.blinking = False
  602.         self.itemSort = sorts.ItemSort()
  603.         self.itemSortDownloading = sorts.ItemSort()
  604.         self.itemSortWatchable = sorts.ItemSortUnwatchedFirst()
  605.         self.inlineSearchTerm = None
  606.  
  607.     isBlinking, setBlinking = makeSimpleGetSet('blinking',
  608.             changeNeedsSave=False)
  609.  
  610.     def setInlineSearchTerm(self, term):
  611.         self.inlineSearchTerm = term
  612.  
  613.     def blink(self):
  614.         self.setBlinking(True)
  615.         def timeout():
  616.             if self.idExists():
  617.                 self.setBlinking(False)
  618.         eventloop.addTimeout(0.5, timeout, 'unblink feed')
  619.  
  620.     # Returns the ID of this feed. Deprecated.
  621.     def getFeedID(self):
  622.         return self.getID()
  623.  
  624.     def getID(self):
  625.         return DDBObject.getID(self)
  626.  
  627.     def hasError(self):
  628.         self.confirmDBThread()
  629.         return self.errorState
  630.  
  631.     @returnsUnicode
  632.     def getOriginalURL(self):
  633.         self.confirmDBThread()
  634.         return self.origURL
  635.  
  636.     @returnsUnicode
  637.     def getSearchTerm(self):
  638.         self.confirmDBThread()
  639.         return self.searchTerm
  640.  
  641.     @returnsUnicode
  642.     def getError(self):
  643.         return u"Could not load feed"
  644.  
  645.     def isUpdating(self):
  646.         return self.loading or (self.actualFeed and self.actualFeed.updating)
  647.  
  648.     def isScraped(self):
  649.         return isinstance(self.actualFeed, ScraperFeedImpl)
  650.  
  651.     @returnsUnicode
  652.     def getTitle(self):
  653.         if self.userTitle is None:
  654.             title = self.actualFeed.getTitle()
  655.             if self.searchTerm is not None:
  656.                 title = u"'%s' on %s" % (self.searchTerm, title)
  657.             return title
  658.         else:
  659.             return self.userTitle
  660.  
  661.     def setTitle(self, title):
  662.         self.confirmDBThread()
  663.         self.userTitle = title
  664.         self.signalChange()
  665.  
  666.     def unsetTitle(self):
  667.         self.setTitle(None)
  668.  
  669.     @returnsUnicode
  670.     def getAutoDownloadMode(self):
  671.         self.confirmDBThread()
  672.         if self.autoDownloadable:
  673.             if self.getEverything:
  674.                 return u'all'
  675.             else:
  676.                 return u'new'
  677.         else:
  678.             return u'off'
  679.  
  680.     def setAutoDownloadMode(self, mode):
  681.         if mode == u'all':
  682.             self.setGetEverything(True)
  683.             self.setAutoDownloadable(True)
  684.         elif mode == u'new':
  685.             self.setGetEverything(False)
  686.             self.setAutoDownloadable(True)
  687.         elif mode == u'off':
  688.             self.setAutoDownloadable(False)
  689.         else:
  690.             raise ValueError("Bad auto-download mode: %s" % mode)
  691.  
  692.     def getCurrentAutoDownloadableItems(self):
  693.         auto = set()
  694.         for item in self.items:
  695.             if item.isPendingAutoDownload():
  696.                 auto.add(item)
  697.         return auto
  698.  
  699.     ##
  700.     # Switch the auto-downloadable state
  701.     def setAutoDownloadable(self, automatic):
  702.         self.confirmDBThread()
  703.         if self.autoDownloadable == automatic:
  704.             return
  705.         self.autoDownloadable = automatic
  706.  
  707.         if self.autoDownloadable:
  708.             # When turning on auto-download, existing items shouldn't be
  709.             # considered "new"
  710.             for item in self.items:
  711.                 if item.eligibleForAutoDownload:
  712.                     item.eligibleForAutoDownload = False
  713.                     item.signalChange()
  714.  
  715.         for item in self.items:
  716.             if item.isEligibleForAutoDownload():
  717.                 item.signalChange(needsSave=False)
  718.  
  719.         self.signalChange()
  720.  
  721.     ##
  722.     # Sets the 'getEverything' attribute, True or False
  723.     def setGetEverything(self, everything):
  724.         self.confirmDBThread()
  725.         if everything == self.getEverything:
  726.             return
  727.         if not self.autoDownloadable:
  728.             self.getEverything = everything
  729.             self.signalChange()
  730.             return
  731.  
  732.         updates = set()
  733.         if everything:
  734.             for item in self.items:
  735.                 if not item.isEligibleForAutoDownload():
  736.                     updates.add(item)
  737.         else:
  738.             for item in self.items:
  739.                 if item.isEligibleForAutoDownload():
  740.                     updates.add(item)
  741.  
  742.         self.getEverything = everything
  743.         self.signalChange()
  744.  
  745.         if everything:
  746.             for item in updates:
  747.                 if item.isEligibleForAutoDownload():
  748.                     item.signalChange(needsSave=False)
  749.         else:
  750.             for item in updates:
  751.                 if not item.isEligibleForAutoDownload():
  752.                     item.signalChange(needsSave=False)
  753.  
  754.     ##
  755.     # Sets the expiration attributes. Valid types are 'system', 'feed' and 'never'
  756.     # Expiration time is in hour(s).
  757.     def setExpiration(self, type, time):
  758.         self.confirmDBThread()
  759.         self.expire = type
  760.         self.expireTime = timedelta(hours=time)
  761.  
  762.         if self.expire == "never":
  763.             for item in self.items:
  764.                 if item.isDownloaded():
  765.                     item.save()
  766.  
  767.         self.signalChange()
  768.         for item in self.items:
  769.             item.signalChange(needsSave=False)
  770.  
  771.     ##
  772.     # Sets the maxNew attributes. -1 means unlimited.
  773.     def setMaxNew(self, maxNew):
  774.         self.confirmDBThread()
  775.         oldMaxNew = self.maxNew
  776.         self.maxNew = maxNew
  777.         self.signalChange()
  778. #        for item in self.items:
  779. #            item.signalChange(needsSave=False)
  780.         if self.maxNew >= oldMaxNew or self.maxNew < 0:
  781.             import autodler
  782.             autodler.autoDownloader.startDownloads()
  783.  
  784.     def makeContextMenu(self, templateName, view):
  785.         items = [
  786.             (self.update, _('Update Channel Now')),
  787.             (lambda: app.delegate.copyTextToClipboard(self.getURL()),
  788.                 _('Copy URL to clipboard')),
  789.             (self.rename, _('Rename Channel')),
  790.         ]
  791.  
  792.         if self.userTitle:
  793.             items.append((self.unsetTitle, _('Revert Title to Default')))
  794.         items.append((lambda: app.controller.removeFeed(self), _('Remove')))
  795.         return menu.makeMenu(items)
  796.  
  797.     def rename(self):
  798.         title = _("Rename Channel")
  799.         text = _("Enter a new name for the channel %s" % self.getTitle())
  800.         def callback(dialog):
  801.             if self.idExists() and dialog.choice == dialogs.BUTTON_OK:
  802.                 self.setTitle(dialog.value)
  803.         dialogs.TextEntryDialog(title, text, dialogs.BUTTON_OK,
  804.             dialogs.BUTTON_CANCEL, prefillCallback=lambda:self.getTitle()).run(callback)
  805.  
  806.     def update(self):
  807.         self.confirmDBThread()
  808.         if not self.idExists():
  809.             return
  810.         if self.loading:
  811.             return
  812.         elif self.errorState:
  813.             self.loading = True
  814.             self.errorState = False
  815.             self.signalChange()
  816.             return self.generateFeed()
  817.         self.actualFeed.update()
  818.  
  819.     def getFolder(self):
  820.         self.confirmDBThread()
  821.         if self.folder_id is not None:
  822.             return self.dd.getObjectByID(self.folder_id)
  823.         else:
  824.             return None
  825.  
  826.     def setFolder(self, newFolder):
  827.         self.confirmDBThread()
  828.         oldFolder = self.getFolder()
  829.         if newFolder is not None:
  830.             self.folder_id = newFolder.getID()
  831.         else:
  832.             self.folder_id = None
  833.         self.signalChange()
  834.         for item in self.items:
  835.             item.signalChange(needsSave=False, needsUpdateXML=False)
  836.         if newFolder:
  837.             newFolder.signalChange(needsSave=False)
  838.         if oldFolder:
  839.             oldFolder.signalChange(needsSave=False)
  840.  
  841.     def generateFeed(self, removeOnError=False):
  842.         newFeed = None
  843.         if (self.origURL == u"dtv:directoryfeed"):
  844.             newFeed = DirectoryFeedImpl(self)
  845.         elif (self.origURL.startswith(u"dtv:directoryfeed:")):
  846.             url = self.origURL[len(u"dtv:directoryfeed:"):]
  847.             dir = unmakeURLSafe(url)
  848.             newFeed = DirectoryWatchFeedImpl(self, dir)
  849.         elif (self.origURL == u"dtv:search"):
  850.             newFeed = SearchFeedImpl(self)
  851.         elif (self.origURL == u"dtv:searchDownloads"):
  852.             newFeed = SearchDownloadsFeedImpl(self)
  853.         elif (self.origURL == u"dtv:manualFeed"):
  854.             newFeed = ManualFeedImpl(self)
  855.         elif (self.origURL == u"dtv:singleFeed"):
  856.             newFeed = SingleFeedImpl(self)
  857.         elif (self.origURL.startswith (u"dtv:searchTerm:")):
  858.  
  859.             url = self.origURL[len(u"dtv:searchTerm:"):]
  860.             (url, search) = url.rsplit("?", 1)
  861.             url = urldecode(url)
  862.             # search terms encoded as utf-8, but our URL attribute is then
  863.             # converted to unicode.  So we need to:
  864.             #  - convert the unicode to a raw string
  865.             #  - urldecode that string
  866.             #  - utf-8 decode the result.
  867.             search = urldecode(search.encode('ascii')).decode('utf-8')
  868.             self.searchTerm = search
  869.             self.download = grabURL(url,
  870.                     lambda info:self._generateFeedCallback(info, removeOnError),
  871.                     lambda error:self._generateFeedErrback(error, removeOnError),
  872.                     defaultMimeType=u'application/rss+xml')
  873.         else:
  874.             self.download = grabURL(self.origURL,
  875.                     lambda info:self._generateFeedCallback(info, removeOnError),
  876.                     lambda error:self._generateFeedErrback(error, removeOnError),
  877.                     defaultMimeType=u'application/rss+xml')
  878.             logging.debug ("added async callback to create feed %s", self.origURL)
  879.         if newFeed:
  880.             self.actualFeed = newFeed
  881.             self.loading = False
  882.  
  883.             self.signalChange()
  884.  
  885.     def _handleFeedLoadingError(self, errorDescription):
  886.         self.download = None
  887.         self.errorState = True
  888.         self.loading = False
  889.         self.signalChange()
  890.         if self.informOnError:
  891.             title = _('Error loading feed')
  892.             description = _("Couldn't load the feed at %s (%s).") % (
  893.                     self.url, errorDescription)
  894.             description += "\n\n"
  895.             description += _("Would you like to keep the feed?")
  896.             d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_KEEP,
  897.                     dialogs.BUTTON_DELETE)
  898.             def callback(dialog):
  899.                 if dialog.choice == dialogs.BUTTON_DELETE and self.idExists():
  900.                     self.remove()
  901.             d.run(callback)
  902.             self.informOnError = False
  903.         delay = config.get(prefs.CHECK_CHANNELS_EVERY_X_MN)
  904.         eventloop.addTimeout(delay, self.update, "update failed feed")
  905.  
  906.     def _generateFeedErrback(self, error, removeOnError):
  907.         if not self.idExists():
  908.             return
  909.         logging.info ("Warning couldn't load feed at %s (%s)",
  910.                       self.origURL, error)
  911.         self._handleFeedLoadingError(error.getFriendlyDescription())
  912.  
  913.     def _generateFeedCallback(self, info, removeOnError):
  914.         """This is called by grabURL to generate a feed based on
  915.         the type of data found at the given URL
  916.         """
  917.         # FIXME: This probably should be split up a bit. The logic is
  918.         #        a bit daunting
  919.  
  920.  
  921.         # Note that all of the raw XML and HTML in this function is in
  922.         # byte string format
  923.  
  924.         if not self.idExists():
  925.             return
  926.         self.download = None
  927.         modified = unicodify(info.get('last-modified'))
  928.         etag = unicodify(info.get('etag'))
  929.         contentType = unicodify(info.get('content-type', u'text/html'))
  930.         
  931.         # Some smarty pants serve RSS feeds with a text/html content-type...
  932.         # So let's do some really simple sniffing first.
  933.         apparentlyRSS = re.compile(r'<\?xml.*\?>\s*<rss').match(info['body']) is not None
  934.  
  935.         #Definitely an HTML feed
  936.         if (contentType.startswith(u'text/html') or 
  937.             contentType.startswith(u'application/xhtml+xml')) and not apparentlyRSS:
  938.             #print "Scraping HTML"
  939.             html = info['body']
  940.             if info.has_key('charset'):
  941.                 html = fixHTMLHeader(html,info['charset'])
  942.                 charset = unicodify(info['charset'])
  943.             else:
  944.                 charset = None
  945.             self.askForScrape(info, html, charset)
  946.         #It's some sort of feed we don't know how to scrape
  947.         elif (contentType.startswith(u'application/rdf+xml') or
  948.               contentType.startswith(u'application/atom+xml')):
  949.             #print "ATOM or RDF"
  950.             html = info['body']
  951.             if info.has_key('charset'):
  952.                 xmldata = fixXMLHeader(html,info['charset'])
  953.             else:
  954.                 xmldata = html
  955.             self.finishGenerateFeed(RSSFeedImpl(unicodify(info['updated-url']),
  956.                 initialHTML=xmldata,etag=etag,modified=modified, ufeed=self))
  957.             # If it's not HTML, we can't be sure what it is.
  958.             #
  959.             # If we get generic XML, it's probably RSS, but it still could
  960.             # be XHTML.
  961.             #
  962.             # application/rss+xml links are definitely feeds. However, they
  963.             # might be pre-enclosure RSS, so we still have to download them
  964.             # and parse them before we can deal with them correctly.
  965.         elif (apparentlyRSS or
  966.               contentType.startswith(u'application/rss+xml') or
  967.               contentType.startswith(u'application/podcast+xml') or
  968.               contentType.startswith(u'text/xml') or 
  969.               contentType.startswith(u'application/xml') or
  970.               (contentType.startswith(u'text/plain') and
  971.                (unicodify(info['updated-url']).endswith(u'.xml') or
  972.                 unicodify(info['updated-url']).endswith(u'.rss')))):
  973.             #print " It's doesn't look like HTML..."
  974.             html = info["body"]
  975.             if info.has_key('charset'):
  976.                 xmldata = fixXMLHeader(html,info['charset'])
  977.                 html = fixHTMLHeader(html,info['charset'])
  978.                 charset = unicodify(info['charset'])
  979.             else:
  980.                 xmldata = html
  981.                 charset = None
  982.             # FIXME html and xmldata can be non-unicode at this point
  983.             parser = xml.sax.make_parser()
  984.             parser.setFeature(xml.sax.handler.feature_namespaces, 1)
  985.             try: parser.setFeature(xml.sax.handler.feature_external_ges, 0)
  986.             except: pass
  987.             handler = RSSLinkGrabber(unicodify(info['redirected-url']),charset)
  988.             parser.setContentHandler(handler)
  989.             parser.setErrorHandler(handler)
  990.             try:
  991.                 parser.parse(StringIO(xmldata))
  992.             except UnicodeDecodeError:
  993.                 logging.exception ("Unicode issue parsing... %s", xmldata[0:300])
  994.                 self.finishGenerateFeed(None)
  995.                 if removeOnError:
  996.                     self.remove()
  997.             except:
  998.                 #it doesn't parse as RSS, so it must be HTML
  999.                 #print " Nevermind! it's HTML"
  1000.                 self.askForScrape(info, html, charset)
  1001.             else:
  1002.                 #print " It's RSS with enclosures"
  1003.                 self.finishGenerateFeed(RSSFeedImpl(
  1004.                     unicodify(info['updated-url']),
  1005.                     initialHTML=xmldata, etag=etag, modified=modified,
  1006.                     ufeed=self))
  1007.         else:
  1008.             self._handleFeedLoadingError(_("Bad content-type"))
  1009.  
  1010.     def finishGenerateFeed(self, feedImpl):
  1011.         self.confirmDBThread()
  1012.         self.loading = False
  1013.         if feedImpl is not None:
  1014.             self.actualFeed = feedImpl
  1015.             self.errorState = False
  1016.         else:
  1017.             self.errorState = True
  1018.         self.signalChange()
  1019.  
  1020.     def askForScrape(self, info, initialHTML, charset):
  1021.         title = Template(_("Channel is not compatible with $shortAppName!")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME))
  1022.         descriptionTemplate = Template(_("""\
  1023. But we'll try our best to grab the files. It may take extra time to list the \
  1024. videos, and descriptions may look funny.  Please contact the publishers of \
  1025. $url and ask if they can supply a feed in a format that will work with \
  1026. $shortAppName.\n\nDo you want to try to load this channel anyway?"""))
  1027.         description = descriptionTemplate.substitute(url=info['updated-url'],
  1028.                                 shortAppName=config.get(prefs.SHORT_APP_NAME))
  1029.         dialog = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_YES,
  1030.                 dialogs.BUTTON_NO)
  1031.  
  1032.         def callback(dialog):
  1033.             if not self.idExists():
  1034.                 return
  1035.             if dialog.choice == dialogs.BUTTON_YES:
  1036.                 uinfo = unicodify(info)
  1037.                 impl = ScraperFeedImpl(uinfo['updated-url'],
  1038.                     initialHTML=initialHTML, etag=uinfo.get('etag'),
  1039.                     modified=uinfo.get('modified'), charset=charset,
  1040.                     ufeed=self) 
  1041.                 self.finishGenerateFeed(impl)
  1042.             else:
  1043.                 self.remove()
  1044.         dialog.run(callback)
  1045.  
  1046.     def getActualFeed(self):
  1047.         return self.actualFeed
  1048.  
  1049.     def __getattr__(self,attr):
  1050.         return getattr(self.actualFeed,attr)
  1051.  
  1052.     def remove(self, moveItemsTo=None):
  1053.         """Remove the feed.  If moveItemsTo is None (the default), the items
  1054.         in this feed will be removed too.  If moveItemsTo is given, the items
  1055.         in this feed will be moved to that feed.
  1056.         """
  1057.  
  1058.         self.confirmDBThread()
  1059.  
  1060.         if isinstance (self.actualFeed, DirectoryWatchFeedImpl):
  1061.             moveItemsTo = None
  1062.         self.cancelUpdateEvents()
  1063.         if self.download is not None:
  1064.             self.download.cancel()
  1065.             self.download = None
  1066.         for item in self.items:
  1067.             if moveItemsTo is not None and item.isDownloaded():
  1068.                 item.setFeed(moveItemsTo.getID())
  1069.             else:
  1070.                 item.remove()
  1071.         if self.iconCache is not None:
  1072.             self.iconCache.remove()
  1073.             self.iconCache = None
  1074.         DDBObject.remove(self)
  1075.         self.actualFeed.onRemove()
  1076.  
  1077.     @returnsUnicode
  1078.     def getThumbnail(self):
  1079.         self.confirmDBThread()
  1080.         if self.iconCache and self.iconCache.isValid():
  1081.             path = self.iconCache.getResizedFilename(76, 76)
  1082.             return resources.absoluteUrl(path)
  1083.         else:
  1084.             return defaultFeedIconURL()
  1085.  
  1086.     @returnsUnicode
  1087.     def getTablistThumbnail(self):
  1088.         self.confirmDBThread()
  1089.         if self.iconCache and self.iconCache.isValid():
  1090.             path = self.iconCache.getResizedFilename(20, 20)
  1091.             return resources.absoluteUrl(path)
  1092.         else:
  1093.             return defaultFeedIconURLTablist()
  1094.  
  1095.     @returnsUnicode
  1096.     def getItemThumbnail(self, width, height):
  1097.         self.confirmDBThread()
  1098.         if self.iconCache and self.iconCache.isValid():
  1099.             path = self.iconCache.getResizedFilename(width, height)
  1100.             return resources.absoluteUrl(path)
  1101.         else:
  1102.             return None
  1103.  
  1104.     def hasDownloadedItems(self):
  1105.         self.confirmDBThread()
  1106.         for item in self.items:
  1107.             if item.isDownloaded():
  1108.                 return True
  1109.         return False
  1110.  
  1111.     def hasDownloadingItems(self):
  1112.         self.confirmDBThread()
  1113.         for item in self.items:
  1114.             if item.getState() in (u'downloading', u'paused'):
  1115.                 return True
  1116.         return False
  1117.  
  1118.     def updateIcons(self):
  1119.         iconCacheUpdater.clearVital()
  1120.         for item in self.items:
  1121.             item.iconCache.requestUpdate(True)
  1122.         for feed in views.feeds:
  1123.             feed.iconCache.requestUpdate(True)
  1124.  
  1125.     @returnsUnicode
  1126.     def getDragDestType(self):
  1127.         self.confirmDBThread()
  1128.         if self.folder_id is not None:
  1129.             return u'channel'
  1130.         else:
  1131.             return u'channel:channelfolder'
  1132.  
  1133.     def onRestore(self):
  1134.         if (self.iconCache == None):
  1135.             self.iconCache = IconCache (self, is_vital = True)
  1136.         else:
  1137.             self.iconCache.dbItem = self
  1138.             self.iconCache.requestUpdate(True)
  1139.         self.informOnError = False
  1140.         self._initRestore()
  1141.         if self.actualFeed.__class__ == FeedImpl:
  1142.             # Our initial FeedImpl was never updated, call generateFeed again
  1143.             self.loading = True
  1144.             eventloop.addIdle(lambda:self.generateFeed(True), "generateFeed")
  1145.  
  1146.     def __str__(self):
  1147.         return "Feed - %s" % self.getTitle()
  1148.  
  1149. def _entry_equal(a, b):
  1150.     if type(a) == list and type(b) == list:
  1151.         if len(a) != len(b):
  1152.             return False
  1153.         for i in xrange (len(a)):
  1154.             if not _entry_equal(a[i], b[i]):
  1155.                 return False
  1156.         return True
  1157.     try:
  1158.         return a.equal(b)
  1159.     except:
  1160.         try:
  1161.             return b.equal(a)
  1162.         except:
  1163.             return a == b
  1164.  
  1165. class RSSFeedImpl(FeedImpl):
  1166.     firstImageRE = re.compile('\<\s*img\s+[^>]*src\s*=\s*"(.*?)"[^>]*\>',re.I|re.M)
  1167.     
  1168.     def __init__(self,url,ufeed,title = None,initialHTML = None, etag = None, modified = None, visible=True):
  1169.         FeedImpl.__init__(self,url,ufeed,title,visible=visible)
  1170.         self.initialHTML = initialHTML
  1171.         self.etag = etag
  1172.         self.modified = modified
  1173.         self.download = None
  1174.         self.scheduleUpdateEvents(0)
  1175.  
  1176.     @returnsUnicode
  1177.     def getBaseHref(self):
  1178.         try:
  1179.             return escape(self.parsed.link)
  1180.         except:
  1181.             return FeedImpl.getBaseHref(self)
  1182.  
  1183.     ##
  1184.     # Returns the description of the feed
  1185.     @returnsUnicode
  1186.     def getDescription(self):
  1187.         self.ufeed.confirmDBThread()
  1188.         try:
  1189.             return xhtmlify(u'<span>'+unescape(self.parsed.feed.description)+u'</span>')
  1190.         except:
  1191.             return u"<span />"
  1192.  
  1193.     ##
  1194.     # Returns a link to a webpage associated with the feed
  1195.     @returnsUnicode
  1196.     def getLink(self):
  1197.         self.ufeed.confirmDBThread()
  1198.         try:
  1199.             return self.parsed.link
  1200.         except:
  1201.             return u""
  1202.  
  1203.     ##
  1204.     # Returns the URL of the library associated with the feed
  1205.     @returnsUnicode
  1206.     def getLibraryLink(self):
  1207.         self.ufeed.confirmDBThread()
  1208.         try:
  1209.             return self.parsed.libraryLink
  1210.         except:
  1211.             return u""
  1212.  
  1213.     def feedparser_finished (self):
  1214.         self.updating = False
  1215.         self.ufeed.signalChange(needsSave=False)
  1216.         self.scheduleUpdateEvents(-1)
  1217.  
  1218.     def feedparser_errback (self, e):
  1219.         if not self.ufeed.idExists():
  1220.             return
  1221.         logging.info ("Error updating feed: %s: %s", self.url, e)
  1222.         self.updating = False
  1223.         self.ufeed.signalChange()
  1224.         self.scheduleUpdateEvents(-1)
  1225.  
  1226.     def feedparser_callback (self, parsed):
  1227.         self.ufeed.confirmDBThread()
  1228.         if not self.ufeed.idExists():
  1229.             return
  1230.         start = clock()
  1231.         self.updateUsingParsed(parsed)
  1232.         self.feedparser_finished()
  1233.         end = clock()
  1234.         if end - start > 1.0:
  1235.             logging.timing ("feed update for: %s too slow (%.3f secs)", self.url, end - start)
  1236.  
  1237.     def call_feedparser (self, html):
  1238.         self.ufeed.confirmDBThread()
  1239.         in_thread = False
  1240.         if in_thread:
  1241.             try:
  1242.                 parsed = feedparser.parse(html)
  1243.                 self.updateUsingParsed(parsed)
  1244.             except:
  1245.                 logging.warning ("Error updating feed: %s", self.url)
  1246.                 self.updating = False
  1247.                 self.ufeed.signalChange(needsSave=False)
  1248.                 raise
  1249.             self.feedparser_finished()
  1250.         else:
  1251.             eventloop.callInThread (self.feedparser_callback, self.feedparser_errback, feedparser.parse, "Feedparser callback - %s" % self.url, html)
  1252.  
  1253.     ##
  1254.     # Updates a feed
  1255.     def update(self):
  1256.         self.ufeed.confirmDBThread()
  1257.         if not self.ufeed.idExists():
  1258.             return
  1259.         if self.updating:
  1260.             return
  1261.         else:
  1262.             self.updating = True
  1263.             self.ufeed.signalChange(needsSave=False)
  1264.         if hasattr(self, 'initialHTML') and self.initialHTML is not None:
  1265.             html = self.initialHTML
  1266.             self.initialHTML = None
  1267.             self.call_feedparser (html)
  1268.         else:
  1269.             try:
  1270.                 etag = self.etag
  1271.             except:
  1272.                 etag = None
  1273.             try:
  1274.                 modified = self.modified
  1275.             except:
  1276.                 modified = None
  1277.             self.download = grabURL(self.url, self._updateCallback,
  1278.                     self._updateErrback, etag=etag,modified=modified,defaultMimeType=u'application/rss+xml',)
  1279.  
  1280.     def _updateErrback(self, error):
  1281.         if not self.ufeed.idExists():
  1282.             return
  1283.         logging.info ("WARNING: error in Feed.update for %s -- %s", self.ufeed, error)
  1284.         self.scheduleUpdateEvents(-1)
  1285.         self.updating = False
  1286.         self.ufeed.signalChange(needsSave=False)
  1287.  
  1288.     def _updateCallback(self,info):
  1289.         if not self.ufeed.idExists():
  1290.             return
  1291.         if info.get('status') == 304:
  1292.             self.scheduleUpdateEvents(-1)
  1293.             self.updating = False
  1294.             self.ufeed.signalChange()
  1295.             return
  1296.         html = info['body']
  1297.         if info.has_key('charset'):
  1298.             html = fixXMLHeader(html,info['charset'])
  1299.  
  1300.         # FIXME HTML can be non-unicode here --NN        
  1301.         self.url = unicodify(info['updated-url'])
  1302.         if info.has_key('etag'):
  1303.             self.etag = unicodify(info['etag'])
  1304.         if info.has_key('last-modified'):
  1305.             self.modified = unicodify(info['last-modified'])
  1306.         self.call_feedparser (html)
  1307.  
  1308.     def _handleNewEntryForItem(self, item, entry):
  1309.         """Handle when we get a different entry for an item.
  1310.  
  1311.         This happens when the feed sets the RSS GUID attribute, then changes
  1312.         the entry for it.  Most of the time we will just update the item, but
  1313.         if the user has already downloaded the item then we need to make sure
  1314.         that we don't throw away the download.
  1315.         """
  1316.  
  1317.         videoEnc = getFirstVideoEnclosure(entry)
  1318.         if videoEnc is not None:
  1319.             entryURL = videoEnc.get('url')
  1320.         else:
  1321.             entryURL = None
  1322.         if item.isDownloaded() and item.getURL() != entryURL:
  1323.             item.removeRSSID()
  1324.             self._handleNewEntry(entry)
  1325.         else:
  1326.             item.update(entry)
  1327.  
  1328.     def _handleNewEntry(self, entry):
  1329.         """Handle getting a new entry from a feed."""
  1330.         item = Item(entry, feed_id=self.ufeed.id)
  1331.         if not filters.matchingItems(item, self.ufeed.searchTerm):
  1332.             item.remove()
  1333.  
  1334.     def updateUsingParsed(self, parsed):
  1335.         """Update the feed using parsed XML passed in"""
  1336.         self.parsed = unicodify(parsed)
  1337.  
  1338.         # This is a HACK for Yahoo! search which doesn't provide
  1339.         # enclosures
  1340.         for entry in parsed['entries']:
  1341.             if 'enclosures' not in entry:
  1342.                 try:
  1343.                     url = entry['link']
  1344.                 except:
  1345.                     continue
  1346.                 mimetype = filetypes.guessMimeType(url)
  1347.                 if mimetype is not None:
  1348.                     entry['enclosures'] = [{'url':toUni(url), 'type':toUni(mimetype)}]
  1349.                 else:
  1350.                     logging.info('unknown url type %s, not generating enclosure' % url)
  1351.  
  1352.         try:
  1353.             self.title = self.parsed["feed"]["title"]
  1354.         except KeyError:
  1355.             try:
  1356.                 self.title = self.parsed["channel"]["title"]
  1357.             except KeyError:
  1358.                 pass
  1359.         if (self.parsed.feed.has_key('image') and 
  1360.             self.parsed.feed.image.has_key('url')):
  1361.             self.thumbURL = self.parsed.feed.image.url
  1362.             self.ufeed.iconCache.requestUpdate(is_vital=True)
  1363.         items_byid = {}
  1364.         items_byURLTitle = {}
  1365.         items_nokey = []
  1366.         old_items = set()
  1367.         for item in self.items:
  1368.             old_items.add(item)
  1369.             try:
  1370.                 items_byid[item.getRSSID()] = item
  1371.             except KeyError:
  1372.                 items_nokey.append (item)
  1373.             entry = item.getRSSEntry()
  1374.             videoEnc = getFirstVideoEnclosure(entry)
  1375.             if videoEnc is not None:
  1376.                 entryURL = videoEnc.get('url')
  1377.             else:
  1378.                 entryURL = None
  1379.             title = entry.get("title")
  1380.             if title is not None or entryURL is not None:
  1381.                 items_byURLTitle[(entryURL, title)] = item
  1382.         for entry in self.parsed.entries:
  1383.             entry = self.addScrapedThumbnail(entry)
  1384.             new = True
  1385.             if entry.has_key("id"):
  1386.                 id = entry["id"]
  1387.                 if items_byid.has_key (id):
  1388.                     item = items_byid[id]
  1389.                     if not _entry_equal(entry, item.getRSSEntry()):
  1390.                         self._handleNewEntryForItem(item, entry)
  1391.                     new = False
  1392.                     old_items.discard(item)
  1393.             if new:
  1394.                 videoEnc = getFirstVideoEnclosure(entry)
  1395.                 if videoEnc is not None:
  1396.                     entryURL = videoEnc.get('url')
  1397.                 else:
  1398.                     entryURL = None
  1399.                 title = entry.get("title")
  1400.                 if title is not None or entryURL is not None:
  1401.                     if items_byURLTitle.has_key ((entryURL, title)):
  1402.                         item = items_byURLTitle[(entryURL, title)]
  1403.                         if not _entry_equal(entry, item.getRSSEntry()):
  1404.                             self._handleNewEntryForItem(item, entry)
  1405.                         new = False
  1406.                         old_items.discard(item)
  1407.             if new:
  1408.                 for item in items_nokey:
  1409.                     if _entry_equal(entry, item.getRSSEntry()):
  1410.                         new = False
  1411.                     else:
  1412.                         try:
  1413.                             if _entry_equal (entry["enclosures"], item.getRSSEntry()["enclosures"]):
  1414.                                 self._handleNewEntryForItem(item, entry)
  1415.                                 new = False
  1416.                                 old_items.discard(item)
  1417.                         except:
  1418.                             pass
  1419.             if (new and entry.has_key('enclosures') and
  1420.                     getFirstVideoEnclosure(entry) != None):
  1421.                 self._handleNewEntry(entry)
  1422.         try:
  1423.             updateFreq = self.parsed["feed"]["ttl"]
  1424.         except KeyError:
  1425.             updateFreq = 0
  1426.         self.setUpdateFrequency(updateFreq)
  1427.         
  1428.         if self.initialUpdate:
  1429.             self.initialUpdate = False
  1430.             startfrom = None
  1431.             itemToUpdate = None
  1432.             for item in self.items:
  1433.                 itemTime = item.getPubDateParsed()
  1434.                 if startfrom is None or itemTime > startfrom:
  1435.                     startfrom = itemTime
  1436.                     itemToUpdate = item
  1437.             for item in self.items:
  1438.                 if item == itemToUpdate:
  1439.                     item.eligibleForAutoDownload = True
  1440.                 else:
  1441.                     item.eligibleForAutoDownload = False
  1442.                 item.signalChange()
  1443.             self.ufeed.signalChange()
  1444.  
  1445.         self.truncateOldItems(old_items)
  1446.  
  1447.     def truncateOldItems(self, old_items):
  1448.         """Truncate items so that the number of items in this feed doesn't
  1449.         exceed prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS.
  1450.  
  1451.         old_items should be an iterable that contains items that aren't in the
  1452.         feed anymore.
  1453.  
  1454.         Items are only truncated if they don't exist in the feed anymore, and
  1455.         if the user hasn't downloaded them.
  1456.         """
  1457.         limit = config.get(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS)
  1458.         extra = len(self.items) - limit
  1459.         if extra <= 0:
  1460.             return
  1461.  
  1462.         candidates = []
  1463.         for item in old_items:
  1464.             if item.downloader is None:
  1465.                 candidates.append((item.creationTime, item))
  1466.         candidates.sort()
  1467.         for time, item in candidates[:extra]:
  1468.             item.remove()
  1469.  
  1470.     def addScrapedThumbnail(self,entry):
  1471.         # skip this if the entry already has a thumbnail.
  1472.         if entry.has_key('thumbnail'):
  1473.             return entry
  1474.         if entry.has_key('enclosures'):
  1475.             for enc in entry['enclosures']:
  1476.                 if enc.has_key('thumbnail'):
  1477.                     return entry
  1478.         # try to scape the thumbnail from the description.
  1479.         if not entry.has_key('description'):
  1480.             return entry
  1481.         desc = RSSFeedImpl.firstImageRE.search(unescape(entry['description']))
  1482.         if not desc is None:
  1483.             entry['thumbnail'] = FeedParserDict({'url': desc.expand("\\1")})
  1484.         return entry
  1485.  
  1486.     ##
  1487.     # Returns the URL of the license associated with the feed
  1488.     @returnsUnicode
  1489.     def getLicense(self):
  1490.         try:
  1491.             ret = self.parsed.license
  1492.         except:
  1493.             ret = u""
  1494.         return ret
  1495.  
  1496.     def onRemove(self):
  1497.         if self.download is not None:
  1498.             self.download.cancel()
  1499.             self.download = None
  1500.  
  1501.     ##
  1502.     # Called by pickle during deserialization
  1503.     def onRestore(self):
  1504.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  1505.         #FIXME: the update dies if all of the items aren't restored, so we 
  1506.         # wait a little while before we start the update
  1507.         FeedImpl.onRestore(self)
  1508.         self.download = None
  1509.         self.scheduleUpdateEvents(0.1)
  1510.  
  1511.  
  1512. ##
  1513. # A DTV Collection of items -- similar to a playlist
  1514. class Collection(FeedImpl):
  1515.     def __init__(self,ufeed,title = None):
  1516.         FeedImpl.__init__(self,ufeed,url = "dtv:collection",title = title,visible = False)
  1517.  
  1518.     ##
  1519.     # Adds an item to the collection
  1520.     def addItem(self,item):
  1521.         if isinstance(item,Item):
  1522.             self.ufeed.confirmDBThread()
  1523.             self.removeItem(item)
  1524.             self.items.append(item)
  1525.             return True
  1526.         else:
  1527.             return False
  1528.  
  1529.     ##
  1530.     # Moves an item to another spot in the collection
  1531.     def moveItem(self,item,pos):
  1532.         self.ufeed.confirmDBThread()
  1533.         self.removeItem(item)
  1534.         if pos < len(self.items):
  1535.             self.items[pos:pos] = [item]
  1536.         else:
  1537.             self.items.append(item)
  1538.  
  1539.     ##
  1540.     # Removes an item from the collection
  1541.     def removeItem(self,item):
  1542.         self.ufeed.confirmDBThread()
  1543.         for x in range(0,len(self.items)):
  1544.             if self.items[x] == item:
  1545.                 self.items[x:x+1] = []
  1546.                 break
  1547.         return True
  1548.  
  1549. ##
  1550. # A feed based on un unformatted HTML or pre-enclosure RSS
  1551. class ScraperFeedImpl(FeedImpl):
  1552.     def __init__(self,url,ufeed, title = None, visible = True, initialHTML = None,etag=None,modified = None,charset = None):
  1553.         FeedImpl.__init__(self,url,ufeed,title,visible)
  1554.         self.initialHTML = initialHTML
  1555.         self.initialCharset = charset
  1556.         self.linkHistory = {}
  1557.         self.linkHistory[url] = {}
  1558.         self.tempHistory = {}
  1559.         if not etag is None:
  1560.             self.linkHistory[url]['etag'] = unicodify(etag)
  1561.         if not modified is None:
  1562.             self.linkHistory[url]['modified'] = unicodify(modified)
  1563.         self.downloads = set()
  1564.         self.setUpdateFrequency(360)
  1565.         self.scheduleUpdateEvents(0)
  1566.  
  1567.     @returnsUnicode
  1568.     def getMimeType(self,link):
  1569.         raise StandardError, "ScraperFeedImpl.getMimeType not implemented"
  1570.  
  1571.     ##
  1572.     # This puts all of the caching information in tempHistory into the
  1573.     # linkHistory. This should be called at the end of an updated so that
  1574.     # the next time we update we don't unnecessarily follow old links
  1575.     def saveCacheHistory(self):
  1576.         self.ufeed.confirmDBThread()
  1577.         for url in self.tempHistory.keys():
  1578.             self.linkHistory[url] = self.tempHistory[url]
  1579.         self.tempHistory = {}
  1580.     ##
  1581.     # grabs HTML at the given URL, then processes it
  1582.     def getHTML(self, urlList, depth = 0, linkNumber = 0, top = False):
  1583.         url = urlList.pop(0)
  1584.         #print "Grabbing %s" % url
  1585.         etag = None
  1586.         modified = None
  1587.         if self.linkHistory.has_key(url):
  1588.             if self.linkHistory[url].has_key('etag'):
  1589.                 etag = self.linkHistory[url]['etag']
  1590.             if self.linkHistory[url].has_key('modified'):
  1591.                 modified = self.linkHistory[url]['modified']
  1592.         def callback(info):
  1593.             if not self.ufeed.idExists():
  1594.                 return
  1595.             self.downloads.discard(download)
  1596.             try:
  1597.                 self.processDownloadedHTML(info, urlList, depth,linkNumber, top)
  1598.             finally:
  1599.                 self.checkDone()
  1600.         def errback(error):
  1601.             if not self.ufeed.idExists():
  1602.                 return
  1603.             self.downloads.discard(download)
  1604.             logging.info ("WARNING unhandled error for ScraperFeedImpl.getHTML: %s", error)
  1605.             self.checkDone()
  1606.         download = grabURL(url, callback, errback, etag=etag,
  1607.                 modified=modified,defaultMimeType='text/html',)
  1608.         self.downloads.add(download)
  1609.  
  1610.     def processDownloadedHTML(self, info, urlList, depth, linkNumber, top = False):
  1611.         self.ufeed.confirmDBThread()
  1612.         #print "Done grabbing %s" % info['updated-url']
  1613.         
  1614.         if not self.tempHistory.has_key(info['updated-url']):
  1615.             self.tempHistory[info['updated-url']] = {}
  1616.         if info.has_key('etag'):
  1617.             self.tempHistory[info['updated-url']]['etag'] = unicodify(info['etag'])
  1618.         if info.has_key('last-modified'):
  1619.             self.tempHistory[info['updated-url']]['modified'] = unicodify(info['last-modified'])
  1620.  
  1621.         if (info['status'] != 304) and (info.has_key('body')):
  1622.             if info.has_key('charset'):
  1623.                 subLinks = self.scrapeLinks(info['body'], info['redirected-url'],charset=info['charset'], setTitle = top)
  1624.             else:
  1625.                 subLinks = self.scrapeLinks(info['body'], info['redirected-url'], setTitle = top)
  1626.             if top:
  1627.                 self.processLinks(subLinks,0,linkNumber)
  1628.             else:
  1629.                 self.processLinks(subLinks,depth+1,linkNumber)
  1630.         if len(urlList) > 0:
  1631.             self.getHTML(urlList, depth, linkNumber)
  1632.  
  1633.     def checkDone(self):
  1634.         if len(self.downloads) == 0:
  1635.             self.saveCacheHistory()
  1636.             self.updating = False
  1637.             self.ufeed.signalChange()
  1638.             self.scheduleUpdateEvents(-1)
  1639.  
  1640.     def addVideoItem(self,link,dict,linkNumber):
  1641.         link = unicodify(link.strip())
  1642.         if dict.has_key('title'):
  1643.             title = dict['title']
  1644.         else:
  1645.             title = link
  1646.         for item in self.items:
  1647.             if item.getURL() == link:
  1648.                 return
  1649.         # Anywhere we call this, we need to convert the input back to unicode
  1650.         title = feedparser.sanitizeHTML (title, "utf-8").decode('utf-8')
  1651.         if dict.has_key('thumbnail') > 0:
  1652.             i=Item(FeedParserDict({'title':title,'enclosures':[FeedParserDict({'url':link,'thumbnail':FeedParserDict({'url':dict['thumbnail']})})]}),linkNumber = linkNumber, feed_id=self.ufeed.id)
  1653.         else:
  1654.             i=Item(FeedParserDict({'title':title,'enclosures':[FeedParserDict({'url':link})]}),linkNumber = linkNumber, feed_id=self.ufeed.id)
  1655.         if self.ufeed.searchTerm is not None and not filters.matchingItems(i, self.ufeed.searchTerm):
  1656.             i.remove()
  1657.             return
  1658.  
  1659.     #FIXME: compound names for titles at each depth??
  1660.     def processLinks(self,links, depth = 0,linkNumber = 0):
  1661.         maxDepth = 2
  1662.         urls = links[0]
  1663.         links = links[1]
  1664.         # List of URLs that should be downloaded
  1665.         newURLs = []
  1666.         
  1667.         if depth<maxDepth:
  1668.             for link in urls:
  1669.                 if depth == 0:
  1670.                     linkNumber += 1
  1671.                 #print "Processing %s (%d)" % (link,linkNumber)
  1672.  
  1673.                 # FIXME: Using file extensions totally breaks the
  1674.                 # standard and won't work with Broadcast Machine or
  1675.                 # Blog Torrent. However, it's also a hell of a lot
  1676.                 # faster than checking the mime type for every single
  1677.                 # file, so for now, we're being bad boys. Uncomment
  1678.                 # the elif to make this use mime types for HTTP GET URLs
  1679.  
  1680.                 mimetype = filetypes.guessMimeType(link)
  1681.                 if mimetype is None:
  1682.                     mimetype = 'text/html'
  1683.  
  1684.                 #This is text of some sort: HTML, XML, etc.
  1685.                 if ((mimetype.startswith('text/html') or
  1686.                      mimetype.startswith('application/xhtml+xml') or 
  1687.                      mimetype.startswith('text/xml')  or
  1688.                      mimetype.startswith('application/xml') or
  1689.                      mimetype.startswith('application/rss+xml') or
  1690.                      mimetype.startswith('application/podcast+xml') or
  1691.                      mimetype.startswith('application/atom+xml') or
  1692.                      mimetype.startswith('application/rdf+xml') ) and
  1693.                     depth < maxDepth -1):
  1694.                     newURLs.append(link)
  1695.  
  1696.                 #This is a video
  1697.                 elif (mimetype.startswith('video/') or 
  1698.                       mimetype.startswith('audio/') or
  1699.                       mimetype == "application/ogg" or
  1700.                       mimetype == "application/x-annodex" or
  1701.                       mimetype == "application/x-bittorrent"):
  1702.                     self.addVideoItem(link, links[link],linkNumber)
  1703.             if len(newURLs) > 0:
  1704.                 self.getHTML(newURLs, depth, linkNumber)
  1705.  
  1706.     def onRemove(self):
  1707.         for download in self.downloads:
  1708.             logging.info ("cancling download: %s", download.url)
  1709.             download.cancel()
  1710.         self.downloads = set()
  1711.  
  1712.     #FIXME: go through and add error handling
  1713.     def update(self):
  1714.         self.ufeed.confirmDBThread()
  1715.         if not self.ufeed.idExists():
  1716.             return
  1717.         if self.updating:
  1718.             return
  1719.         else:
  1720.             self.updating = True
  1721.             self.ufeed.signalChange(needsSave=False)
  1722.  
  1723.         if not self.initialHTML is None:
  1724.             html = self.initialHTML
  1725.             self.initialHTML = None
  1726.             redirURL=self.url
  1727.             status = 200
  1728.             charset = self.initialCharset
  1729.             self.initialCharset = None
  1730.             subLinks = self.scrapeLinks(html, redirURL, charset=charset, setTitle = True)
  1731.             self.processLinks(subLinks,0,0)
  1732.             self.checkDone()
  1733.         else:
  1734.             self.getHTML([self.url], top = True)
  1735.  
  1736.     def scrapeLinks(self,html,baseurl,setTitle = False,charset = None):
  1737.         try:
  1738.             if not charset is None:
  1739.                 html = fixHTMLHeader(html,charset)
  1740.             xmldata = html
  1741.             parser = xml.sax.make_parser()
  1742.             parser.setFeature(xml.sax.handler.feature_namespaces, 1)
  1743.             try: parser.setFeature(xml.sax.handler.feature_external_ges, 0)
  1744.             except: pass
  1745.             if charset is not None:
  1746.                 handler = RSSLinkGrabber(baseurl,charset)
  1747.             else:
  1748.                 handler = RSSLinkGrabber(baseurl)
  1749.             parser.setContentHandler(handler)
  1750.             try:
  1751.                 parser.parse(StringIO(xmldata))
  1752.             except IOError, e:
  1753.                 pass
  1754.             except AttributeError:
  1755.                 # bug in the python standard library causes this to be raised
  1756.                 # sometimes.  See #3201.
  1757.                 pass
  1758.             links = handler.links
  1759.             linkDict = {}
  1760.             for link in links:
  1761.                 if link[0].startswith('http://') or link[0].startswith('https://'):
  1762.                     if not linkDict.has_key(toUni(link[0],charset)):
  1763.                         linkDict[toUni(link[0],charset)] = {}
  1764.                     if not link[1] is None:
  1765.                         linkDict[toUni(link[0],charset)]['title'] = toUni(link[1],charset).strip()
  1766.                     if not link[2] is None:
  1767.                         linkDict[toUni(link[0],charset)]['thumbnail'] = toUni(link[2],charset)
  1768.             if setTitle and not handler.title is None:
  1769.                 self.ufeed.confirmDBThread()
  1770.                 try:
  1771.                     self.title = toUni(handler.title,charset)
  1772.                 finally:
  1773.                     self.ufeed.signalChange()
  1774.             return ([x[0] for x in links if x[0].startswith('http://') or x[0].startswith('https://')], linkDict)
  1775.         except (xml.sax.SAXException, ValueError, IOError, xml.sax.SAXNotRecognizedException):
  1776.             (links, linkDict) = self.scrapeHTMLLinks(html,baseurl,setTitle=setTitle, charset=charset)
  1777.             return (links, linkDict)
  1778.  
  1779.     ##
  1780.     # Given a string containing an HTML file, return a dictionary of
  1781.     # links to titles and thumbnails
  1782.     def scrapeHTMLLinks(self,html, baseurl,setTitle=False, charset = None):
  1783.         lg = HTMLLinkGrabber()
  1784.         links = lg.getLinks(html, baseurl)
  1785.         if setTitle and not lg.title is None:
  1786.             self.ufeed.confirmDBThread()
  1787.             try:
  1788.                 self.title = toUni(lg.title, charset)
  1789.             finally:
  1790.                 self.ufeed.signalChange()
  1791.             
  1792.         linkDict = {}
  1793.         for link in links:
  1794.             if link[0].startswith('http://') or link[0].startswith('https://'):
  1795.                 if not linkDict.has_key(toUni(link[0],charset)):
  1796.                     linkDict[toUni(link[0],charset)] = {}
  1797.                 if not link[1] is None:
  1798.                     linkDict[toUni(link[0],charset)]['title'] = toUni(link[1],charset).strip()
  1799.                 if not link[2] is None:
  1800.                     linkDict[toUni(link[0],charset)]['thumbnail'] = toUni(link[2],charset)
  1801.         return ([x[0] for x in links if x[0].startswith('http://') or x[0].startswith('https://')],linkDict)
  1802.         
  1803.     ##
  1804.     # Called by pickle during deserialization
  1805.     def onRestore(self):
  1806.         FeedImpl.onRestore(self)
  1807.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  1808.  
  1809.         #FIXME: the update dies if all of the items aren't restored, so we 
  1810.         # wait a little while before we start the update
  1811.         self.downloads = set()
  1812.         self.tempHistory = {}
  1813.         self.scheduleUpdateEvents(.1)
  1814.  
  1815. class DirectoryWatchFeedImpl(FeedImpl):
  1816.     def __init__(self,ufeed, directory, visible = True):
  1817.         self.dir = directory
  1818.         self.firstUpdate = True
  1819.         if directory is not None:
  1820.             url = u"dtv:directoryfeed:%s" % (makeURLSafe (directory),)
  1821.         else:
  1822.             url = u"dtv:directoryfeed"
  1823.         title = directory
  1824.         if title[-1] == '/':
  1825.             title = title[:-1]
  1826.         title = filenameToUnicode(os.path.basename(title)) + "/"
  1827.         FeedImpl.__init__(self,url = url,ufeed=ufeed,title = title,visible = visible)
  1828.  
  1829.         self.setUpdateFrequency(5)
  1830.         self.scheduleUpdateEvents(0)
  1831.  
  1832.     ##
  1833.     # Directory Items shouldn't automatically expire
  1834.     def expireItems(self):
  1835.         pass
  1836.  
  1837.     def setUpdateFrequency(self, frequency):
  1838.         newFreq = frequency*60
  1839.         if newFreq != self.updateFreq:
  1840.             self.updateFreq = newFreq
  1841.             self.scheduleUpdateEvents(-1)
  1842.  
  1843.     def setVisible(self, visible):
  1844.         if self.visible == visible:
  1845.             return
  1846.         self.visible = visible
  1847.         self.signalChange()
  1848.  
  1849.     def update(self):
  1850.         def isBasenameHidden(filename):
  1851.             if filename[-1] == os.sep:
  1852.                 filename = filename[:-1]
  1853.             return os.path.basename(filename)[0] == FilenameType('.')
  1854.         self.ufeed.confirmDBThread()
  1855.  
  1856.         # Files known about by real feeds (other than other directory
  1857.         # watch feeds)
  1858.         knownFiles = set()
  1859.         for item in views.toplevelItems:
  1860.             if not item.getFeed().getURL().startswith("dtv:directoryfeed"):
  1861.                 knownFiles.add(os.path.normcase(item.getFilename()))
  1862.  
  1863.         # Remove items that are in feeds, but we have in our list
  1864.         for item in self.items:
  1865.             if item.getFilename() in knownFiles:
  1866.                 item.remove()
  1867.  
  1868.         # Now that we've checked for items that need to be removed, we
  1869.         # add our items to knownFiles so that they don't get added
  1870.         # multiple times to this feed.
  1871.         for x in self.items:
  1872.             knownFiles.add(os.path.normcase (x.getFilename()))
  1873.  
  1874.         #Adds any files we don't know about
  1875.         #Files on the filesystem
  1876.         if os.path.isdir(self.dir):
  1877.             all_files = []
  1878.             files, dirs = miro_listdir(self.dir)
  1879.             for file in files:
  1880.                 all_files.append(file)
  1881.             for dir in dirs:
  1882.                 subfiles, subdirs = miro_listdir(dir)
  1883.                 for subfile in subfiles:
  1884.                     all_files.append(subfile)
  1885.             for file in all_files:
  1886.                 if file not in knownFiles and filetypes.isVideoFilename(platformutils.filenameToUnicode(file)):
  1887.                     FileItem(file, feed_id=self.ufeed.id)
  1888.  
  1889.         for item in self.items:
  1890.             if not os.path.isfile(item.getFilename()):
  1891.                 item.remove()
  1892.         if self.firstUpdate:
  1893.             for item in self.items:
  1894.                 item.markItemSeen()
  1895.             self.firstUpdate = False
  1896.  
  1897.         self.scheduleUpdateEvents(-1)
  1898.  
  1899.     def onRestore(self):
  1900.         FeedImpl.onRestore(self)
  1901.         #FIXME: the update dies if all of the items aren't restored, so we 
  1902.         # wait a little while before we start the update
  1903.         self.scheduleUpdateEvents(.1)
  1904.  
  1905. ##
  1906. # A feed of all of the Movies we find in the movie folder that don't
  1907. # belong to a "real" feed.  If the user changes her movies folder, this feed
  1908. # will continue to remember movies in the old folder.
  1909. #
  1910. class DirectoryFeedImpl(FeedImpl):
  1911.     def __init__(self,ufeed):
  1912.         FeedImpl.__init__(self,url = u"dtv:directoryfeed",ufeed=ufeed,title = u"Feedless Videos",visible = False)
  1913.  
  1914.         self.setUpdateFrequency(5)
  1915.         self.scheduleUpdateEvents(0)
  1916.  
  1917.     ##
  1918.     # Directory Items shouldn't automatically expire
  1919.     def expireItems(self):
  1920.         pass
  1921.  
  1922.     def setUpdateFrequency(self, frequency):
  1923.         newFreq = frequency*60
  1924.         if newFreq != self.updateFreq:
  1925.                 self.updateFreq = newFreq
  1926.                 self.scheduleUpdateEvents(-1)
  1927.  
  1928.     def update(self):
  1929.         self.ufeed.confirmDBThread()
  1930.         moviesDir = config.get(prefs.MOVIES_DIRECTORY)
  1931.         # Files known about by real feeds
  1932.         knownFiles = set()
  1933.         for item in views.toplevelItems:
  1934.             if item.feed_id is not self.ufeed.id:
  1935.                 knownFiles.add(os.path.normcase(item.getFilename()))
  1936.             if item.isContainerItem:
  1937.                 item.findNewChildren()
  1938.  
  1939.         knownFiles.add(os.path.normcase(os.path.join(moviesDir, "Incomplete Downloads")))
  1940.         # thumbs.db is a windows file that speeds up thumbnails.  We know it's
  1941.         # not a movie file.
  1942.         knownFiles.add(os.path.normcase(os.path.join(moviesDir, "thumbs.db")))
  1943.  
  1944.         # Remove items that are in feeds, but we have in our list
  1945.         for item in self.items:
  1946.             if item.getFilename() in knownFiles:
  1947.                 item.remove()
  1948.  
  1949.         # Now that we've checked for items that need to be removed, we
  1950.         # add our items to knownFiles so that they don't get added
  1951.         # multiple times to this feed.
  1952.         for x in self.items:
  1953.             knownFiles.add(os.path.normcase (x.getFilename()))
  1954.  
  1955.         #Adds any files we don't know about
  1956.         #Files on the filesystem
  1957.         if os.path.isdir(moviesDir):
  1958.             files, dirs = miro_listdir(moviesDir)
  1959.             for file in files:
  1960.                 if not file in knownFiles:
  1961.                     FileItem(file, feed_id=self.ufeed.id)
  1962.             for dir in dirs:
  1963.                 if dir in knownFiles:
  1964.                     continue
  1965.                 found = 0
  1966.                 not_found = []
  1967.                 subfiles, subdirs = miro_listdir(dir)
  1968.                 for subfile in subfiles:
  1969.                     if subfile in knownFiles:
  1970.                         found = found + 1
  1971.                     else:
  1972.                         not_found.append(subfile)
  1973.                 for subdir in subdirs:
  1974.                     if subdir in knownFiles:
  1975.                         found = found + 1
  1976.                 # If every subfile or subdirectory is
  1977.                 # already in the database (including
  1978.                 # the case where the directory is
  1979.                 # empty) do nothing.
  1980.                 if len(not_found) > 0:
  1981.                     # If there were any files found,
  1982.                     # this is probably a channel
  1983.                     # directory that someone added
  1984.                     # some thing to.  There are few
  1985.                     # other cases where a directory
  1986.                     # would have some things shown.
  1987.                     if found != 0:
  1988.                         for subfile in not_found:
  1989.                             FileItem(subfile, feed_id=self.ufeed.id)
  1990.                     # But if not, it's probably a
  1991.                     # directory added wholesale.
  1992.                     else:
  1993.                         FileItem(dir, feed_id=self.ufeed.id)
  1994.  
  1995.         for item in self.items:
  1996.             if not os.path.exists(item.getFilename()):
  1997.                 item.remove()
  1998.  
  1999.         self.scheduleUpdateEvents(-1)
  2000.  
  2001.     def onRestore(self):
  2002.         FeedImpl.onRestore(self)
  2003.         #FIXME: the update dies if all of the items aren't restored, so we 
  2004.         # wait a little while before we start the update
  2005.         self.scheduleUpdateEvents(.1)
  2006.  
  2007. ##
  2008. # Search and Search Results feeds
  2009.  
  2010. class SearchFeedImpl (RSSFeedImpl):
  2011.     
  2012.     def __init__(self, ufeed):
  2013.         RSSFeedImpl.__init__(self, url=u'', ufeed=ufeed, title=u'dtv:search', visible=False)
  2014.         self.initialUpdate = True
  2015.         self.setUpdateFrequency(-1)
  2016.         self.searching = False
  2017.         self.lastEngine = u'youtube'
  2018.         self.lastQuery = u''
  2019.         self.ufeed.autoDownloadable = False
  2020.         self.ufeed.signalChange()
  2021.  
  2022.     @returnsUnicode
  2023.     def quoteLastQuery(self):
  2024.         return escape(self.lastQuery)
  2025.  
  2026.     @returnsUnicode
  2027.     def getURL(self):
  2028.         return u'dtv:search'
  2029.  
  2030.     @returnsUnicode
  2031.     def getTitle(self):
  2032.         return _(u'Search')
  2033.  
  2034.     @returnsUnicode
  2035.     def getStatus(self):
  2036.         status = u'idle-empty'
  2037.         if self.searching:
  2038.             status =  u'searching'
  2039.         elif len(self.items) > 0:
  2040.             status =  u'idle-with-results'
  2041.         elif self.url:
  2042.             status = u'idle-no-results'
  2043.         return status
  2044.  
  2045.     def reset(self, url=u'', searchState=False):
  2046.         self.ufeed.confirmDBThread()
  2047.         try:
  2048.             for item in self.items:
  2049.                 item.remove()
  2050.             self.url = url
  2051.             self.searching = searchState
  2052.             self.thumbURL = defaultFeedIconURL()
  2053.             self.ufeed.iconCache.remove()
  2054.             self.ufeed.iconCache = IconCache(self.ufeed, is_vital = True)
  2055.             self.ufeed.iconCache.requestUpdate(True)
  2056.             self.initialHTML = None
  2057.             self.etag = None
  2058.             self.modified = None
  2059.             self.parsed = None
  2060.         finally:
  2061.             self.ufeed.signalChange()
  2062.     
  2063.     def preserveDownloads(self, downloadsFeed):
  2064.         self.ufeed.confirmDBThread()
  2065.         for item in self.items:
  2066.             if item.getState() not in ('new', 'not-downloaded'):
  2067.                 item.setFeed(downloadsFeed.id)
  2068.         
  2069.     def lookup(self, engine, query):
  2070.         checkU(engine)
  2071.         checkU(query)
  2072.         url = searchengines.getRequestURL(engine, query)
  2073.         self.reset(url, True)
  2074.         self.lastQuery = query
  2075.         self.lastEngine = engine
  2076.         self.update()
  2077.         self.ufeed.signalChange()
  2078.  
  2079.     def _handleNewEntry(self, entry):
  2080.         """Handle getting a new entry from a feed."""
  2081.         videoEnc = getFirstVideoEnclosure(entry)
  2082.         if videoEnc is not None:
  2083.             url = videoEnc.get('url')
  2084.             if url is not None:
  2085.                 dl = downloader.getExistingDownloaderByURL(url)
  2086.                 if dl is not None:
  2087.                     for item in dl.itemList:
  2088.                         if item.getFeedURL() == 'dtv:searchDownloads' and item.getURL() == url:
  2089.                             try:
  2090.                                 if entry["id"] == item.getRSSID():
  2091.                                     item.setFeed(self.ufeed.id)
  2092.                                     if not _entry_equal(entry, item.getRSSEntry()):
  2093.                                         self._handleNewEntryForItem(item, entry)
  2094.                                     return
  2095.                             except KeyError:
  2096.                                 pass
  2097.                             title = entry.get("title")
  2098.                             oldtitle = item.entry.get("title")
  2099.                             if title == oldtitle:
  2100.                                 item.setFeed(self.ufeed.id)
  2101.                                 if not _entry_equal(entry, item.getRSSEntry()):
  2102.                                     self._handleNewEntryForItem(item, entry)
  2103.                                 return
  2104.         RSSFeedImpl._handleNewEntry(self, entry)
  2105.  
  2106.     def updateUsingParsed(self, parsed):
  2107.         self.searching = False
  2108.         RSSFeedImpl.updateUsingParsed(self, parsed)
  2109.  
  2110.     def update(self):
  2111.         if self.url is not None and self.url != u'':
  2112.             RSSFeedImpl.update(self)
  2113.  
  2114.     def feedparser_errback(self, e):
  2115.         if self.searching:
  2116.             self.searching = False
  2117.         RSSFeedImpl.feedparser_errback(self, e)
  2118.  
  2119.     def _updateErrback(self, error):
  2120.         if self.searching:
  2121.             self.searching = False
  2122.         RSSFeedImpl._updateErrback(self, error)
  2123.  
  2124. class SearchDownloadsFeedImpl(FeedImpl):
  2125.     def __init__(self, ufeed):
  2126.         FeedImpl.__init__(self, url=u'dtv:searchDownloads', ufeed=ufeed, 
  2127.                 title=None, visible=False)
  2128.         self.setUpdateFrequency(-1)
  2129.  
  2130.     @returnsUnicode
  2131.     def getTitle(self):
  2132.         return _(u'Search')
  2133.  
  2134. class ManualFeedImpl(FeedImpl):
  2135.     """Downloaded Videos/Torrents that have been added using by the
  2136.     user opening them with democracy.
  2137.     """
  2138.     def __init__(self, ufeed):
  2139.         FeedImpl.__init__(self, url=u'dtv:manualFeed', ufeed=ufeed, 
  2140.                 title=None, visible=False)
  2141.         self.ufeed.expire = u'never'
  2142.         self.setUpdateFrequency(-1)
  2143.  
  2144.     @returnsUnicode
  2145.     def getTitle(self):
  2146.         return _(u'Local Files')
  2147.  
  2148. class SingleFeedImpl(FeedImpl):
  2149.     """Single Video that is playing that has been added by the user
  2150.     opening them with democracy.
  2151.     """
  2152.     def __init__(self, ufeed):
  2153.         FeedImpl.__init__(self, url=u'dtv:singleFeed', ufeed=ufeed, 
  2154.                 title=None, visible=False)
  2155.         self.ufeed.expire = u'never'
  2156.         self.setUpdateFrequency(-1)
  2157.  
  2158.     @returnsUnicode
  2159.     def getTitle(self):
  2160.         return _(u'Playing File')
  2161.  
  2162. ##
  2163. # Parse HTML document and grab all of the links and their title
  2164. # FIXME: Grab link title from ALT tags in images
  2165. # FIXME: Grab document title from TITLE tags
  2166. class HTMLLinkGrabber(HTMLParser):
  2167.     linkPattern = re.compile("<(a|embed)\s[^>]*(href|src)\s*=\s*\"([^\"]*)\"[^>]*>(.*?)</a(.*)", re.S)
  2168.     imgPattern = re.compile(".*<img\s.*?src\s*=\s*\"(.*?)\".*?>", re.S)
  2169.     tagPattern = re.compile("<.*?>")
  2170.     def getLinks(self,data, baseurl):
  2171.         self.links = []
  2172.         self.lastLink = None
  2173.         self.inLink = False
  2174.         self.inObject = False
  2175.         self.baseurl = baseurl
  2176.         self.inTitle = False
  2177.         self.title = None
  2178.         self.thumbnailUrl = None
  2179.  
  2180.         match = HTMLLinkGrabber.linkPattern.search(data)
  2181.         while match:
  2182.             try:
  2183.                 linkURL = match.group(3).encode('ascii')
  2184.             except UnicodeError:
  2185.                 linkURL = match.group(3)
  2186.                 i = len (linkURL) - 1
  2187.                 while (i >= 0):
  2188.                     if 127 < ord(linkURL[i]) <= 255:
  2189.                         linkURL = linkURL[:i] + "%%%02x" % (ord(linkURL[i])) + linkURL[i+1:]
  2190.                     i = i - 1
  2191.  
  2192.             link = urljoin(baseurl, linkURL)
  2193.             desc = match.group(4)
  2194.             imgMatch = HTMLLinkGrabber.imgPattern.match(desc)
  2195.             if imgMatch:
  2196.                 try:
  2197.                     thumb = urljoin(baseurl, imgMatch.group(1).encode('ascii'))
  2198.                 except UnicodeError:
  2199.                     thumb = None
  2200.             else:
  2201.                 thumb = None
  2202.             desc =  HTMLLinkGrabber.tagPattern.sub(' ',desc)
  2203.             self.links.append((link, desc, thumb))
  2204.             match = HTMLLinkGrabber.linkPattern.search(match.group(5))
  2205.         return self.links
  2206.  
  2207. class RSSLinkGrabber(xml.sax.handler.ContentHandler, xml.sax.handler.ErrorHandler):
  2208.     def __init__(self,baseurl,charset=None):
  2209.         self.baseurl = baseurl
  2210.         self.charset = charset
  2211.     def startDocument(self):
  2212.         #print "Got start document"
  2213.         self.enclosureCount = 0
  2214.         self.itemCount = 0
  2215.         self.links = []
  2216.         self.inLink = False
  2217.         self.inDescription = False
  2218.         self.inTitle = False
  2219.         self.inItem = False
  2220.         self.descHTML = ''
  2221.         self.theLink = ''
  2222.         self.title = None
  2223.         self.firstTag = True
  2224.         self.errors = 0
  2225.         self.fatalErrors = 0
  2226.  
  2227.     def startElementNS(self, name, qname, attrs):
  2228.         uri = name[0]
  2229.         tag = name[1]
  2230.         if self.firstTag:
  2231.             self.firstTag = False
  2232.             if tag not in ['rss','feed']:
  2233.                 raise xml.sax.SAXNotRecognizedException, "Not an RSS file"
  2234.         if tag.lower() == 'enclosure' or tag.lower() == 'content':
  2235.             self.enclosureCount += 1
  2236.         elif tag.lower() == 'link':
  2237.             self.inLink = True
  2238.             self.theLink = ''
  2239.         elif tag.lower() == 'description':
  2240.             self.inDescription = True
  2241.             self.descHTML = ''
  2242.         elif tag.lower() == 'item':
  2243.             self.itemCount += 1
  2244.             self.inItem = True
  2245.         elif tag.lower() == 'title' and not self.inItem:
  2246.             self.inTitle = True
  2247.  
  2248.     def endElementNS(self, name, qname):
  2249.         uri = name[0]
  2250.         tag = name[1]
  2251.         if tag.lower() == 'description':
  2252.             lg = HTMLLinkGrabber()
  2253.             try:
  2254.                 html = xhtmlify(unescape(self.descHTML),addTopTags=True)
  2255.                 if not self.charset is None:
  2256.                     html = fixHTMLHeader(html,self.charset)
  2257.                 self.links[:0] = lg.getLinks(html,self.baseurl)
  2258.             except HTMLParseError: # Don't bother with bad HTML
  2259.                 logging.info ("bad HTML in description for %s", self.baseurl)
  2260.             self.inDescription = False
  2261.         elif tag.lower() == 'link':
  2262.             self.links.append((self.theLink,None,None))
  2263.             self.inLink = False
  2264.         elif tag.lower() == 'item':
  2265.             self.inItem == False
  2266.         elif tag.lower() == 'title' and not self.inItem:
  2267.             self.inTitle = False
  2268.  
  2269.     def characters(self, data):
  2270.         if self.inDescription:
  2271.             self.descHTML += data
  2272.         elif self.inLink:
  2273.             self.theLink += data
  2274.         elif self.inTitle:
  2275.             if self.title is None:
  2276.                 self.title = data
  2277.             else:
  2278.                 self.title += data
  2279.  
  2280.     def error(self, exception):
  2281.         self.errors += 1
  2282.  
  2283.     def fatalError(self, exception):
  2284.         self.fatalErrors += 1
  2285.  
  2286. # Grabs the feed link from the given webpage
  2287. class HTMLFeedURLParser(HTMLParser):
  2288.     def getLink(self,baseurl,data):
  2289.         self.baseurl = baseurl
  2290.         self.link = None
  2291.         try:
  2292.             self.feed(data)
  2293.         except HTMLParseError:
  2294.             logging.info ("error parsing %s", baseurl)
  2295.         try:
  2296.             self.close()
  2297.         except HTMLParseError:
  2298.             logging.info ("error closing %s", baseurl)
  2299.         return self.link
  2300.  
  2301.     def handle_starttag(self, tag, attrs):
  2302.         attrdict = {}
  2303.         for (key, value) in attrs:
  2304.             attrdict[key.lower()] = value
  2305.         if (tag.lower() == 'link' and attrdict.has_key('rel') and 
  2306.             attrdict.has_key('type') and attrdict.has_key('href') and
  2307.             attrdict['rel'].lower() == 'alternate' and 
  2308.             attrdict['type'].lower() in ['application/rss+xml',
  2309.                                          'application/podcast+xml',
  2310.                                          'application/rdf+xml',
  2311.                                          'application/atom+xml',
  2312.                                          'text/xml',
  2313.                                          'application/xml']):
  2314.             self.link = urljoin(self.baseurl,attrdict['href'])
  2315.  
  2316. def expireItems():
  2317.     try:
  2318.         for feed in views.feeds:
  2319.             feed.expireItems()
  2320.     finally:
  2321.         eventloop.addTimeout(300, expireItems, "Expire Items")
  2322.  
  2323. def getFeedByURL(url):
  2324.     return views.feeds.getItemWithIndex(indexes.feedsByURL, url)
  2325.