home *** CD-ROM | disk | FTP | other *** search
/ PC World 2007 December (DVD) / PCWorld_2007-12_DVD.iso / multimedia / miro / Miro_Installer.exe / xulrunner / python / app.py < prev    next >
Encoding:
Python Source  |  2007-10-31  |  95.1 KB  |  2,524 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. import config       # IMPORTANT!! config MUST be imported before downloader
  19. import prefs
  20.  
  21. import database
  22. db = database.defaultDatabase
  23.  
  24. import views
  25. import indexes
  26. import sorts
  27. # import filters
  28. import maps
  29.  
  30. import menu
  31. import util
  32. import feed
  33. import item
  34. import playlist
  35. import tabs
  36.  
  37. import opml
  38. import folder
  39. import autodler
  40. import databaseupgrade
  41. import resources
  42. import selection
  43. import template
  44. import singleclick
  45. import storedatabase
  46. import subscription
  47. import downloader
  48. import autoupdate
  49. import xhtmltools
  50. import guide
  51. import idlenotifier 
  52. import eventloop
  53. import searchengines
  54. import download_utils
  55.  
  56. import os
  57. import re
  58. import shutil
  59. import cgi
  60. import traceback
  61. import threading
  62. import platform
  63. import dialogs
  64. import iconcache
  65. import moviedata
  66. import platformutils
  67. import logging
  68. import theme
  69.  
  70. # These are Python templates for string substitution, not at all
  71. # related to our HTML based templates
  72. from string import Template
  73.  
  74. # Something needs to import this outside of Pyrex. Might as well be app
  75. import templatehelper
  76. import databasehelper
  77. # import fasttypes
  78. import urllib
  79. import menubar # Needed because the XUL port only includes this in pybridge
  80. from gtcache import gettext as _
  81. from gtcache import ngettext
  82. from clock import clock
  83.  
  84. # Global Controller singleton
  85. controller = None
  86.  
  87. # Backend delegate singleton
  88. delegate = None
  89.  
  90. # Run the application. Call this, not start(), on platforms where we
  91. # are responsible for the event loop.
  92. def main():
  93.     platformutils.setupLogging()
  94.     util.setupLogging()
  95.     Controller().Run()
  96.  
  97. # Start up the application and return. Call this, not main(), on
  98. # platform where we are not responsible for the event loop.
  99. def start():
  100.     platformutils.setupLogging()
  101.     util.setupLogging()
  102.     Controller().runNonblocking()
  103.  
  104. def startupFunction(func):
  105.     """Decorator for startup functions.  If they throw an exception, miro will
  106.     show a error dialog and quit.
  107.     """
  108.  
  109.     def wrapped(*args, **kwargs):
  110.         try:
  111.             func(*args, **kwargs)
  112.         except:
  113.             util.failedExn("while finishing starting up")
  114.             frontend.exit(1)
  115.     return wrapped
  116.  
  117. ###############################################################################
  118. #### The Playback Controller base class                                    ####
  119. ###############################################################################
  120.  
  121. class PlaybackControllerBase:
  122.     
  123.     def __init__(self):
  124.         self.currentPlaylist = None
  125.         self.justPlayOne = False
  126.         self.currentItem = None
  127.         self.updateVideoTimeDC = None
  128.  
  129.     def configure(self, view, firstItemId=None, justPlayOne=False):
  130.         self.currentPlaylist = Playlist(view, firstItemId)
  131.         self.justPlayOne = justPlayOne
  132.     
  133.     def reset(self):
  134.         if self.currentPlaylist is not None:
  135.             eventloop.addIdle (self.currentPlaylist.reset, "Reset Playlist")
  136.             self.currentPlaylist = None
  137.  
  138.     def configureWithSelection(self):
  139.         itemSelection = controller.selection.itemListSelection
  140.         view = itemSelection.currentView
  141.         if itemSelection.currentView is None:
  142.             return
  143.  
  144.         for item in view:
  145.             itemid = item.getID()
  146.             if itemSelection.isSelected(view, itemid) and item.isDownloaded():
  147.                 self.configure(view, itemid)
  148.                 break
  149.     
  150.     def enterPlayback(self):
  151.         if self.currentPlaylist is None:
  152.             self.configureWithSelection()
  153.         if self.currentPlaylist is not None:
  154.             startItem = self.currentPlaylist.cur()
  155.             if startItem is not None:
  156.                 self.playItem(startItem)
  157.         
  158.     def exitPlayback(self, switchDisplay=True):
  159.         self.reset()
  160.         if switchDisplay:
  161.             controller.selection.displayCurrentTabContent()
  162.     
  163.     def playPause(self):
  164.         videoDisplay = controller.videoDisplay
  165.         frame = controller.frame
  166.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  167.             videoDisplay.playPause()
  168.         else:
  169.             self.enterPlayback()
  170.  
  171.     def pause(self):
  172.         videoDisplay = controller.videoDisplay
  173.         frame = controller.frame
  174.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  175.             videoDisplay.pause()
  176.  
  177.     def removeItem(self, item):
  178.         if item.idExists():
  179.             item.executeExpire()
  180.  
  181.     def playItem(self, anItem):
  182.         try:
  183.             if self.currentItem:
  184.                 self.currentItem.onViewedCancel()
  185.             self.currentItem = None
  186.             while not os.path.exists(anItem.getVideoFilename()):
  187.                 logging.info ("movie file '%s' is missing, skipping to next",
  188.                               anItem.getVideoFilename())
  189.                 eventloop.addIdle(self.removeItem, "Remove deleted item", args=(anItem.item,))
  190.                 anItem = self.currentPlaylist.getNext()
  191.                 if anItem is None:
  192.                     self.stop()
  193.                     return
  194.  
  195.             self.currentItem = anItem
  196.             if anItem is not None:
  197.                 videoDisplay = controller.videoDisplay
  198.                 videoRenderer = videoDisplay.getRendererForItem(anItem)
  199.                 if videoRenderer is not None:
  200.                     self.playItemInternally(anItem, videoDisplay, videoRenderer)
  201.                 else:
  202.                     frame = controller.frame
  203.                     if frame.getDisplay(frame.mainDisplay) is videoDisplay:
  204.                         if videoDisplay.isFullScreen:
  205.                             videoDisplay.exitFullScreen()
  206.                         videoDisplay.stop()
  207.                     self.scheduleExternalPlayback(anItem)
  208.         except:
  209.             util.failedExn('when trying to play a video')
  210.             self.stop()
  211.  
  212.     def playItemInternally(self, anItem, videoDisplay, videoRenderer):
  213.         logging.info("Playing item with renderer: %s" % videoRenderer)
  214.         controller.videoDisplay.setExternal(False)
  215.         frame = controller.frame
  216.         if frame.getDisplay(frame.mainDisplay) is not videoDisplay:
  217.             frame.selectDisplay(videoDisplay, frame.mainDisplay)
  218.         videoDisplay.selectItem(anItem, videoRenderer)
  219.         if config.get(prefs.RESUME_VIDEOS_MODE) and anItem.resumeTime > 10:
  220.             videoDisplay.playFromTime(anItem.resumeTime)
  221.         else:
  222.             videoDisplay.play()
  223.         self.startUpdateVideoTime()
  224.  
  225.     def playItemExternally(self, itemID):
  226.         anItem = mapToPlaylistItem(db.getObjectByID(int(itemID)))
  227.         controller.videoInfoItem = anItem
  228.         newDisplay = TemplateDisplay('external-playback-continue','default')
  229.         frame = controller.frame
  230.         frame.selectDisplay(newDisplay, frame.mainDisplay)
  231.         return anItem
  232.         
  233.     def scheduleExternalPlayback(self, anItem):
  234.         controller.videoDisplay.setExternal(True)
  235.         controller.videoDisplay.stopOnDeselect = False
  236.         controller.videoInfoItem = anItem
  237.         newDisplay = TemplateDisplay('external-playback','default')
  238.         frame = controller.frame
  239.         frame.selectDisplay(newDisplay, frame.mainDisplay)
  240.         anItem.markItemSeen()
  241.  
  242.     def startUpdateVideoTime(self):
  243.         if not self.updateVideoTimeDC:
  244.             self.updateVideoTimeDC = eventloop.addTimeout(.5, self.updateVideoTime, "Update Video Time")
  245.  
  246.     def stopUpdateVideoTime(self):
  247.         if self.updateVideoTimeDC:
  248.             self.updateVideoTimeDC.cancel()
  249.             self.updateVideoTimeDC = None
  250.  
  251.     def updateVideoTime(self, repeat=True):
  252.         t = controller.videoDisplay.getCurrentTime()
  253.         if t != None and self.currentItem:
  254.             self.currentItem.setResumeTime(t)
  255.         if repeat:
  256.             self.updateVideoTimeDC = eventloop.addTimeout(.5, self.updateVideoTime, "Update Video Time")
  257.  
  258.     def stop(self, switchDisplay=True, markAsViewed=False):
  259.         controller.videoDisplay.setExternal(False)
  260.         if self.updateVideoTimeDC:
  261.             self.updateVideoTime(repeat=False)
  262.             self.stopUpdateVideoTime()
  263.         if self.currentItem:
  264.             self.currentItem.onViewedCancel()
  265.         self.currentItem = None
  266.         frame = controller.frame
  267.         videoDisplay = controller.videoDisplay
  268.         if frame.getDisplay(frame.mainDisplay) == videoDisplay:
  269.             videoDisplay.stop()
  270.         self.exitPlayback(switchDisplay)
  271.  
  272.     def skip(self, direction, allowMovieReset=True):
  273.         frame = controller.frame
  274.         currentDisplay = frame.getDisplay(frame.mainDisplay)
  275.         if self.currentPlaylist is None:
  276.             self.stop()
  277.         elif (allowMovieReset and direction == -1
  278.                 and hasattr(currentDisplay, 'getCurrentTime') 
  279.                 and currentDisplay.getCurrentTime() > 2.0):
  280.             currentDisplay.goToBeginningOfMovie()
  281.         elif config.get(prefs.SINGLE_VIDEO_PLAYBACK_MODE) or self.justPlayOne:
  282.             self.stop()
  283.         else:
  284.             if direction == 1:
  285.                 nextItem = self.currentPlaylist.getNext()
  286.             else:
  287.                 nextItem = self.currentPlaylist.getPrev()
  288.             if nextItem is None:
  289.                 self.stop()
  290.             else:
  291.                 if self.updateVideoTimeDC:
  292.                     self.updateVideoTime(repeat=False)
  293.                     self.stopUpdateVideoTime()
  294.                 self.playItem(nextItem)
  295.  
  296.     def onMovieFinished(self):
  297.         self.stopUpdateVideoTime()
  298.         setToStart = False
  299.         if self.currentItem:
  300.             self.currentItem.setResumeTime(0)
  301.             if self.currentItem.getFeedURL() == 'dtv:singleFeed':
  302.                 setToStart = True
  303.         if setToStart:
  304.             frame = controller.frame
  305.             currentDisplay = frame.getDisplay(frame.mainDisplay)
  306.             currentDisplay.pause()
  307.             currentDisplay.goToBeginningOfMovie()
  308.             currentDisplay.pause()
  309.         else:
  310.             return self.skip(1, False)
  311.  
  312.  
  313. ###############################################################################
  314. #### Base class for displays                                               ####
  315. #### This must be defined before we import the frontend                    ####
  316. ###############################################################################
  317.  
  318. class Display:
  319.     "Base class representing a display in a MainFrame's right-hand pane."
  320.  
  321.     def __init__(self):
  322.         self.currentFrame = None # tracks the frame that currently has us selected
  323.  
  324.     def isSelected(self):
  325.         return self.currentFrame is not None
  326.  
  327.     def onSelected(self, frame):
  328.         "Called when the Display is shown in the given MainFrame."
  329.         pass
  330.  
  331.     def onDeselected(self, frame):
  332.         """Called when the Display is no longer shown in the given
  333.         MainFrame. This function is called on the Display losing the
  334.         selection before onSelected is called on the Display gaining the
  335.         selection."""
  336.         pass
  337.  
  338.     def onSelected_private(self, frame):
  339.         assert(self.currentFrame == None)
  340.         self.currentFrame = frame
  341.  
  342.     def onDeselected_private(self, frame):
  343.         assert(self.currentFrame == frame)
  344.         self.currentFrame = None
  345.  
  346.     # The MainFrame wants to know if we're ready to display (eg, if the
  347.     # a HTML display has finished loading its contents, so it can display
  348.     # immediately without flicker.) We're to call hook() when we're ready
  349.     # to be displayed.
  350.     def callWhenReadyToDisplay(self, hook):
  351.         hook()
  352.  
  353.     def cancel(self):
  354.         """Called when the Display is not shown because it is not ready yet
  355.         and another display will take its place"""
  356.         pass
  357.  
  358.     def getWatchable(self):
  359.         """Subclasses can implement this if they can return a database view
  360.         of watchable items"""
  361.         return None
  362.  
  363.  
  364. ###############################################################################
  365. #### Provides cross platform part of Video Display                         ####
  366. #### This must be defined before we import the frontend                    ####
  367. ###############################################################################
  368.  
  369. class VideoDisplayBase (Display):
  370.     
  371.     def __init__(self):
  372.         Display.__init__(self)
  373.         self.playbackController = None
  374.         self.volume = 1.0
  375.         self.previousVolume = 1.0
  376.         self.isPlaying = False
  377.         self.isFullScreen = False
  378.         self.isExternal = False
  379.         self.stopOnDeselect = True
  380.         self.renderers = list()
  381.         self.activeRenderer = None
  382.  
  383.     def initRenderers(self):
  384.         pass
  385.  
  386.     def setExternal(self, external):
  387.         self.isExternal = external
  388.  
  389.     def fillMovieData (self, filename, movie_data, callback):
  390.         for renderer in self.renderers:
  391.             success = renderer.fillMovieData(filename, movie_data)
  392.             if success:
  393.                 callback ()
  394.                 return
  395.         callback ()
  396.         
  397.     def getRendererForItem(self, anItem):
  398.         for renderer in self.renderers:
  399.             if renderer.canPlayItem(anItem):
  400.                 return renderer
  401.         return None
  402.  
  403.     def canPlayItem(self, anItem):
  404.         return self.getRendererForItem(anItem) is not None
  405.     
  406.     def canPlayFile(self, filename):
  407.         for renderer in self.renderers:
  408.             if renderer.canPlayFile(filename):
  409.                 return True
  410.         return False
  411.     
  412.     def selectItem(self, anItem, renderer):
  413.         self.stopOnDeselect = True
  414.         controller.videoInfoItem = anItem
  415.         templ = TemplateDisplay('video-info', 'default')
  416.         area = controller.frame.videoInfoDisplay
  417.         controller.frame.selectDisplay(templ, area)
  418.  
  419.         self.setActiveRenderer(renderer)
  420.         self.activeRenderer.selectItem(anItem)
  421.         self.activeRenderer.setVolume(self.getVolume())
  422.  
  423.     def setActiveRenderer (self, renderer):
  424.         self.activeRenderer = renderer
  425.  
  426.     def reset(self):
  427.         self.isPlaying = False
  428.         self.stopOnDeselect = True
  429.         if self.activeRenderer is not None:
  430.             self.activeRenderer.reset()
  431.         self.activeRenderer = None
  432.  
  433.     def goToBeginningOfMovie(self):
  434.         if self.activeRenderer is not None:
  435.             self.activeRenderer.goToBeginningOfMovie()
  436.  
  437.     def playPause(self):
  438.         if self.isPlaying:
  439.             self.pause()
  440.         else:
  441.             self.play()
  442.  
  443.     def playFromTime(self, startTime):
  444.         if self.activeRenderer is not None:
  445.             self.activeRenderer.playFromTime(startTime)
  446.         self.isPlaying = True
  447.  
  448.     def play(self):
  449.         if self.activeRenderer is not None:
  450.             self.activeRenderer.play()
  451.         self.isPlaying = True
  452.  
  453.     def pause(self):
  454.         if self.activeRenderer is not None:
  455.             self.activeRenderer.pause()
  456.         self.isPlaying = False
  457.  
  458.     def stop(self):
  459.         if self.isFullScreen:
  460.             self.exitFullScreen()
  461.         if self.activeRenderer is not None:
  462.             self.activeRenderer.stop()
  463.         self.reset()
  464.  
  465.     def goFullScreen(self):
  466.         self.isFullScreen = True
  467.         if not self.isPlaying:
  468.             self.play()
  469.  
  470.     def exitFullScreen(self):
  471.         self.isFullScreen = False
  472.  
  473.     def getCurrentTime(self):
  474.         if self.activeRenderer is not None:
  475.             return self.activeRenderer.getCurrentTime()
  476.         return None
  477.  
  478.     def setCurrentTime(self, seconds):
  479.         if self.activeRenderer is not None:
  480.             self.activeRenderer.setCurrentTime(seconds)
  481.  
  482.     def getProgress(self):
  483.         if self.activeRenderer is not None:
  484.             return self.activeRenderer.getProgress()
  485.         return 0.0
  486.  
  487.     def setProgress(self, progress):
  488.         if self.activeRenderer is not None:
  489.             return self.activeRenderer.setProgress(progress)
  490.  
  491.     def getDuration(self):
  492.         if self.activeRenderer is not None:
  493.             return self.activeRenderer.getDuration()
  494.         return None
  495.  
  496.     def setVolume(self, level):
  497.         if level > 1.0:
  498.             level = 1.0
  499.         if level < 0.0:
  500.             level = 0.0
  501.         self.volume = level
  502.         config.set(prefs.VOLUME_LEVEL, level)
  503.         if self.activeRenderer is not None:
  504.             self.activeRenderer.setVolume(level)
  505.  
  506.     def getVolume(self):
  507.         return self.volume
  508.  
  509.     def muteVolume(self):
  510.         self.previousVolume = self.getVolume()
  511.         self.setVolume(0.0)
  512.  
  513.     def restoreVolume(self):
  514.         self.setVolume(self.previousVolume)
  515.  
  516.     def onDeselected(self, frame):
  517.         if self.isPlaying and self.stopOnDeselect:
  518.             controller.playbackController.stop(False)
  519.     
  520. ###############################################################################
  521. #### Video renderer base class                                             ####
  522. ###############################################################################
  523.  
  524. class VideoRenderer:
  525.         
  526.     def __init__(self):
  527.         self.interactivelySeeking = False
  528.     
  529.     def canPlayItem(self, anItem):
  530.         return self.canPlayFile (anItem.getVideoFilename())
  531.     
  532.     def canPlayFile(self, filename):
  533.         return False
  534.  
  535.     def fillMovieData(self, filename, movie_data):
  536.         return False
  537.     
  538.     def getDisplayTime(self):
  539.         seconds = self.getCurrentTime()
  540.         return util.formatTimeForUser(seconds)
  541.         
  542.     def getDisplayDuration(self):
  543.         seconds = self.getDuration()
  544.         return util.formatTimeForUser(seconds)
  545.  
  546.     def getDisplayRemainingTime(self):
  547.         seconds = abs(self.getCurrentTime() - self.getDuration())
  548.         return util.formatTimeForUser(seconds, -1)
  549.  
  550.     def getProgress(self):
  551.         duration = self.getDuration()
  552.         if duration == 0 or duration == None:
  553.             return 0.0
  554.         return self.getCurrentTime() / duration
  555.  
  556.     def setProgress(self, progress):
  557.         if progress > 1.0:
  558.             progress = 1.0
  559.         if progress < 0.0:
  560.             progress = 0.0
  561.         self.setCurrentTime(self.getDuration() * progress)
  562.  
  563.     def selectItem(self, anItem):
  564.         self.selectFile (anItem.getVideoFilename())
  565.  
  566.     def selectFile(self, filename):
  567.         pass
  568.         
  569.     def reset(self):
  570.         pass
  571.  
  572.     def setCurrentTime(self, seconds):
  573.         pass
  574.  
  575.     def getDuration(self):
  576.         return 0.0
  577.  
  578.     def setVolume(self, level):
  579.         pass
  580.                 
  581.     def goToBeginningOfMovie(self):
  582.         pass
  583.  
  584.     def getCurrentTime(self):
  585.         return None
  586.         
  587.     def playFromTime(self, position):
  588.         self.play()
  589.         self.setCurrentTime(position)
  590.         
  591.     def play(self):
  592.         pass
  593.         
  594.     def pause(self):
  595.         pass
  596.         
  597.     def stop(self):
  598.         pass
  599.     
  600.     def getRate(self):
  601.         return 1.0
  602.     
  603.     def setRate(self, rate):
  604.         pass
  605.  
  606.     def movieDataProgramInfo(self, videoPath, thumbnailPath):
  607.         raise NotImplementedError()
  608.         
  609. # We can now safely import the frontend module
  610. import frontend
  611.  
  612. ###############################################################################
  613. #### The main application controller object, binding model to view         ####
  614. ###############################################################################
  615.  
  616. class Controller (frontend.Application):
  617.  
  618.     def __init__(self):
  619.         global controller
  620.         global delegate
  621.         frontend.Application.__init__(self)
  622.         assert controller is None
  623.         assert delegate is None
  624.         controller = self
  625.         delegate = frontend.UIBackendDelegate()
  626.         self.frame = None
  627.         self.inQuit = False
  628.         self.guideURL = None
  629.         self.guide = None
  630.         self.initial_feeds = False # True if this is the first run and there's an initial-feeds.democracy file.
  631.         self.finishedStartup = False
  632.         self.idlingNotifier = None
  633.         self.gatheredVideos = None
  634.         self.sendingCrashReport = 0
  635.         self.librarySearchTerm = None
  636.         self.newVideosSearchTerm = None
  637.  
  638.     ### Startup and shutdown ###
  639.  
  640.     def onStartup(self, gatheredVideos=None):
  641.         logging.info ("Starting up %s", config.get(prefs.LONG_APP_NAME))
  642.         logging.info ("Version:    %s", config.get(prefs.APP_VERSION))
  643.         logging.info ("Revision:   %s", config.get(prefs.APP_REVISION))
  644.         logging.info ("Builder:    %s", config.get(prefs.BUILD_MACHINE))
  645.         logging.info ("Build Time: %s", config.get(prefs.BUILD_TIME))
  646.  
  647.         util.print_mem_usage("Pre everything memory check")
  648.         
  649.         logging.info ("Loading preferences...")
  650.  
  651.         config.load()
  652.         config.addChangeCallback(self.configDidChange)
  653.         
  654.         global delegate
  655.         feed.setDelegate(delegate)
  656.         feed.setSortFunc(sorts.item)
  657.         autoupdate.setDelegate(delegate)
  658.         database.setDelegate(delegate)
  659.         dialogs.setDelegate(delegate)
  660.         
  661.         if not config.get(prefs.STARTUP_TASKS_DONE):
  662.             logging.info ("Showing startup dialog...")
  663.             delegate.performStartupTasks(self.finishStartup)
  664.             config.set(prefs.STARTUP_TASKS_DONE, True)
  665.             config.save()
  666.         else:
  667.             self.finishStartup(gatheredVideos)
  668.         logging.info ("Starting event loop thread")
  669.         eventloop.startup()
  670.  
  671.     def finishStartup(self, gatheredVideos=None):
  672.         self.gatheredVideos = gatheredVideos
  673.         eventloop.addUrgentCall(self.initializeDatabase, "Initializing database")
  674.  
  675.     @startupFunction
  676.     def initializeDatabase(self):
  677.         try:
  678.             views.initialize()
  679.             util.print_mem_usage("Pre-database memory check:")
  680.             logging.info ("Restoring database...")
  681.             database.defaultDatabase.liveStorage = storedatabase.LiveStorage()
  682.             db.recomputeFilters()
  683.             eventloop.addUrgentCall(self.checkMoviesDirectoryGone, 
  684.                     "checking movies directory")
  685.         except databaseupgrade.DatabaseTooNewError:
  686.             title = _("Database too new")
  687.             description = Template(_("""\
  688. You have a database that was saved with a newer version of $shortAppName. \
  689. You must download the latest version of $shortAppName and run that.""")).substitute(shortAppName = config.get(prefs.SHORT_APP_NAME))
  690.             def callback(dialog):
  691.                 eventloop.quit()
  692.                 frontend.quit(True)
  693.             dialogs.MessageBoxDialog(title, description).run(callback)
  694.  
  695.     @startupFunction
  696.     def checkMoviesDirectoryGone(self):
  697.         if not self.moviesDirectoryGone():
  698.             eventloop.addUrgentCall(self.finalizeStartup, "finalizing startup")
  699.             return
  700.  
  701.         title = _("Video Directory Missing")
  702.         description = _("""
  703. Miro can't find your primary video directory.  This may be because it's \
  704. located on an external drive that is currently disconnected.
  705.  
  706. If you continue, the video directory will be reset to a location on this \
  707. drive (this will cause you to lose some details about the videos on the \
  708. external drive).  You can also quit, connect the drive, and relaunch Miro.""")
  709.         dialog = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_QUIT,
  710.                 dialogs.BUTTON_LAUNCH_MIRO)
  711.         def callback(dialog):
  712.             if dialog.choice == dialogs.BUTTON_LAUNCH_MIRO:
  713.                 eventloop.addUrgentCall(self.finalizeStartup, "finalizing startup")
  714.             else:
  715.                 eventloop.quit()
  716.                 frontend.quit(True)
  717.         dialog.run(callback)
  718.  
  719.     @startupFunction
  720.     def finalizeStartup(self):
  721.         downloader.startupDownloader()
  722.  
  723.         util.print_mem_usage("Post-downloader memory check")
  724.  
  725.         self.setupGlobalFeed(u'dtv:manualFeed', initiallyAutoDownloadable=False)
  726.         self.setupGlobalFeed(u'dtv:singleFeed', initiallyAutoDownloadable=False)
  727.  
  728.         # Set up the search objects
  729.         self.setupGlobalFeed(u'dtv:search', initiallyAutoDownloadable=False)
  730.         self.setupGlobalFeed(u'dtv:searchDownloads')
  731.  
  732.         # Set up tab list
  733.         tabs.reloadStaticTabs()
  734.         try:
  735.             channelTabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  736.         except LookupError:
  737.             logging.info ("Creating channel tab order")
  738.             channelTabOrder = tabs.TabOrder(u'channel')
  739.         try:
  740.             playlistTabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  741.         except LookupError:
  742.             logging.info ("Creating playlist tab order")
  743.             playlistTabOrder = tabs.TabOrder(u'playlist')
  744.  
  745.         # Set up search engines
  746.         searchengines.createEngines()
  747.  
  748.         # FIXME - channelGuide never gets used.
  749.         (newGuide, channelGuide) = _getInitialChannelGuide()
  750.  
  751.         # This needs to happen after the first channel guide has been created
  752.         _getThemeHistory()
  753.  
  754.         if newGuide:
  755.             if config.get(prefs.MAXIMIZE_ON_FIRST_RUN).lower() not in ['false','no','0']:
  756.                 delegate.maximizeWindow()
  757.             for temp_guide in unicode(config.get(prefs.ADDITIONAL_CHANNEL_GUIDES)).split():
  758.                 if views.guides.getItemWithIndex(indexes.guidesByURL, temp_guide) is None:
  759.                     guide.ChannelGuide(temp_guide)
  760.  
  761.         # Keep a ref of the 'new' and 'download' tabs, we'll need'em later
  762.         self.newTab = None
  763.         self.downloadTab = None
  764.         for tab in views.allTabs:
  765.             if tab.tabTemplateBase == 'newtab':
  766.                 self.newTab = tab
  767.             elif tab.tabTemplateBase == 'downloadtab':
  768.                 self.downloadTab = tab
  769.         views.unwatchedItems.addAddCallback(self.onUnwatchedItemsCountChange)
  770.         views.unwatchedItems.addRemoveCallback(self.onUnwatchedItemsCountChange)
  771.         views.downloadingItems.addAddCallback(self.onDownloadingItemsCountChange)
  772.         views.downloadingItems.addRemoveCallback(self.onDownloadingItemsCountChange)
  773.         self.onUnwatchedItemsCountChange(None, None)
  774.         self.onDownloadingItemsCountChange(None, None)
  775.  
  776.         # If we're missing the file system videos feed, create it
  777.         self.setupGlobalFeed(u'dtv:directoryfeed')
  778.  
  779.         # Start the automatic downloader daemon
  780.         logging.info ("Spawning auto downloader...")
  781.         autodler.startDownloader()
  782.  
  783.         # Start the idle notifier daemon
  784.         if config.get(prefs.LIMIT_UPSTREAM) is True:
  785.             logging.info ("Spawning idle notifier")
  786.             self.idlingNotifier = idlenotifier.IdleNotifier(self)
  787.             self.idlingNotifier.start()
  788.  
  789.         # Set up the playback controller
  790.         self.playbackController = frontend.PlaybackController()
  791.  
  792.         util.print_mem_usage("Pre-UI memory check")
  793.  
  794.         # Put up the main frame
  795.         logging.info ("Displaying main frame...")
  796.         self.frame = frontend.MainFrame(self)
  797.  
  798.         logging.info ("Creating video display...")
  799.         # Set up the video display
  800.         self.videoDisplay = frontend.VideoDisplay()
  801.         self.videoDisplay.initRenderers()
  802.         self.videoDisplay.playbackController = self.playbackController
  803.         self.videoDisplay.setVolume(config.get(prefs.VOLUME_LEVEL))
  804.         util.print_mem_usage("Post-UI memory check")
  805.  
  806.         # create our selection handler
  807.         
  808.         self.selection = selection.SelectionHandler()
  809.  
  810.         self.selection.selectFirstGuide()
  811.  
  812.         if self.initial_feeds:
  813.             views.feedTabs.resetCursor()
  814.             tab = views.feedTabs.getNext()
  815.             if tab is not None:
  816.                 self.selection.selectTabByObject(tab.obj)
  817.  
  818.         util.print_mem_usage("Post-selection memory check")
  819.  
  820.         # Reconnect items to downloaders.
  821.         item.reconnectDownloaders()
  822.  
  823.         util.print_mem_usage("Post-item reconnect memory check")
  824.  
  825.         eventloop.addTimeout (3, autoupdate.checkForUpdates, "Check for updates")
  826.         feed.expireItems()
  827.  
  828.         self.tabDisplay = TemplateDisplay('tablist', 'default',
  829.                 playlistTabOrder=playlistTabOrder,
  830.                 channelTabOrder=channelTabOrder)
  831.         self.frame.selectDisplay(self.tabDisplay, self.frame.channelsDisplay)
  832.  
  833.         # If we have newly available items, provide feedback
  834.         self.updateAvailableItemsCountFeedback()
  835.  
  836.         # Now adding the video files we possibly gathered from the startup
  837.         # dialog
  838.         if self.gatheredVideos is not None and len(self.gatheredVideos) > 0:
  839.             singleclick.resetCommandLineView()
  840.             for v in self.gatheredVideos:
  841.                 try:
  842.                     singleclick.addVideo(v)
  843.                 except Exception, e:
  844.                     logging.info ("error while adding file %s", v)
  845.                     logging.info (e)
  846.  
  847.         util.print_mem_usage("Pre single-click memory check")
  848.  
  849.         # Use an idle for parseCommandLineArgs because the frontend may
  850.         # have put in idle calls to do set up video playback or similar
  851.         # things.
  852.         eventloop.addIdle(singleclick.parseCommandLineArgs, 
  853.                 'parse command line')
  854.  
  855.         util.print_mem_usage("Post single-click memory check")
  856.  
  857.         starttime = clock()
  858.         iconcache.clearOrphans()
  859.         logging.timing ("Icon clear: %.3f", clock() - starttime)
  860.         logging.info ("Starting movie data updates")
  861.         moviedata.movieDataUpdater.startThread()
  862.  
  863.         logging.info ("Finished startup sequence")
  864.         self.finishStartupSequence()
  865.  
  866.     def finishStartupSequence(self):
  867.         self.finishedStartup = True
  868.         frontend.Application.finishStartupSequence(self)
  869.  
  870.     def setupGlobalFeed(self, url, *args, **kwargs):
  871.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  872.         try:
  873.             if feedView.len() == 0:
  874.                 logging.info ("Spawning global feed %s", url)
  875.                 # FIXME - variable d never gets used.
  876.                 d = feed.Feed(url, *args, **kwargs)
  877.             elif feedView.len() > 1:
  878.                 allFeeds = [f for f in feedView]
  879.                 for extra in allFeeds[1:]:
  880.                     extra.remove()
  881.                 util.failed("Too many db objects for %s" % url)
  882.         finally:
  883.             feedView.unlink()
  884.  
  885.     def moviesDirectoryGone(self):
  886.         movies_dir = config.get(prefs.MOVIES_DIRECTORY)
  887.         if not movies_dir.endswith(os.path.sep):
  888.             movies_dir += os.path.sep
  889.         try:
  890.             contents = os.listdir(movies_dir)
  891.         except OSError:
  892.             # We can't access the directory.  Seems like it's gone.
  893.             return True
  894.         if contents != []:
  895.             # There's something inside the directory consider it present  (even
  896.             # if all our items are missing.
  897.             return False
  898.         # make sure that we have actually downloaded something into the movies
  899.         # directory. 
  900.         for downloader in views.remoteDownloads:
  901.             if (downloader.isFinished() and
  902.                     downloader.getFilename().startswith(movies_dir)):
  903.                 return True
  904.         return False
  905.  
  906.     def getGlobalFeed(self, url):
  907.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  908.         rv = feedView[0]
  909.         feedView.unlink()
  910.         return rv
  911.  
  912.     def removeGlobalFeed(self, url):
  913.         feedView = views.feeds.filterWithIndex(indexes.feedsByURL, url)
  914.         feedView.resetCursor()
  915.         nextfeed = feedView.getNext()
  916.         feedView.unlink()
  917.         if nextfeed is not None:
  918.             logging.info ("Removing global feed %s", url)
  919.             nextfeed.remove()
  920.  
  921.     def copyCurrentFeedURL(self):
  922.         tabs = self.selection.getSelectedTabs()
  923.         if len(tabs) == 1 and tabs[0].isFeed():
  924.             delegate.copyTextToClipboard(tabs[0].obj.getURL())
  925.  
  926.     def recommendCurrentFeed(self):
  927.         tabs = self.selection.getSelectedTabs()
  928.         if len(tabs) == 1 and tabs[0].isFeed():
  929.             # See also dynamic.js if changing this URL
  930.             feed = tabs[0].obj
  931.             delegate.openExternalURL('http://www.videobomb.com/democracy_channel/email_friend?url=%s&title=%s' % (feed.getURL(), feed.getTitle()))
  932.  
  933.     def copyCurrentItemURL(self):
  934.         tabs = self.selection.getSelectedItems()
  935.         if len(tabs) == 1 and isinstance(tabs[0], item.Item):
  936.             url = tabs[0].getURL()
  937.             if url:
  938.                 delegate.copyTextToClipboard(url)
  939.  
  940.     def selectAllItems(self):
  941.         self.selection.itemListSelection.selectAll()
  942.         self.selection.setTabListActive(False)
  943.  
  944.     def removeCurrentSelection(self):
  945.         if self.selection.tabListActive:
  946.             selection = self.selection.tabListSelection
  947.         else:
  948.             selection = self.selection.itemListSelection
  949.         seltype = selection.getType()
  950.         if seltype == 'channeltab':
  951.             self.removeCurrentFeed()
  952.         elif seltype == 'addedguidetab':
  953.             self.removeCurrentGuide()
  954.         elif seltype == 'playlisttab':
  955.             self.removeCurrentPlaylist()
  956.         elif seltype == 'item':
  957.             self.removeCurrentItems()
  958.  
  959.     def removeCurrentFeed(self):
  960.         if self.selection.tabListSelection.getType() == 'channeltab':
  961.             feeds = [t.obj for t in self.selection.getSelectedTabs()]
  962.             self.removeFeeds(feeds)
  963.  
  964.     def removeCurrentGuide(self):
  965.         if self.selection.tabListSelection.getType() == 'addedguidetab':
  966.             guides = [t.obj for t in self.selection.getSelectedTabs()]
  967.             if len(guides) != 1:
  968.                 raise AssertionError("Multiple guides selected")
  969.             self.removeGuide(guides[0])
  970.  
  971.     def removeCurrentPlaylist(self):
  972.         if self.selection.tabListSelection.getType() == 'playlisttab':
  973.             playlists = [t.obj for t in self.selection.getSelectedTabs()]
  974.             self.removePlaylists(playlists)
  975.  
  976.     def removeCurrentItems(self):
  977.         if self.selection.itemListSelection.getType() != 'item':
  978.             return
  979.         selected = self.selection.getSelectedItems()
  980.         if self.selection.tabListSelection.getType() != 'playlisttab':
  981.             removable = [i for i in selected if (i.isDownloaded() or i.isExternal()) ]
  982.             if removable:
  983.                 item.expireItems(removable)
  984.         else:
  985.             playlist = self.selection.getSelectedTabs()[0].obj
  986.             for i in selected:
  987.                 playlist.removeItem(i)
  988.  
  989.     def renameCurrentTab(self, typeCheckList=None):
  990.         selected = self.selection.getSelectedTabs()
  991.         if len(selected) != 1:
  992.             return
  993.         obj = selected[0].obj
  994.         if typeCheckList is None:
  995.             typeCheckList = (playlist.SavedPlaylist, folder.ChannelFolder,
  996.                 folder.PlaylistFolder, feed.Feed)
  997.         if obj.__class__ in typeCheckList:
  998.             obj.rename()
  999.         else:
  1000.             logging.warning ("Bad object type in renameCurrentTab() %s", obj.__class__)
  1001.  
  1002.     def renameCurrentChannel(self):
  1003.         self.renameCurrentTab(typeCheckList=[feed.Feed, folder.ChannelFolder])
  1004.  
  1005.     def renameCurrentPlaylist(self):
  1006.         self.renameCurrentTab(typeCheckList=[playlist.SavedPlaylist,
  1007.                 folder.PlaylistFolder])
  1008.  
  1009.     def downloadCurrentItems(self):
  1010.         selected = self.selection.getSelectedItems()
  1011.         downloadable = [i for i in selected if i.isDownloadable() ]
  1012.         for item in downloadable:
  1013.             item.download()
  1014.  
  1015.     def stopDownloadingCurrentItems(self):
  1016.         selected = self.selection.getSelectedItems()
  1017.         downloading = [i for i in selected if i.getState() == 'downloading']
  1018.         for item in downloading:
  1019.             item.expire()
  1020.  
  1021.     def pauseDownloadingCurrentItems(self):
  1022.         selected = self.selection.getSelectedItems()
  1023.         downloading = [i for i in selected if i.getState() == 'downloading']
  1024.         for item in downloading:
  1025.             item.pause()
  1026.  
  1027.     def updateCurrentFeed(self):
  1028.         for tab in self.selection.getSelectedTabs():
  1029.             if tab.isFeed():
  1030.                 tab.obj.update()
  1031.  
  1032.     def updateAllFeeds(self):
  1033.         for f in views.feeds:
  1034.             f.update()
  1035.  
  1036.     def removeGuide(self, guide):
  1037.         if guide.getDefault():
  1038.             logging.warning ("attempt to remove default guide")
  1039.             return
  1040.         title = _('Remove %s') % guide.getTitle()
  1041.         description = _("Are you sure you want to remove the guide %s?") % (guide.getTitle(),)
  1042.         dialog = dialogs.ChoiceDialog(title, description, 
  1043.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1044.         def dialogCallback(dialog):
  1045.             if guide.idExists() and dialog.choice == dialogs.BUTTON_YES:
  1046.                 guide.remove()
  1047.         dialog.run(dialogCallback)
  1048.  
  1049.     def removePlaylist(self, playlist):
  1050.         return self.removePlaylists([playlist])
  1051.  
  1052.     def removePlaylists(self, playlists):
  1053.         if len(playlists) == 1:
  1054.             title = _('Remove %s') % playlists[0].getTitle()
  1055.             description = _("Are you sure you want to remove %s") % \
  1056.                     playlists[0].getTitle()
  1057.         else:
  1058.             title = _('Remove %s channels') % len(playlists)
  1059.             description = \
  1060.                     _("Are you sure you want to remove these %s playlists") % \
  1061.                     len(playlists)
  1062.         dialog = dialogs.ChoiceDialog(title, description, 
  1063.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1064.         def dialogCallback(dialog):
  1065.             if dialog.choice == dialogs.BUTTON_YES:
  1066.                 for playlist in playlists:
  1067.                     if playlist.idExists():
  1068.                         playlist.remove()
  1069.         dialog.run(dialogCallback)
  1070.  
  1071.     def removeFeed(self, feed):
  1072.         return self.removeFeeds([feed])
  1073.  
  1074.     def removeFeeds(self, feeds):
  1075.         downloads = False
  1076.         downloading = False
  1077.         allDirectories = True
  1078.         for feed in feeds:
  1079.             # We only care about downloaded items in non directory feeds.
  1080.             if isinstance(feed, folder.ChannelFolder) or not feed.getURL().startswith("dtv:directoryfeed"):
  1081.                 allDirectories = False
  1082.                 if feed.hasDownloadedItems():
  1083.                     downloads = True
  1084.                     break
  1085.                 if feed.hasDownloadingItems():
  1086.                     downloading = True
  1087.         if downloads:
  1088.             self.removeFeedsWithDownloads(feeds)
  1089.         elif downloading:
  1090.             self.removeFeedsWithDownloading(feeds)
  1091.         elif allDirectories:
  1092.             self.removeDirectoryFeeds(feeds)
  1093.         else:
  1094.             self.removeFeedsNormal(feeds)
  1095.  
  1096.     def removeFeedsWithDownloads(self, feeds):
  1097.         if len(feeds) == 1:
  1098.             title = _('Remove %s') % feeds[0].getTitle()
  1099.             description = _("""\
  1100. What would you like to do with the videos in this channel that you've \
  1101. downloaded?""")
  1102.         else:
  1103.             title = _('Remove %s channels') % len(feeds)
  1104.             description = _("""\
  1105. What would you like to do with the videos in these channels that you've \
  1106. downloaded?""")
  1107.         dialog = dialogs.ThreeChoiceDialog(title, description, 
  1108.                 dialogs.BUTTON_KEEP_VIDEOS, dialogs.BUTTON_DELETE_VIDEOS,
  1109.                 dialogs.BUTTON_CANCEL)
  1110.         def dialogCallback(dialog):
  1111.             if dialog.choice == dialogs.BUTTON_KEEP_VIDEOS:
  1112.                 manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1113.                 for feed in feeds:
  1114.                     if feed.idExists():
  1115.                         feed.remove(moveItemsTo=manualFeed)
  1116.             elif dialog.choice == dialogs.BUTTON_DELETE_VIDEOS:
  1117.                 for feed in feeds:
  1118.                     if feed.idExists():
  1119.                         feed.remove()
  1120.         dialog.run(dialogCallback)
  1121.  
  1122.     def removeFeedsWithDownloading(self, feeds):
  1123.         if len(feeds) == 1:
  1124.             title = _('Remove %s') % feeds[0].getTitle()
  1125.             description = _("""\
  1126. Are you sure you want to remove %s?  Any downloads in progress will \
  1127. be canceled.""") % feeds[0].getTitle()
  1128.         else:
  1129.             title = _('Remove %s channels') % len(feeds)
  1130.             description = _("""\
  1131. Are you sure you want to remove these %s channels?  Any downloads in \
  1132. progress will be canceled.""") % len(feeds)
  1133.         dialog = dialogs.ChoiceDialog(title, description, 
  1134.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1135.         def dialogCallback(dialog):
  1136.             if dialog.choice == dialogs.BUTTON_YES:
  1137.                 for feed in feeds:
  1138.                     if feed.idExists():
  1139.                         feed.remove()
  1140.         dialog.run(dialogCallback)
  1141.  
  1142.     def removeFeedsNormal(self, feeds):
  1143.         if len(feeds) == 1:
  1144.             title = _('Remove %s') % feeds[0].getTitle()
  1145.             description = _("""\
  1146. Are you sure you want to remove %s?""") % feeds[0].getTitle()
  1147.         else:
  1148.             title = _('Remove %s channels') % len(feeds)
  1149.             description = _("""\
  1150. Are you sure you want to remove these %s channels?""") % len(feeds)
  1151.         dialog = dialogs.ChoiceDialog(title, description, 
  1152.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1153.         def dialogCallback(dialog):
  1154.             if dialog.choice == dialogs.BUTTON_YES:
  1155.                 for feed in feeds:
  1156.                     if feed.idExists():
  1157.                         feed.remove()
  1158.         dialog.run(dialogCallback)
  1159.  
  1160.     def removeDirectoryFeeds(self, feeds):
  1161.         if len(feeds) == 1:
  1162.             title = _('Stop watching %s') % feeds[0].getTitle()
  1163.             description = _("""\
  1164. Are you sure you want to stop watching %s?""") % feeds[0].getTitle()
  1165.         else:
  1166.             title = _('Stop watching %s directories') % len(feeds)
  1167.             description = _("""\
  1168. Are you sure you want to stop watching these %s directories?""") % len(feeds)
  1169.         dialog = dialogs.ChoiceDialog(title, description, 
  1170.                 dialogs.BUTTON_YES, dialogs.BUTTON_NO)
  1171.         def dialogCallback(dialog):
  1172.             if dialog.choice == dialogs.BUTTON_YES:
  1173.                 for feed in feeds:
  1174.                     if feed.idExists():
  1175.                         feed.remove()
  1176.         dialog.run(dialogCallback)
  1177.  
  1178.     def playView(self, view, firstItemId=None, justPlayOne=False):
  1179.         self.playbackController.configure(view, firstItemId, justPlayOne)
  1180.         self.playbackController.enterPlayback()
  1181.  
  1182.     def downloaderShutdown(self):
  1183.         logging.info ("Closing Database...")
  1184.         database.defaultDatabase.liveStorage.close()
  1185.         logging.info ("Shutting down event loop")
  1186.         eventloop.quit()
  1187.         logging.info ("Shutting down frontend")
  1188.         frontend.quit()
  1189.  
  1190.     @eventloop.asUrgent
  1191.     def quit(self):
  1192.         global delegate
  1193.         if self.inQuit:
  1194.             return
  1195.         downloadsCount = views.downloadingItems.len()
  1196.             
  1197.         if (downloadsCount > 0 and config.get(prefs.WARN_IF_DOWNLOADING_ON_QUIT)) or (self.sendingCrashReport > 0):
  1198.             title = _("Are you sure you want to quit?")
  1199.             if self.sendingCrashReport > 0:
  1200.                 message = _("Miro is still uploading your crash report. If you quit now the upload will be canceled.  Quit Anyway?")
  1201.                 dialog = dialogs.ChoiceDialog(title, message,
  1202.                                               dialogs.BUTTON_QUIT,
  1203.                                               dialogs.BUTTON_CANCEL)
  1204.             else:
  1205.                 message = ngettext ("You have %d download still in progress.  Quit Anyway?", 
  1206.                                     "You have %d downloads still in progress.  Quit Anyway?", 
  1207.                                     downloadsCount) % (downloadsCount,)
  1208.                 warning = _ ("Warn me when I attempt to quit with downloads in progress")
  1209.                 dialog = dialogs.CheckboxDialog(title, message, warning, True,
  1210.                         dialogs.BUTTON_QUIT, dialogs.BUTTON_CANCEL)
  1211.  
  1212.             def callback(dialog):
  1213.                 if dialog.choice == dialogs.BUTTON_QUIT:
  1214.                     if isinstance(dialog, dialogs.CheckboxDialog):
  1215.                         config.set(prefs.WARN_IF_DOWNLOADING_ON_QUIT,
  1216.                                    dialog.checkbox_value)
  1217.                     self.quitStage2()
  1218.                 else:
  1219.                     self.inQuit = False
  1220.             dialog.run(callback)
  1221.             self.inQuit = True
  1222.         else:
  1223.             self.quitStage2()
  1224.  
  1225.     def quitStage2(self):
  1226.         logging.info ("Shutting down Downloader...")
  1227.         downloader.shutdownDownloader(self.downloaderShutdown)
  1228.  
  1229.     @eventloop.asUrgent
  1230.     def setGuideURL(self, guideURL):
  1231.         """Change the URL of the current channel guide being displayed.  If no
  1232.         guide is being display, pass in None.
  1233.  
  1234.         This method must be called from the onSelectedTabChange in the
  1235.         platform code.  URLs are legal within guideURL will be allow
  1236.         through in onURLLoad().
  1237.         """
  1238.         self.guide = None
  1239.         if guideURL is not None:
  1240.             self.guideURL = guideURL
  1241.             for guideObj in views.guides:
  1242.                 if guideObj.getURL() == controller.guideURL:
  1243.                     self.guide = guideObj
  1244.         else:
  1245.             self.guideURL = None
  1246.  
  1247.     @eventloop.asIdle
  1248.     def setLastVisitedGuideURL(self, url):
  1249.         selectedTabs = self.selection.getSelectedTabs()
  1250.         selectedObjects = [t.obj for t in selectedTabs]
  1251.         if (len(selectedTabs) != 1 or 
  1252.                 not isinstance(selectedObjects[0], guide.ChannelGuide)):
  1253.             logging.warn("setLastVisitedGuideURL called, but a channelguide "
  1254.                     "isn't selected.  Selection: %s" % selectedObjects)
  1255.             return
  1256.         if selectedObjects[0].isPartOfGuide(url) and (
  1257.             url.startswith(u"http://") or url.startswith(u"https://")):
  1258.             selectedObjects[0].lastVisitedURL = url
  1259.         else:
  1260.             logging.warn("setLastVisitedGuideURL called, but the guide is no "
  1261.                     "longer selected")
  1262.  
  1263.     def onShutdown(self):
  1264.         try:
  1265.             eventloop.join()        
  1266.             logging.info ("Saving preferences...")
  1267.             config.save()
  1268.  
  1269. #             logging.info ("Removing search feed")
  1270. #             TemplateActionHandler(None, None).resetSearch()
  1271. #             self.removeGlobalFeed('dtv:search')
  1272.  
  1273.             logging.info ("Shutting down icon cache updates")
  1274.             iconcache.iconCacheUpdater.shutdown()
  1275.             logging.info ("Shutting down movie data updates")
  1276.             moviedata.movieDataUpdater.shutdown()
  1277.  
  1278. #             logging.info ("Removing static tabs...")
  1279. #             views.allTabs.unlink() 
  1280. #             tabs.removeStaticTabs()
  1281.  
  1282.             if self.idlingNotifier is not None:
  1283.                 logging.info ("Shutting down IdleNotifier")
  1284.                 self.idlingNotifier.join()
  1285.  
  1286.             logging.info ("Done shutting down.")
  1287.             logging.info ("Remaining threads are:")
  1288.             for thread in threading.enumerate():
  1289.                 logging.info ("%s", thread)
  1290.  
  1291.         except:
  1292.             util.failedExn("while shutting down")
  1293.             frontend.exit(1)
  1294.  
  1295.     ### Handling config/prefs changes
  1296.     
  1297.     def configDidChange(self, key, value):
  1298.         if key is prefs.LIMIT_UPSTREAM.key:
  1299.             if value is False:
  1300.                 # The Windows version can get here without creating an
  1301.                 # idlingNotifier
  1302.                 try:
  1303.                     self.idlingNotifier.join()
  1304.                 except:
  1305.                     pass
  1306.                 self.idlingNotifier = None
  1307.             elif self.idlingNotifier is None:
  1308.                 self.idlingNotifier = idlenotifier.IdleNotifier(self)
  1309.                 self.idlingNotifier.start()
  1310.  
  1311.     ### Handling system idle events
  1312.     
  1313.     def systemHasBeenIdlingSince(self, seconds):
  1314.         self.setUpstreamLimit(False)
  1315.  
  1316.     def systemIsActiveAgain(self):
  1317.         self.setUpstreamLimit(True)
  1318.  
  1319.     ### Handling events received from the OS (via our base class) ###
  1320.  
  1321.     # Called by Frontend via Application base class in response to OS request.
  1322.     def addAndSelectFeed(self, url = None, showTemplate = None):
  1323.         return GUIActionHandler().addFeed(url, showTemplate)
  1324.  
  1325.     def addAndSelectGuide(self, url = None):
  1326.         return GUIActionHandler().addGuide(url)
  1327.  
  1328.     def addSearchFeed(self, term=None, style=dialogs.SearchChannelDialog.CHANNEL, location = None):
  1329.         return GUIActionHandler().addSearchFeed(term, style, location)
  1330.  
  1331.     def testSearchFeedDialog(self):
  1332.         return GUIActionHandler().testSearchFeedDialog()
  1333.  
  1334.     ### Handling 'DTVAPI' events from the channel guide ###
  1335.  
  1336.     def addFeed(self, url = None):
  1337.         return GUIActionHandler().addFeed(url, selected = None)
  1338.  
  1339.     def selectFeed(self, url):
  1340.         return GUIActionHandler().selectFeed(url)
  1341.  
  1342.     ### Keep track of currently available+downloading items and refresh the
  1343.     ### corresponding tabs accordingly.
  1344.  
  1345.     def onUnwatchedItemsCountChange(self, obj, id):
  1346.         assert self.newTab is not None
  1347.         self.newTab.redraw()
  1348.         self.updateAvailableItemsCountFeedback()
  1349.         if hasattr(frontend.Application, "onUnwatchedItemsCountChange"):
  1350.             frontend.Application.onUnwatchedItemsCountChange(self, obj, id)
  1351.  
  1352.     def onDownloadingItemsCountChange(self, obj, id):
  1353.         assert self.downloadTab is not None
  1354.         self.downloadTab.redraw()
  1355.         if hasattr(frontend.Application, "onDownloadingItemsCountChange"):
  1356.             frontend.Application.onDownloadingItemsCountChange(self, obj, id)
  1357.  
  1358.     def updateAvailableItemsCountFeedback(self):
  1359.         global delegate
  1360.         count = views.unwatchedItems.len()
  1361.         delegate.updateAvailableItemsCountFeedback(count)
  1362.  
  1363.     ### Chrome search:
  1364.     ### Switch to the search tab and perform a search using the specified engine.
  1365.  
  1366.     def performSearch(self, engine, query):
  1367.         util.checkU(engine)
  1368.         util.checkU(query)
  1369.         handler = TemplateActionHandler(None, None)
  1370.         handler.updateLastSearchEngine(engine)
  1371.         handler.updateLastSearchQuery(query)
  1372.         handler.performSearch(engine, query)
  1373.         self.selection.selectTabByTemplateBase('searchtab')
  1374.  
  1375.     ### ----
  1376.  
  1377.     def setUpstreamLimit(self, setLimit):
  1378.         if setLimit:
  1379.             limit = config.get(prefs.UPSTREAM_LIMIT_IN_KBS)
  1380.             # upstream limit should be set here
  1381.         else:
  1382.             # upstream limit should be unset here
  1383.             pass
  1384.  
  1385.     def handleURIDrop(self, data, **kwargs):
  1386.         """Handle an external drag that contains a text/uri-list mime-type.
  1387.         data should be the text/uri-list data, in escaped form.
  1388.  
  1389.         kwargs is thrown away.  It exists to catch weird URLs, like
  1390.         javascript: which sometime result in us getting extra arguments.
  1391.         """
  1392.  
  1393.         lastAddedFeed = None
  1394.         data = urllib.unquote(data)
  1395.         for url in data.split(u"\n"):
  1396.             url = url.strip()
  1397.             if url == u"":
  1398.                 continue
  1399.             if url.startswith(u"file://"):
  1400.                 filename = download_utils.getFileURLPath(url)
  1401.                 filename = platformutils.osFilenameToFilenameType(filename)
  1402.                 eventloop.addIdle (singleclick.openFile,
  1403.                     "Open Dropped file", args=(filename,))
  1404.             elif url.startswith(u"http:") or url.startswith(u"https:"):
  1405.                 url = feed.normalizeFeedURL(url)
  1406.                 if feed.validateFeedURL(url) and not feed.getFeedByURL(url):
  1407.                     lastAddedFeed = feed.Feed(url)
  1408.  
  1409.         if lastAddedFeed:
  1410.             controller.selection.selectTabByObject(lastAddedFeed)
  1411.  
  1412.     def handleDrop(self, dropData, type, sourceData):
  1413.         try:
  1414.             destType, destID = dropData.split("-")
  1415.             if destID == 'END':
  1416.                 destObj = None
  1417.             elif destID == 'START':
  1418.                 if destType == 'channel':
  1419.                     tabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  1420.                 else:
  1421.                     tabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  1422.                 for tab in tabOrder.getView():
  1423.                     destObj = tab.obj
  1424.                     break
  1425.             else:
  1426.                 destObj = db.getObjectByID(int(destID))
  1427.             sourceArea, sourceID = sourceData.split("-")
  1428.             sourceID = int(sourceID)
  1429.             draggedIDs = self.selection.calcSelection(sourceArea, sourceID)
  1430.         except:
  1431.             logging.exception ("error parsing drop (%r, %r, %r)",
  1432.                                dropData, type, sourceData)
  1433.             return
  1434.  
  1435.         if destType == 'playlist' and type == 'downloadeditem':
  1436.             # dropping an item on a playlist
  1437.             destObj.handleDNDAppend(draggedIDs)
  1438.         elif ((destType == 'channelfolder' and type == 'channel') or
  1439.                 (destType == 'playlistfolder' and type == 'playlist')):
  1440.             # Dropping a channel/playlist onto a folder
  1441.             obj = db.getObjectByID(int(destID))
  1442.             obj.handleDNDAppend(draggedIDs)
  1443.         elif (destType in ('playlist', 'playlistfolder') and 
  1444.                 type in ('playlist', 'playlistfolder')):
  1445.             # Reording the playlist tabs
  1446.             tabOrder = util.getSingletonDDBObject(views.playlistTabOrder)
  1447.             tabOrder.handleDNDReorder(destObj, draggedIDs)
  1448.         elif (destType in ('channel', 'channelfolder') and
  1449.                 type in ('channel', 'channelfolder')):
  1450.             # Reordering the channel tabs
  1451.             tabOrder = util.getSingletonDDBObject(views.channelTabOrder)
  1452.             tabOrder.handleDNDReorder(destObj, draggedIDs)
  1453.         elif destType == "playlistitem" and type == "downloadeditem":
  1454.             # Reording items in a playlist
  1455.             playlist = self.selection.getSelectedTabs()[0].obj
  1456.             playlist.handleDNDReorder(destObj, draggedIDs)
  1457.         else:
  1458.             logging.info ("Can't handle drop. Dest type: %s Dest id: %s Type: %s",
  1459.                           destType, destID, type)
  1460.  
  1461.     def addToNewPlaylist(self):
  1462.         selected = controller.selection.getSelectedItems()
  1463.         childIDs = [i.getID() for i in selected if i.isDownloaded()]
  1464.         playlist.createNewPlaylist(childIDs)
  1465.  
  1466.     def startUploads(self):
  1467.         selected = controller.selection.getSelectedItems()
  1468.         for i in selected:
  1469.             i.startUpload()
  1470.  
  1471.     def newDownload(self, url = None):
  1472.         return GUIActionHandler().addDownload(url)
  1473.         
  1474.     def importChannels(self):
  1475.         importer = opml.Importer()
  1476.         importer.importSubscriptions()
  1477.     
  1478.     def exportChannels(self):
  1479.         exporter = opml.Exporter()
  1480.         exporter.exportSubscriptions()
  1481.  
  1482. ###############################################################################
  1483. #### TemplateDisplay: a HTML-template-driven right-hand display panel      ####
  1484. ###############################################################################
  1485.  
  1486. class TemplateDisplay(frontend.HTMLDisplay):
  1487.  
  1488.     def __init__(self, templateName, templateState, frameHint=None, areaHint=None, 
  1489.             baseURL=None, *args, **kargs):
  1490.         """'templateName' is the name of the inital template file.  'data' is
  1491.         keys for the template. 'templateState' is a string with the state of the
  1492.         template.
  1493.         """
  1494.  
  1495.         logging.debug ("Processing %s", templateName)
  1496.         self.templateName = templateName
  1497.         self.templateState = templateState
  1498.         (tch, self.templateHandle) = template.fillTemplate(templateName,
  1499.                 self, self.getDTVPlatformName(), self.getEventCookie(),
  1500.                 self.getBodyTagExtra(), templateState = templateState,
  1501.                                                            *args, **kargs)
  1502.         self.args = args
  1503.         self.kargs = kargs
  1504.         html = tch.read()
  1505.  
  1506.         self.actionHandlers = [
  1507.             ModelActionHandler(delegate),
  1508.             GUIActionHandler(),
  1509.             TemplateActionHandler(self, self.templateHandle),
  1510.             ]
  1511.  
  1512.         loadTriggers = self.templateHandle.getTriggerActionURLsOnLoad()
  1513.         newPage = self.runActionURLs(loadTriggers)
  1514.  
  1515.         if newPage:
  1516.             self.templateHandle.unlinkTemplate()
  1517.             # FIXME - url is undefined here!
  1518.             self.__init__(re.compile(r"^template:(.*)$").match(url).group(1), frameHint, areaHint, baseURL)
  1519.         else:
  1520.             frontend.HTMLDisplay.__init__(self, html, frameHint=frameHint, areaHint=areaHint, baseURL=baseURL)
  1521.  
  1522.             self.templateHandle.initialFillIn()
  1523.  
  1524.     def __eq__(self, other):
  1525.         return (other.__class__ == TemplateDisplay and 
  1526.                 self.templateName == other.templateName and 
  1527.                 self.args == other.args and 
  1528.                 self.kargs == other.kargs)
  1529.  
  1530.     def __str__(self):
  1531.         return "Template <%s> args=%s kargs=%s" % (self.templateName, self.args, self.kargs)
  1532.  
  1533.     def reInit(self, *args, **kargs):
  1534.         self.args = args
  1535.         self.kargs = kargs
  1536.         try:
  1537.             self.templateHandle.templateVars['reInit'](*args, **kargs)
  1538.         except:
  1539.             pass
  1540.         self.templateHandle.forceUpdate()
  1541.         
  1542.     def runActionURLs(self, triggers):
  1543.         newPage = False
  1544.         for url in triggers:
  1545.             if url.startswith('action:'):
  1546.                 self.onURLLoad(url)
  1547.             elif url.startswith('template:'):
  1548.                 newPage = True
  1549.                 break
  1550.         return newPage
  1551.  
  1552.     def parseEventURL(self, url):
  1553.         match = re.match(r"[a-zA-Z]+:([^?]+)(\?(.*))?$", url)
  1554.         if match:
  1555.             path = match.group(1)
  1556.             argString = match.group(3)
  1557.             if argString is None:
  1558.                 argString = u''
  1559.             argString = argString.encode('utf8')
  1560.             # argString is turned into a str since parse_qs will fail on utf8 that has been url encoded.
  1561.             argLists = cgi.parse_qs(argString, keep_blank_values=True)
  1562.  
  1563.             # argLists is a dictionary from parameter names to a list
  1564.             # of values given for that parameter. Take just one value
  1565.             # for each parameter, raising an error if more than one
  1566.             # was given.
  1567.             args = {}
  1568.             for key in argLists.keys():
  1569.                 value = argLists[key]
  1570.                 if len(value) != 1:
  1571.                     import template_compiler
  1572.                     raise template_compiler.TemplateError, "Multiple values of '%s' argument passed to '%s' action" % (key, url)
  1573.                 # Cast the value results back to unicode
  1574.                 try:
  1575.                     args[key.encode('ascii','replace')] = value[0].decode('utf8')
  1576.                 except:
  1577.                     args[key.encode('ascii','replace')] = value[0].decode('ascii', 'replace')
  1578.             return path, args
  1579.         else:
  1580.             raise ValueError("Badly formed eventURL: %s" % url)
  1581.  
  1582.  
  1583.     # Returns true if the browser should handle the URL.
  1584.     def onURLLoad(self, url):
  1585.         util.checkU(url)
  1586.         logging.info ("got %s", url)
  1587.         try:
  1588.             # Special-case non-'action:'-format URL
  1589.             if url.startswith (u"template:"):
  1590.                 name, args = self.parseEventURL(url)
  1591.                 self.dispatchAction('switchTemplate', name=name, **args)
  1592.                 return False
  1593.  
  1594.             # Standard 'action:' URL
  1595.             if url.startswith (u"action:"):
  1596.                 action, args = self.parseEventURL(url)
  1597.                 self.dispatchAction(action, **args)
  1598.                 return False
  1599.  
  1600.             # Let channel guide URLs pass through
  1601.             if controller.guide is not None and \
  1602.                    controller.guide.isPartOfGuide(url):
  1603.                 controller.setLastVisitedGuideURL(url)
  1604.                 return True
  1605.             if url.startswith(u'file://'):
  1606.                 path = download_utils.getFileURLPath(url)
  1607.                 return os.path.exists(path)
  1608.  
  1609.             # If we get here, this isn't a DTV URL. We should open it
  1610.             # in an external browser.
  1611.             if (url.startswith(u'http://') or url.startswith(u'https://') or
  1612.                 url.startswith(u'ftp://') or url.startswith(u'mailto:') or
  1613.                 url.startswith(u'feed://')):
  1614.                 self.handleCandidateExternalURL(url)
  1615.                 return False
  1616.  
  1617.         except:
  1618.             details = "Handling action URL '%s'" % (url, )
  1619.             util.failedExn("while handling a request", details = details)
  1620.  
  1621.         return True
  1622.  
  1623.     @eventloop.asUrgent
  1624.     def handleCandidateExternalURL(self, url):
  1625.         """Open a URL that onURLLoad thinks is an external URL.
  1626.         handleCandidateExternalURL does extra checks that onURLLoad can't do
  1627.         because it's happens in the gui thread and can't access the DB.
  1628.         """
  1629.  
  1630.         # check for subscribe.getdemocracy.com links
  1631.         type, subscribeURLs = subscription.findSubscribeLinks(url)
  1632.  
  1633.         # check if the url that came from a guide, but the user switched tabs
  1634.         # before it went through.
  1635.         if len(subscribeURLs) == 0:
  1636.             for guideObj in views.guides:
  1637.                 if guideObj.isPartOfGuide(url):
  1638.                     return
  1639.  
  1640.         normalizedURLs = []
  1641.         for url in subscribeURLs:
  1642.             normalized = feed.normalizeFeedURL(url)
  1643.             if feed.validateFeedURL(normalized):
  1644.                 normalizedURLs.append(normalized)
  1645.         if normalizedURLs:
  1646.             if type == 'feed':
  1647.                 for url in normalizedURLs:
  1648.                     if feed.getFeedByURL(url) is None:
  1649.                         newFeed = feed.Feed(url)
  1650.                         newFeed.blink()
  1651.             elif type == 'download':
  1652.                 for url in normalizedURLs:
  1653.                     filename = platformutils.unicodeToFilename(url)
  1654.                     singleclick.downloadURL(filename)
  1655.             elif type == 'guide':
  1656.                 for url in normalizedURLs:
  1657.                     if guide.getGuideByURL (url) is None:
  1658.                         guide.ChannelGuide(url)
  1659.             else:
  1660.                 raise AssertionError("Unkown subscribe type")
  1661.             return
  1662.  
  1663.         if url.startswith(u'feed://'):
  1664.             url = u"http://" + url[len(u"feed://"):]
  1665.             f = feed.getFeedByURL(url)
  1666.             if f is None:
  1667.                 f = feed.Feed(url)
  1668.             f.blink()
  1669.             return
  1670.  
  1671.         delegate.openExternalURL(url)
  1672.  
  1673.     @eventloop.asUrgent
  1674.     def dispatchAction(self, action, **kwargs):
  1675.         called = False
  1676.         start = clock()
  1677.         for handler in self.actionHandlers:
  1678.             if hasattr(handler, action):
  1679.                 getattr(handler, action)(**kwargs)
  1680.                 called = True
  1681.                 break
  1682.         end = clock()
  1683.         if end - start > 0.5:
  1684.             logging.timing ("dispatch action %s too slow (%.3f secs)", action, end - start)
  1685.         if not called:
  1686.             logging.warning ("Ignored bad action URL: action=%s", action)
  1687.  
  1688.     @eventloop.asUrgent
  1689.     def onDeselected(self, frame):
  1690.         unloadTriggers = self.templateHandle.getTriggerActionURLsOnUnload()
  1691.         self.runActionURLs(unloadTriggers)
  1692.         self.unlink()
  1693.         frontend.HTMLDisplay.onDeselected(self, frame)
  1694.  
  1695.     def unlink(self):
  1696.         self.templateHandle.unlinkTemplate()
  1697.         self.actionHandlers = []
  1698.  
  1699. ###############################################################################
  1700. #### Handlers for actions generated from templates, the OS, etc            ####
  1701. ###############################################################################
  1702.  
  1703. # Functions that are safe to call from action: URLs that do nothing
  1704. # but manipulate the database.
  1705. class ModelActionHandler:
  1706.     
  1707.     def __init__(self, backEndDelegate):
  1708.         self.backEndDelegate = backEndDelegate
  1709.     
  1710.     def setAutoDownloadMode(self, feed, mode):
  1711.         obj = db.getObjectByID(int(feed))
  1712.         obj.setAutoDownloadMode(mode)
  1713.  
  1714.     def setExpiration(self, feed, type, time):
  1715.         obj = db.getObjectByID(int(feed))
  1716.         obj.setExpiration(type, int(time))
  1717.  
  1718.     def setMaxNew(self, feed, maxNew):
  1719.         obj = db.getObjectByID(int(feed))
  1720.         obj.setMaxNew(int(maxNew))
  1721.  
  1722.     def invalidMaxNew(self, value):
  1723.         title = _("Invalid Value")
  1724.         description = _("%s is invalid.  You must enter a non-negative "
  1725.                 "number.") % value
  1726.         dialogs.MessageBoxDialog(title, description).run()
  1727.  
  1728.     def startDownload(self, item):
  1729.         try:
  1730.             obj = db.getObjectByID(int(item))
  1731.             obj.download()
  1732.         except database.ObjectNotFoundError:
  1733.             pass
  1734.  
  1735.     def removeFeed(self, id):
  1736.         try:
  1737.             feed = db.getObjectByID(int(id))
  1738.             controller.removeFeed(feed)
  1739.         except database.ObjectNotFoundError:
  1740.             pass
  1741.  
  1742.     def removeCurrentFeed(self):
  1743.         controller.removeCurrentFeed()
  1744.  
  1745.     def removeCurrentPlaylist(self):
  1746.         controller.removeCurrentPlaylist()
  1747.  
  1748.     def removeCurrentItems(self):
  1749.         controller.removeCurrentItems()
  1750.  
  1751.     def mergeToFolder(self):
  1752.         tls = controller.selection.tabListSelection
  1753.         selectionType = tls.getType()
  1754.         childIDs = set(tls.currentSelection)
  1755.         if selectionType == 'channeltab':
  1756.             folder.createNewChannelFolder(childIDs)
  1757.         elif selectionType == 'playlisttab':
  1758.             folder.createNewPlaylistFolder(childIDs)
  1759.         else:
  1760.             logging.warning ("bad selection type %s in mergeToFolder",
  1761.                              selectionType)
  1762.  
  1763.     def remove(self, area, id):
  1764.         selectedIDs = controller.selection.calcSelection(area, int(id))
  1765.         selectedObjects = [db.getObjectByID(id) for id in selectedIDs]
  1766.         objType = selectedObjects[0].__class__
  1767.  
  1768.         if objType in (feed.Feed, folder.ChannelFolder):
  1769.             controller.removeFeeds(selectedObjects)
  1770.         elif objType in (playlist.SavedPlaylist, folder.PlaylistFolder):
  1771.             controller.removePlaylists(selectedObjects)
  1772.         elif objType == guide.ChannelGuide:
  1773.             if len(selectedObjects) != 1:
  1774.                 raise AssertionError("Multiple guides selected in remove")
  1775.             controller.removeGuide(selectedObjects[0])
  1776.         elif objType == item.Item:
  1777.             pl = controller.selection.getSelectedTabs()[0].obj
  1778.             pl.handleRemove(destObj, selectedIDs)
  1779.         else:
  1780.             logging.warning ("Can't handle type %s in remove()", objType)
  1781.  
  1782.     def rename(self, id):
  1783.         try:
  1784.             obj = db.getObjectByID(int(id))
  1785.         except:
  1786.             logging.warning ("tried to rename object that doesn't exist with id %d", int(feed))
  1787.             return
  1788.         if obj.__class__ in (playlist.SavedPlaylist, folder.ChannelFolder,
  1789.                 folder.PlaylistFolder):
  1790.             obj.rename()
  1791.         else:
  1792.             logging.warning ("Unknown object type in remove() %s", type(obj))
  1793.  
  1794.     def updateFeed(self, feed):
  1795.         obj = db.getObjectByID(int(feed))
  1796.         obj.update()
  1797.  
  1798.     def copyFeedURL(self, feed):
  1799.         obj = db.getObjectByID(int(feed))
  1800.         url = obj.getURL()
  1801.         self.backEndDelegate.copyTextToClipboard(url)
  1802.  
  1803.     def markFeedViewed(self, feed):
  1804.         try:
  1805.             obj = db.getObjectByID(int(feed))
  1806.             obj.markAsViewed()
  1807.         except database.ObjectNotFoundError:
  1808.             pass
  1809.  
  1810.     def updateIcons(self, feed):
  1811.         try:
  1812.             obj = db.getObjectByID(int(feed))
  1813.             obj.updateIcons()
  1814.         except database.ObjectNotFoundError:
  1815.             pass
  1816.  
  1817.     def expireItem(self, item):
  1818.         try:
  1819.             obj = db.getObjectByID(int(item))
  1820.             obj.expire()
  1821.         except database.ObjectNotFoundError:
  1822.             logging.warning ("tried to expire item that doesn't exist with id %d", int(item))
  1823.  
  1824.     def expirePlayingItem(self, item):
  1825.         self.expireItem(item)
  1826.         controller.playbackController.skip(1)
  1827.  
  1828.     def addItemToLibrary(self, item):
  1829.         obj = db.getObjectByID(int(item))
  1830.         manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1831.         obj.setFeed(manualFeed.getID())
  1832.  
  1833.     def keepItem(self, item):
  1834.         obj = db.getObjectByID(int(item))
  1835.         obj.save()
  1836.  
  1837.     def stopUploadItem(self, item):
  1838.         obj = db.getObjectByID(int(item))
  1839.         obj.stopUpload()
  1840.  
  1841.     def toggleMoreItemInfo(self, item):
  1842.         obj = db.getObjectByID(int(item))
  1843.         obj.toggleShowMoreInfo()
  1844.  
  1845.     def revealItem(self, item):
  1846.         obj = db.getObjectByID(int(item))
  1847.         filename = obj.getFilename()
  1848.         if not os.path.exists(filename):
  1849.             basename = os.path.basename(filename)
  1850.             title = _("Error Revealing File")
  1851.             msg = _("The file \"%s\" was deleted from outside Miro.") % basename
  1852.             dialogs.MessageBoxDialog(title, msg).run()
  1853.         else:
  1854.             self.backEndDelegate.revealFile(filename)
  1855.  
  1856.     def clearTorrents (self):
  1857.         items = views.items.filter(lambda x: x.getFeed().url == u'dtv:manualFeed' and x.isNonVideoFile() and not x.getState() == u"downloading")
  1858.         for i in items:
  1859.             if i.downloader is not None:
  1860.                 i.downloader.setDeleteFiles(False)
  1861.             i.remove()
  1862.  
  1863.     def pauseDownload(self, item):
  1864.         obj = db.getObjectByID(int(item))
  1865.         obj.pause()
  1866.         
  1867.     def resumeDownload(self, item):
  1868.         obj = db.getObjectByID(int(item))
  1869.         obj.resume()
  1870.  
  1871.     def pauseAll (self):
  1872.         autodler.pauseDownloader()
  1873.         for item in views.downloadingItems:
  1874.             item.pause()
  1875.  
  1876.     def resumeAll (self):
  1877.         for item in views.pausedItems:
  1878.             item.resume()
  1879.         autodler.resumeDownloader()
  1880.  
  1881.     def toggleExpand(self, id):
  1882.         obj = db.getObjectByID(int(id))
  1883.         obj.setExpanded(not obj.getExpanded())
  1884.  
  1885.     def setRunAtStartup(self, value):
  1886.         value = (value == "1")
  1887.         self.backEndDelegate.setRunAtStartup(value)
  1888.  
  1889.     def setCheckEvery(self, value):
  1890.         value = int(value)
  1891.         config.set(prefs.CHECK_CHANNELS_EVERY_X_MN,value)
  1892.  
  1893.     def setLimitUpstream(self, value):
  1894.         value = (value == "1")
  1895.         config.set(prefs.LIMIT_UPSTREAM,value)
  1896.  
  1897.     def setMaxUpstream(self, value):
  1898.         value = int(value)
  1899.         config.set(prefs.UPSTREAM_LIMIT_IN_KBS,value)
  1900.  
  1901.     def setPreserveDiskSpace(self, value):
  1902.         value = (value == "1")
  1903.         config.set(prefs.PRESERVE_DISK_SPACE,value)
  1904.  
  1905.     def setDefaultExpiration(self, value):
  1906.         value = int(value)
  1907.         config.set(prefs.EXPIRE_AFTER_X_DAYS,value)
  1908.  
  1909.     def videoBombExternally(self, item):
  1910.         obj = db.getObjectByID(int(item))
  1911.         paramList = {}
  1912.         paramList["title"] = obj.getTitle()
  1913.         paramList["info_url"] = obj.getLink()
  1914.         paramList["hookup_url"] = obj.getPaymentLink()
  1915.         try:
  1916.             rss_url = obj.getFeed().getURL()
  1917.             if (not rss_url.startswith(u'dtv:')):
  1918.                 paramList["rss_url"] = rss_url
  1919.         except:
  1920.             pass
  1921.         thumb_url = obj.getThumbnailURL()
  1922.         if thumb_url is not None:
  1923.             paramList["thumb_url"] = thumb_url
  1924.  
  1925.         # FIXME: add "explicit" and "tags" parameters when we get them in item
  1926.  
  1927.         paramString = ""
  1928.         glue = '?'
  1929.        
  1930.         # This should be first, since it's most important.
  1931.         url = obj.getURL()
  1932.         url.encode('utf-8', 'replace')
  1933.         if (not url.startswith('file:')):
  1934.             paramString = "?url=%s" % xhtmltools.urlencode(url)
  1935.             glue = '&'
  1936.  
  1937.         for key in paramList.keys():
  1938.             if len(paramList[key]) > 0:
  1939.                 paramString = "%s%s%s=%s" % (paramString, glue, key, xhtmltools.urlencode(paramList[key]))
  1940.                 glue = '&'
  1941.  
  1942.         # This should be last, so that if it's extra long it 
  1943.         # cut off all the other parameters
  1944.         description = obj.getDescription()
  1945.         if len(description) > 0:
  1946.             paramString = "%s%sdescription=%s" % (paramString, glue,
  1947.                     xhtmltools.urlencode(description))
  1948.         url = config.get(prefs.VIDEOBOMB_URL) + paramString
  1949.         self.backEndDelegate.openExternalURL(url)
  1950.  
  1951.     def changeMoviesDirectory(self, newDir, migrate):
  1952.         changeMoviesDirectory(newDir, migrate == '1')
  1953.  
  1954. # Test shim for test* functions on GUIActionHandler
  1955. class printResultThread(threading.Thread):
  1956.  
  1957.     def __init__(self, format, func):
  1958.         self.format = format
  1959.         self.func = func
  1960.         threading.Thread.__init__(self)
  1961.  
  1962.     def run(self):
  1963.         print (self.format % (self.func(), ))
  1964.  
  1965. # Functions that are safe to call from action: URLs that can change
  1966. # the GUI presentation (and may or may not manipulate the database.)
  1967. class GUIActionHandler:
  1968.  
  1969.     def playUnwatched(self):
  1970.         controller.playView(views.unwatchedItems)
  1971.  
  1972.     def openFile(self, path):
  1973.         singleclick.openFile(path)
  1974.  
  1975.     def addSearchFeed(self, term=None, style = dialogs.SearchChannelDialog.CHANNEL, location = None):
  1976.         def doAdd(dialog):
  1977.             if dialog.choice == dialogs.BUTTON_CREATE_CHANNEL:
  1978.                 self.addFeed(dialog.getURL())
  1979.         dialog = dialogs.SearchChannelDialog(term, style, location)
  1980.         if location == None:
  1981.             dialog.run(doAdd)
  1982.         else:
  1983.             self.addFeed(dialog.getURL())
  1984.  
  1985.     def addChannelSearchFeed(self, id):
  1986.         feed = db.getObjectByID(int(id))
  1987.         self.addSearchFeed(feed.inlineSearchTerm, dialogs.SearchChannelDialog.CHANNEL, int(id))
  1988.  
  1989.     def addEngineSearchFeed(self, term, name):
  1990.         self.addSearchFeed(term, dialogs.SearchChannelDialog.ENGINE, name)
  1991.         
  1992.     def testSearchFeedDialog(self):
  1993.         def finish(dialog):
  1994.             pass
  1995.         def thirdDialog(dialog):
  1996.             dialog = dialogs.SearchChannelDialog("Should select URL http://testurl/", dialogs.SearchChannelDialog.URL, "http://testurl/")
  1997.             dialog.run(finish)
  1998.         def secondDialog(dialog):
  1999.             dialog = dialogs.SearchChannelDialog("Should select YouTube engine", dialogs.SearchChannelDialog.ENGINE, "youtube")
  2000.             dialog.run(thirdDialog)
  2001.         dialog = dialogs.SearchChannelDialog("Should select third channel in list", dialogs.SearchChannelDialog.CHANNEL, -1)
  2002.         dialog.run(secondDialog)
  2003.         
  2004.     def addURL(self, title, message, callback, url = None):
  2005.         util.checkU(url)
  2006.         util.checkU(title)
  2007.         util.checkU(message)
  2008.         def createDialog(ltitle, lmessage, prefill = None):
  2009.             def prefillCallback():
  2010.                 if prefill:
  2011.                     return prefill
  2012.                 else:
  2013.                     return None
  2014.             dialog = dialogs.TextEntryDialog(ltitle, lmessage, dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL, prefillCallback, fillWithClipboardURL=(prefill is None))
  2015.             def callback(dialog):
  2016.                 if dialog.choice == dialogs.BUTTON_OK:
  2017.                     doAdd(dialog.value)
  2018.             dialog.run(callback)
  2019.         def doAdd(url):
  2020.             normalizedURL = feed.normalizeFeedURL(url)
  2021.             if not feed.validateFeedURL(normalizedURL):
  2022.                 ltitle = title + _(" - Invalid URL")
  2023.                 lmessage = _("The address you entered is not a valid URL.\nPlease double check and try again.\n\n") + message
  2024.                 createDialog(ltitle, lmessage, url)
  2025.                 return
  2026.             callback(normalizedURL)
  2027.         if url is None:
  2028.             createDialog(title, message)
  2029.         else:
  2030.             doAdd(url)
  2031.         
  2032.     # NEEDS: name should change to addAndSelectFeed; then we should create
  2033.     # a non-GUI addFeed to match removeFeed. (requires template updates)
  2034.     def addFeed(self, url = None, showTemplate = None, selected = '1'):
  2035.         if url:
  2036.             util.checkU(url)
  2037.         def doAdd (url):
  2038.             db.confirmDBThread()
  2039.             myFeed = feed.getFeedByURL (url)
  2040.             if myFeed is None:
  2041.                 myFeed = feed.Feed(url)
  2042.     
  2043.             if selected == '1':
  2044.                 controller.selection.selectTabByObject(myFeed)
  2045.             else:
  2046.                 myFeed.blink()
  2047.         self.addURL (Template(_("$shortAppName - Add Channel")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the channel to add"), doAdd, url)
  2048.  
  2049.     def selectFeed(self, url):
  2050.         url = feed.normalizeFeedURL(url)
  2051.         db.confirmDBThread()
  2052.         # Find the feed
  2053.         myFeed = feed.getFeedByURL (url)
  2054.         if myFeed is None:
  2055.             logging.warning ("selectFeed: no such feed: %s", url)
  2056.             return
  2057.         controller.selection.selectTabByObject(myFeed)
  2058.         
  2059.     def addGuide(self, url = None, selected = '1'):
  2060.         def doAdd(url):
  2061.             db.confirmDBThread()
  2062.             myGuide = guide.getGuideByURL (url)
  2063.             if myGuide is None:
  2064.                 myGuide = guide.ChannelGuide(url)
  2065.     
  2066.             if selected == '1':
  2067.                 controller.selection.selectTabByObject(myGuide)
  2068.         self.addURL (Template(_("$shortAppName - Add Miro Guide")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the Miro Guide to add"), doAdd, url)
  2069.  
  2070.     def addDownload(self, url = None):
  2071.         def doAdd(url):
  2072.             db.confirmDBThread()
  2073.             singleclick.downloadURL(platformutils.unicodeToFilename(url))
  2074.         self.addURL (Template(_("$shortAppName - Download Video")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME)), _("Enter the URL of the video to download"), doAdd, url)
  2075.  
  2076.     def handleDrop(self, data, type, sourcedata):
  2077.         controller.handleDrop(data, type, sourcedata)
  2078.  
  2079.     def handleURIDrop(self, data, **kwargs):
  2080.         controller.handleURIDrop(data, **kwargs)
  2081.  
  2082.     def showHelp(self):
  2083.         delegate.openExternalURL(config.get(prefs.HELP_URL))
  2084.  
  2085.     def reportBug(self):
  2086.         delegate.openExternalURL(config.get(prefs.BUG_REPORT_URL))
  2087.  
  2088. # Functions that are safe to call from action: URLs that change state
  2089. # specific to a particular instantiation of a template, and so have to
  2090. # be scoped to a particular HTML display widget.
  2091. class TemplateActionHandler:
  2092.     
  2093.     def __init__(self, display, templateHandle):
  2094.         self.display = display
  2095.         self.templateHandle = templateHandle
  2096.         self.currentName = None
  2097.  
  2098.     def switchTemplate(self, name, state='default', baseURL=None, *args, **kargs):
  2099.         self.templateHandle.unlinkTemplate()
  2100.         # Switch to new template. It get the same variable
  2101.         # dictionary as we have.
  2102.         # NEEDS: currently we hardcode the display area. This means
  2103.         # that these links always affect the right-hand 'content'
  2104.         # area, even if they are loaded from the left-hand 'tab'
  2105.         # area. Actually this whole invocation is pretty hacky.
  2106.         template = TemplateDisplay(name, state, frameHint=controller.frame,
  2107.                 areaHint=controller.frame.mainDisplay, baseURL=baseURL,
  2108.                 *args, **kargs)
  2109.         controller.frame.selectDisplay(template, controller.frame.mainDisplay)
  2110.         self.currentName = name
  2111.  
  2112.     def setViewFilter(self, viewName, fieldKey, functionKey, parameter, invert):
  2113.         logging.warning ("setViewFilter deprecated")
  2114.  
  2115.     def setViewSort(self, viewName, fieldKey, functionKey, reverse="false"):
  2116.         logging.warning ("setViewSort deprecated")
  2117.  
  2118.     def setSearchString(self, searchString):
  2119.         try:
  2120.             self.templateHandle.getTemplateVariable('updateSearchString')(unicode(searchString))
  2121.         except KeyError, e:
  2122.             logging.warning ("KeyError in getTemplateVariable ('updateSearchString')")
  2123.  
  2124.     def toggleDownloadsView(self):
  2125.         try:
  2126.             self.templateHandle.getTemplateVariable('toggleDownloadsView')(self.templateHandle)
  2127.         except KeyError, e:
  2128.             logging.warning ("KeyError in getTemplateVariable ('toggleDownloadsView')")
  2129.  
  2130.     def toggleWatchableView(self):
  2131.         try:
  2132.             self.templateHandle.getTemplateVariable('toggleWatchableView')(self.templateHandle)
  2133.         except KeyError, e:
  2134.             logging.warning ("KeyError in getTemplateVariable ('toggleWatchableView')")
  2135.  
  2136.     def toggleNewItemsView(self):
  2137.         try:
  2138.             self.templateHandle.getTemplateVariable('toggleNewItemsView')(self.templateHandle)
  2139.         except KeyError, e:
  2140.             logging.warning ("KeyError in getTemplateVariable ('toggleNewItemsView')")            
  2141.  
  2142.     def toggleAllItemsMode(self):
  2143.         try:
  2144.             self.templateHandle.getTemplateVariable('toggleAllItemsMode')(self.templateHandle)
  2145.         except KeyError, e:
  2146.             logging.warning ("KeyError in getTemplateVariable ('toggleAllItemsMode')")
  2147.  
  2148.     def pauseDownloads(self):
  2149.         try:
  2150.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2151.         except KeyError, e:
  2152.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during pauseDownloads()")
  2153.             return
  2154.         for item in view:
  2155.             item.pause()
  2156.  
  2157.     def resumeDownloads(self):
  2158.         try:
  2159.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2160.         except KeyError, e:
  2161.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during resumeDownloads()")
  2162.             return
  2163.         for item in view:
  2164.             item.resume()
  2165.  
  2166.     def cancelDownloads(self):
  2167.         try:
  2168.             view = self.templateHandle.getTemplateVariable('allDownloadingItems')
  2169.         except KeyError, e:
  2170.             logging.warning ("KeyError in getTemplateVariable ('allDownloadingItems') during cancelDownloads()")
  2171.             return
  2172.         for item in view:
  2173.             item.expire()
  2174.  
  2175.     def playViewNamed(self, viewName, firstItemId):
  2176.         try:
  2177.             view = self.templateHandle.getTemplateVariable(viewName)
  2178.         except KeyError, e:
  2179.             logging.warning ("KeyError in getTemplateVariable (%s) during playViewNamed()" % (viewName,))
  2180.             return
  2181.         controller.playView(view, firstItemId)
  2182.  
  2183.     def playOneItem(self, viewName, itemID):
  2184.         try:
  2185.             view = self.templateHandle.getTemplateVariable(viewName)
  2186.         except KeyError, e:
  2187.             logging.warning ("KeyError in getTemplateVariable (%s) during playOneItem()" % (viewName,))
  2188.             return
  2189.         controller.playView(view, itemID, justPlayOne=True)
  2190.  
  2191.     def playNewVideos(self, id):
  2192.         try:
  2193.             obj = db.getObjectByID(int(id))
  2194.         except database.ObjectNotFoundError:
  2195.             return
  2196.  
  2197.         def myUnwatchedItems(obj):
  2198.             return (obj.getState() == u'newly-downloaded' and
  2199.                     not obj.isNonVideoFile() and
  2200.                     not obj.isContainerItem)
  2201.  
  2202.         controller.selection.selectTabByObject(obj, displayTabContent=False)
  2203.         if isinstance(obj, feed.Feed):
  2204.             feedView = views.items.filterWithIndex(indexes.itemsByFeed,
  2205.                     obj.getID())
  2206.             view = feedView.filter(myUnwatchedItems,
  2207.                                    sortFunc=sorts.item)
  2208.             controller.playView(view)
  2209.             view.unlink()
  2210.         elif isinstance(obj, folder.ChannelFolder):
  2211.             folderView = views.items.filterWithIndex(
  2212.                     indexes.itemsByChannelFolder, obj)
  2213.             view = folderView.filter(myUnwatchedItems,
  2214.                                      sortFunc=sorts.item)
  2215.             controller.playView(view)
  2216.             view.unlink()
  2217.         elif isinstance(obj, tabs.StaticTab): # new videos tab
  2218.             view = views.unwatchedItems
  2219.             controller.playView(view)
  2220.         else:
  2221.             raise TypeError("Can't get new videos for %s (type: %s)" % 
  2222.                     (obj, type(obj)))
  2223.  
  2224.     def playItemExternally(self, itemID):
  2225.         controller.playbackController.playItemExternally(itemID)
  2226.         
  2227.     def skipItem(self, itemID):
  2228.         controller.playbackController.skip(1)
  2229.     
  2230.     def updateLastSearchEngine(self, engine):
  2231.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2232.         if searchFeed is not None:
  2233.             searchFeed.lastEngine = engine
  2234.     
  2235.     def updateLastSearchQuery(self, query):
  2236.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2237.         if searchFeed is not None:
  2238.             searchFeed.lastQuery = query
  2239.         
  2240.     def performSearch(self, engine, query):
  2241.         util.checkU(engine)
  2242.         util.checkU(query)
  2243.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2244.         if searchFeed is not None and searchDownloadsFeed is not None:
  2245.             searchFeed.preserveDownloads(searchDownloadsFeed)
  2246.             searchFeed.lookup(engine, query)
  2247.  
  2248.     def resetSearch(self):
  2249.         searchFeed, searchDownloadsFeed = self.__getSearchFeeds()
  2250.         if searchFeed is not None and searchDownloadsFeed is not None:
  2251.             searchFeed.preserveDownloads(searchDownloadsFeed)
  2252.             searchFeed.reset()
  2253.  
  2254.     def sortBy(self, by, section):
  2255.         try:
  2256.             self.templateHandle.getTemplateVariable('setSortBy')(by, section, self.templateHandle)
  2257.         except KeyError, e:
  2258.             logging.warning ("KeyError in getTemplateVariable ('setSortBy')")
  2259.  
  2260.     def handleSelect(self, area, viewName, id, shiftDown, ctrlDown):
  2261.         try:
  2262.             view = self.templateHandle.getTemplateVariable(viewName)
  2263.         except KeyError, e: # user switched templates before we got this
  2264.             logging.warning ("KeyError in getTemplateVariable (%s) during handleSelect()" % (viewName,))
  2265.             return
  2266.         shift = (shiftDown == '1')
  2267.         ctrl = (ctrlDown == '1')
  2268.         controller.selection.selectItem(area, view, int(id), shift, ctrl)
  2269.  
  2270.     def handleContextMenuSelect(self, id, area, viewName):
  2271.         try:
  2272.             obj = db.getObjectByID(int(id))
  2273.         except:
  2274.             traceback.print_exc()
  2275.         else:
  2276.             try:
  2277.                 view = self.templateHandle.getTemplateVariable(viewName)
  2278.             except KeyError, e: # user switched templates before we got this
  2279.                 logging.warning ("KeyError in getTemplateVariable (%s) during handleContextMenuSelect()" % (viewName,))
  2280.                 return
  2281.             if not controller.selection.isSelected(area, view, int(id)):
  2282.                 self.handleSelect(area, viewName, id, False, False)
  2283.             popup = menu.makeContextMenu(self.currentName, view,
  2284.                     controller.selection.getSelectionForArea(area), int(id))
  2285.             if popup:
  2286.                 delegate.showContextMenu(popup)
  2287.  
  2288.     def __getSearchFeeds(self):
  2289.         searchFeed = controller.getGlobalFeed('dtv:search')
  2290.         assert searchFeed is not None
  2291.         
  2292.         searchDownloadsFeed = controller.getGlobalFeed('dtv:searchDownloads')
  2293.         assert searchDownloadsFeed is not None
  2294.  
  2295.         return (searchFeed, searchDownloadsFeed)
  2296.  
  2297.     # The Windows XUL port can send a setVolume or setVideoProgress at
  2298.     # any time, even when there's no video display around. We can just
  2299.     # ignore it
  2300.     def setVolume(self, level):
  2301.         pass
  2302.     def setVideoProgress(self, pos):
  2303.         pass
  2304.  
  2305. # Helper: liberally interpret the provided string as a boolean
  2306. def stringToBoolean(string):
  2307.     if string == "" or string == "0" or string == "false":
  2308.         return False
  2309.     else:
  2310.         return True
  2311.  
  2312. ###############################################################################
  2313. #### Playlist & Video clips                                                ####
  2314. ###############################################################################
  2315.  
  2316. class Playlist:
  2317.     
  2318.     def __init__(self, view, firstItemId):
  2319.         self.initialView = view
  2320.         self.filteredView = self.initialView.filter(mappableToPlaylistItem)
  2321.         self.view = self.filteredView.map(mapToPlaylistItem)
  2322.  
  2323.         # Move the cursor to the requested item; if there's no
  2324.         # such item in the view, move the cursor to the first
  2325.         # item
  2326.         self.view.confirmDBThread()
  2327.         self.view.resetCursor()
  2328.         while True:
  2329.             cur = self.view.getNext()
  2330.             if cur == None:
  2331.                 # Item not found in view. Put cursor at the first
  2332.                 # item, if any.
  2333.                 self.view.resetCursor()
  2334.                 self.view.getNext()
  2335.                 break
  2336.             if firstItemId is None or cur.getID() == int(firstItemId):
  2337.                 # The cursor is now on the requested item.
  2338.                 break
  2339.  
  2340.     def reset(self):
  2341.         self.initialView.removeView(self.filteredView)
  2342.         self.initialView = None
  2343.         self.filteredView = None
  2344.         self.view = None
  2345.  
  2346.     def cur(self):
  2347.         return self.itemMarkedAsViewed(self.view.cur())
  2348.  
  2349.     def getNext(self):
  2350.         return self.itemMarkedAsViewed(self.view.getNext())
  2351.         
  2352.     def getPrev(self):
  2353.         return self.itemMarkedAsViewed(self.view.getPrev())
  2354.  
  2355.     def itemMarkedAsViewed(self, anItem):
  2356.         if anItem is not None:
  2357.             eventloop.addIdle(anItem.onViewed, "Mark item viewed")
  2358.         return anItem
  2359.  
  2360. class PlaylistItemFromItem:
  2361.  
  2362.     def __init__(self, anItem):
  2363.         self.item = anItem
  2364.         self.dcOnViewed = None
  2365.  
  2366.     def getTitle(self):
  2367.         return self.item.getTitle()
  2368.  
  2369.     def getVideoFilename(self):
  2370.         return self.item.getVideoFilename()
  2371.  
  2372.     def getLength(self):
  2373.         # NEEDS
  2374.         return 42.42
  2375.  
  2376.     def onViewedExecute(self):
  2377.         if self.item.idExists():
  2378.             self.item.markItemSeen()
  2379.         self.dcOnViewed = None
  2380.  
  2381.     def onViewed(self):
  2382.         if self.dcOnViewed or self.item.getSeen():
  2383.             return
  2384.         self.dcOnViewed = eventloop.addTimeout(5, self.onViewedExecute, "Mark item viewed")
  2385.  
  2386.     def onViewedCancel(self):
  2387.         if self.dcOnViewed:
  2388.             self.dcOnViewed.cancel()
  2389.             self.dcOnViewed = None
  2390.  
  2391.     # Return the ID that is used by a template to indicate this item 
  2392.     def getID(self):
  2393.         return self.item.getID()
  2394.  
  2395.     def __getattr__(self, attr):
  2396.         return getattr(self.item, attr)
  2397.  
  2398. def mappableToPlaylistItem(obj):
  2399.     return (isinstance(obj, item.Item) and obj.isDownloaded())
  2400.  
  2401. def mapToPlaylistItem(obj):
  2402.     return PlaylistItemFromItem(obj)
  2403.  
  2404. def _defaultFeeds():
  2405.     if config.get(prefs.DEFAULT_CHANNELS_FILE) is not None:
  2406.         importer = opml.Importer()
  2407.         try:
  2408.             if ((config.get(prefs.THEME_NAME) is not None) and 
  2409.                 (config.get(prefs.THEME_DIRECTORY) is not None)):
  2410.                 filepath = os.path.join(
  2411.                     config.get(prefs.THEME_DIRECTORY),
  2412.                     config.get(prefs.THEME_NAME),
  2413.                     config.get(prefs.DEFAULT_CHANNELS_FILE))
  2414.             else:
  2415.                 filepath = os.path.join(
  2416.                     config.get(prefs.SUPPORT_DIRECTORY),
  2417.                     config.get(prefs.DEFAULT_CHANNELS_FILE))
  2418.             importer.importSubscriptionsFrom(filepath,
  2419.                                              showSummary = False)
  2420.             logging.info("Imported %s" % filepath)
  2421.         except:
  2422.             logging.warn("Could not import %s" % filepath)
  2423.         return
  2424.     logging.info("Adding default feeds")
  2425.     if platform.system() == 'Darwin':
  2426.         defaultFeedURLs = [u'http://www.getmiro.com/screencasts/mac/mac.feed.rss']
  2427.     elif platform.system() == 'Windows':
  2428.         defaultFeedURLs = [u'http://www.getmiro.com/screencasts/windows/win.feed.rss']
  2429.     else:
  2430.         defaultFeedURLs = [u'http://www.getmiro.com/screencasts/windows/win.feed.rss']
  2431.     defaultFeedURLs.extend([ (_('Starter Channels'),
  2432.                               [u'http://richie-b.blip.tv/posts/?skin=rss',
  2433.                                u'http://feeds.pbs.org/pbs/kcet/wiredscience-video',
  2434.                                u'http://www.jpl.nasa.gov/multimedia/rss/podfeed-hd.xml',
  2435.                                u'http://www.linktv.org/rss/hq/mosaic.xml']),
  2436.                            ])
  2437.  
  2438.     for default in defaultFeedURLs:
  2439.         print repr(default)
  2440.         if isinstance(default, tuple): # folder
  2441.             defaultFolder = default
  2442.             c_folder = folder.ChannelFolder(defaultFolder[0])
  2443.             for url in defaultFolder[1]:
  2444.                 d_feed = feed.Feed(url, initiallyAutoDownloadable=False)
  2445.                 d_feed.setFolder(c_folder)
  2446.         else: # feed
  2447.             d_feed = feed.Feed(default, initiallyAutoDownloadable=False)
  2448.     playlist.SavedPlaylist(_(u"Example Playlist"))
  2449.  
  2450. def _getThemeHistory():
  2451.     if len(views.themeHistories) > 0:
  2452.         return views.themeHistories[0]
  2453.     else:
  2454.         return theme.ThemeHistory()
  2455.  
  2456. def _getInitialChannelGuide():
  2457.     default_guide = None
  2458.     newGuide = False
  2459.     for guideObj in views.guides:
  2460.         if default_guide is None:
  2461.             if guideObj.getDefault():
  2462.                 default_guide = guideObj
  2463.  
  2464.     if default_guide is None:
  2465.         newGuide = True
  2466.         logging.info ("Spawning Miro Guide...")
  2467.         default_guide = guide.ChannelGuide()
  2468.         initialFeeds = resources.path("initial-feeds.democracy")
  2469.         if os.path.exists(initialFeeds):
  2470.             urls = subscription.parseFile(initialFeeds)
  2471.             if urls is not None:
  2472.                 for url in urls:
  2473.                     feed.Feed(url, initiallyAutoDownloadable=False)
  2474.             dialog = dialogs.MessageBoxDialog(_("Custom Channels"), Template(_("You are running a version of $longAppName with a custom set of channels.")).substitute(longAppName=config.get(prefs.LONG_APP_NAME)))
  2475.             dialog.run()
  2476.             controller.initial_feeds = True
  2477.         else:
  2478.             _defaultFeeds()
  2479.     return (newGuide, default_guide)
  2480.  
  2481. # Race conditions:
  2482.  
  2483. # We do the migration in the dl_daemon if the dl_daemon knows about it
  2484. # so that we don't get a race condition.
  2485.  
  2486. @eventloop.asUrgent
  2487. def changeMoviesDirectory(newDir, migrate):
  2488.     if not util.directoryWritable(newDir):
  2489.         dialog = dialogs.MessageBoxDialog(_("Error Changing Movies Directory"), 
  2490.                 _("You don't have permission to write to the directory you selected.  Miro will continue to use the old videos directory."))
  2491.         dialog.run()
  2492.         return
  2493.  
  2494.     oldDir = config.get(prefs.MOVIES_DIRECTORY)
  2495.     config.set(prefs.MOVIES_DIRECTORY, newDir)
  2496.     if migrate:
  2497.         views.remoteDownloads.confirmDBThread()
  2498.         for download in views.remoteDownloads:
  2499.             if download.isFinished():
  2500.                 logging.info ("migrating %s", download.getFilename())
  2501.                 download.migrate(newDir)
  2502.         # Pass in case they don't exist or are not empty:
  2503.         try:
  2504.             os.rmdir(os.path.join (oldDir, 'Incomplete Downloads'))
  2505.         except:
  2506.             pass
  2507.         try:
  2508.             os.rmdir(oldDir)
  2509.         except:
  2510.             pass
  2511.     util.getSingletonDDBObject(views.directoryFeed).update()
  2512.  
  2513. @eventloop.asUrgent
  2514. def saveVideo(currentPath, savePath):
  2515.     logging.info("saving video %s to %s" % (currentPath, savePath))
  2516.     try:
  2517.         shutil.copyfile(currentPath, savePath)
  2518.     except:
  2519.         title = _('Error Saving Video')
  2520.         name = os.path.basename(currentPath)
  2521.         text = _('An error occured while trying to save %s.  Please check that the file has not been deleted and try again.') % util.clampText(name, 50)
  2522.         dialogs.MessageBoxDialog(title, text).run()
  2523.         logging.warn("Error saving video: %s" % traceback.format_exc())
  2524.